✈️ Loot Tracker

A comprehensive Torn.com travel farming companion for TornPDA & Tampermonkey. Tracks plushie, flower, and prehistoric point sets, meteorite & fossil counts, live overseas stock via YATA, Xanax counter with faction totals, points value estimation, and Bits 'n' Bobs shop shortcuts. Features section toggles, draggable panel, colour-coded stock warnings, and an interactive hint carousel. Fork of "Points Museum" by SuperNovae [2637223] — extended and rebuilt by Phillip_J_Fry / OSDevscape.

2026/03/07のページです。最新版はこちら

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         ✈️ Loot Tracker
// @namespace    https://greatest.deepsurf.us/users/OSMays8338
// @version      4.0.2
// @description  A comprehensive Torn.com travel farming companion for TornPDA & Tampermonkey. Tracks plushie, flower, and prehistoric point sets, meteorite & fossil counts, live overseas stock via YATA, Xanax counter with faction totals, points value estimation, and Bits 'n' Bobs shop shortcuts. Features section toggles, draggable panel, colour-coded stock warnings, and an interactive hint carousel. Fork of "Points Museum" by SuperNovae [2637223] — extended and rebuilt by Phillip_J_Fry / OSDevscape.
// @author       Phillip_J_Fry (OSMays8338 on Greasyfork) — OSDevscape
// @license      MIT — Original base "Points Museum" by SuperNovae [2637223] (Victor the great on Greasyfork). All extensions, modifications, and new features by OSDevscape / Phillip_J_Fry.
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      yata.yt
// @connect      api.torn.com
// @run-at       document-end
// ==/UserScript==

(function () {
'use strict';

// =============================================================================
//  ✈️  PJFry's Loot Tracker  —  v4.0.0
// =============================================================================
//
//  Author  : Phillip_J_Fry  |  Torn: Phillip_J_Fry  |  GF: OSMays8338
//  Company : OSDevscape
//  Repo    : https://greatest.deepsurf.us/users/OSMays8338
//
//  Based on "✈️ Points Museum" (v3.5.0)
//  Original author : SuperNovae [2637223]  (Victor the great on Greasyfork)
//  Original thread : https://www.torn.com/forums.php#/p=threads&f=67&t=16530836
//  Original script : https://greatest.deepsurf.us/en/scripts/559534-points-museum
//
//  What's new in this fork (v4.0.0):
//    - Full TornPDA compatibility — GM shim layer, localStorage persistence
//    - Draggable floating toggle with snap-to-edge behaviour
//    - Prehistoric Points section (Quartz, Chalcedony, Basalt, Quartzite,
//      Chert, Obsidian) — 25 pts/set
//    - Special items section (Meteorite Fragment, Patagonian Fossil)
//    - Xanax tracker with personal count, carry limit & faction armoury total
//    - Bits 'n' Bobs shop shortcut for Sheep, Teddy Bear & Kitten plushies
//    - Live abroad stock via YATA with colour-coded thresholds
//    - Section visibility toggles in settings menu
//    - Manual refresh button in panel header
//    - Interactive 5-slide hint carousel (retrigger from settings)
//    - Column header tooltips explaining Own / Abroad columns
//    - Points market value in summary bar
//    - Robust timeout handling — no more stuck "UPDATING..."
//    - Gold & Green theme
//
//  License : MIT
//  Original base "Points Museum" copyright SuperNovae [2637223]
//  All extensions and modifications copyright OSDevscape / Phillip_J_Fry
//
// =============================================================================

/* ================= STORAGE (localStorage fallback — never touches window.GM) ================= */
// TornPDA seals window.GM as non-configurable. We NEVER assign to window.GM_*.
// Instead use localStorage directly for persistence, and route XHR through
// TornPDA's native GM_xmlhttpRequest (already in scope via @grant) with fetch fallback.

const _store = {
    get(key, def) {
        try { const v = localStorage.getItem('pjfry_' + key); return v === null ? def : JSON.parse(v); }
        catch { return def; }
    },
    set(key, val) {
        try { localStorage.setItem('pjfry_' + key, JSON.stringify(val)); } catch {}
    }
};

// Cross-platform GM storage — works on TornPDA and Tampermonkey
// Neither platform allows redeclaring GM_getValue/GM_setValue, so we
// wrap them once here and use _gmGet/_gmSet throughout.
const _gmGet = typeof GM_getValue === 'function' ? GM_getValue : (k, d) => _store.get(k, d);
const _gmSet = typeof GM_setValue === 'function' ? GM_setValue : (k, v) => _store.set(k, v);

// Style injection — plain DOM, no GM_addStyle
function GM_addStyle(css) {
    const s = document.createElement('style');
    s.textContent = css;
    (document.head || document.documentElement).appendChild(s);
}

function withTimeout(promise, ms, fallback) {
    return Promise.race([
        promise,
        new Promise(resolve => setTimeout(() => resolve(fallback), ms))
    ]);
}

// External JSON fetch — uses TornPDA's GM_xmlhttpRequest if available, else fetch
function gmJSON(url, timeoutMs = 10000) {
    const req = new Promise(resolve => {
        // GM_xmlhttpRequest is injected by TornPDA into scope (not on window)
        if (typeof GM_xmlhttpRequest === 'function') {
            GM_xmlhttpRequest({
                method: 'GET', url,
                timeout: timeoutMs,
                onload:    r => { try { resolve(JSON.parse(r.responseText)); } catch { resolve({}); } },
                onerror:   () => resolve({}),
                ontimeout: () => resolve({})
            });
        } else {
            fetch(url).then(r => r.json()).then(resolve).catch(() => resolve({}));
        }
    });
    return withTimeout(req, timeoutMs, {});
}

/* ================= CONFIG ================= */
const PANEL_ID     = 'travel_mini_hud';
const TOGGLE_ID    = 'travel_mini_toggle';
const API_PANEL_ID = 'travel_api_panel';
const POLL = 45000;
const PRE_PTS = 25, FLO_PTS = 10, PLU_PTS = 10, MET_PTS = 15, FOS_PTS = 20;

const POINTS_ENDPOINT = 'https://api.torn.com/v2/market/pointsmarket';
let currentPointsPrice = 0;
let pointsPriceCache = { time: 0, price: 0, history: [] };
const POINTS_CACHE_DURATION = 300000;
const POINTS_HISTORY_SIZE   = 5;

const PLUSHIE_THRESHOLD = 2000;
const FLOWER_THRESHOLD  = 5000;

const XANAX_ITEM_ID     = 206;
const XANAX_NAME        = 'Xanax';
const XANAX_STORAGE_KEY = 'xanax_personal_count';
const XANAX_CARRY_KEY   = 'xanax_carry_limit';

// Hint bubble storage key
const HINTS_SHOWN_KEY = 'hints_shown';

// Section visibility settings
const SECTION_KEYS = {
    prehistoric: 'show_prehistoric',
    plushies:    'show_plushies',
    flowers:     'show_flowers',
    special:     'show_special',
    xanax:       'show_xanax',
};

const itemImg = id => `https://www.torn.com/images/items/${id}/large.png`;

// BoB shop item IDs — stock scraped from shops page (no API available)
const BOB_ITEM_IDS = new Set([186, 187, 215]); // Sheep, Teddy Bear, Kitten

/* ================= LOCATION MAP ================= */
const LOCATIONS = {
    "Mexico":         { flag: "🇲🇽", label: "Mexico" },
    "Hawaii":         { flag: "🏝️",  label: "Hawaii" },
    "South Africa":   { flag: "🇿🇦", label: "South Africa" },
    "Japan":          { flag: "🇯🇵", label: "Japan" },
    "China":          { flag: "🇨🇳", label: "China" },
    "Argentina":      { flag: "🇦🇷", label: "Argentina" },
    "Switzerland":    { flag: "🇨🇭", label: "Switzerland" },
    "Canada":         { flag: "🇨🇦", label: "Canada" },
    "UK":             { flag: "🇬🇧", label: "United Kingdom" },
    "UAE":            { flag: "🇦🇪", label: "UAE" },
    "Cayman Islands": { flag: "🇰🇾", label: "Cayman Islands" },
    "BoB":            { flag: "🏪",  label: "Big Al's Bag of Bits (BoB)" },
};

/* ================= DATA ================= */
const SPECIAL_ITEMS = {
    "Meteorite Fragment": { id: 512, s: "Meteor", loc: "Argentina" },
    "Patagonian Fossil":  { id: 513, s: "Fossil", loc: "Argentina" }
};

const GROUPS = {
    Prehistoric: {
        pts: PRE_PTS,
        items: {
            "Quartz Point":    { id: 619, s: "Quartz",  loc: "Canada" },
            "Chalcedony Point":{ id: 620, s: "Chalced", loc: "Argentina" },
            "Basalt Point":    { id: 621, s: "Basalt",  loc: "Hawaii" },
            "Quartzite Point": { id: 622, s: "Quartzit",loc: "South Africa" },
            "Chert Point":     { id: 623, s: "Chert",   loc: "UK" },
            "Obsidian Point":  { id: 624, s: "Obsidian",loc: "Mexico" }
        }
    },
    Plushies: {
        pts: PLU_PTS,
        items: {
            "Sheep Plushie":      { id: 186, s: "Sheep",     loc: "BoB" },
            "Teddy Bear Plushie": { id: 187, s: "Teddy",     loc: "BoB" },
            "Kitten Plushie":     { id: 215, s: "Kitten",    loc: "BoB" },
            "Jaguar Plushie":     { id: 258, s: "Jaguar",    loc: "Mexico" },
            "Wolverine Plushie":  { id: 261, s: "Wolverine", loc: "Canada" },
            "Nessie Plushie":     { id: 266, s: "Nessie",    loc: "UK" },
            "Red Fox Plushie":    { id: 268, s: "Fox",       loc: "UK" },
            "Monkey Plushie":     { id: 269, s: "Monkey",    loc: "Argentina" },
            "Chamois Plushie":    { id: 273, s: "Chamois",   loc: "Switzerland" },
            "Panda Plushie":      { id: 274, s: "Panda",     loc: "China" },
            "Lion Plushie":       { id: 281, s: "Lion",      loc: "South Africa" },
            "Camel Plushie":      { id: 384, s: "Camel",     loc: "UAE" },
            "Stingray Plushie":   { id: 618, s: "Stingray",  loc: "Cayman Islands" }
        }
    },
    Flowers: {
        pts: FLO_PTS,
        items: {
            "Dahlia":            { id: 260, s: "Dahlia",    loc: "Mexico" },
            "Orchid":            { id: 264, s: "Orchid",    loc: "Hawaii" },
            "African Violet":    { id: 282, s: "Violet",    loc: "South Africa" },
            "Cherry Blossom":    { id: 277, s: "Blossoms",  loc: "Japan" },
            "Peony":             { id: 276, s: "Peony",     loc: "China" },
            "Ceibo Flower":      { id: 271, s: "Ceibo",     loc: "Argentina" },
            "Edelweiss":         { id: 272, s: "Edelweiss", loc: "Switzerland" },
            "Crocus":            { id: 263, s: "Crocus",    loc: "Canada" },
            "Heather":           { id: 267, s: "Heather",   loc: "UK" },
            "Tribulus Omanense": { id: 385, s: "Tribulus",  loc: "UAE" },
            "Banana Orchid":     { id: 617, s: "B. Orchid", loc: "Cayman Islands" }
        }
    }
};

/* ================= STYLES ================= */
GM_addStyle(`
@keyframes fadeSlide {
    from { opacity:0; transform:translateX(20px) scale(0.95); }
    to   { opacity:1; transform:translateX(0) scale(1); }
}
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
@keyframes gentlePulse { 0%,100%{opacity:.8;} 50%{opacity:1;} }
@keyframes subtleFloat { 0%,100%{transform:translateY(0);} 50%{transform:translateY(-2px);} }
@keyframes highlightPulse { 0%,100%{box-shadow:0 0 0 0 rgba(0,255,0,.4);} 50%{box-shadow:0 0 0 3px rgba(0,255,0,0);} }
@keyframes warningPulse  { 0%,100%{box-shadow:0 0 0 0 rgba(255,165,0,.4);} 50%{box-shadow:0 0 0 3px rgba(255,165,0,0);} }
@keyframes dangerPulse   { 0%,100%{box-shadow:0 0 0 0 rgba(255,0,0,.4);} 50%{box-shadow:0 0 0 3px rgba(255,0,0,0);} }
@keyframes xanPopIn {
    from { opacity:0; transform:scale(0.88) translateY(6px); }
    to   { opacity:1; transform:scale(1) translateY(0); }
}

/* ── API Panel ── */
#${API_PANEL_ID} {
    position:fixed; top:50%; left:50%; transform:translate(-50%,-50%);
    width:320px;
    background:linear-gradient(145deg,rgba(0,0,0,0.98),rgba(10,15,25,0.97));
    color:#e0f0ff; font:12px 'Segoe UI',sans-serif;
    border:1px solid rgba(76,175,80,0.35); border-radius:12px;
    z-index:1000001;
    box-shadow:inset 0 0 30px rgba(255,215,0,0.1),0 10px 40px rgba(0,0,0,0.7),0 0 50px rgba(76,175,80,0.35);
    backdrop-filter:blur(10px); overflow:hidden; animation:fadeSlide 0.4s ease-out;
}
#${API_PANEL_ID} .api-header {
    padding:16px; font-weight:600;
    background:linear-gradient(90deg,rgba(255,215,0,0.2),transparent);
    color:#FFD700; border-bottom:1px solid rgba(76,175,80,0.35);
    font-size:13px; letter-spacing:0.5px; text-transform:uppercase;
    display:flex; align-items:center; gap:8px;
}
#${API_PANEL_ID} .api-content { padding:16px; }
#${API_PANEL_ID} .api-input-group { margin-bottom:16px; }
#${API_PANEL_ID} .api-label { display:block; margin-bottom:6px; color:#8BC34A; font-weight:600; font-size:11px; }
#${API_PANEL_ID} .api-input {
    width:100%; padding:10px 12px; box-sizing:border-box;
    background:rgba(30,40,55,0.8); border:1px solid rgba(255,215,0,0.3);
    border-radius:6px; color:#fff; font-size:12px; font-family:'Consolas',monospace;
    transition:all 0.2s ease;
}
#${API_PANEL_ID} .api-input:focus {
    outline:none; border-color:#FFD700;
    box-shadow:0 0 0 2px rgba(255,215,0,0.2); background:rgba(40,50,65,0.9);
}
#${API_PANEL_ID} .api-note {
    background:rgba(255,165,0,0.1); border-left:3px solid rgba(255,165,0,0.6);
    padding:10px 12px; margin:12px 0; border-radius:4px;
    font-size:10.5px; color:#ffcc88; line-height:1.4;
}
#${API_PANEL_ID} .api-note strong { color:#ffaa00; font-weight:700; }
#${API_PANEL_ID} .api-buttons { display:flex; gap:10px; margin-top:20px; }
#${API_PANEL_ID} .api-button {
    flex:1; padding:10px;
    background:linear-gradient(145deg,#3a5233,#253822);
    border:1px solid rgba(76,175,80,0.4); border-radius:6px;
    color:#8BC34A; font-weight:600; font-size:11px;
    cursor:pointer; text-align:center;
}
#${API_PANEL_ID} .api-button.primary {
    background:linear-gradient(145deg,#b39c1a,#8a7a0d);
    border-color:rgba(255,215,0,0.6); color:#fff;
}
.api-backdrop {
    position:fixed; top:0; left:0; right:0; bottom:0;
    background:rgba(0,0,0,0.7); backdrop-filter:blur(3px);
    z-index:1000000; animation:fadeIn 0.3s ease;
}

/* ── Toggle ── */
#${TOGGLE_ID} {
    position:fixed; width:44px; height:44px;
    background:linear-gradient(145deg,#3d3b1e,#252a0d);
    border:1px solid rgba(255,215,0,0.3); border-radius:8px;
    color:#FFD700; font-size:22px;
    display:flex; align-items:center; justify-content:center;
    cursor:grab; z-index:1000000;
    box-shadow:0 2px 8px rgba(0,0,0,0.3),inset 0 1px 0 rgba(255,255,255,0.1);
    user-select:none; backdrop-filter:blur(4px);
    font-weight:600; text-shadow:0 0 6px rgba(255,215,0,0.5);
    touch-action:none;
}
#${TOGGLE_ID}:active { cursor:grabbing; }

/* ── Panel ── */
#${PANEL_ID} {
    position:fixed; width:0; height:auto; max-height:55vh;
    background:linear-gradient(145deg,rgba(15,20,30,0.98),rgba(8,12,20,0.97));
    color:#e0f0ff; font:11px 'Segoe UI',sans-serif;
    border:1px solid rgba(255,215,0,0.2); border-radius:8px;
    z-index:999999; display:flex; flex-direction:column;
    box-shadow:inset 0 0 20px rgba(255,215,0,0.05),0 4px 20px rgba(0,0,0,0.5);
    backdrop-filter:blur(8px); overflow:hidden;
    transition:width 0.25s ease,opacity 0.2s ease;
    opacity:0; border-right:none;
}
#${PANEL_ID}.open {
    width:260px; opacity:1;
    border-right:1px solid rgba(255,215,0,0.2); border-radius:8px;
}
#${PANEL_ID} .h {
    padding:7px 10px; font-weight:600;
    background:linear-gradient(90deg,rgba(255,215,0,0.15),transparent);
    color:#FFD700; border-bottom:1px solid rgba(255,215,0,0.15);
    display:flex; align-items:center; gap:5px;
    font-size:10px; letter-spacing:0.5px; text-transform:uppercase;
}
#${PANEL_ID} .h::before {
    content:'✈'; font-size:10px; opacity:0.8;
    animation:subtleFloat 3s ease-in-out infinite;
}
#${PANEL_ID} .h-refresh {
    margin-left:auto; background:none; border:none;
    color:rgba(255,215,0,0.5); font-size:14px;
    cursor:pointer; padding:0 2px; line-height:1;
}
#${PANEL_ID} .h-refresh:hover { color:#FFD700; }
#${PANEL_ID} .s {
    padding:6px 10px; background:rgba(46,125,50,0.2);
    font-weight:700; color:#8BC34A; text-align:center;
    border-bottom:1px solid rgba(76,175,80,0.1); font-size:10px;
}
#${PANEL_ID} .b {
    overflow-y:auto; overflow-x:hidden; flex:1;
    scrollbar-width:thin;
    scrollbar-color:rgba(255,215,0,0.5) rgba(25,35,50,0.2);
}
#${PANEL_ID} .b::-webkit-scrollbar { width:3px; }
#${PANEL_ID} .b::-webkit-scrollbar-track { background:rgba(25,35,50,0.2); }
#${PANEL_ID} .b::-webkit-scrollbar-thumb { background:rgba(255,215,0,0.5); border-radius:2px; }
#${PANEL_ID} .a {
    background:rgba(255,75,75,0.1); border-left:2px solid rgba(255,100,100,0.6);
    margin:3px 8px; padding:5px 8px 5px 20px;
    font-weight:600; border-radius:3px; color:#ff8888;
    position:relative; font-size:9.5px; line-height:1.3;
}
#${PANEL_ID} .a::before {
    content:'!'; position:absolute; left:5px; top:50%;
    transform:translateY(-50%); font-size:9px; font-weight:900; opacity:0.8;
}
#${PANEL_ID} .t {
    padding:5px 10px; background:rgba(76,175,80,0.08); color:#8BC34A;
    font-weight:600; border-top:1px solid rgba(76,175,80,0.1);
    border-bottom:1px solid rgba(76,175,80,0.05);
    font-size:9.5px; letter-spacing:0.3px; text-transform:uppercase;
    display:flex; align-items:center; justify-content:space-between;
}
#${PANEL_ID} .t-sets { color:#FFD700; font-weight:700; font-size:9px; opacity:0.9; }

/* ── Rows ── */
#${PANEL_ID} .r {
    padding:3px 8px;
    display:grid; grid-template-columns:32px 32px 34px 1fr;
    gap:6px; align-items:center;
    min-height:36px;
    border-bottom:1px solid rgba(255,255,255,0.02);
    transition:background 0.15s ease;
}
#${PANEL_ID} .r:hover { background:rgba(255,215,0,0.05) !important; }
#${PANEL_ID} .r:nth-child(even) { background:rgba(30,40,55,0.1); }
#${PANEL_ID} .r .item-img {
    width:30px; height:30px; object-fit:contain;
    border-radius:2px; background:rgba(255,255,255,0.05);
    border:1px solid rgba(255,255,255,0.1);
    display:block; transition:transform 0.2s ease;
}
#${PANEL_ID} .r:hover .item-img { transform:scale(1.15); }
#${PANEL_ID} .r .item-fallback {
    display:none; width:30px; height:30px;
    font-size:7px; font-weight:700; text-align:center; line-height:30px;
    border-radius:2px; border:1px solid rgba(255,255,255,0.1);
    background:rgba(255,255,255,0.05); color:#c0e0ff;
    font-family:'Consolas',monospace;
}
#${PANEL_ID} .r .col-local {
    color:#7fff7f; background:rgba(127,255,127,0.08);
    font-weight:700; text-align:center;
    border:1px solid rgba(127,255,127,0.1);
    font-family:'Consolas',monospace;
    padding:2px 4px; border-radius:2px; font-size:10px;
}
#${PANEL_ID} .r .col-abroad {
    font-family:'Consolas',monospace; text-align:center; font-weight:700;
    padding:2px 4px; border-radius:2px; border:1px solid; font-size:10px;
    transition:all 0.3s ease;
}
#${PANEL_ID} .r .col-abroad.status-green {
    color:#00ff00 !important; background:rgba(0,255,0,0.12) !important;
    border-color:rgba(0,255,0,0.3) !important;
    animation:highlightPulse 2s ease-in-out infinite;
    text-shadow:0 0 4px rgba(0,255,0,0.5);
}
#${PANEL_ID} .r .col-abroad.status-orange {
    color:#ffa500 !important; background:rgba(255,165,0,0.12) !important;
    border-color:rgba(255,165,0,0.3) !important;
    animation:warningPulse 2s ease-in-out infinite;
    text-shadow:0 0 4px rgba(255,165,0,0.5);
}
#${PANEL_ID} .r .col-abroad.status-red {
    color:#ff0000 !important; background:rgba(255,0,0,0.12) !important;
    border-color:rgba(255,0,0,0.3) !important;
    animation:dangerPulse 2s ease-in-out infinite;
    text-shadow:0 0 4px rgba(255,0,0,0.5);
}
#${PANEL_ID} .r .col-abroad:not([class*="status-"]) {
    color:#ffa0a0; background:rgba(255,160,160,0.08);
    border-color:rgba(255,160,160,0.12);
}
#${PANEL_ID} .r .col-flag {
    display:flex; align-items:center; justify-content:center;
    font-size:15px; line-height:1;
}
#${PANEL_ID} .r .col-bob-btn {
    display:flex; align-items:center; justify-content:center;
    font-size:8.5px; font-weight:700; text-decoration:none;
    color:#FFD700; background:rgba(255,215,0,0.1);
    border:1px solid rgba(255,215,0,0.35); border-radius:4px;
    padding:2px 4px; cursor:pointer; white-space:nowrap;
    transition:all 0.15s ease;
}
#${PANEL_ID} .r .col-bob-btn:hover {
    background:rgba(255,215,0,0.25); border-color:rgba(255,215,0,0.7);
    color:#fff; transform:scale(1.05);
}
#${PANEL_ID} .loading { animation:gentlePulse 1.5s ease-in-out infinite; }
#${PANEL_ID} .xan-count-display {
    color:#7fff7f; background:rgba(127,255,127,0.08);
    font-weight:700; text-align:center;
    border:1px solid rgba(127,255,127,0.1);
    font-family:'Consolas',monospace;
    padding:2px 4px; border-radius:2px; font-size:10px;
    cursor:pointer; user-select:none; display:block; line-height:1.6;
}
#${PANEL_ID} .r#xan-row { cursor:pointer; }
#${PANEL_ID} .col-price {
    font-family:'Consolas',monospace; text-align:center; font-weight:700; font-size:9px;
    padding:2px 4px; border-radius:2px;
    color:#FFD700; background:rgba(255,215,0,0.08);
    border:1px solid rgba(255,215,0,0.2);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}

/* ── Xanax Popup ── */
.xan-popup {
    position:fixed;
    background:linear-gradient(145deg,rgba(12,17,28,0.99),rgba(6,10,18,0.98));
    border:1px solid rgba(255,215,0,0.35); border-radius:10px;
    z-index:1000005; overflow:hidden;
    box-shadow:0 10px 40px rgba(0,0,0,0.8),inset 0 0 20px rgba(255,215,0,0.04);
    backdrop-filter:blur(14px);
    animation:xanPopIn 0.22s cubic-bezier(0.34,1.56,0.64,1);
    min-width:248px; color:#e0f0ff; font:11px 'Segoe UI',sans-serif;
}
.xan-popup-header {
    padding:11px 14px; font-weight:700;
    background:linear-gradient(90deg,rgba(255,215,0,0.18),transparent);
    color:#FFD700; border-bottom:1px solid rgba(255,215,0,0.18);
    font-size:12px; letter-spacing:0.6px; text-transform:uppercase;
    display:flex; align-items:center; gap:7px;
}
.xan-popup-body { padding:14px 14px 12px; }
.xan-popup-row { display:flex; align-items:center; gap:8px; margin-bottom:11px; }
.xan-popup-label {
    color:#8BC34A; font-weight:600; font-size:10px;
    width:44px; flex-shrink:0; text-align:right; letter-spacing:0.3px;
}
.xan-popup-num {
    font-family:'Consolas',monospace; font-size:13px; font-weight:700;
    text-align:center; width:66px; padding:5px 4px; box-sizing:border-box;
    background:rgba(22,30,45,0.95); border:1px solid rgba(255,215,0,0.28);
    border-radius:5px; color:#fff; outline:none; -moz-appearance:textfield;
}
.xan-popup-num::-webkit-inner-spin-button,
.xan-popup-num::-webkit-outer-spin-button { -webkit-appearance:none; }
.xan-popup-num:focus {
    border-color:#FFD700; box-shadow:0 0 0 2px rgba(255,215,0,0.18);
    background:rgba(32,44,62,0.98);
}
.xan-stepper {
    padding:5px 10px; border-radius:5px; flex-shrink:0;
    border:1px solid rgba(255,215,0,0.35);
    background:linear-gradient(145deg,#36360f,#222208);
    color:#FFD700; font-weight:800; font-size:15px;
    cursor:pointer; line-height:1; user-select:none;
}
.xan-stepper:active { transform:scale(0.9); }
.xan-section-divider { height:1px; background:rgba(255,215,0,0.1); margin:2px 0 12px; border:none; }
.xan-add-btn {
    flex-shrink:0; padding:6px 10px; border-radius:5px;
    background:linear-gradient(145deg,#3a5233,#253822);
    border:1px solid rgba(76,175,80,0.4);
    color:#8BC34A; font-weight:700; font-size:10px;
    cursor:pointer; white-space:nowrap;
}
.xan-add-btn:active { transform:scale(0.95); }
.xan-popup-save {
    width:100%; padding:9px 0; margin-top:4px; display:block;
    background:linear-gradient(145deg,rgba(255,215,0,0.14),rgba(255,215,0,0.07));
    border:1px solid rgba(255,215,0,0.3); border-radius:6px;
    color:#FFD700; font-weight:700; font-size:11px; letter-spacing:0.5px; cursor:pointer;
}
.xan-light-backdrop {
    position:fixed; top:0; left:0; right:0; bottom:0;
    z-index:1000004; background:transparent;
}

/* ── Settings popup ── */
.settings-row {
    display:flex; align-items:center; gap:10px;
    padding:6px 2px; cursor:pointer;
    border-bottom:1px solid rgba(255,215,0,0.06);
}
.settings-row:last-of-type { border-bottom:none; }
.settings-chk {
    width:16px; height:16px; cursor:pointer; accent-color:#FFD700;
    flex-shrink:0;
}
.settings-label {
    font-size:10px; color:#e0f0ff; font-weight:600;
}
.settings-row:hover .settings-label { color:#FFD700; }


/* ── Menu items ── */
.pjfry-menu-item {
    padding:8px 12px; font-size:11px; color:#c0e0ff; cursor:pointer;
}
.pjfry-menu-item:hover { background:rgba(255,215,0,0.15); color:#fff; }

/* ── Hint carousel bubble ── */
.tt-hint {
    position:fixed; z-index:1000010; pointer-events:auto;
    background:linear-gradient(145deg,rgba(10,14,24,0.99),rgba(6,9,16,0.98));
    border:1px solid rgba(255,215,0,0.45); border-radius:10px;
    width:240px; max-width:calc(100vw - 20px);
    font:11px 'Segoe UI',sans-serif; color:#e0f0ff;
    box-shadow:0 8px 32px rgba(0,0,0,0.8),0 0 16px rgba(255,215,0,0.12);
}
.hint-header {
    display:flex; align-items:center; justify-content:space-between;
    padding:10px 12px 6px;
    border-bottom:1px solid rgba(255,215,0,0.12);
}
.tt-title {
    font-size:11px; font-weight:700; color:#FFD700; flex:1;
}
.hint-close {
    background:none; border:none; color:rgba(255,215,0,0.5);
    cursor:pointer; font-size:12px; padding:0 0 0 8px; line-height:1;
}
.hint-close:hover { color:#FFD700; }
.hint-body {
    padding:10px 12px; font-size:10px; line-height:1.6;
    color:#c8e0f8; white-space:pre-wrap;
    min-height:44px;
}
.hint-footer {
    display:flex; align-items:center; justify-content:space-between;
    padding:6px 10px 10px;
    border-top:1px solid rgba(255,215,0,0.08);
}
.hint-dots { display:flex; gap:5px; align-items:center; }
.hint-dot {
    width:6px; height:6px; border-radius:50%;
    background:rgba(255,215,0,0.2); cursor:pointer;
    transition:background 0.2s, transform 0.2s;
}
.hint-dot.active { background:#FFD700; transform:scale(1.3); }
.hint-dot:hover  { background:rgba(255,215,0,0.6); }
.hint-nav { display:flex; align-items:center; gap:6px; }
.hint-btn {
    background:rgba(255,215,0,0.1); border:1px solid rgba(255,215,0,0.3);
    border-radius:5px; color:#FFD700; cursor:pointer;
    font-size:14px; font-weight:700; padding:1px 8px; line-height:1.4;
    transition:all 0.15s;
}
.hint-btn:hover:not([disabled]) { background:rgba(255,215,0,0.25); }
.hint-btn[disabled] { opacity:0.25; cursor:default; }
.hint-count { font-size:9px; color:rgba(255,215,0,0.5); min-width:28px; text-align:center; }
@keyframes hintIn {
    from { opacity:0; transform:scale(0.9) translateY(6px); }
    to   { opacity:1; transform:scale(1) translateY(0); }
}

/* ── Column header row ── */
#${PANEL_ID} .col-header {
    display:grid; grid-template-columns:32px 32px 34px 1fr;
    gap:6px; padding:3px 8px;
    background:rgba(255,215,0,0.04);
    border-bottom:1px solid rgba(255,215,0,0.08);
}
#${PANEL_ID} .col-header span {
    font:600 8px 'Segoe UI',sans-serif;
    color:rgba(255,215,0,0.5); text-align:center;
    text-transform:uppercase; letter-spacing:0.4px;
    cursor:help; user-select:none;
    position:relative;
}
/* column header tooltips handled via JS carousel */


/* ── Tooltip ── */
.points-tooltip { position:relative; cursor:help; }
.points-tooltip::after {
    content:attr(data-tooltip);
    position:absolute; bottom:100%; left:50%; transform:translateX(-50%);
    background:rgba(20,25,40,0.95); color:#8BC34A;
    padding:8px 12px; border-radius:6px; font-size:10px; white-space:pre-line;
    border:1px solid rgba(255,215,0,0.4); opacity:0; visibility:hidden;
    transition:opacity 0.2s; z-index:100000; pointer-events:none;
    min-width:200px; text-align:center;
}
.points-tooltip:hover::after { opacity:1; visibility:visible; }
`);

/* ================= API PANEL ================= */
function createApiPanel() {
    if (_gmGet('tornAPIKey', '')) return;
    const backdrop = document.createElement('div');
    backdrop.className = 'api-backdrop';
    const apiPanel = document.createElement('div');
    apiPanel.id = API_PANEL_ID;
    apiPanel.innerHTML = `
        <div class="api-header">🔑 API KEY REQUIRED</div>
        <div class="api-content">
            <div class="api-input-group">
                <label class="api-label" for="torn-api-key">Torn API Key:</label>
                <input type="text" id="torn-api-key" class="api-input"
                       placeholder="Enter your limited API key..." maxlength="16">
            </div>
            <div class="api-note">
                <strong>⚠️ SECURITY NOTE:</strong><br>
                Use a <strong>LIMITED API KEY</strong> with only <strong>DISPLAY</strong> access.<br>
                This script only needs to read your displayed items.<br>
                <em>Create at: <strong>Torn.com → Settings → API</strong></em>
            </div>
            <div class="api-buttons">
                <button class="api-button" id="save-api-key">Save API Key</button>
                <button class="api-button primary" id="skip-api">Skip (Limited)</button>
            </div>
        </div>`;
    document.body.appendChild(backdrop);
    document.body.appendChild(apiPanel);

    const saveBtn  = apiPanel.querySelector('#save-api-key');
    const skipBtn  = apiPanel.querySelector('#skip-api');
    const apiInput = apiPanel.querySelector('#torn-api-key');
    setTimeout(() => apiInput.focus(), 100);

    saveBtn.onclick = () => {
        const k = apiInput.value.trim();
        if (!k) { apiInput.style.borderColor = '#ff4444'; setTimeout(() => { apiInput.style.borderColor = ''; }, 1000); return; }
        if (k.length !== 16) { alert('Torn API keys are exactly 16 characters.'); return; }
        _gmSet('tornAPIKey', k);
        backdrop.remove(); apiPanel.remove();
        showNotification('API key saved! Starting tracker...');
        setTimeout(initializeTracker, 1000);
    };
    skipBtn.onclick = () => {
        backdrop.remove(); apiPanel.remove();
        showNotification('Limited mode. Long-press toggle to add key later.');
        initializeTracker();
    };
    apiInput.addEventListener('keypress', e => { if (e.key === 'Enter') saveBtn.click(); });
    backdrop.onclick = e => { if (e.target === backdrop) { backdrop.remove(); apiPanel.remove(); initializeTracker(); } };
}

/* ================= NOTIFICATION ================= */
function showNotification(msg) {
    const n = document.createElement('div');
    n.style.cssText = `position:fixed;top:20px;left:50%;transform:translateX(-50%);
        background:linear-gradient(145deg,#3a5233,#253822);color:#8BC34A;
        padding:12px 20px;border-radius:8px;border:1px solid rgba(255,215,0,0.4);
        box-shadow:0 4px 20px rgba(0,0,0,0.6);z-index:1000002;
        font:600 12px 'Segoe UI',sans-serif;backdrop-filter:blur(8px);
        text-align:center;max-width:300px;`;
    n.textContent = msg;
    document.body.appendChild(n);
    setTimeout(() => {
        n.style.opacity = '0'; n.style.transition = 'opacity 0.3s';
        setTimeout(() => n.remove(), 300);
    }, 3000);
}

/* ================= CREATE ELEMENTS ================= */
function createMainElements() {
    const toggle = document.createElement('div');
    toggle.id = TOGGLE_ID;
    toggle.innerHTML = '✈';
    toggle.title = 'Travel Tracker';

    const panel = document.createElement('div');
    panel.id = PANEL_ID;
    panel.innerHTML = `
        <div class="h">TRAVEL TRACKER <button class="h-refresh" id="tt-refresh" title="Refresh now">⟳</button></div>
        <div class="s">LOADING...</div>
        <div class="b"></div>`;

    document.body.appendChild(toggle);
    document.body.appendChild(panel);
    return { toggle, panel };
}

/* ================= API MENU ================= */
function showApiManagementMenu() {
    document.getElementById('api-management-menu')?.remove();
    const menu = document.createElement('div');
    menu.id = 'api-management-menu';
    menu.style.cssText = `position:fixed;
        background:linear-gradient(145deg,rgba(20,25,40,0.98),rgba(10,15,25,0.97));
        border:1px solid rgba(255,215,0,0.3);border-radius:8px;padding:8px 0;
        min-width:180px;z-index:1000003;backdrop-filter:blur(10px);
        box-shadow:0 4px 20px rgba(0,0,0,0.7);font:11px 'Segoe UI',sans-serif;`;

    const currentKey = _gmGet('tornAPIKey', '');
    menu.innerHTML = `
        <div style="padding:8px 12px;font-size:11px;color:#8BC34A;border-bottom:1px solid rgba(76,175,80,0.2);">
            API Key Management
        </div>
        ${currentKey
            ? `<div class="pjfry-menu-item" data-action="view">View Current Key</div>
               <div class="pjfry-menu-item" data-action="change">Change API Key</div>
               <div class="pjfry-menu-item" data-action="remove">Remove API Key</div>`
            : `<div class="pjfry-menu-item" data-action="add">Add API Key</div>`}
        <div class="pjfry-menu-item" data-action="help">API Key Help</div>
        <div class="pjfry-menu-item" data-action="settings">⚙️ Section Settings</div>`;

    const rect = document.getElementById(TOGGLE_ID).getBoundingClientRect();
    menu.style.top   = (rect.bottom + 5) + 'px';
    menu.style.right = (window.innerWidth - rect.right) + 'px';
    document.body.appendChild(menu);

    menu.addEventListener('click', e => {
        const item = e.target.closest('.pjfry-menu-item');
        if (!item) return;
        menu.remove();
        switch (item.dataset.action) {
            case 'view':   alert(`API Key: ${currentKey}\nEnds with: ...${currentKey.slice(-4)}`); break;
            case 'change':
            case 'add':    _gmSet('tornAPIKey', ''); createApiPanel(); break;
            case 'remove':
                if (confirm('Remove API key?')) { _gmSet('tornAPIKey', ''); showNotification('Key removed. Refresh page.'); }
                break;
            case 'settings': showSettingsPopup(); break;
            case 'help':
                alert('API KEY SETUP:\n1. Torn.com → Settings → API\n2. Create New Key\n3. Select ONLY "Display"\n4. Copy & paste 16-char key here');
                break;
        }
    });

    setTimeout(() => {
        const close = e => {
            if (!menu.contains(e.target) && e.target.id !== TOGGLE_ID) { menu.remove(); document.removeEventListener('click', close); }
        };
        document.addEventListener('click', close);
    }, 100);
}

/* ================= GLOBALS ================= */
let toggle, panel, sum, body, isPanelOpen = false, pollTimer = null;

/* ================= INIT ================= */
function initializeTracker() {
    const els = createMainElements();
    toggle = els.toggle; panel = els.panel;
    sum = panel.querySelector('.s'); body = panel.querySelector('.b');
    setupToggleFunctionality();
    setupDrag();

    // Refresh button
    panel.querySelector('#tt-refresh').addEventListener('click', () => {
        if (pollTimer) clearTimeout(pollTimer);
        mainLoop();
    });

    // Show hint bubbles on first load
    if (!_gmGet(HINTS_SHOWN_KEY, false)) {
        setTimeout(showHints, 1800);
    }

    const apiKey = _gmGet('tornAPIKey', '');
    if (apiKey) {
        mainLoop();
    } else {
        sum.textContent = 'API KEY REQUIRED';
        body.innerHTML = `<div style="padding:20px 12px;text-align:center;color:#ff8888;font-size:10px;">
            ⚠️ No API key.<br><br>
            <span style="color:#8BC34A;font-size:9px;">Long-press the toggle<br>to add your API key.</span>
        </div>`;
    }
}

/* ================= TOGGLE ================= */
function positionPanel() {
    const tr = toggle.getBoundingClientRect();
    panel.style.top = tr.top + 'px'; panel.style.bottom = 'auto';
    if (toggle._snapSide === 'left') {
        panel.style.left = (tr.right + 6) + 'px'; panel.style.right = 'auto';
    } else {
        panel.style.right = (window.innerWidth - tr.left + 6) + 'px'; panel.style.left = 'auto';
    }
}

function setupToggleFunctionality() {
    toggle.addEventListener('click', () => {
        if (toggle._dragged) { toggle._dragged = false; return; }
        isPanelOpen = !isPanelOpen;
        panel.classList.toggle('open', isPanelOpen);
        toggle.style.color = isPanelOpen ? '#FFEB3B' : '#FFD700';
        toggle.innerHTML   = isPanelOpen ? '×' : '✈';
        positionPanel();
    });

    let longPressTimer = null;
    toggle.addEventListener('touchstart', e => {
        longPressTimer = setTimeout(() => { longPressTimer = null; showApiManagementMenu(); }, 600);
    }, { passive: true });
    toggle.addEventListener('touchend',   () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } });
    toggle.addEventListener('touchmove',  () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } });
    toggle.addEventListener('contextmenu', e => { e.preventDefault(); showApiManagementMenu(); });
}

/* ================= DRAG ================= */
function setupDrag() {
    const SZ = 36, GAP = 6;

    function snapTo(side, top) {
        toggle._snapSide = side;
        toggle.style.top = top + 'px'; toggle.style.bottom = 'auto';
        toggle.style.left  = side === 'left'  ? GAP + 'px' : 'auto';
        toggle.style.right = side === 'right' ? GAP + 'px' : 'auto';
        positionPanel();
    }

    try {
        const saved = _gmGet('togglePos', null);
        if (saved) {
            const { top, side } = saved;
            snapTo(side || 'right', Math.max(GAP, Math.min(top, window.innerHeight - SZ - GAP)));
        } else { snapTo('right', 350); }
    } catch { snapTo('right', 350); }

    let active = false, startX, startY, startLeft, startTop, dragReady = false;

    function beginDrag(cx, cy) {
        dragReady = true; toggle._dragged = false;
        startX = cx; startY = cy;
        const r = toggle.getBoundingClientRect();
        startLeft = r.left; startTop = r.top;
        toggle.style.opacity = '0.75'; toggle.style.transform = 'scale(1.1)';
    }
    function doMove(cx, cy) {
        if (!dragReady) return;
        const dx = cx - startX, dy = cy - startY;
        if (!toggle._dragged && Math.hypot(dx, dy) < 4) return;
        toggle._dragged = true; active = true;
        toggle.style.left   = Math.max(GAP, Math.min(startLeft + dx, window.innerWidth  - SZ - GAP)) + 'px';
        toggle.style.right  = 'auto';
        toggle.style.top    = Math.max(GAP, Math.min(startTop  + dy, window.innerHeight - SZ - GAP)) + 'px';
        toggle.style.bottom = 'auto';
        positionPanel();
    }
    function endDrag() {
        toggle.style.opacity = ''; toggle.style.transform = '';
        dragReady = false;
        if (!active) return; active = false;
        const r = toggle.getBoundingClientRect();
        const side = (r.left + SZ / 2) < window.innerWidth / 2 ? 'left' : 'right';
        snapTo(side, r.top);
        _gmSet('togglePos', { top: r.top, side });
    }

    toggle.addEventListener('touchstart', e => beginDrag(e.touches[0].clientX, e.touches[0].clientY), { passive: true });
    toggle.addEventListener('touchmove',  e => { e.preventDefault(); doMove(e.touches[0].clientX, e.touches[0].clientY); }, { passive: false });
    toggle.addEventListener('touchend',    () => endDrag());
    toggle.addEventListener('touchcancel', () => { dragReady = false; active = false; toggle.style.opacity = ''; toggle.style.transform = ''; });
    toggle.addEventListener('mousedown', e => { if (e.button === 0) beginDrag(e.clientX, e.clientY); });
    document.addEventListener('mousemove', e => doMove(e.clientX, e.clientY));
    document.addEventListener('mouseup',   () => endDrag());
}

/* ================= DATA FETCHING ================= */
async function localItems() {
    const key = _gmGet('tornAPIKey', '');
    if (!key) throw new Error('No API key configured');
    const r = await fetch(`https://api.torn.com/user/?selections=display&key=${key}`);
    const data = await r.json();
    if (data.error) throw new Error(data.error.error || 'API Error');
    const items = {};
    (data.display || []).forEach(item => { items[item.name] = (items[item.name] || 0) + item.quantity; });
    return items;
}

// Build reverse lookup: item ID -> item name
function buildIdToNameMap() {
    const map = {};
    for (const group of Object.values(GROUPS)) {
        for (const [name, data] of Object.entries(group.items)) {
            map[data.id] = name;
        }
    }
    map[XANAX_ITEM_ID] = XANAX_NAME;
    return map;
}

async function abroadItems() {
    const data = await gmJSON('https://yata.yt/api/v1/travel/export/');
    const idToName = buildIdToNameMap();
    const map = {};
    if (!data) return map;

    // YATA v1: array of country objects  [ { country, items: [{id,quantity,cost}] }, ... ]
    // YATA alt: object keyed by country   { MX: { items: [...] }, ... }
    // YATA alt2: { stocks: { MX: { items: [...] } } }
    let entries = [];
    if (Array.isArray(data)) {
        entries = data;
    } else if (data.stocks && typeof data.stocks === 'object') {
        entries = Object.values(data.stocks);
    } else {
        // Try treating top-level values as country objects
        entries = Object.values(data).filter(v => v && typeof v === 'object' && v.items);
    }

    for (const country of entries) {
        const items = Array.isArray(country?.items) ? country.items
                    : Array.isArray(country?.stocks) ? country.stocks
                    : [];
        for (const item of items) {
            const name = idToName[Number(item.id)];
            if (name) map[name] = (map[name] || 0) + Number(item.quantity || 0);
        }
    }
    return map;
}



async function factionXanax(apiKey) {
    try {
        const data = await gmJSON(`https://api.torn.com/v2/faction/items?key=${apiKey}`);
        if (!data.error) {
            const items = data.items || {};
            const direct = items[String(XANAX_ITEM_ID)];
            if (direct !== undefined) return direct.quantity || 0;
            let total = 0;
            Object.values(items).forEach(i => { if (i.name === XANAX_NAME || Number(i.id) === XANAX_ITEM_ID) total += (i.quantity || 0); });
            return total;
        }
        const d2 = await gmJSON(`https://api.torn.com/v1/faction/?selections=armoury&key=${apiKey}`);
        if (d2.error) return null;
        let total = 0;
        Object.values(d2.armoury || {}).forEach(i => {
            if (i.name === XANAX_NAME || Number(i.ID) === XANAX_ITEM_ID || Number(i.id) === XANAX_ITEM_ID)
                total += (i.quantity || i.qty || 0);
        });
        return total;
    } catch { return null; }
}

async function xanaxAbroadSA() {
    try {
        const data = await gmJSON('https://yata.yt/api/v1/travel/export/');
        const SA_COUNTRY_KEYS = ['sou', 'saf', 'zaf', 'za', 'south_africa', 'South Africa'];
        // Handle both array and object response formats
        const entries = Array.isArray(data) ? data
            : Array.isArray(data?.exports) ? data.exports
            : Object.entries(data?.stocks || {}).map(([k, v]) => ({ ...v, _key: k }));
        for (const country of entries) {
            const key = country._key || country.country || '';
            const isSA = SA_COUNTRY_KEYS.some(k => key.toLowerCase().includes(k.toLowerCase()));
            if (!isSA) continue;
            const items = country?.items || country?.stocks || [];
            const hit = items.find(i => Number(i.id) === XANAX_ITEM_ID);
            if (hit) return { qty: hit.quantity || 0, price: hit.cost || hit.price || 0 };
        }
    } catch(e) { console.error('xanaxAbroadSA error:', e); }
    return { qty: 0, price: 0 };
}

async function fetchPointsPrice(apiKey) {
    const now = Date.now();
    if (pointsPriceCache.time && now - pointsPriceCache.time < POINTS_CACHE_DURATION) return pointsPriceCache.price;
    try {
        const data = await gmJSON(`${POINTS_ENDPOINT}?key=${apiKey}`);
        if (data.pointsmarket) {
            const listings = Object.values(data.pointsmarket).filter(l => l.quantity > 0).map(l => l.cost).sort((a,b) => a-b);
            if (listings.length) {
                const top = listings.slice(0, Math.min(5, listings.length));
                const avg = Math.round(top.reduce((s,p) => s+p, 0) / top.length);
                pointsPriceCache.history.push(avg);
                if (pointsPriceCache.history.length > POINTS_HISTORY_SIZE) pointsPriceCache.history.shift();
                const stable = Math.round(pointsPriceCache.history.reduce((s,p) => s+p, 0) / pointsPriceCache.history.length);
                pointsPriceCache = { time: now, price: stable, history: pointsPriceCache.history };
                currentPointsPrice = stable;
                return stable;
            }
        }
    } catch(e) { console.error('Points price error:', e); }
    return currentPointsPrice || 0;
}

/* ================= LOGIC ================= */
function calcSet(inv, items) { const v = Object.keys(items).map(k => inv[k] || 0); return v.length ? Math.min(...v) : 0; }
function lowestTwo(inv, items, sets) {
    const counts = Object.entries(items).map(([name, data]) => ({
        code: data.s, location: LOCATIONS[data.loc]?.label || data.loc, count: (inv[name] || 0) - sets
    })).sort((a,b) => a.count - b.count).slice(0,2).filter(i => i.count < 5);
    if (!counts.length) return null;
    const parts = counts.map(i => `${i.code} → ${i.location}`);
    return parts.length === 2 ? `Need ${parts[0]} & ${parts[1]}` : `Need ${parts[0]}`;
}
function getSortedItems(inv, items, sets) {
    return Object.entries(items).map(([name, data]) => ({ name, data, remaining: (inv[name] || 0) - sets }))
        .sort((a, b) => a.remaining - b.remaining);
}
function getStatusClass(name, abroad) {
    if (abroad === 0) return 'status-red';
    if (name.includes('Plushie')) return abroad >= PLUSHIE_THRESHOLD ? 'status-green' : 'status-orange';
    if (Object.keys(GROUPS.Flowers.items).includes(name)) return abroad >= FLOWER_THRESHOLD ? 'status-green' : 'status-orange';
    return '';
}



/* ================= HINT CAROUSEL ================= */
const HINT_SLIDES = [
    {
        title: '✈️ Travel Tracker',
        body:  'Tap the ✈ button to open or close the tracker panel.',
        anchor: () => document.getElementById(TOGGLE_ID)
    },
    {
        title: '⚙️ Settings & API',
        body:  'Long-press the button (or right-click on desktop) to open the settings menu — manage your API key, toggle sections, and more.',
        anchor: () => document.getElementById(TOGGLE_ID)
    },
    {
        title: '🟩 Own column',
        body:  'Shows how many of each item you have after subtracting completed sets. The lowest number is your current bottleneck.',
        anchor: () => document.querySelector('.col-header')
    },
    {
        title: '🌍 Abroad column',
        body:  '🌍 Overseas items → live YATA data\n🟢 Plushie \u22652000 / Flower \u22655000\n🟠 Below threshold  🔴 Zero\n\n🏪 Sheep, Teddy Bear, Kitten →\nTap the 🏪 BoB button to go\ndirectly to Bits n Bobs shop.',
        anchor: () => document.querySelector('.col-header')
    },
    {
        title: '🧪 Xanax row',
        body:  'Tap the Xanax row to open the counter. Set your personal count and carry limit, or tap "+ Add Limit" to add a carry run worth.',
        anchor: () => document.getElementById('xan-row')
    },
];

let _hintIndex = 0;
let _hintEl = null;
let _hintBackdrop = null;

function showHints() {
    _hintIndex = 0;
    _gmSet(HINTS_SHOWN_KEY, true);
    _renderHint();
}

function _renderHint() {
    dismissHints();
    const slide = HINT_SLIDES[_hintIndex];
    if (!slide) return;

    // Backdrop (click outside to dismiss)
    _hintBackdrop = document.createElement('div');
    _hintBackdrop.id = 'hint-backdrop';
    _hintBackdrop.style.cssText = 'position:fixed;inset:0;z-index:1000008;';
    _hintBackdrop.addEventListener('click', dismissHints);

    // Bubble
    _hintEl = document.createElement('div');
    _hintEl.className = 'tt-hint';
    _hintEl.id = 'hint-bubble';

    const total = HINT_SLIDES.length;
    const dots  = Array.from({length: total}, (_, i) =>
        `<span class="hint-dot${i === _hintIndex ? ' active' : ''}" data-i="${i}"></span>`
    ).join('');

    _hintEl.innerHTML = `
        <div class="hint-header">
            <span class="tt-title">${slide.title}</span>
            <button class="hint-close" title="Dismiss">✕</button>
        </div>
        <div class="hint-body">${slide.body.replace(/\n/g, '<br>')}</div>
        <div class="hint-footer">
            <div class="hint-dots">${dots}</div>
            <div class="hint-nav">
                <button class="hint-btn" id="hint-prev" ${_hintIndex === 0 ? 'disabled' : ''}>‹</button>
                <span class="hint-count">${_hintIndex + 1} / ${total}</span>
                <button class="hint-btn" id="hint-next">${_hintIndex === total - 1 ? '✓ Done' : '›'}</button>
            </div>
        </div>`;

    document.body.appendChild(_hintBackdrop);
    document.body.appendChild(_hintEl);

    // Position bubble near anchor or toggle
    const anchor = (slide.anchor && slide.anchor()) || document.getElementById(TOGGLE_ID);
    _positionHint(_hintEl, anchor);

    // Wire controls
    _hintEl.querySelector('.hint-close').addEventListener('click', e => { e.stopPropagation(); dismissHints(); });
    _hintEl.querySelector('#hint-next').addEventListener('click', e => {
        e.stopPropagation();
        if (_hintIndex >= HINT_SLIDES.length - 1) { dismissHints(); return; }
        _hintIndex++; _renderHint();
    });
    _hintEl.querySelector('#hint-prev').addEventListener('click', e => {
        e.stopPropagation();
        if (_hintIndex <= 0) return;
        _hintIndex--; _renderHint();
    });
    _hintEl.querySelectorAll('.hint-dot').forEach(dot => {
        dot.addEventListener('click', e => { e.stopPropagation(); _hintIndex = parseInt(dot.dataset.i); _renderHint(); });
    });
    _hintEl.addEventListener('click', e => e.stopPropagation());

    // Fade in
    _hintEl.style.opacity = '0';
    requestAnimationFrame(() => { _hintEl.style.transition = 'opacity 0.25s'; _hintEl.style.opacity = '1'; });
}

function _positionHint(el, anchor) {
    el.style.visibility = 'hidden';
    requestAnimationFrame(() => {
        const W = el.offsetWidth  || 240;
        const H = el.offsetHeight || 140;
        const vw = window.innerWidth, vh = window.innerHeight;
        let top, left;

        // Fall back to toggle if anchor missing or has zero size (panel closed)
        const toggleEl = document.getElementById(TOGGLE_ID);
        const a = (anchor && anchor.offsetWidth > 0) ? anchor : toggleEl;

        if (a) {
            const r = a.getBoundingClientRect();
            top  = r.bottom + 10;
            left = r.left + r.width / 2 - W / 2;
            if (top + H > vh - 10) top = r.top - H - 10;
            if (left + W > vw - 10) left = vw - W - 10;
        } else {
            top  = vh / 2 - H / 2;
            left = vw / 2 - W / 2;
        }

        el.style.top  = Math.max(8, top)  + 'px';
        el.style.left = Math.max(8, left) + 'px';
        el.style.visibility = 'visible';
    });
}

function dismissHints() {
    _hintEl?._remove?.call(_hintEl) || _hintEl?.remove();
    _hintBackdrop?.remove();
    _hintEl = null; _hintBackdrop = null;
}

/* ================= SETTINGS POPUP ================= */
function showSettingsPopup() {
    document.getElementById('settings-popup')?.remove();
    document.getElementById('settings-backdrop')?.remove();

    const backdrop = document.createElement('div');
    backdrop.id = 'settings-backdrop';
    backdrop.className = 'xan-light-backdrop';

    const sections = [
        { key: 'prehistoric', label: '🪨 Prehistoric Points' },
        { key: 'plushies',    label: '🧸 Plushies' },
        { key: 'flowers',     label: '🌸 Flowers' },
        { key: 'special',     label: '☄️ Special (Meteor/Fossil)' },
        { key: 'xanax',       label: '🧪 Xanax' },
    ];

    const checksHtml = sections.map(s => {
        const checked = _gmGet(SECTION_KEYS[s.key], true) !== false ? 'checked' : '';
        return `
        <label class="settings-row">
            <input type="checkbox" class="settings-chk" data-key="${s.key}" ${checked}>
            <span class="settings-label">${s.label}</span>
        </label>`;
    }).join('');

    const popup = document.createElement('div');
    popup.id = 'settings-popup';
    popup.className = 'xan-popup';
    popup.style.minWidth = '230px';
    popup.innerHTML = `
        <div class="xan-popup-header">⚙️ Section Visibility</div>
        <div class="xan-popup-body" style="padding:12px 14px 10px;">
            <div style="font-size:9px;color:#8BC34A;margin-bottom:10px;opacity:0.8;">
                Toggle sections to hide/show them
            </div>
            ${checksHtml}
            <button class="xan-popup-save" id="settings-close" style="margin-top:12px;">✓ Save &amp; Close</button>
            <button class="xan-popup-save" id="settings-hints" style="margin-top:6px;background:linear-gradient(145deg,rgba(255,215,0,0.1),rgba(255,215,0,0.05));font-size:10px;">💡 Show Hints Again</button>
        </div>`;

    document.body.appendChild(backdrop);
    document.body.appendChild(popup);

    // Position near toggle
    const toggleEl = document.getElementById(TOGGLE_ID);
    const tr = toggleEl.getBoundingClientRect();
    const PW = 244, PH = 260;
    const top  = Math.max(6, Math.min(tr.top, window.innerHeight - PH - 6));
    const left = toggle._snapSide === 'left'
        ? tr.right + 8
        : Math.max(6, tr.left - PW - 8);
    popup.style.top  = top  + 'px';
    popup.style.left = left + 'px';

    function saveAndClose() {
        popup.querySelectorAll('.settings-chk').forEach(chk => {
            _gmSet(SECTION_KEYS[chk.dataset.key], chk.checked);
        });
        backdrop.remove(); popup.remove();
        render(); // re-render immediately
    }

    popup.querySelector('#settings-close').addEventListener('click', saveAndClose);
    popup.querySelector('#settings-hints').addEventListener('click', () => {
        saveAndClose();
        _gmSet(HINTS_SHOWN_KEY, false);
        setTimeout(showHints, 400);
    });
    backdrop.addEventListener('click', saveAndClose);
    popup.addEventListener('click', e => e.stopPropagation());
    popup.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === 'Escape') saveAndClose(); });
}

/* ================= XANAX POPUP ================= */
function showXanaxPopup(anchorEl) {
    document.getElementById('xan-popup')?.remove();
    document.getElementById('xan-popup-backdrop')?.remove();

    const backdrop = document.createElement('div');
    backdrop.id = 'xan-popup-backdrop'; backdrop.className = 'xan-light-backdrop';

    const popup = document.createElement('div');
    popup.id = 'xan-popup'; popup.className = 'xan-popup';
    popup.innerHTML = `
        <div class="xan-popup-header">🧪 Xanax Count</div>
        <div class="xan-popup-body">
            <div class="xan-popup-row">
                <span class="xan-popup-label">Count:</span>
                <button class="xan-stepper" id="xan-dec">−</button>
                <input type="number" class="xan-popup-num" id="xan-count-val" value="${_gmGet(XANAX_STORAGE_KEY, 0)}" min="0">
                <button class="xan-stepper" id="xan-inc">+</button>
            </div>
            <div class="xan-section-divider"></div>
            <div class="xan-popup-row">
                <span class="xan-popup-label">Carry:</span>
                <input type="number" class="xan-popup-num" id="xan-carry-val" value="${_gmGet(XANAX_CARRY_KEY, 0)}" min="0">
                <button class="xan-add-btn" id="xan-add-carry">+ Add Limit</button>
            </div>
            <button class="xan-popup-save" id="xan-save">✓ Save &amp; Close</button>
        </div>`;

    document.body.appendChild(backdrop); document.body.appendChild(popup);

    const panelEl = document.getElementById(PANEL_ID);
    const pr = panelEl ? panelEl.getBoundingClientRect() : anchorEl.getBoundingClientRect();
    const ar = anchorEl.getBoundingClientRect();
    const PW = 252, PH = 182;
    const top  = Math.max(6, Math.min(ar.top - 8, window.innerHeight - PH - 6));
    const tryL = pr.left - PW - 8, tryR = pr.right + 8;
    const left = tryL >= 6 ? tryL : (tryR + PW <= window.innerWidth - 6 ? tryR : Math.max(6, tryL));
    popup.style.top = top + 'px'; popup.style.left = left + 'px';

    const ci = popup.querySelector('#xan-count-val'), cri = popup.querySelector('#xan-carry-val');

    function saveAndClose() {
        _gmSet(XANAX_STORAGE_KEY, Math.max(0, parseInt(ci.value) || 0));
        _gmSet(XANAX_CARRY_KEY,   Math.max(0, parseInt(cri.value) || 0));
        const badge = document.getElementById('xan-count-display');
        if (badge) badge.textContent = _gmGet(XANAX_STORAGE_KEY, 0);
        backdrop.remove(); popup.remove();
    }

    popup.querySelector('#xan-inc').onclick = () => { ci.value = (parseInt(ci.value) || 0) + 1; };
    popup.querySelector('#xan-dec').onclick = () => { ci.value = Math.max(0, (parseInt(ci.value) || 0) - 1); };
    popup.querySelector('#xan-add-carry').onclick = () => {
        ci.value = (parseInt(ci.value) || 0) + (parseInt(cri.value) || 0);
        ci.style.borderColor = '#8BC34A'; setTimeout(() => { ci.style.borderColor = ''; }, 600);
    };
    popup.querySelector('#xan-save').onclick = saveAndClose;
    backdrop.onclick = saveAndClose;
    popup.addEventListener('click', e => e.stopPropagation());
    ci.addEventListener('focus',  () => ci.select());
    cri.addEventListener('focus', () => cri.select());
    popup.addEventListener('keydown', e => { if (e.key === 'Enter') saveAndClose(); });
    setTimeout(() => ci.select(), 60);
}

/* ================= RENDER ================= */
async function render() {
    try {
        sum.classList.add('loading'); sum.textContent = 'UPDATING...';
        const apiKey = _gmGet('tornAPIKey', '');

        console.log('[TT] Starting fetch...');
        const t0 = Date.now();

        const [inventory, abroad, xanFaction, xanSA] = await Promise.all([
            withTimeout(localItems(),    12000, {}).then(r => { console.log('[TT] localItems done', Date.now()-t0, 'ms', r); return r; }),
            withTimeout(abroadItems(),   12000, {}).then(r => { console.log('[TT] abroadItems done', Date.now()-t0, 'ms', Object.keys(r).length, 'items'); return r; }),
            apiKey ? withTimeout(factionXanax(apiKey).catch(() => null), 10000, null).then(r => { console.log('[TT] factionXanax done', Date.now()-t0, 'ms', r); return r; }) : Promise.resolve(null),
            withTimeout(xanaxAbroadSA(), 10000, { qty: 0, price: 0 }).then(r => { console.log('[TT] xanaxAbroadSA done', Date.now()-t0, 'ms', r); return r; })
        ]);
        console.log('[TT] All fetches complete', Date.now()-t0, 'ms');
        const xanPersonal = _gmGet(XANAX_STORAGE_KEY, 0);
        let totalSets = 0, totalPoints = 0, html = '';
        console.log('[TT] Building HTML, inventory keys:', Object.keys(inventory).length, 'abroad keys:', Object.keys(abroad).length);

        // Column header row with tooltips
        html += `<div class="col-header">
            <span></span>
            <span data-tip="Your display items minus\ncompleted sets.\nLowest item = your bottleneck.">Own</span>
            <span data-tip="Live overseas stock from YATA.\n🟢 Plushie ≥2000 / Flower ≥5000\n🟠 Below threshold\n🔴 Zero stock\n\n🏪 Sheep/Teddy/Kitten show a\nshortcut to Bits 'n' Bobs shop.">Abroad</span>
            <span></span>
        </div>`;

        for (const [groupName, group] of Object.entries(GROUPS)) {
            const sectionKey = groupName.toLowerCase();
            if (_gmGet(SECTION_KEYS[sectionKey], true) === false) continue;
            const sets = calcSet(inventory, group.items);
            const warn = lowestTwo(inventory, group.items, sets);
            totalSets += sets; totalPoints += sets * group.pts;
            html += `<div class="t">${groupName.toUpperCase()} <span class="t-sets">• ${sets} Sets</span></div>`;
            if (warn) html += `<div class="a">${warn}</div>`;
            for (const { name, data, remaining } of getSortedItems(inventory, group.items, sets)) {
                const isBob = BOB_ITEM_IDS.has(data.id);
                const loc   = LOCATIONS[data.loc] || { flag: '❓', label: data.loc };
                if (isBob) {
                    html += `
                    <div class="r">
                        <img class="item-img" src="${itemImg(data.id)}" alt="${data.s}" title="${name}"
                             onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
                        <span class="item-fallback" title="${name}">${data.s}</span>
                        <span class="col-local" title="You have ${remaining} extra after completing ${sets} sets">${remaining}</span>
                        <a class="col-bob-btn" href="https://www.torn.com/shops.php?step=bitsnbobs" title="Open Bits 'n' Bobs shop">🏪 BoB</a>
                        <span class="col-flag" title="Bits 'n' Bobs shop">🏪</span>
                    </div>`;
                } else {
                    const ac = abroad[name] || 0;
                    html += `
                    <div class="r">
                        <img class="item-img" src="${itemImg(data.id)}" alt="${data.s}" title="${name}"
                             onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
                        <span class="item-fallback" title="${name}">${data.s}</span>
                        <span class="col-local" title="You have ${remaining} extra after completing ${sets} sets">${remaining}</span>
                        <span class="col-abroad ${getStatusClass(name, ac)}" title="Overseas stock (YATA): ${ac}">${ac}</span>
                        <span class="col-flag" title="${loc.label}">${loc.flag}</span>
                    </div>`;
                }
            }
        }

        /* ── Special items: Meteorite + Fossil ── */
        if (_gmGet(SECTION_KEYS.special, true) !== false) {
        const meteorite = inventory["Meteorite Fragment"] || 0;
        const fossil     = inventory["Patagonian Fossil"]  || 0;
        totalPoints += meteorite * MET_PTS + fossil * FOS_PTS;
        const metAbroad = abroad["Meteorite Fragment"] || 0;
        const fosAbroad = abroad["Patagonian Fossil"]  || 0;
        html += `<div class="t">SPECIAL <span class="t-sets">• ${meteorite + fossil} Items</span></div>`;
        html += `
        <div class="r">
            <img class="item-img" src="${itemImg(512)}" alt="Meteor" title="Meteorite Fragment"
                 onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
            <span class="item-fallback">MTR</span>
            <span class="col-local">${meteorite}</span>
            <span class="col-abroad ${getStatusClass('Meteorite Fragment', metAbroad)}">${metAbroad}</span>
            <span class="col-flag" title="Argentina">🇦🇷</span>
        </div>
        <div class="r">
            <img class="item-img" src="${itemImg(513)}" alt="Fossil" title="Patagonian Fossil"
                 onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
            <span class="item-fallback">FSL</span>
            <span class="col-local">${fossil}</span>
            <span class="col-abroad ${getStatusClass('Patagonian Fossil', fosAbroad)}">${fosAbroad}</span>
            <span class="col-flag" title="Argentina">🇦🇷</span>
        </div>`;
        } // end special section

        if (_gmGet(SECTION_KEYS.xanax, true) !== false) {
        const fSuffix = xanFaction === null ? '' : ` <span class="t-sets">• F = ${xanFaction.toLocaleString()}</span>`;
        html += `<div class="t">XANAX${fSuffix}</div>
        <div class="r" id="xan-row" title="Click to edit Xanax count">
            <img class="item-img" src="${itemImg(XANAX_ITEM_ID)}" alt="XAN" title="Xanax"
                 onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">
            <span class="item-fallback">XAN</span>
            <span class="xan-count-display" id="xan-count-display">${xanPersonal}</span>
            <span class="col-abroad">${xanSA.qty}</span>
            <span class="col-price">${xanSA.price > 0 ? '$' + xanSA.price.toLocaleString() : '—'}</span>
        </div>`;
        } // end xanax section

        // ── Points value & summary bar — always runs ──
        let pvFmt = '';
        if (apiKey && totalPoints > 0) {
            const pp = await fetchPointsPrice(apiKey);
            if (pp > 0) {
                const pv = totalPoints * pp;
                pvFmt = pv >= 1000000 ? `$${(pv/1000000).toFixed(1)}M` : pv >= 1000 ? `$${Math.round(pv/1000)}k` : `$${pv}`;
                sum.classList.add('points-tooltip');
                sum.setAttribute('data-tooltip', `Points: ${totalPoints}\nPer point: $${pp.toLocaleString()}\nTotal: $${pv.toLocaleString()}`);
            }
        }

        sum.classList.remove('loading');
        sum.innerHTML = pvFmt
            ? `✈ ${totalSets} SETS • ${totalPoints} PTS • <span style="color:#7fff7f">${pvFmt}</span>`
            : `✈ ${totalSets} SETS • ${totalPoints} PTS`;
        if (!pvFmt) { sum.classList.remove('points-tooltip'); sum.removeAttribute('data-tooltip'); }

        body.innerHTML = html;
        body.scrollTop = 0;

        // Wire column header tooltip popovers
        body.querySelectorAll('.col-header span[data-tip]').forEach(el => {
            let tipEl = null;
            el.addEventListener('mouseenter', () => {
                tipEl = document.createElement('div');
                tipEl.style.cssText = `position:fixed;z-index:1000020;background:linear-gradient(145deg,rgba(10,14,24,0.99),rgba(6,9,16,0.98));border:1px solid rgba(255,215,0,0.45);border-radius:7px;padding:8px 11px;font:10px 'Segoe UI',sans-serif;color:#e0f0ff;white-space:pre-line;line-height:1.6;box-shadow:0 6px 20px rgba(0,0,0,0.7);pointer-events:none;max-width:200px;`;
                tipEl.textContent = el.dataset.tip;
                document.body.appendChild(tipEl);
                const r = el.getBoundingClientRect();
                const tw = tipEl.offsetWidth, th = tipEl.offsetHeight;
                let left = r.left + r.width/2 - tw/2;
                let top  = r.bottom + 6;
                if (top + th > window.innerHeight - 6) top = r.top - th - 6;
                if (left + tw > window.innerWidth - 6) left = window.innerWidth - tw - 6;
                tipEl.style.left = Math.max(6,left) + 'px';
                tipEl.style.top  = Math.max(6,top)  + 'px';
            });
            el.addEventListener('mouseleave', () => { tipEl?.remove(); tipEl = null; });
        });
        body.querySelector('#xan-row')?.addEventListener('click', () => showXanaxPopup(body.querySelector('#xan-row')));

    } catch (err) {
        sum.classList.remove('loading'); sum.textContent = 'ERROR';
        body.innerHTML = `<div style="padding:20px 12px;text-align:center;color:#ff8888;font-size:10px;">
            ⚠️ ${err.message}<br><br>
            <span style="color:#8BC34A;font-size:9px;">Check API key.<br>Long-press toggle to manage.</span>
        </div>`;
    }
}

/* ================= MAIN LOOP ================= */
async function mainLoop() {
    const apiKey = _gmGet('tornAPIKey', '');
    if (apiKey) fetchPointsPrice(apiKey).catch(() => {});
    await render();
    pollTimer = setTimeout(mainLoop, POLL);
}

/* ================= CLEANUP + START ================= */
function cleanupExistingInstance() {
    if (pollTimer) { clearTimeout(pollTimer); pollTimer = null; }
    [PANEL_ID, TOGGLE_ID, API_PANEL_ID, 'api-management-menu', 'xan-popup', 'xan-popup-backdrop']
        .forEach(id => document.getElementById(id)?.remove());
    document.querySelector('.api-backdrop')?.remove();
    document.querySelector('.xan-light-backdrop')?.remove();
}

if (document.getElementById(TOGGLE_ID)) return;
cleanupExistingInstance();

if (!_gmGet('tornAPIKey', '')) {
    setTimeout(createApiPanel, 1000);
} else {
    setTimeout(initializeTracker, 500);
}
})();