✈️ Points museum

Compact travel points tracker with item images, colour coding, flags, API key setup, points value estimation, dual bottleneck warnings, sorted items, Museum Day bonus, reorderable categories, and Xanax tracking.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         ✈️ Points museum
// @namespace    http://tampermonkey.net/
// @version      4.2.0
// @description  Compact travel points tracker with item images, colour coding, flags, API key setup, points value estimation, dual bottleneck warnings, sorted items, Museum Day bonus, reorderable categories, and Xanax tracking.
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function () {
'use strict';

/* ================= 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 CATEGORY_ORDER_KEY = 'travel_category_order';

// Default category order
const DEFAULT_CATEGORY_ORDER = ['Prehistoric', 'Flowers', 'Plushies', 'Special', 'Xanax'];

// Points Market Configuration
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; // 5 minutes
const POINTS_HISTORY_SIZE = 5; // Keep last 5 prices for averaging

// Colour coding thresholds
const PLUSHIE_THRESHOLD = 2000;
const FLOWER_THRESHOLD = 5000;
const XANAX_THRESHOLD_GREEN = 1500;
const XANAX_THRESHOLD_ORANGE = 950;

/* ================= DATA ================= */
const GROUPS = {
 Prehistoric:{
  pts:PRE_PTS,
  items:{
   "Quartz Point":{s:"Q",loc:"CA 🇨🇦",color:"#87CEEB",id:1504},
   "Chalcedony Point":{s:"CH",loc:"AR 🇦🇷",color:"#9ACD32",id:1503},
   "Basalt Point":{s:"B",loc:"HW 🏝️",color:"#4682B4",id:1502},
   "Quartzite Point":{s:"QZ",loc:"SA 🇿🇦",color:"#5F9EA0",id:1500},
   "Chert Point":{s:"CT",loc:"UK 🇬🇧",color:"#708090",id:1501},
   "Obsidian Point":{s:"O",loc:"MX 🇲🇽",color:"#00008B",id:1499}
  }
 },
 Flowers:{
  pts:FLO_PTS,
  items:{
   "Dahlia":{s:"DH",loc:"MX 🇲🇽",color:"#FF69B4",id:260},
   "Orchid":{s:"OR",loc:"HW 🏝️",color:"#DA70D6",id:264},
   "African Violet":{s:"V",loc:"SA 🇿🇦",color:"#EE82EE",id:282},
   "Cherry Blossom":{s:"CB",loc:"JP 🇯🇵",color:"#FFB6C1",id:277},
   "Peony":{s:"P",loc:"CN 🇨🇳",color:"#FF1493",id:276},
   "Ceibo Flower":{s:"CE",loc:"AR 🇦🇷",color:"#DC143C",id:271},
   "Edelweiss":{s:"E",loc:"CH 🇨🇭",color:"#F0FFF0",id:272},
   "Crocus":{s:"CR",loc:"CA 🇨🇦",color:"#9370DB",id:263},
   "Heather":{s:"H",loc:"UK 🇬🇧",color:"#DDA0DD",id:267},
   "Tribulus Omanense":{s:"T",loc:"AE 🇦🇪",color:"#FFD700",id:385},
   "Banana Orchid":{s:"BO",loc:"KY 🇰🇾",color:"#FFFF00",id:617}
  }
 },
 Plushies:{
  pts:PLU_PTS,
  items:{
   "Sheep Plushie":{s:"SH",loc:"B.B",color:"#FFFFFF",id:186},
   "Teddy Bear Plushie":{s:"TB",loc:"B.B",color:"#8B4513",id:187},
   "Kitten Plushie":{s:"KT",loc:"B.B",color:"#696969",id:215},
   "Jaguar Plushie":{s:"J",loc:"MX 🇲🇽",color:"#FF8C00",id:258},
   "Wolverine Plushie":{s:"W",loc:"CA 🇨🇦",color:"#A0522D",id:261},
   "Nessie Plushie":{s:"N",loc:"UK 🇬🇧",color:"#008080",id:266},
   "Red Fox Plushie":{s:"F",loc:"UK 🇬🇧",color:"#FF4500",id:268},
   "Monkey Plushie":{s:"M",loc:"AR 🇦🇷",color:"#D2691E",id:269},
   "Chamois Plushie":{s:"CM",loc:"CH 🇨🇭",color:"#DEB887",id:273},
   "Panda Plushie":{s:"PD",loc:"CN 🇨🇳",color:"#000000",id:274},
   "Lion Plushie":{s:"L",loc:"SA 🇿🇦",color:"#FFD700",id:281},
   "Camel Plushie":{s:"CA",loc:"AE 🇦🇪",color:"#F4A460",id:384},
   "Stingray Plushie":{s:"SR",loc:"KY 🇰🇾",color:"#2F4F4F",id:618}
  }
 }
};

// Xanax special tracking
const XANAX_ITEM = {
 name: "Xanax",
 id: 206,
 loc: "JP 🇯🇵",
 color: "#98FB98",
 s: "XAN"
};

// Helper function to get item image URL
function getItemImageUrl(itemId) {
    return `https://www.torn.com/images/items/${itemId}/medium.png`;
}

// Get saved category order or use default
function getCategoryOrder() {
    const saved = GM_getValue(CATEGORY_ORDER_KEY, DEFAULT_CATEGORY_ORDER);
    // Validate that all categories exist
    const validOrder = saved.filter(cat => cat === 'Prehistoric' || cat === 'Flowers' || cat === 'Plushies' || cat === 'Special' || cat === 'Xanax');
    // Add any missing categories at the end
    DEFAULT_CATEGORY_ORDER.forEach(cat => {
        if (!validOrder.includes(cat)) {
            validOrder.push(cat);
        }
    });
    return validOrder;
}

// Save category order
function saveCategoryOrder(order) {
    GM_setValue(CATEGORY_ORDER_KEY, order);
}

// Move category up in order
function moveCategoryUp(category) {
    const order = getCategoryOrder();
    const index = order.indexOf(category);
    if (index > 0) {
        [order[index-1], order[index]] = [order[index], order[index-1]];
        saveCategoryOrder(order);
        render(); // Re-render with new order
    }
}

// Move category down in order
function moveCategoryDown(category) {
    const order = getCategoryOrder();
    const index = order.indexOf(category);
    if (index < order.length - 1) {
        [order[index], order[index+1]] = [order[index+1], order[index]];
        saveCategoryOrder(order);
        render(); // Re-render with new order
    }
}

// Styles
GM_addStyle(`
/* API Key Panel */
#${API_PANEL_ID} {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 320px;
    background: linear-gradient(145deg, rgba(20, 25, 40, 0.98), rgba(10, 15, 25, 0.97));
    color: #e0f0ff;
    font: 12px 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
    border: 1px solid rgba(64, 156, 255, 0.3);
    border-radius: 12px;
    z-index: 1000001;
    box-shadow: 
        inset 0 0 30px rgba(64, 156, 255, 0.1),
        0 10px 40px rgba(0, 0, 0, 0.7),
        0 0 50px rgba(64, 156, 255, 0.2);
    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(64, 156, 255, 0.2), transparent);
    color: #64b4ff;
    border-bottom: 1px solid rgba(64, 156, 255, 0.2);
    font-size: 13px;
    letter-spacing: 0.5px;
    text-transform: uppercase;
    display: flex;
    align-items: center;
    gap: 8px;
}

#${API_PANEL_ID} .api-header::before {
    content: '🔑';
    font-size: 14px;
}

#${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: #88ccff;
    font-weight: 600;
    font-size: 11px;
}

#${API_PANEL_ID} .api-input {
    width: 100%;
    padding: 10px 12px;
    background: rgba(30, 40, 55, 0.8);
    border: 1px solid rgba(64, 156, 255, 0.3);
    border-radius: 6px;
    color: #ffffff;
    font-size: 12px;
    font-family: 'Consolas', 'Monaco', monospace;
    transition: all 0.2s ease;
}

#${API_PANEL_ID} .api-input:focus {
    outline: none;
    border-color: #64b4ff;
    box-shadow: 0 0 0 2px rgba(100, 180, 255, 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, #2a3b52, #152438);
    border: 1px solid rgba(64, 156, 255, 0.4);
    border-radius: 6px;
    color: #64b4ff;
    font-weight: 600;
    font-size: 11px;
    cursor: pointer;
    transition: all 0.2s ease;
    text-align: center;
}

#${API_PANEL_ID} .api-button:hover {
    background: linear-gradient(145deg, #3a4b62, #253448);
    border-color: rgba(100, 180, 255, 0.6);
    color: #88ccff;
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}

#${API_PANEL_ID} .api-button.primary {
    background: linear-gradient(145deg, #1a5ca3, #0d3d7a);
    border-color: rgba(64, 156, 255, 0.6);
    color: #ffffff;
}

#${API_PANEL_ID} .api-button.primary:hover {
    background: linear-gradient(145deg, #2a6cb3, #1d4d8a);
    border-color: rgba(100, 180, 255, 0.8);
}

/* API Panel Backdrop */
.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;
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

/* Animation keyframes */
@keyframes fadeSlide {
    from { opacity: 0; transform: translateX(20px) scale(0.95); }
    to { opacity: 1; transform: translateX(0) scale(1); }
}

@keyframes gentlePulse {
    0%, 100% { opacity: 0.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, 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, 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, 0.4); }
    50% { box-shadow: 0 0 0 3px rgba(255, 0, 0, 0); }
}

/* Points 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: #88ccff;
    padding: 8px 12px;
    border-radius: 6px;
    font-size: 10px;
    white-space: pre-line;
    border: 1px solid rgba(64, 156, 255, 0.4);
    opacity: 0;
    visibility: hidden;
    transition: opacity 0.2s;
    z-index: 100000;
    pointer-events: none;
    min-width: 200px;
    text-align: center;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(8px);
}

.points-tooltip:hover::after {
    opacity: 1;
    visibility: visible;
}

/* Toggle Button */
#${TOGGLE_ID} {
    position: fixed;
    right: 6px;
    top: 70px;
    width: 36px;
    height: 36px;
    background: linear-gradient(145deg, #1e2b3d, #0d1725);
    border: 1px solid rgba(64, 156, 255, 0.3);
    border-radius: 8px;
    color: #409cff;
    font-size: 18px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 1000000;
    box-shadow: 
        0 2px 8px rgba(0, 0, 0, 0.3),
        inset 0 1px 0 rgba(255, 255, 255, 0.1);
    transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
    user-select: none;
    backdrop-filter: blur(4px);
    font-weight: 600;
    text-shadow: 0 0 6px rgba(64, 156, 255, 0.5);
}

#${TOGGLE_ID}:hover {
    background: linear-gradient(145deg, #2a3b52, #152438);
    border-color: rgba(100, 180, 255, 0.5);
    color: #64b4ff;
    transform: scale(1.08);
    box-shadow: 
        0 4px 12px rgba(0, 0, 0, 0.4),
        0 0 20px rgba(64, 156, 255, 0.3),
        inset 0 1px 0 rgba(255, 255, 255, 0.15);
}

#${TOGGLE_ID}:active {
    transform: scale(0.95);
    transition: transform 0.1s;
}

/* Panel */
#${PANEL_ID} {
    position: fixed;
    right: 6px;
    top: 70px;
    width: 0;
    height: auto;
    max-height: 60vh;
    background: linear-gradient(145deg, rgba(15, 20, 30, 0.98), rgba(8, 12, 20, 0.97));
    color: #e0f0ff;
    font: 10px 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
    border: 1px solid rgba(64, 156, 255, 0.2);
    border-radius: 10px 0 0 10px;
    z-index: 999999;
    display: flex;
    flex-direction: column;
    box-shadow: 
        inset 0 0 20px rgba(64, 156, 255, 0.05),
        0 4px 20px rgba(0, 0, 0, 0.5);
    backdrop-filter: blur(8px);
    overflow: hidden;
    transition: 
        width 0.3s cubic-bezier(0.4, 0, 0.2, 1),
        opacity 0.2s ease;
    opacity: 0;
    transform: translateX(10px);
    border-right: none;
}

#${PANEL_ID}.open {
    width: 250px;
    opacity: 1;
    transform: translateX(0);
    border-right: 1px solid rgba(64, 156, 255, 0.2);
    border-radius: 10px;
    animation: fadeSlide 0.3s ease-out;
}

/* Grid background */
#${PANEL_ID}::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-image: 
        linear-gradient(rgba(64, 156, 255, 0.03) 1px, transparent 1px),
        linear-gradient(90deg, rgba(64, 156, 255, 0.03) 1px, transparent 1px);
    background-size: 20px 20px;
    background-position: 0 0;
    opacity: 0.3;
    z-index: 0;
}

/* Header */
#${PANEL_ID} .h {
    padding: 8px 12px;
    font-weight: 600;
    background: linear-gradient(90deg, rgba(64, 156, 255, 0.15), transparent);
    color: #64b4ff;
    border-bottom: 1px solid rgba(64, 156, 255, 0.15);
    display: flex;
    align-items: center;
    gap: 6px;
    position: relative;
    z-index: 1;
    font-size: 10px;
    letter-spacing: 0.5px;
    text-transform: uppercase;
}

#${PANEL_ID} .h::before {
    content: '✈';
    font-size: 12px;
    opacity: 0.8;
    animation: subtleFloat 3s ease-in-out infinite;
}

/* Summary section - main display */
#${PANEL_ID} .s {
    padding: 6px 12px;
    background: rgba(25, 35, 50, 0.7);
    font-weight: 700;
    color: #4dabff;
    text-align: center;
    border-bottom: 1px solid rgba(64, 156, 255, 0.1);
    position: relative;
    z-index: 1;
    font-size: 10px;
    backdrop-filter: blur(2px);
    transition: all 0.2s ease;
}

#${PANEL_ID} .s:hover {
    background: rgba(30, 40, 55, 0.8);
    color: #64b4ff;
}

/* Museum Day bonus line */
#${PANEL_ID} .museum-bonus {
    padding: 4px 12px;
    background: rgba(50, 40, 20, 0.7);
    font-weight: 700;
    color: #FFD700;
    text-align: center;
    border-bottom: 1px solid rgba(255, 215, 0, 0.2);
    border-top: 1px solid rgba(255, 215, 0, 0.1);
    position: relative;
    z-index: 1;
    font-size: 9px;
    backdrop-filter: blur(2px);
    letter-spacing: 0.3px;
    text-shadow: 0 0 5px rgba(255, 215, 0, 0.3);
}

#${PANEL_ID} .museum-bonus::before {
    content: '🏛️';
    margin-right: 4px;
    font-size: 10px;
}

#${PANEL_ID} .museum-bonus:hover {
    background: rgba(60, 50, 30, 0.8);
    color: #FFD700;
}

/* Body */
#${PANEL_ID} .b {
    overflow-y: auto;
    overflow-x: hidden;
    position: relative;
    z-index: 1;
    background: transparent;
    flex: 1;
    max-height: calc(60vh - 70px);
    scrollbar-width: thin;
    scrollbar-color: rgba(64, 156, 255, 0.5) rgba(25, 35, 50, 0.2);
}

#${PANEL_ID} .b::-webkit-scrollbar {
    width: 4px;
}

#${PANEL_ID} .b::-webkit-scrollbar-track {
    background: rgba(25, 35, 50, 0.2);
    border-radius: 2px;
}

#${PANEL_ID} .b::-webkit-scrollbar-thumb {
    background: rgba(64, 156, 255, 0.5);
    border-radius: 2px;
    border: none;
}

#${PANEL_ID} .b::-webkit-scrollbar-thumb:hover {
    background: rgba(100, 180, 255, 0.7);
}

/* Warning Alerts */
#${PANEL_ID} .a {
    background: rgba(255, 75, 75, 0.1);
    border-left: 2px solid rgba(255, 100, 100, 0.6);
    margin: 4px 8px;
    padding: 4px 8px 4px 20px;
    font-weight: 600;
    border-radius: 4px;
    color: #ff8888;
    position: relative;
    font-size: 9px;
    line-height: 1.2;
    transition: all 0.2s ease;
}

#${PANEL_ID} .a:hover {
    transform: translateX(2px);
    background: rgba(255, 75, 75, 0.15);
}

#${PANEL_ID} .a::before {
    content: '!';
    position: absolute;
    left: 6px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 10px;
    font-weight: 900;
    opacity: 0.8;
}

/* Category Titles with up/down buttons */
#${PANEL_ID} .t {
    padding: 5px 8px 5px 8px;
    background: rgba(64, 156, 255, 0.08);
    color: #88ccff;
    font-weight: 600;
    border-top: 1px solid rgba(64, 156, 255, 0.1);
    border-bottom: 1px solid rgba(64, 156, 255, 0.05);
    position: relative;
    font-size: 9px;
    letter-spacing: 0.3px;
    text-transform: uppercase;
    backdrop-filter: blur(2px);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

#${PANEL_ID} .t .category-controls {
    display: flex;
    gap: 4px;
}

#${PANEL_ID} .t .category-btn {
    width: 14px;
    height: 14px;
    background: rgba(64, 156, 255, 0.2);
    border: 1px solid rgba(64, 156, 255, 0.3);
    border-radius: 3px;
    color: #88ccff;
    font-size: 8px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.2s ease;
    user-select: none;
}

#${PANEL_ID} .t .category-btn:hover {
    background: rgba(64, 156, 255, 0.4);
    border-color: rgba(64, 156, 255, 0.6);
    color: #ffffff;
    transform: scale(1.1);
}

#${PANEL_ID} .t .category-btn:active {
    transform: scale(0.9);
}

#${PANEL_ID} .t .category-btn.up-btn::before {
    content: '▲';
}

#${PANEL_ID} .t .category-btn.down-btn::before {
    content: '▼';
}

/* Rows - Colour Coded Grid with Images */
#${PANEL_ID} .r {
    padding: 3px 12px;
    display: grid;
    grid-template-columns: 30px 35px 35px auto;
    gap: 6px;
    align-items: center;
    transition: all 0.15s ease;
    position: relative;
    font-size: 9.5px;
    min-height: 26px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.02);
}

#${PANEL_ID} .r:hover {
    background: rgba(64, 156, 255, 0.05);
}

#${PANEL_ID} .r:nth-child(even) {
    background: rgba(30, 40, 55, 0.1);
}

#${PANEL_ID} .r span {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 1px 4px;
    border-radius: 3px;
    transition: all 0.2s ease;
}

/* Item image styling */
#${PANEL_ID} .r .item-image {
    width: 20px;
    height: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: rgba(0, 0, 0, 0.2);
    border-radius: 3px;
    padding: 2px;
}

#${PANEL_ID} .r .item-image img {
    max-width: 18px;
    max-height: 18px;
    object-fit: contain;
}

/* Abroad items colour coding */
#${PANEL_ID} .r span:nth-child(3) {
    font-family: 'Consolas', 'Monaco', monospace;
    text-align: center;
    border: 1px solid;
    font-weight: 700;
    transition: all 0.3s ease;
}

/* Green status - Above threshold */
#${PANEL_ID} .r span.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);
}

/* Orange status - Below threshold */
#${PANEL_ID} .r span.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);
}

/* Red status - Zero */
#${PANEL_ID} .r span.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);
}

/* Xanax specific status */
#${PANEL_ID} .r span.status-xan-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 span.status-xan-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 span.status-xan-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);
}

/* Normal abroad count (no status) */
#${PANEL_ID} .r span:nth-child(3):not([class*="status-"]) {
    color: #ffa0a0;
    background: rgba(255, 160, 160, 0.08);
    border: 1px solid rgba(255, 160, 160, 0.1);
}

#${PANEL_ID} .r:hover span:nth-child(3):not([class*="status-"]) {
    background: rgba(255, 160, 160, 0.12);
    border-color: rgba(255, 160, 160, 0.2);
}

/* Local count - NOW SHOWING REMAINING */
#${PANEL_ID} .r span:nth-child(2) {
    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', 'Monaco', monospace;
}

#${PANEL_ID} .r:hover span:nth-child(2) {
    background: rgba(127, 255, 127, 0.12);
    border-color: rgba(127, 255, 127, 0.2);
}

/* Location with flag */
#${PANEL_ID} .r span:nth-child(4) {
    color: #88ccff;
    background: rgba(136, 204, 255, 0.08);
    text-align: center;
    font-weight: 600;
    font-size: 9px;
    border: 1px solid rgba(136, 204, 255, 0.1);
    padding: 1px 6px;
}

#${PANEL_ID} .r:hover span:nth-child(4) {
    background: rgba(136, 204, 255, 0.12);
    border-color: rgba(136, 204, 255, 0.2);
}

/* Loading state */
#${PANEL_ID} .loading {
    background: linear-gradient(90deg, transparent, rgba(64, 156, 255, 0.1), transparent);
    background-size: 200% 100%;
    animation: gentlePulse 1.5s ease-in-out infinite;
}

/* Compact layout for special items */
#${PANEL_ID} .special-row {
    grid-template-columns: 40px 35px 35px auto !important;
}

/* Xanax row styling */
#${PANEL_ID} .xanax-row {
    grid-template-columns: 40px 35px 35px auto !important;
}

/* Support Footer */
#${PANEL_ID} .support-footer {
    padding: 8px 12px;
    background: linear-gradient(145deg, rgba(30, 40, 55, 0.8), rgba(20, 30, 45, 0.9));
    border-top: 1px solid rgba(255, 215, 0, 0.3);
    text-align: center;
    font-size: 9px;
    color: #ffd700;
    margin-top: 4px;
    backdrop-filter: blur(4px);
    position: relative;
    z-index: 2;
}

#${PANEL_ID} .support-footer a {
    color: #ffd700;
    text-decoration: none;
    font-weight: 600;
    transition: all 0.2s ease;
    padding: 2px 6px;
    border-radius: 4px;
    background: rgba(255, 215, 0, 0.1);
    border: 1px solid rgba(255, 215, 0, 0.2);
}

#${PANEL_ID} .support-footer a:hover {
    background: rgba(255, 215, 0, 0.2);
    border-color: rgba(255, 215, 0, 0.4);
    text-shadow: 0 0 8px rgba(255, 215, 0, 0.5);
    transform: translateY(-1px);
}

#${PANEL_ID} .support-footer .heart {
    color: #ff6b6b;
    animation: subtleFloat 2s ease-in-out infinite;
    display: inline-block;
    margin: 0 4px;
}

/* Responsive */
@media (max-height: 600px) {
    #${PANEL_ID} {
        max-height: 70vh;
    }
    #${PANEL_ID} .b {
        max-height: calc(70vh - 70px);
    }
    #${PANEL_ID} .r {
        padding: 2px 10px;
        min-height: 22px;
        gap: 4px;
    }
    #${PANEL_ID} .support-footer {
        padding: 6px 10px;
        font-size: 8.5px;
    }
}

/* Smooth scroll sync */
.scrolling-smooth {
    transition: top 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
}

/* Reset order button */
.reset-order-btn {
    background: rgba(255, 215, 0, 0.15);
    border: 1px solid rgba(255, 215, 0, 0.3);
    border-radius: 3px;
    color: #ffd700;
    font-size: 8px;
    padding: 2px 6px;
    margin-left: 6px;
    cursor: pointer;
    transition: all 0.2s ease;
    text-transform: none;
}

.reset-order-btn:hover {
    background: rgba(255, 215, 0, 0.3);
    border-color: rgba(255, 215, 0, 0.5);
}
`);

/* ================= CREATE API KEY PANEL ================= */
function createApiPanel() {
    // Check if API key already exists
    const existingApiKey = GM_getValue('tornAPIKey');
    
    // Don't show panel if API key already exists
    if (existingApiKey) return;
    
    // Create backdrop
    const backdrop = document.createElement('div');
    backdrop.className = 'api-backdrop';
    
    // Create API panel
    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 - no other permissions needed.<br>
                <em>Create key 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 Mode)</button>
            </div>
        </div>
    `;
    
    // Add to page
    document.body.appendChild(backdrop);
    document.body.appendChild(apiPanel);
    
    // Event listeners
    const saveButton = apiPanel.querySelector('#save-api-key');
    const skipButton = apiPanel.querySelector('#skip-api');
    const apiInput = apiPanel.querySelector('#torn-api-key');
    
    // Auto-focus input
    setTimeout(() => apiInput.focus(), 100);
    
    // Save API key
    saveButton.onclick = () => {
        const apiKey = apiInput.value.trim();
        if (!apiKey) {
            apiInput.style.borderColor = '#ff4444';
            apiInput.style.boxShadow = '0 0 0 2px rgba(255, 68, 68, 0.2)';
            setTimeout(() => {
                apiInput.style.borderColor = '';
                apiInput.style.boxShadow = '';
            }, 1000);
            return;
        }
        
        // Validate API key format (Torn API keys are 16 chars)
        if (apiKey.length !== 16) {
            alert('Invalid API key format. Torn API keys are exactly 16 characters long.');
            return;
        }
        
        // Save the API key
        GM_setValue('tornAPIKey', apiKey);
        
        // Remove panel
        document.body.removeChild(backdrop);
        document.body.removeChild(apiPanel);
        
        // Show success message and start tracker
        showNotification('API key saved successfully! Starting tracker...');
        setTimeout(() => {
            initializeTracker();
        }, 1000);
    };
    
    // Skip API key entry
    skipButton.onclick = () => {
        // Remove panel
        document.body.removeChild(backdrop);
        document.body.removeChild(apiPanel);
        
        // Show message about limited functionality
        showNotification('Running in limited mode. Add API key later via toggle right-click.');
        
        // Start tracker anyway (will show API missing message)
        initializeTracker();
    };
    
    // Allow Enter key to save
    apiInput.addEventListener('keypress', (e) => {
        if (e.key === 'Enter') {
            saveButton.click();
        }
    });
    
    // Close on backdrop click
    backdrop.onclick = (e) => {
        if (e.target === backdrop) {
            document.body.removeChild(backdrop);
            document.body.removeChild(apiPanel);
            initializeTracker(); // Start without API
        }
    };
}

/* ================= NOTIFICATION FUNCTION ================= */
function showNotification(message) {
    const notification = document.createElement('div');
    notification.style.cssText = `
        position: fixed;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        background: linear-gradient(145deg, #2a3b52, #152438);
        color: #88ccff;
        padding: 12px 20px;
        border-radius: 8px;
        border: 1px solid rgba(64, 156, 255, 0.4);
        box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
        z-index: 1000002;
        font-size: 12px;
        font-weight: 600;
        backdrop-filter: blur(8px);
        animation: fadeSlide 0.3s ease-out;
        text-align: center;
        max-width: 300px;
    `;
    
    notification.textContent = message;
    document.body.appendChild(notification);
    
    setTimeout(() => {
        if (notification.parentNode) {
            notification.style.opacity = '0';
            notification.style.transform = 'translateX(-50%) translateY(-20px)';
            setTimeout(() => {
                if (notification.parentNode) {
                    document.body.removeChild(notification);
                }
            }, 300);
        }
    }, 3000);
}

/* ================= CREATE MAIN ELEMENTS ================= */
function createMainElements() {
    // Create toggle
    const toggle = document.createElement('div');
    toggle.id = TOGGLE_ID;
    toggle.innerHTML = '✈';
    toggle.title = 'Travel Tracker - Right click to manage API key';
    
    // Add right-click for API management
    toggle.addEventListener('contextmenu', (e) => {
        e.preventDefault();
        showApiManagementMenu();
    });
    
    // Create panel
    const panel = document.createElement('div');
    panel.id = PANEL_ID;
    panel.innerHTML = `
    <div class="h">TRAVEL TRACKER</div>
    <div class="s">API KEY NEEDED</div>
    <div class="museum-bonus">🏛️ MUSEUM BONUS: +0 PTS • +$0</div>
    <div class="b"></div>
    `;
    
    // Add to page
    document.body.appendChild(toggle);
    document.body.appendChild(panel);
    
    return { toggle, panel };
}

/* ================= API MANAGEMENT MENU ================= */
function showApiManagementMenu() {
    // Remove existing menu if present
    const existingMenu = document.getElementById('api-management-menu');
    if (existingMenu) document.body.removeChild(existingMenu);
    
    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(64, 156, 255, 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);
        animation: fadeSlide 0.2s ease;
    `;
    
    const currentKey = GM_getValue('tornAPIKey');
    
    menu.innerHTML = `
        <div style="padding: 8px 12px; font-size: 11px; color: #88ccff; border-bottom: 1px solid rgba(64, 156, 255, 0.2);">
            API Key Management
        </div>
        ${currentKey ? `
            <div class="menu-item" data-action="view">View Current Key</div>
            <div class="menu-item" data-action="change">Change API Key</div>
            <div class="menu-item" data-action="remove">Remove API Key</div>
        ` : `
            <div class="menu-item" data-action="add">Add API Key</div>
        `}
        <div class="menu-item" data-action="reset-order">↺ Reset Category Order</div>
        <div class="menu-item" data-action="support">❤️ Support Development</div>
        <div class="menu-item" data-action="help">API Key Help</div>
    `;
    
    // Add menu item styles
    GM_addStyle(`
        .menu-item {
            padding: 8px 12px;
            font-size: 11px;
            color: #c0e0ff;
            cursor: pointer;
            transition: all 0.2s ease;
        }
        .menu-item:hover {
            background: rgba(64, 156, 255, 0.15);
            color: #ffffff;
        }
        .menu-item[data-action="support"] {
            color: #ffd700;
            border-top: 1px solid rgba(255, 215, 0, 0.2);
            margin-top: 4px;
        }
        .menu-item[data-action="support"]:hover {
            background: rgba(255, 215, 0, 0.15);
            color: #ffff00;
        }
        .menu-item[data-action="reset-order"] {
            color: #88ccff;
            border-top: 1px solid rgba(64, 156, 255, 0.2);
        }
        .menu-item[data-action="reset-order"]:hover {
            background: rgba(64, 156, 255, 0.15);
        }
    `);
    
    // Position near toggle
    const toggle = document.getElementById(TOGGLE_ID);
    const toggleRect = toggle.getBoundingClientRect();
    menu.style.top = toggleRect.bottom + 5 + 'px';
    menu.style.right = (window.innerWidth - toggleRect.right) + 'px';
    
    document.body.appendChild(menu);
    
    // Event delegation for menu items
    menu.addEventListener('click', (e) => {
        if (!e.target.classList.contains('menu-item')) return;
        
        const action = e.target.dataset.action;
        document.body.removeChild(menu);
        
        switch(action) {
            case 'view':
                if (currentKey) {
                    alert(`Current API Key: ${currentKey}\n\nKey ends with: ...${currentKey.slice(-4)}`);
                }
                break;
            case 'change':
            case 'add':
                createApiPanel();
                break;
            case 'remove':
                if (confirm('Are you sure you want to remove the API key?\n\nThe tracker will work in limited mode.')) {
                    GM_setValue('tornAPIKey', '');
                    showNotification('API key removed. Please refresh the page.');
                }
                break;
            case 'reset-order':
                GM_setValue(CATEGORY_ORDER_KEY, DEFAULT_CATEGORY_ORDER);
                showNotification('Category order reset to default');
                render();
                break;
            case 'support':
                // Open Supernova's profile for support
                window.open('https://www.torn.com/profiles.php?XID=2637223', '_blank');
                showNotification('Thank you for considering support! ❤️');
                break;
            case 'help':
                alert(`🔑 API KEY SETUP GUIDE:

1. Go to Torn.com → Settings → API
2. Click "Create New Key"
3. Set PERMISSIONS:
   • Select ONLY "Display" checkbox
   • Uncheck ALL other permissions
4. Copy the 16-character API key
5. Paste it into the Travel Tracker

⚠️ SECURITY: Only use LIMITED keys with DISPLAY access!
This script only needs to read your displayed items.`);
                break;
        }
    });
    
    // Close menu when clicking elsewhere
    setTimeout(() => {
        const closeMenu = (e) => {
            if (!menu.contains(e.target) && e.target.id !== TOGGLE_ID) {
                document.body.removeChild(menu);
                document.removeEventListener('click', closeMenu);
            }
        };
        document.addEventListener('click', closeMenu);
    }, 100);
}

/* ================= GLOBAL VARIABLES ================= */
let toggle, panel, sum, museumBonus, body, isPanelOpen = false;

/* ================= INITIALIZE TRACKER ================= */
function initializeTracker() {
    const elements = createMainElements();
    toggle = elements.toggle;
    panel = elements.panel;
    sum = panel.querySelector('.s');
    museumBonus = panel.querySelector('.museum-bonus');
    body = panel.querySelector('.b');
    
    setupToggleFunctionality();
    setupScrollSync();
    
    // Check if we have API key and start rendering
    const apiKey = GM_getValue('tornAPIKey');
    if (apiKey) {
        mainLoop();
    } else {
        // Show API missing message
        sum.textContent = 'API KEY REQUIRED';
        museumBonus.style.display = 'none'; // Hide museum bonus if no API
        body.innerHTML = `
            <div style="padding: 20px 12px; text-align: center; color: #ff8888; font-size: 10px;">
                ⚠️ No API key configured.<br><br>
                <span style="color: #88ccff; font-size: 9px;">
                    Right-click the toggle button<br>
                    to add your API key.<br><br>
                    Only "Display" permission needed.
                </span>
            </div>
        `;
    }
}

/* ================= TOGGLE FUNCTIONALITY ================= */
function setupToggleFunctionality() {
    toggle.onclick = () => {
        isPanelOpen = !isPanelOpen;
        panel.classList.toggle('open', isPanelOpen);
        
        // Toggle animation
        toggle.style.transform = isPanelOpen ? 
            'translateX(-244px) scale(1.05)' : 
            'translateX(0) scale(1)';
        
        toggle.title = isPanelOpen ? 'Close Tracker' : 'Open Tracker';
        toggle.style.color = isPanelOpen ? '#88ccff' : '#409cff';
        
        if (isPanelOpen) {
            toggle.innerHTML = '×';
        } else {
            toggle.innerHTML = '✈';
        }
    };
}

/* ================= SMOOTH SCROLL SYNC ================= */
function setupScrollSync() {
    let lastScrollY = window.scrollY;
    let scrollAnimationFrame;
    
    function handleSmoothScroll() {
        const currentScrollY = window.scrollY;
        
        // Add smooth class during scroll
        if (!toggle.classList.contains('scrolling-smooth')) {
            toggle.classList.add('scrolling-smooth');
            panel.classList.add('scrolling-smooth');
        }
        
        // Calculate new position
        const newTop = 70 + (currentScrollY - lastScrollY) * 0.3;
        const boundedTop = Math.max(4, Math.min(newTop, window.innerHeight - 40));
        
        toggle.style.top = `${boundedTop}px`;
        panel.style.top = `${boundedTop}px`;
        
        lastScrollY = currentScrollY;
        
        // Remove smooth class after settling
        clearTimeout(scrollAnimationFrame);
        scrollAnimationFrame = setTimeout(() => {
            toggle.classList.remove('scrolling-smooth');
            panel.classList.remove('scrolling-smooth');
        }, 150);
    }
    
    // Optimized scroll handler
    let scrollThrottle;
    window.addEventListener('scroll', () => {
        if (scrollThrottle) return;
        scrollThrottle = requestAnimationFrame(() => {
            handleSmoothScroll();
            scrollThrottle = null;
        });
    });
    
    // Initial position
    handleSmoothScroll();
}

/* ================= FETCH FUNCTIONS ================= */
async function localItems() {
    const key = GM_getValue('tornAPIKey');
    if (!key) {
        throw new Error('No API key configured');
    }
    
    try {
        // Only request display data (not inventory) since we only need display permission
        const response = await fetch(`https://api.torn.com/user/?selections=display&key=${key}`).then(r => r.json());
        
        if (response.error) {
            throw new Error(response.error.error || 'API Error');
        }
        
        const items = {};
        // Only use display items since that's all we have permission for
        if (response.display) {
            response.display.forEach(item => {
                items[item.name] = (items[item.name] || 0) + item.quantity;
            });
        }
        
        return items;
    } catch (error) {
        console.error('Error fetching local items:', error);
        throw error;
    }
}

function gmJSON(url) {
    return new Promise(resolve => {
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            onload: r => {
                try {
                    resolve(JSON.parse(r.responseText));
                } catch {
                    resolve({});
                }
            },
            onerror: () => resolve({})
        });
    });
}

async function abroadItems() {
    const [yataData, promData] = await Promise.all([
        gmJSON('https://yata.yt/api/v1/travel/export/'),
        gmJSON('https://api.prombot.co.uk/api/travel')
    ]);
    
    const abroadMap = {};
    
    // Process YATA data
    [yataData?.stocks, yataData].forEach(source => {
        Object.values(source || {}).forEach(country => {
            (country?.stocks || []).forEach(item => {
                abroadMap[item.name] = (abroadMap[item.name] || 0) + (item.quantity || item.qty || 0);
            });
        });
    });
    
    return abroadMap;
}

/* ================= POINTS MARKET PRICE FETCHER ================= */
async function fetchPointsPrice(apiKey) {
    const now = Date.now();
    
    // Return cached price if still valid
    if (pointsPriceCache.time && (now - pointsPriceCache.time) < POINTS_CACHE_DURATION) {
        return pointsPriceCache.price;
    }
    
    try {
        const response = await fetch(`${POINTS_ENDPOINT}?key=${apiKey}`);
        const data = await response.json();
        
        if (data.pointsmarket) {
            // Get all available listings
            const listings = Object.values(data.pointsmarket)
                .filter(listing => listing.quantity > 0) // Only listings with points available
                .map(listing => listing.cost)
                .sort((a, b) => a - b); // Sort cheapest first
            
            if (listings.length > 0) {
                // Calculate average of cheapest 5 listings for stable price
                const topListings = listings.slice(0, Math.min(5, listings.length));
                const avgPrice = Math.round(topListings.reduce((sum, price) => sum + price, 0) / topListings.length);
                
                // Update cache with history
                pointsPriceCache.history.push(avgPrice);
                if (pointsPriceCache.history.length > POINTS_HISTORY_SIZE) {
                    pointsPriceCache.history.shift();
                }
                
                // Calculate moving average if we have history
                const stablePrice = pointsPriceCache.history.length > 0 ?
                    Math.round(pointsPriceCache.history.reduce((sum, price) => sum + price, 0) / pointsPriceCache.history.length) :
                    avgPrice;
                
                pointsPriceCache = {
                    time: now,
                    price: stablePrice,
                    history: pointsPriceCache.history
                };
                
                currentPointsPrice = stablePrice;
                return stablePrice;
            }
        }
    } catch (error) {
        console.error('Error fetching points price:', error);
    }
    
    return currentPointsPrice || 0;
}

/* ================= CALCULATION LOGIC ================= */
function calcSet(inventory, items) {
    const values = Object.keys(items).map(key => inventory[key] || 0);
    const sets = values.length ? Math.min(...values) : 0;
    return { sets };
}

/* ================= UPDATED: FIND TWO LOWEST ITEMS PER CATEGORY ================= */
function lowestTwo(inventory, items, sets) {
    // Create array of items with their counts AFTER sets have been subtracted
    const itemCounts = Object.entries(items).map(([name, data]) => {
        const count = inventory[name] || 0;
        const remaining = count - sets;
        return {
            name,
            code: data.s,
            location: data.loc,
            count: remaining
        };
    });
    
    // Sort by remaining count (ascending - lowest first)
    itemCounts.sort((a, b) => a.count - b.count);
    
    // Take the two lowest items
    const lowestItems = itemCounts.slice(0, 2);
    
    // Filter out items that are not actually low (count < 5)
    const lowItems = lowestItems.filter(item => item.count < 5 && item.count >= 0);
    
    if (lowItems.length === 0) {
        return null; // No items are low
    }
    
    // Create warning message with both items
    const warningParts = lowItems.map(item => `${item.code} → ${item.location}`);
    
    // Join with " & " if we have two items
    if (warningParts.length === 2) {
        return `Need ${warningParts[0]} & ${warningParts[1]}`;
    } else {
        return `Need ${warningParts[0]}`;
    }
}

/* ================= GET SORTED ITEMS FOR DISPLAY ================= */
function getSortedItems(inventory, items, sets) {
    // Create array of items with their counts AFTER sets have been subtracted
    const itemCounts = Object.entries(items).map(([name, data]) => {
        const count = inventory[name] || 0;
        const remaining = count - sets;
        return {
            name,
            data,
            remaining
        };
    });
    
    // Sort by remaining count (ascending - lowest first)
    itemCounts.sort((a, b) => a.remaining - b.remaining);
    
    return itemCounts;
}

/* ================= GET STATUS CLASS FOR ITEMS ================= */
function getStatusClass(itemName, abroadCount) {
    // Xanax specific thresholds
    if (itemName === "Xanax") {
        if (abroadCount >= XANAX_THRESHOLD_GREEN) {
            return 'status-xan-green';
        } else if (abroadCount >= XANAX_THRESHOLD_ORANGE) {
            return 'status-xan-orange';
        } else {
            return 'status-xan-red';
        }
    }
    
    // Regular items
    if (abroadCount === 0) {
        return 'status-red';
    }
    
    // Check if it's a plushie
    if (itemName.includes('Plushie')) {
        if (abroadCount >= PLUSHIE_THRESHOLD) {
            return 'status-green';
        } else {
            return 'status-orange';
        }
    }
    
    // Check if it's a flower
    if (itemName.includes('Dahlia') || itemName.includes('Orchid') || itemName.includes('African Violet') || 
        itemName.includes('Cherry Blossom') || itemName.includes('Peony') || itemName.includes('Ceibo Flower') || 
        itemName.includes('Edelweiss') || itemName.includes('Crocus') || itemName.includes('Heather') || 
        itemName.includes('Tribulus Omanense') || itemName.includes('Banana Orchid')) {
        if (abroadCount >= FLOWER_THRESHOLD) {
            return 'status-green';
        } else {
            return 'status-orange';
        }
    }
    
    // For other items (prehistoric points, meteorite, fossil)
    return '';
}

/* ================= HEX TO RGB HELPER ================= */
function hexToRgb(hex) {
    const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
    return result ? {
        r: parseInt(result[1], 16),
        g: parseInt(result[2], 16),
        b: parseInt(result[3], 16)
    } : { r: 0, g: 0, b: 0 };
}

/* ================= RENDER FUNCTION ================= */
async function render() {
    try {
        sum.classList.add('loading');
        sum.textContent = 'UPDATING...';
        
        const inventory = await localItems();
        const abroad = await abroadItems();
        
        let totalSets = 0;
        let totalPoints = 0;
        let categoryHtml = {};
        let categoryWarnings = {};
        
        // Process Prehistoric
        const preHistResult = processCategory('Prehistoric', GROUPS.Prehistoric, inventory, abroad);
        categoryHtml['Prehistoric'] = preHistResult.html;
        categoryWarnings['Prehistoric'] = preHistResult.warning;
        totalSets += preHistResult.sets;
        totalPoints += preHistResult.points;
        
        // Process Flowers
        const flowersResult = processCategory('Flowers', GROUPS.Flowers, inventory, abroad);
        categoryHtml['Flowers'] = flowersResult.html;
        categoryWarnings['Flowers'] = flowersResult.warning;
        totalSets += flowersResult.sets;
        totalPoints += flowersResult.points;
        
        // Process Plushies
        const plushiesResult = processCategory('Plushies', GROUPS.Plushies, inventory, abroad);
        categoryHtml['Plushies'] = plushiesResult.html;
        categoryWarnings['Plushies'] = plushiesResult.warning;
        totalSets += plushiesResult.sets;
        totalPoints += plushiesResult.points;
        
        // Process Special (Meteorite & Fossil)
        const meteorite = inventory["Meteorite Fragment"] || 0;
        const fossil = inventory["Patagonian Fossil"] || 0;
        const specialPoints = (meteorite * MET_PTS) + (fossil * FOS_PTS);
        totalPoints += specialPoints;
        
        const meteorColor = "#FF4500";
        const fossilColor = "#8B4513";
        const meteorRgb = hexToRgb(meteorColor);
        const fossilRgb = hexToRgb(fossilColor);
        
        const meteorAbroad = abroad["Meteorite Fragment"] || 0;
        const fossilAbroad = abroad["Patagonian Fossil"] || 0;
        
        categoryHtml['Special'] = `
        <div class="t">
            <span>SPECIAL</span>
            <span class="category-controls">
                <span class="category-btn up-btn" data-category="Special" title="Move Up"></span>
                <span class="category-btn down-btn" data-category="Special" title="Move Down"></span>
            </span>
        </div>
        <div class="r special-row">
            <div class="item-image">
                <img src="${getItemImageUrl(1488)}" alt="METR" title="Meteorite Fragment">
            </div>
            <span>${meteorite}</span>
            <span class="${getStatusClass('Meteorite Fragment', meteorAbroad)}">${meteorAbroad}</span>
            <span>AR 🇦🇷</span>
        </div>
        <div class="r special-row">
            <div class="item-image">
                <img src="${getItemImageUrl(1487)}" alt="FOSL" title="Patagonian Fossil">
            </div>
            <span>${fossil}</span>
            <span class="${getStatusClass('Patagonian Fossil', fossilAbroad)}">${fossilAbroad}</span>
            <span>AR 🇦🇷</span>
        </div>`;
        
        // Check for low items in Special category
        const meteorLow = meteorite < 5 && meteorite > 0 ? `Need METR → AR` : null;
        const fossilLow = fossil < 5 && fossil > 0 ? `Need FOSL → AR` : null;
        if (meteorLow || fossilLow) {
            if (meteorLow && fossilLow) {
                categoryWarnings['Special'] = `Need METR → AR & FOSL → AR`;
            } else {
                categoryWarnings['Special'] = meteorLow || fossilLow;
            }
        }
        
        // Process Xanax
        const xanax = inventory["Xanax"] || 0;
        const xanaxAbroad = abroad["Xanax"] || 0;
        
        categoryHtml['Xanax'] = `
        <div class="t">
            <span>XANAX</span>
            <span class="category-controls">
                <span class="category-btn up-btn" data-category="Xanax" title="Move Up"></span>
                <span class="category-btn down-btn" data-category="Xanax" title="Move Down"></span>
            </span>
        </div>
        <div class="r xanax-row">
            <div class="item-image">
                <img src="${getItemImageUrl(206)}" alt="XAN" title="Xanax">
            </div>
            <span>${xanax}</span>
            <span class="${getStatusClass('Xanax', xanaxAbroad)}">${xanaxAbroad}</span>
            <span>JP 🇯🇵</span>
        </div>`;
        
        // Check for low Xanax warning (below 950)
        if (xanaxAbroad < XANAX_THRESHOLD_ORANGE && xanaxAbroad > 0) {
            categoryWarnings['Xanax'] = `⚠️ Low Xanax stock: ${xanaxAbroad} abroad`;
        } else if (xanaxAbroad === 0) {
            categoryWarnings['Xanax'] = `❌ No Xanax abroad!`;
        }
        
        // Points value calculation
        let pointsValue = 0;
        let pointsValueFormatted = '';
        const apiKey = GM_getValue('tornAPIKey');
        let pointsPrice = 0;
        
        if (apiKey && totalPoints > 0) {
            pointsPrice = await fetchPointsPrice(apiKey);
            if (pointsPrice > 0) {
                pointsValue = totalPoints * pointsPrice;
                
                if (pointsValue >= 1000000) {
                    pointsValueFormatted = `$${(pointsValue / 1000000).toFixed(1)}M`;
                } else if (pointsValue >= 1000) {
                    pointsValueFormatted = `$${Math.round(pointsValue / 1000)}k`;
                } else {
                    pointsValueFormatted = `$${pointsValue}`;
                }
                
                sum.classList.add('points-tooltip');
                sum.setAttribute('data-tooltip', 
                    `Total Points: ${totalPoints.toLocaleString()}\n` +
                    `Average Price per Point: $${pointsPrice.toLocaleString()}\n` +
                    `Total Value: $${pointsValue.toLocaleString()}`
                );
            }
        }
        
        // Museum Day bonus
        if (apiKey && totalPoints > 0 && pointsPrice > 0) {
            const bonusPoints = Math.round(totalPoints * 0.1);
            const bonusValue = bonusPoints * pointsPrice;
            
            let bonusPointsFormatted = bonusPoints.toLocaleString();
            let bonusValueFormatted = '';
            
            if (bonusValue >= 1000000) {
                bonusValueFormatted = `$${(bonusValue / 1000000).toFixed(1)}M`;
            } else if (bonusValue >= 1000) {
                bonusValueFormatted = `$${Math.round(bonusValue / 1000)}k`;
            } else {
                bonusValueFormatted = `$${bonusValue}`;
            }
            
            museumBonus.innerHTML = `🏛️ MUSEUM BONUS: +${bonusPointsFormatted} PTS • +${bonusValueFormatted}`;
            museumBonus.style.display = 'block';
            
            museumBonus.classList.add('points-tooltip');
            museumBonus.setAttribute('data-tooltip', 
                `10% bonus if redeemed on Museum Day (May 18)\n` +
                `Extra Points: ${bonusPointsFormatted}\n` +
                `Extra Value: ${bonusValueFormatted}`
            );
        } else {
            museumBonus.style.display = 'none';
        }
        
        // Build HTML in saved order
        let html = '';
        const categoryOrder = getCategoryOrder();
        
        categoryOrder.forEach(cat => {
            if (categoryWarnings[cat]) {
                html += `<div class="a">${categoryWarnings[cat]}</div>`;
            }
            if (categoryHtml[cat]) {
                html += categoryHtml[cat];
            }
        });
        
        html += `
        <div class="support-footer">
            <span class="heart">❤️</span>
            <a href="https://www.torn.com/profiles.php?XID=2637223" target="_blank" title="Support Supernova's development">
                Support Development
            </a>
            <span class="heart">❤️</span>
        </div>`;
        
        sum.classList.remove('loading');
        if (pointsValueFormatted) {
            sum.innerHTML = `✈ ${totalSets.toLocaleString()} SETS • ${totalPoints.toLocaleString()} PTS • ${pointsValueFormatted}`;
        } else {
            sum.textContent = `✈ ${totalSets.toLocaleString()} SETS • ${totalPoints.toLocaleString()} PTS`;
            sum.classList.remove('points-tooltip');
            sum.removeAttribute('data-tooltip');
        }
        
        body.innerHTML = html;
        
        // Add event listeners to category buttons
        document.querySelectorAll('.category-btn.up-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                const category = btn.dataset.category;
                moveCategoryUp(category);
            });
        });
        
        document.querySelectorAll('.category-btn.down-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                const category = btn.dataset.category;
                moveCategoryDown(category);
            });
        });
        
        body.scrollTop = 0;
        
    } catch (error) {
        sum.classList.remove('loading');
        sum.textContent = 'API ERROR';
        museumBonus.style.display = 'none';
        body.innerHTML = `
            <div style="padding: 20px 12px; text-align: center; color: #ff8888; font-size: 10px;">
                ⚠️ Error: ${error.message}<br><br>
                <span style="color: #88ccff; font-size: 9px;">
                    Check your API key permissions.<br>
                    Only "Display" permission is needed.<br><br>
                    Right-click toggle to manage API key.
                </span>
                <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(255, 215, 0, 0.2);">
                    <a href="https://www.torn.com/profiles.php?XID=2637223" target="_blank" 
                       style="color: #ffd700; text-decoration: none; font-weight: 600;">
                        ❤️ Support Development
                    </a>
                </div>
            </div>
        `;
    }
}

// Helper function to process a category
function processCategory(catName, group, inventory, abroad) {
    const { sets } = calcSet(inventory, group.items);
    let html = '';
    let warning = null;
    
    const displayName = catName === 'Prehistoric' ? 'PREHIST' : 
                        catName === 'Flowers' ? 'FLOWERS' : 
                        catName === 'Plushies' ? 'PLUSH' : catName;
    
    html += `
    <div class="t">
        <span>${displayName}</span>
        <span class="category-controls">
            <span class="category-btn up-btn" data-category="${catName}" title="Move Up"></span>
            <span class="category-btn down-btn" data-category="${catName}" title="Move Down"></span>
        </span>
    </div>`;
    
    warning = lowestTwo(inventory, group.items, sets);
    
    const sortedItems = getSortedItems(inventory, group.items, sets);
    
    sortedItems.forEach(({ name, data, remaining }) => {
        const abroadCount = abroad[name] || 0;
        const statusClass = getStatusClass(name, abroadCount);
        
        html += `
        <div class="r">
            <div class="item-image">
                <img src="${getItemImageUrl(data.id)}" alt="${data.s}" title="${name}">
            </div>
            <span>${remaining}</span>
            <span class="${statusClass}">${abroadCount}</span>
            <span>${data.loc}</span>
        </div>`;
    });
    
    return { sets, points: sets * group.pts, html, warning };
}

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

/* ================= START EVERYTHING ================= */
if (!GM_getValue('tornAPIKey')) {
    setTimeout(createApiPanel, 1000);
} else {
    setTimeout(initializeTracker, 500);
}
})();