LostMaster TARGET//ZERO

Ultra-digital foreign target intelligence and organized-crime flight warnings for Torn

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         LostMaster TARGET//ZERO
// @namespace    lostmaster.travel
// @version      4.0.3
// @description  Ultra-digital foreign target intelligence and organized-crime flight warnings for Torn
// @author       LostMaster
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @connect      yata.yt
// ==/UserScript==

(function () {
    'use strict';

    const APP = 'LostMaster TARGET//ZERO';
    const VERSION = '4.0.3';
    const API_BASE = 'https://api.torn.com/v2';
    const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
    const ROOT_ID = 'lmx-root';
    const STYLE_ID = 'lmx-style';

    const STORAGE = {
        key: 'lmt_api_key',
        profile: 'lmt_travel_profile',
        minimized: 'lmx_minimized',
        position: 'lmx_panel_position',
        size: 'lmx_panel_size',
        yata: 'lmx_yata_cache',
        stockHistory: 'lmx_xanax_stock_history',
        targetLogs: 'lmx_target_logs',
        oc: 'lmx_oc_cache',
        warningHours: 'lmx_oc_warning_hours'
    };

    const TRAVEL = {
        'Mexico': { minutes: 24, city: 'Ciudad Juarez', yata: 'mex' },
        'Cayman Islands': { minutes: 33, city: 'George Town', yata: 'cay' },
        'Canada': { minutes: 39, city: 'Toronto', yata: 'can' },
        'Hawaii': { minutes: 127, city: 'Honolulu', yata: 'haw' },
        'United Kingdom': { minutes: 151, city: 'London', yata: 'uni' },
        'Argentina': { minutes: 158, city: 'Buenos Aires', yata: 'arg' },
        'Switzerland': { minutes: 166, city: 'Zurich', yata: 'swi' },
        'Japan': { minutes: 213, city: 'Tokyo', yata: 'jap' },
        'China': { minutes: 229, city: 'Beijing', yata: 'chi' },
        'United Arab Emirates': { minutes: 257, city: 'Dubai', yata: 'uae' },
        'South Africa': { minutes: 282, city: 'Johannesburg', yata: 'sou' }
    };

    const YATA_COUNTRY = Object.fromEntries(
        Object.entries(TRAVEL).map(([country, data]) => [data.yata, country])
    );

    const METHOD_MULTIPLIER = {
        standard: 1,
        airstrip: 0.70,
        wlt: 0.50,
        business: 0.30
    };

    const DEFAULT_PROFILE = {
        method: 'standard',
        mailingBook: false,
        capacity: 28,
        businessTicketsFree: false
    };

    const KEY_STOP_ERROR_CODES = new Set([1, 2, 10, 13, 16, 18]);
    const YATA_TTL = 60 * 1000;
    const OC_TTL = 5 * 60 * 1000;
    const WATCH_MS = 1000;
    const HISTORY_MAX_AGE = 7 * 24 * 60 * 60 * 1000;
    const TARGET_MAX_AGE = 120 * 24 * 60 * 60 * 1000;

    let settingsOpen = false;
    let dataBusy = false;
    let dataError = '';
    let yataWarning = '';
    let apiKeyNotice = '';
    let lastSignature = '';
    let lastRenderedMinute = -1;
    let dragState = null;
    let resizeState = null;
    let confirmBypassUntil = 0;

    function esc(value) {
        const node = document.createElement('div');
        node.textContent = String(value ?? '');
        return node.innerHTML;
    }

    function readJson(key, fallback = null) {
        try {
            return JSON.parse(localStorage.getItem(key) || 'null') ?? fallback;
        } catch (_) {
            return fallback;
        }
    }

    function writeJson(key, value) {
        localStorage.setItem(key, JSON.stringify(value));
    }

    function cacheGet(key, ttl = Number.MAX_SAFE_INTEGER) {
        const cached = readJson(key);
        if (!cached || !Number.isFinite(cached.updated)) return null;
        if (Date.now() - cached.updated > ttl) return null;
        return cached.data ?? null;
    }

    function cacheSet(key, data) {
        writeJson(key, { updated: Date.now(), data });
    }

    function cacheIsFresh(key, ttl) {
        const cached = readJson(key);
        return Boolean(cached && Number.isFinite(cached.updated) && Date.now() - cached.updated <= ttl);
    }

    function getKey() {
        return localStorage.getItem(STORAGE.key) || '';
    }

    function setKey(value) {
        localStorage.setItem(STORAGE.key, String(value || '').trim());
        apiKeyNotice = '';
    }

    function getProfile() {
        const raw = readJson(STORAGE.profile, {});
        return {
            method: METHOD_MULTIPLIER[raw.method] !== undefined ? raw.method : DEFAULT_PROFILE.method,
            mailingBook: Boolean(raw.mailingBook),
            capacity: Math.max(1, Math.min(100, Number(raw.capacity) || DEFAULT_PROFILE.capacity)),
            businessTicketsFree: Boolean(raw.businessTicketsFree)
        };
    }

    function saveProfile(profile) {
        writeJson(STORAGE.profile, profile);
    }

    function warningHours() {
        return Math.max(1, Math.min(72, Number(localStorage.getItem(STORAGE.warningHours)) || 12));
    }

    function flightMinutes(country) {
        const profile = getProfile();
        const base = TRAVEL[country]?.minutes || 0;
        const bookMultiplier = profile.mailingBook ? 0.75 : 1;
        return Math.max(1, Math.round(base * METHOD_MULTIPLIER[profile.method] * bookMultiplier));
    }

    function durationFromSeconds(seconds) {
        const total = Math.max(0, Math.floor(Number(seconds) || 0));
        const days = Math.floor(total / 86400);
        const hours = Math.floor((total % 86400) / 3600);
        const minutes = Math.floor((total % 3600) / 60);
        if (days) return `${days}d ${hours}h ${minutes}m`;
        if (hours) return `${hours}h ${minutes}m`;
        return `${minutes}m`;
    }

    function durationFromMinutes(minutes) {
        return durationFromSeconds(Number(minutes) * 60);
    }

    function money(value) {
        return Number.isFinite(Number(value)) ? '$' + Math.round(Number(value)).toLocaleString() : '—';
    }

    function ageText(timestampSeconds) {
        if (!timestampSeconds) return 'unknown age';
        const seconds = Math.max(0, Date.now() / 1000 - Number(timestampSeconds));
        if (seconds < 60) return '<1m old';
        return durationFromSeconds(seconds) + ' old';
    }

    async function rawApi(path, keyOverride = null) {
        const key = keyOverride === null ? getKey() : keyOverride;
        if (!key) throw new Error('No API key configured.');
        const url = new URL(API_BASE + path);
        url.searchParams.set('key', key);
        const response = await fetch(url.toString());
        if (!response.ok) throw new Error(`Torn API HTTP ${response.status}.`);
        const data = await response.json();
        if (data?.error) {
            const code = Number(data.error.code);
            const message = data.error.error || 'Torn API error.';
            if (keyOverride === null && KEY_STOP_ERROR_CODES.has(code)) {
                localStorage.removeItem(STORAGE.key);
                apiKeyNotice = message;
            }
            const error = new Error(message);
            error.tornCode = code;
            throw error;
        }
        return data;
    }

    function gmJson(url) {
        return new Promise((resolve, reject) => {
            if (typeof GM_xmlhttpRequest !== 'function') {
                fetch(url).then(r => {
                    if (!r.ok) throw new Error(`YATA HTTP ${r.status}`);
                    return r.json();
                }).then(resolve).catch(reject);
                return;
            }
            GM_xmlhttpRequest({
                method: 'GET', url, headers: { Accept: 'application/json' }, timeout: 20000,
                onload(response) {
                    if (response.status < 200 || response.status >= 300) {
                        reject(new Error(`YATA HTTP ${response.status}`));
                        return;
                    }
                    try { resolve(JSON.parse(response.responseText || '{}')); }
                    catch (error) { reject(error); }
                },
                onerror: () => reject(new Error('Could not reach YATA.')),
                ontimeout: () => reject(new Error('YATA request timed out.'))
            });
        });
    }

    function normalizeYata(raw) {
        const result = {};
        for (const [code, countryData] of Object.entries(raw?.stocks || {})) {
            const country = YATA_COUNTRY[code];
            if (!country) continue;
            const item = (countryData?.stocks || []).find(entry =>
                String(entry?.name || '').trim().toLowerCase() === 'xanax'
            );
            if (!item) continue;
            result[country] = {
                update: Number(countryData?.update || 0),
                quantity: Math.max(0, Number(item.quantity) || 0),
                cost: Math.max(0, Number(item.cost) || 0),
                nextRestock: item.nextRestock || null
            };
        }
        return result;
    }

    function recordStockSnapshots(data) {
        const history = readJson(STORAGE.stockHistory, {});
        const now = Date.now();
        for (const [country, stock] of Object.entries(data)) {
            const rows = Array.isArray(history[country]) ? history[country] : [];
            const stamp = Number(stock.update) > 0 ? Number(stock.update) * 1000 : now;
            const last = rows[rows.length - 1];
            if (!last || last.timestamp !== stamp || last.quantity !== stock.quantity) {
                rows.push({ timestamp: stamp, quantity: stock.quantity });
            }
            history[country] = rows
                .filter(row => now - Number(row.timestamp) <= HISTORY_MAX_AGE)
                .slice(-250);
        }
        writeJson(STORAGE.stockHistory, history);
    }

    async function getYata(force = false) {
        if (!force) {
            const fresh = cacheGet(STORAGE.yata, YATA_TTL);
            if (fresh) return fresh;
        }
        try {
            const data = normalizeYata(await gmJson(YATA_URL));
            recordStockSnapshots(data);
            cacheSet(STORAGE.yata, data);
            yataWarning = '';
            return data;
        } catch (error) {
            const stale = cacheGet(STORAGE.yata);
            if (!stale) throw error;
            yataWarning = `YATA unavailable; using cached stock (${String(error?.message || error)}).`;
            return stale;
        }
    }

    function stockTrend(country) {
        const rows = (readJson(STORAGE.stockHistory, {})[country] || [])
            .filter(row => Date.now() - Number(row.timestamp) <= HISTORY_MAX_AGE);
        let depleted = 0;
        let observedHours = 0;
        let events = 0;
        for (let i = 1; i < rows.length; i += 1) {
            const hours = (rows[i].timestamp - rows[i - 1].timestamp) / 3600000;
            if (hours <= 0 || hours > 12) continue;
            const delta = rows[i - 1].quantity - rows[i].quantity;
            observedHours += hours;
            events += 1;

            if (delta > 0) {
                depleted += delta;
            }
        }
        if (!events || observedHours < 0.08) {
            return { rate: null, label: 'NO SIGNAL', className: 'neutral', events, observedHours };
        }
        const rate = depleted / observedHours;
        if (rate >= 20) return { rate, label: 'TARGET-RICH', className: 'hot', events, observedHours };
        if (rate >= 8) return { rate, label: 'ACTIVE', className: 'hot', events, observedHours };
        if (rate >= 2) return { rate, label: 'TRACE', className: 'warm', events, observedHours };
        return { rate, label: 'LOW SIGNAL', className: 'cool', events, observedHours };
    }

    async function refreshOc(force = false) {
        if (!getKey()) return null;
        if (!force) {
            const fresh = cacheGet(STORAGE.oc, OC_TTL);
            if (fresh !== null) return fresh;
        }
        const data = await rawApi('/user/organizedcrime');
        const crime = data?.organizedCrime ?? null;
        cacheSet(STORAGE.oc, crime);
        return crime;
    }

    function ocState() {
        const crime = cacheGet(STORAGE.oc);
        if (!crime) return { kind: 'none', text: 'No active OC found', crime: null, seconds: null };
        const readyAt = Number(crime.ready_at);
        if (!readyAt) return { kind: 'neutral', text: `${crime.name || 'OC'} has no ready time`, crime, seconds: null };
        const seconds = readyAt - Math.floor(Date.now() / 1000);
        if (seconds <= 0) return { kind: 'danger', text: `${crime.name || 'OC'} is ready now`, crime, seconds };
        if (seconds <= warningHours() * 3600) {
            return { kind: 'danger', text: `SYSTEM INTERRUPT // ${crime.name || 'OC'} IN ${durationFromSeconds(seconds)} // FLIGHT LOCK`, crime, seconds };
        }
        return { kind: 'safe', text: `MISSION CLEAR // ${crime.name || 'OC'} IN ${durationFromSeconds(seconds)}`, crime, seconds };
    }

    function pageText() {
        if (!document.body) return '';
        const clone = document.body.cloneNode(true);
        clone.querySelector('#' + ROOT_ID)?.remove();
        return String(clone.textContent || '').replace(/\s+/g, ' ').trim();
    }

    function currentCountry() {
        const text = pageText().toLowerCase();
        for (const country of Object.keys(TRAVEL).sort((a, b) => b.length - a.length)) {
            if (text.includes(country.toLowerCase())) return country;
        }
        return null;
    }

    function isTravelAgency() {
        return location.pathname.toLowerCase().endsWith('/travelagency.php');
    }

    function flightRouteText() {
        const section = document.querySelector('section[class*="flightProgressSection"]');
        return String(section?.innerText || section?.textContent || '').replace(/\s+/g, ' ').trim();
    }

    function isFlying() {
        const route = flightRouteText();
        return Boolean(route && /remaining flight time/i.test(route));
    }

    function isAbroad() {
        if (!location.pathname.toLowerCase().endsWith('/shops.php')) return false;
        const text = pageText().toLowerCase();
        const store = text.includes('general store') || text.includes('black market');
        return store && text.includes('stock') && text.includes('amount') && text.includes('buy');
    }

    function isTravelContext() {
        return isTravelAgency() || isAbroad() || isFlying();
    }

    function tctParts(timestamp = Date.now()) {
        const date = new Date(timestamp);
        return {
            day: date.getUTCDay(),
            bucket: Math.floor(date.getUTCHours() / 4)
        };
    }

    function saveTargetLog(country, count) {
        const value = Math.max(0, Math.min(999, Math.round(Number(count))));
        if (!country || !Number.isFinite(value)) return false;
        const logs = readJson(STORAGE.targetLogs, []);
        const now = Date.now();
        const time = tctParts(now);
        logs.push({ country, count: value, timestamp: now, day: time.day, bucket: time.bucket });
        writeJson(STORAGE.targetLogs, logs
            .filter(row => now - Number(row.timestamp) <= TARGET_MAX_AGE)
            .slice(-1000));
        return true;
    }

    function targetEstimate(country) {
        const now = Date.now();
        const time = tctParts(now + flightMinutes(country) * 60000);
        const all = readJson(STORAGE.targetLogs, []).filter(row =>
            row.country === country && now - Number(row.timestamp) <= TARGET_MAX_AGE
        );
        let matched = all.filter(row => row.day === time.day && row.bucket === time.bucket);
        let scope = 'same day/time';
        if (matched.length < 3) {
            matched = all.filter(row => row.bucket === time.bucket);
            scope = 'same time window';
        }
        if (matched.length < 3) {
            matched = all;
            scope = 'all visits';
        }
        if (!matched.length) return { samples: 0, scope, text: 'No personal estimate yet', average: null };
        const counts = matched.map(row => Number(row.count)).filter(Number.isFinite);
        const average = counts.reduce((sum, value) => sum + value, 0) / counts.length;
        const variance = counts.reduce((sum, value) => sum + Math.pow(value - average, 2), 0) / counts.length;
        const spread = Math.max(1, Math.round(Math.sqrt(variance)));
        const low = Math.max(0, Math.round(average - spread));
        const high = Math.max(low, Math.round(average + spread));
        return {
            samples: counts.length,
            scope,
            average,
            text: counts.length === 1 ? `Observed: ${counts[0]}` : `Expected: ${low}–${high} (avg ${average.toFixed(1)})`
        };
    }

    function destinationRows() {
        const yata = cacheGet(STORAGE.yata) || {};
        return Object.entries(yata).map(([country, stock]) => {
            const trend = stockTrend(country);
            const flight = flightMinutes(country);
            const projected = trend.rate === null ? null : Math.max(0, Math.floor(stock.quantity - trend.rate * flight / 60));
            const targets = targetEstimate(country);
            return { country, stock, trend, flight, projected, targets };
        }).sort((a, b) => {
            const aTargets = a.targets.average ?? -1;
            const bTargets = b.targets.average ?? -1;
            if (aTargets !== bTargets) return bTargets - aTargets;
            const aRate = a.trend.rate ?? -1;
            const bRate = b.trend.rate ?? -1;
            return bRate - aRate;
        });
    }

    function injectStyle() {
        if (document.getElementById(STYLE_ID)) return;
        const style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = `
            #${ROOT_ID}{position:fixed;z-index:999999;top:18%;right:10px;width:390px;max-height:72vh;background:linear-gradient(145deg,#020717 0%,#050b22 55%,#090527 100%);color:#b9eaff;border:1px solid #00d9ff;border-radius:2px;clip-path:polygon(0 0,calc(100% - 15px) 0,100% 15px,100% 100%,15px 100%,0 calc(100% - 15px));box-shadow:0 0 0 1px #08113a,0 0 26px #00d9ff45,0 10px 35px #000f;font:12px Consolas,"Courier New",monospace;overflow:hidden;display:flex;flex-direction:column}
            #${ROOT_ID}:before{content:"";position:absolute;inset:0;pointer-events:none;z-index:19;background:linear-gradient(90deg,transparent 0 48%,#00eaff08 50%,transparent 52%),repeating-linear-gradient(90deg,#00d9ff08 0,#00d9ff08 1px,transparent 1px,transparent 24px)}
            #${ROOT_ID}:after{content:"";position:absolute;left:0;right:0;height:42px;top:-45px;pointer-events:none;z-index:20;background:linear-gradient(180deg,transparent,#00efff20,transparent);animation:lmxScan 4.2s linear infinite}
            #${ROOT_ID}.hidden{display:none} #${ROOT_ID}.mini{width:auto!important;height:auto!important;max-height:none!important;background:transparent;border:0;box-shadow:none;top:50%!important;right:0!important;left:auto!important;bottom:auto!important;transform:translateY(-50%)}
            #${ROOT_ID} *{box-sizing:border-box} #${ROOT_ID} button,#${ROOT_ID} input,#${ROOT_ID} select{font:inherit}
            #${ROOT_ID} .head{display:flex;align-items:center;gap:7px;padding:9px 10px;background:linear-gradient(90deg,#061535,#150838);border-bottom:1px solid #00d9ff;box-shadow:0 4px 15px #00d9ff18;cursor:move;user-select:none;touch-action:none;letter-spacing:1.2px}
            #${ROOT_ID} .title{font-weight:900;color:#73f6ff;flex:1;text-shadow:0 0 5px #00eaff,0 0 13px #008cff} #${ROOT_ID} .icon{border:1px solid #00d9ff;background:#071333;color:#75f7ff;border-radius:1px;padding:4px 7px;cursor:pointer;box-shadow:inset 0 0 8px #00d9ff20}
            #${ROOT_ID} .body{overflow:auto;padding:10px;scrollbar-color:#00d9ff #030719} #${ROOT_ID} .oc{padding:9px;border-radius:1px;margin-bottom:9px;font-weight:800;letter-spacing:.5px;clip-path:polygon(0 0,calc(100% - 9px) 0,100% 9px,100% 100%,0 100%)}
            #${ROOT_ID} .oc.safe{background:#022920;color:#63ffb2;border:1px solid #00ff9d;box-shadow:inset 0 0 14px #00ff9d20} #${ROOT_ID} .oc.danger{background:#31000b;color:#ff2452;border:1px solid #ff003c;box-shadow:inset 0 0 18px #ff003340;animation:lmxPulse 1.1s infinite}
            #${ROOT_ID} .oc.neutral,#${ROOT_ID} .oc.none{background:#0b1431;color:#77dfff;border:1px solid #246da0}
            @keyframes lmxPulse{50%{box-shadow:inset 0 0 22px #ff003344,0 0 0 2px #ff003377,0 0 18px #ff003355}}
            @keyframes lmxScan{to{top:calc(100% + 45px)}}
            #${ROOT_ID} .notice{padding:7px;margin-bottom:8px;background:#2d1c07;color:#ffe760;border:1px solid #ffb900;border-radius:1px}
            #${ROOT_ID} .cards{display:grid;gap:8px} #${ROOT_ID} .card{position:relative;background:linear-gradient(135deg,#06142c,#0c0a2c);border:1px solid #176d9d;border-left:3px solid #00d9ff;border-radius:1px;padding:9px;clip-path:polygon(0 0,calc(100% - 9px) 0,100% 9px,100% 100%,0 100%);box-shadow:inset 0 0 17px #008cff16}
            #${ROOT_ID} .card:before{content:"◆";position:absolute;right:8px;bottom:3px;color:#ea35ff;font-size:11px;text-shadow:0 0 7px #ea35ff}
            #${ROOT_ID} .cardTop{display:flex;align-items:center;gap:8px;margin-bottom:5px} #${ROOT_ID} .country{font-weight:900;color:#72f5ff;flex:1;font-size:13px;letter-spacing:1px;text-shadow:0 0 8px #00cfff}
            #${ROOT_ID} .heat{border-radius:1px;padding:2px 7px;font-weight:800;letter-spacing:.7px} #${ROOT_ID} .heat.hot{background:#350013;color:#ff2d63;border:1px solid #ff0051;text-shadow:0 0 7px #ff0051} #${ROOT_ID} .heat.warm{background:#291038;color:#f067ff;border:1px solid #d52bff;text-shadow:0 0 7px #d52bff} #${ROOT_ID} .heat.cool{background:#03273c;color:#52e8ff;border:1px solid #008fc2} #${ROOT_ID} .heat.neutral{background:#10172e;color:#7291b9;border:1px solid #34496d}
            #${ROOT_ID} .grid{display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;color:#688eaf} #${ROOT_ID} .value{color:#9dffbc;text-align:right;text-shadow:0 0 5px #00ff8840}
            #${ROOT_ID} .targets{margin-top:7px;padding-top:6px;border-top:1px solid #174d78;color:#f06cff} #${ROOT_ID} .muted{color:#627f9f;font-size:11px}
            #${ROOT_ID} .log{display:flex;gap:6px;margin-top:9px;padding-top:9px;border-top:1px solid #174d78} #${ROOT_ID} input,#${ROOT_ID} select{background:#020718;color:#a7f6ff;border:1px solid #1676a7;border-radius:1px;padding:6px;min-width:0}
            #${ROOT_ID} .primary{background:linear-gradient(90deg,#003c58,#162766);color:#78f7ff;border:1px solid #00d9ff;border-radius:1px;padding:6px 9px;cursor:pointer;text-transform:uppercase;font-weight:800;box-shadow:inset 0 0 10px #00d9ff24}
            #${ROOT_ID} .settings{margin-top:9px;padding:9px;background:#070d25;border:1px solid #254e82;border-left:3px solid #d52bff;border-radius:1px} #${ROOT_ID} label{display:block;margin:7px 0 3px;color:#7fa8ca}
            #${ROOT_ID} .actions{display:flex;gap:6px;margin-top:9px} #${ROOT_ID} .error{color:#ff9c9c;margin-top:6px}
            #${ROOT_ID} .miniButton{background:#071331;color:#6ff5ff;border:1px solid #00d9ff;border-right:0;border-radius:2px 0 0 2px;padding:10px 9px;cursor:pointer;font-weight:900;text-shadow:0 0 8px #00eaff;box-shadow:0 0 13px #00d9ff66}
            #${ROOT_ID} .resize{position:absolute;z-index:30;right:0;bottom:0;width:30px;height:30px;cursor:nwse-resize;touch-action:none;background:none}
            #${ROOT_ID} .resize:after{content:"";position:absolute;right:3px;bottom:3px;width:11px;height:11px;border-right:2px solid #ff0033;border-bottom:2px solid #ff0033;box-shadow:3px 3px 7px #ff003399,inset -2px -2px 5px #ff003355;pointer-events:none}
            #lmx-page-warning{position:fixed;z-index:999998;top:8px;left:50%;transform:translateX(-50%);background:#210005;color:#ff1744;border:2px solid #ff0033;border-radius:2px;padding:10px 16px;font:bold 14px Consolas,"Courier New",monospace;box-shadow:0 0 26px #ff003388,0 5px 20px #0009;letter-spacing:.5px;text-shadow:0 0 8px #ff0033}
            @media(max-width:700px){#${ROOT_ID}{width:min(94vw,390px);right:3vw;top:12%;max-height:78vh}#${ROOT_ID} .head{min-height:44px}#${ROOT_ID} .resize{width:32px;height:32px}}
        `;
        document.head.appendChild(style);
    }

    function root() {
        let panel = document.getElementById(ROOT_ID);
        if (!panel) {
            panel = document.createElement('div');
            panel.id = ROOT_ID;
            document.body.appendChild(panel);
        }
        return panel;
    }

    function applyPosition(panel) {
        const saved = readJson(STORAGE.position);
        if (!saved) return;
        panel.style.left = Math.max(0, Math.min(window.innerWidth - 80, Number(saved.left) || 0)) + 'px';
        panel.style.top = Math.max(0, Math.min(window.innerHeight - 50, Number(saved.top) || 0)) + 'px';
        panel.style.right = 'auto';
    }

    function applySize(panel) {
        const saved = readJson(STORAGE.size);
        if (!saved) return;
        panel.style.width = Math.max(280, Math.min(window.innerWidth - 20, Number(saved.width) || 390)) + 'px';
        panel.style.maxHeight = Math.max(240, Math.min(window.innerHeight - 20, Number(saved.height) || 600)) + 'px';
        panel.style.height = Math.max(240, Math.min(window.innerHeight - 20, Number(saved.height) || 600)) + 'px';
    }

    function setupUI() {
        return `<div class="body"><div class="oc neutral">NETWORK OFFLINE // API KEY REQUIRED</div>
            <div class="muted">Connect a Torn Minimal Access key. It stays in this browser and is sent only to Torn.</div>
            <label>API key</label><input id="lmx-key" type="password" style="width:100%" autocomplete="off">
            <div class="actions"><button id="lmx-save-key" class="primary">LINK NETWORK</button></div>
            ${apiKeyNotice ? `<div class="error">${esc(apiKeyNotice)}</div>` : ''}</div>`;
    }

    function settingsUI() {
        const p = getProfile();
        return `<div class="body"><div class="oc neutral">SYSTEM CONFIG // TARGET ZERO</div><div class="settings">
            <label>Travel method</label><select id="lmx-method" style="width:100%">
                <option value="standard" ${p.method === 'standard' ? 'selected' : ''}>Standard</option>
                <option value="airstrip" ${p.method === 'airstrip' ? 'selected' : ''}>Private airstrip</option>
                <option value="wlt" ${p.method === 'wlt' ? 'selected' : ''}>WLT benefit</option>
                <option value="business" ${p.method === 'business' ? 'selected' : ''}>Business Class</option>
            </select>
            <label><input id="lmx-book" type="checkbox" ${p.mailingBook ? 'checked' : ''}> Mailing Yourself Abroad (-25%)</label>
            <label>Travel capacity</label><input id="lmx-capacity" type="number" min="1" max="100" value="${p.capacity}" style="width:100%">
            <label>OC flight warning (hours)</label><input id="lmx-warning" type="number" min="1" max="72" value="${warningHours()}" style="width:100%">
            <div class="actions"><button id="lmx-save-settings" class="primary">COMMIT // RETURN</button></div>
            <div class="actions"><button id="lmx-change-key" class="icon">Change API key</button></div>
            <div class="muted" style="margin-top:8px">Target history is stored only in this browser for 120 days. Stock movement history is retained for 7 days.</div>
        </div></div>`;
    }

    function hubUI() {
        if (settingsOpen) return settingsUI();

        const oc = ocState();
        const rows = destinationRows();
        const country = currentCountry();
        const showLog = isAbroad() && country && rows.some(row => row.country === country);
        return `<div class="body">
            <div class="oc ${oc.kind}">${esc(oc.text)}</div>
            ${yataWarning ? `<div class="notice">${esc(yataWarning)}</div>` : ''}
            ${dataError ? `<div class="notice">${esc(dataError)}</div>` : ''}
            ${showLog ? `<div class="card"><div class="country">LOCAL SCAN // ${esc(country)}</div><div class="muted">Count players matching your own mugging rules.</div><div class="log"><input id="lmx-target-count" type="number" min="0" max="999" placeholder="Targets observed" style="flex:1"><button id="lmx-log-targets" class="primary">UPLOAD TRACE</button></div></div>` : ''}
            <div class="cards">${rows.length ? rows.map(row => {
                const purchase = row.stock.cost * getProfile().capacity;
                const projected = row.projected === null ? 'Need trend data' : row.projected.toLocaleString();
                const rate = row.trend.rate === null ? 'BUILDING SIGNAL MAP' : `${row.trend.rate.toFixed(1)}/hr · ${durationFromSeconds(row.trend.observedHours * 3600)} observed`;
                return `<div class="card"><div class="cardTop"><div class="country">${esc(row.country)}</div><div class="heat ${row.trend.className}">${esc(row.trend.label)}</div></div>
                    <div class="grid"><span>FLIGHT RANGE</span><span class="value">${durationFromMinutes(row.flight)}</span><span>XANAX STOCK</span><span class="value">${row.stock.quantity.toLocaleString()}</span><span>ARRIVAL PROJECTION</span><span class="value">${projected}</span><span>XANAX ACTIVITY</span><span class="value">${rate}</span><span>LOADOUT CASH · ${getProfile().capacity}</span><span class="value">${money(purchase)}</span><span>INTEL AGE</span><span class="value">${ageText(row.stock.update)}</span></div>
                    <div class="targets">${esc(row.targets.text)}<div class="muted">${row.targets.samples} sample${row.targets.samples === 1 ? '' : 's'} · ${esc(row.targets.scope)} · arrival TCT window</div></div></div>`;
            }).join('') : '<div class="card">No Xanax destinations in the current YATA report.</div>'}</div>
        </div>`;
    }

    function render() {
        if (!document.body) return;
        injectStyle();
        const panel = root();
        if (!isTravelContext()) {
            panel.className = 'hidden';
            panel.innerHTML = '';
            document.getElementById('lmx-page-warning')?.remove();
            return;
        }
        if (localStorage.getItem(STORAGE.minimized) === '1') {
            panel.className = 'mini';
            panel.innerHTML = '<button id="lmx-restore" class="miniButton" title="TARGET//ZERO">Z//</button>';
            panel.querySelector('#lmx-restore').onclick = () => {
                localStorage.setItem(STORAGE.minimized, '0');
                render();
            };
            updatePageWarning();
            return;
        }
        panel.className = '';
        panel.innerHTML = `<div class="head"><div class="title">${settingsOpen ? '⚙ SYSTEM CONFIG' : 'TARGET//ZERO'} <span class="muted">v${VERSION}</span></div>${settingsOpen ? '<button id="lmx-close-settings" class="icon" title="Return to network">←</button>' : '<button id="lmx-refresh" class="icon" title="Rescan network">↻</button><button id="lmx-open-settings" class="icon" title="System configuration">⚙</button>'}<button id="lmx-minimize" class="icon">—</button></div>${getKey() ? hubUI() : setupUI()}<div class="resize"></div>`;
        applyPosition(panel);
        applySize(panel);
        bind(panel);
        updatePageWarning();
    }

    function bind(panel) {
        panel.querySelector('#lmx-minimize')?.addEventListener('click', () => {
            localStorage.setItem(STORAGE.minimized, '1');
            settingsOpen = false;
            render();
        });
        panel.querySelector('#lmx-open-settings')?.addEventListener('click', () => {
            settingsOpen = true;
            render();
        });
        panel.querySelector('#lmx-close-settings')?.addEventListener('click', () => {
            settingsOpen = false;
            render();
        });
        panel.querySelector('#lmx-refresh')?.addEventListener('click', () => ensureData(true));
        panel.querySelector('#lmx-save-key')?.addEventListener('click', async () => {
            const candidate = String(panel.querySelector('#lmx-key')?.value || '').trim();
            if (!candidate) return;
            dataError = 'Checking key...'; render();
            try {
                await rawApi('/user/organizedcrime', candidate);
                setKey(candidate); dataError = ''; await ensureData(true);
            } catch (error) { dataError = String(error?.message || error); render(); }
        });
        panel.querySelector('#lmx-change-key')?.addEventListener('click', () => {
            localStorage.removeItem(STORAGE.key);
            cacheSet(STORAGE.oc, null);
            settingsOpen = false;
            render();
        });
        panel.querySelector('#lmx-save-settings')?.addEventListener('click', async () => {
            saveProfile({
                method: panel.querySelector('#lmx-method')?.value || 'standard',
                mailingBook: Boolean(panel.querySelector('#lmx-book')?.checked),
                capacity: Math.max(1, Math.min(100, Number(panel.querySelector('#lmx-capacity')?.value) || 28)),
                businessTicketsFree: false
            });
            localStorage.setItem(STORAGE.warningHours, String(Math.max(1, Math.min(72, Number(panel.querySelector('#lmx-warning')?.value) || 12))));
            settingsOpen = false;
            render();
        });
        panel.querySelector('#lmx-log-targets')?.addEventListener('click', () => {
            const input = panel.querySelector('#lmx-target-count');
            const country = currentCountry();
            if (input && input.value !== '' && saveTargetLog(country, input.value)) {
                render();
            }
        });
        const head = panel.querySelector('.head');
        head?.addEventListener('pointerdown', event => {
            if (event.target.closest('button')) return;
            event.preventDefault();
            const rect = panel.getBoundingClientRect();
            dragState = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, left: rect.left, top: rect.top };
            head.setPointerCapture?.(event.pointerId);
        });
        panel.querySelector('.resize')?.addEventListener('pointerdown', event => {
            event.preventDefault(); event.stopPropagation();
            const rect = panel.getBoundingClientRect();
            resizeState = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, width: rect.width, height: rect.height };
            panel.style.height = rect.height + 'px';
            event.target.setPointerCapture?.(event.pointerId);
        });
    }

    function updatePageWarning() {
        let banner = document.getElementById('lmx-page-warning');
        const state = ocState();
        const shouldShow = getKey() && isTravelAgency() && state.kind === 'danger';
        if (!shouldShow) { banner?.remove(); return; }
        if (!banner) {
            banner = document.createElement('div');
            banner.id = 'lmx-page-warning';
            document.body.appendChild(banner);
        }
        banner.textContent = '⚠ ' + state.text;
    }

    function likelyTravelControl(target) {
        if (!isTravelAgency()) return false;
        const clickable = target.closest('button,a,[role="button"],input[type="submit"]');
        if (!clickable || clickable.closest('#' + ROOT_ID)) return false;
        const text = String(clickable.textContent || clickable.value || '').toLowerCase();
        return /travel|fly|depart|purchase/.test(text) || Object.keys(TRAVEL).some(country => text.includes(country.toLowerCase()));
    }

    function handleTravelClick(event) {
        if (Date.now() < confirmBypassUntil || !likelyTravelControl(event.target)) return;
        const state = ocState();
        if (state.kind !== 'danger') return;
        const proceed = window.confirm(`⚠ ${state.text}\n\nTravel anyway?`);
        if (!proceed) {
            event.preventDefault(); event.stopImmediatePropagation();
        } else {
            confirmBypassUntil = Date.now() + 3000;
        }
    }

    async function ensureData(force = false) {
        if (dataBusy) return;
        dataBusy = true;
        dataError = '';
        try {
            const tasks = [getYata(force)];
            if (getKey()) tasks.push(refreshOc(force));
            await Promise.all(tasks);
        } catch (error) {
            dataError = String(error?.message || error);
            console.error(APP, error);
        } finally {
            dataBusy = false;
            lastSignature = '';
            render();
        }
    }

    function signature() {
        return JSON.stringify({
            href: location.href,
            minute: Math.floor(Date.now() / 60000),
            minimized: localStorage.getItem(STORAGE.minimized),
            yata: readJson(STORAGE.yata)?.updated || 0,
            oc: readJson(STORAGE.oc)?.updated || 0,
            targets: (readJson(STORAGE.targetLogs, [])).length,
            profile: getProfile()
        });
    }

    function tick() {
        const minute = Math.floor(Date.now() / 60000);
        const current = signature();
        if (current !== lastSignature && !root().contains(document.activeElement)) {
            lastSignature = current;
            lastRenderedMinute = minute;
            render();
        }
        const yataStale = !cacheIsFresh(STORAGE.yata, YATA_TTL);
        const ocStale = getKey() && !cacheIsFresh(STORAGE.oc, OC_TTL);
        if (isTravelContext() && (yataStale || ocStale) && !dataBusy) ensureData(false);
        if (minute !== lastRenderedMinute) updatePageWarning();
    }

    document.addEventListener('pointermove', event => {
        const panel = document.getElementById(ROOT_ID);
        if (!panel) return;
        if (dragState && event.pointerId === dragState.pointerId) {
            event.preventDefault();
            const left = Math.max(0, Math.min(window.innerWidth - 80, dragState.left + event.clientX - dragState.x));
            const top = Math.max(0, Math.min(window.innerHeight - 50, dragState.top + event.clientY - dragState.y));
            panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.right = 'auto';
        }
        if (resizeState && event.pointerId === resizeState.pointerId) {
            event.preventDefault();
            panel.style.width = Math.max(280, Math.min(window.innerWidth - 20, resizeState.width + event.clientX - resizeState.x)) + 'px';
            const height = Math.max(240, Math.min(window.innerHeight - 20, resizeState.height + event.clientY - resizeState.y));
            panel.style.height = height + 'px';
            panel.style.maxHeight = height + 'px';
        }
    }, { passive: false });

    document.addEventListener('pointerup', event => {
        const panel = document.getElementById(ROOT_ID);
        if (panel && dragState && event.pointerId === dragState.pointerId) {
            writeJson(STORAGE.position, { left: panel.offsetLeft, top: panel.offsetTop });
            dragState = null;
        }
        if (panel && resizeState && event.pointerId === resizeState.pointerId) {
            writeJson(STORAGE.size, { width: panel.offsetWidth, height: panel.getBoundingClientRect().height });
            resizeState = null;
        }
    });

    document.addEventListener('click', handleTravelClick, true);

    function start() {
        if (!document.body || !document.head) { setTimeout(start, 100); return; }
        render();
        if (isTravelContext()) ensureData(false);
        lastSignature = signature();
        setInterval(tick, WATCH_MS);
    }

    start();
})();