Torn — Sort items by value

Sort items by value, show total value, hide equipped items, and sort by circulation. Works on inventory AND market add-items.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Torn — Sort items by value
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Sort items by value, show total value, hide equipped items, and sort by circulation. Works on inventory AND market add-items.
// @author       Charkel [3429133]
// @match        https://www.torn.com/item.php*
// @match        https://www.torn.com/page.php?sid=ItemMarket*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const BUTTON_CONTAINER_ID = 'tm-sort-panel';
    const TOTAL_ID = 'tm-total-value';
    const TOTAL_PLACEHOLDER = 'Total value: (click a sort to calculate)';
    const SORTED_OVERLAY_ID = 'tm-sorted-overlay';
    const FORCE_ALL_KEY = 'tm-force-all-tab';
    const HIDE_EQUIPPED_KEY = 'tm-hide-equipped';

    function isForceAllChecked() {
        const cb = document.getElementById('tm-force-all');
        if (cb) return cb.checked;
        try {
            const v = localStorage.getItem(FORCE_ALL_KEY);
            return v === null ? true : v === '1';
        } catch (e) { return true; }
    }

    function isHideEquippedChecked() {
        const cb = document.getElementById('tm-hide-equipped');
        if (cb) return cb.checked;
        try { return localStorage.getItem(HIDE_EQUIPPED_KEY) === '1'; } catch (e) { return false; }
    }

    let itemsFullyLoaded = false;
    let hideEquipped = false;
    let hasSorted = false;
    let loadingOverlay;
    let suppressGreenObserver = false;

    // Set your Torn API key here to enable "Circulation" sorting.
    const API_KEY = 'API_KEY_HERE'; // ENTER YOUR API KEY HERE (Limited access)
    let circulationMap = null;

    let marketCollectedItems = [];
    let marketCollectedScopeKey = '';
    const marketCacheByScope = new Map();
    const groupedPriceCache = new Map();

    function isMarketPage() {
        return !!(
            document.querySelector('[class*="virtualList"]') ||
            location.pathname.includes('bazaar') ||
            location.pathname.includes('imarket') ||
            location.href.includes('ItemMarket')
        );
    }

    // --- Styles ---
    const style = document.createElement('style');
    style.textContent = `
        #${BUTTON_CONTAINER_ID} {
            position: fixed;
            top: 80px;
            right: 20px;
            z-index: 9998;
            background: rgba(0,0,0,0.85);
            border: 1px solid #333;
            border-radius: 4px;
            padding: 6px 8px;
            font-size: 12px;
            color: #acea00;
            max-width: 220px;
            user-select: none;
            box-shadow: 0 4px 12px rgba(0,0,0,0.5);
        }
        #${BUTTON_CONTAINER_ID}.tm-dragging { opacity: 0.85; }
        #${BUTTON_CONTAINER_ID} .tm-drag-handle {
            cursor: move;
            padding: 3px 6px;
            margin: -6px -8px 4px -8px;
            background: rgba(172, 234, 0, 0.18);
            border-bottom: 1px solid #333;
            border-radius: 4px 4px 0 0;
            font-weight: 700;
            color: #acea00;
            display: flex;
            align-items: center;
            gap: 6px;
        }
        #${BUTTON_CONTAINER_ID} .tm-drag-handle::before {
            content: "⠿";
            opacity: 0.8;
        }
        #${BUTTON_CONTAINER_ID} .tm-sort-btn {
            margin: 2px 3px;
            padding: 3px 7px;
            background: #acea00;
            border: 1px solid #222;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 700;
            color: #000;
        }
        #${BUTTON_CONTAINER_ID} .tm-sort-btn:hover { background: #c4ff33; }
        #${BUTTON_CONTAINER_ID} .tm-total {
            display: block;
            margin-top: 4px;
            font-size: 12px;
            font-weight: 700;
            color: #acea00;
        }
        #${BUTTON_CONTAINER_ID} .tm-status {
            display: block;
            margin-top: 2px;
            font-size: 11px;
            color: #888;
        }
        #${BUTTON_CONTAINER_ID} .tm-checkbox {
            display: flex;
            align-items: center;
            gap: 5px;
            margin-top: 5px;
            font-size: 11px;
            color: #bbb;
            cursor: pointer;
            user-select: none;
        }
        #${BUTTON_CONTAINER_ID} .tm-checkbox input {
            cursor: pointer;
            margin: 0;
        }
        .tm-loading-overlay {
            position: fixed;
            top: 0; left: 0;
            width: 100vw; height: 100vh;
            background: rgba(0,0,0,0.70);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 9999;
            font-size: 36px;
            font-weight: bold;
            color: #fff;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.9);
            pointer-events: none;
            flex-direction: column;
        }
        .tm-loading-overlay .tm-loading-sub {
            font-size: 16px;
            margin-top: 10px;
            color: #ccc;
        }
        .tm-hidden-green { display: none !important; }

        /* Sorted overlay — compact centered panel */
        #${SORTED_OVERLAY_ID} {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 560px;
            max-width: 95vw;
            max-height: 85vh;
            background: #191919;
            border: 1px solid #333;
            border-radius: 5px;
            z-index: 9997;
            overflow-y: auto;
            padding: 0;
            box-sizing: border-box;
            font-family: Arial, Helvetica, sans-serif;
            box-shadow: 0 8px 40px rgba(0,0,0,0.7);
        }
        #${SORTED_OVERLAY_ID}-backdrop {
            position: fixed;
            top: 0; left: 0;
            width: 100vw; height: 100vh;
            background: rgba(0,0,0,0.6);
            z-index: 9996;
        }
        #${SORTED_OVERLAY_ID} .tm-overlay-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px 12px;
            position: sticky;
            top: 0;
            background: #242424;
            border-bottom: 1px solid #333;
            z-index: 2;
        }
        #${SORTED_OVERLAY_ID} .tm-overlay-title {
            font-size: 13px;
            font-weight: 700;
            color: #ddd;
        }
        #${SORTED_OVERLAY_ID} .tm-overlay-close {
            padding: 4px 12px;
            background: #333;
            color: #ccc;
            border: 1px solid #555;
            border-radius: 3px;
            cursor: pointer;
            font-size: 12px;
            font-weight: 700;
        }
        #${SORTED_OVERLAY_ID} .tm-overlay-close:hover { background: #444; color: #fff; }

        /* Item rows — match Torn's native listing rows */
        #${SORTED_OVERLAY_ID} .tm-item-row {
            display: flex;
            align-items: center;
            padding: 4px 10px;
            min-height: 38px;
            border-bottom: 1px solid #252525;
            cursor: pointer;
            font-size: 12px;
            color: #ccc;
            background: #1c1c1c;
            transition: background 0.1s;
        }
        #${SORTED_OVERLAY_ID} .tm-item-row:nth-child(even) {
            background: #202020;
        }
        #${SORTED_OVERLAY_ID} .tm-item-row:hover {
            background: #2a2a2a;
        }

        /* Item image */
        #${SORTED_OVERLAY_ID} .tm-item-img {
            width: 40px;
            height: 30px;
            object-fit: contain;
            margin-right: 10px;
            flex-shrink: 0;
        }
        #${SORTED_OVERLAY_ID} .tm-item-img-placeholder {
            width: 40px;
            height: 30px;
            margin-right: 10px;
            flex-shrink: 0;
        }

        /* Item name */
        #${SORTED_OVERLAY_ID} .tm-item-name {
            flex: 1;
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            color: #ddd;
            font-size: 12px;
        }

        /* Price RRP */
        #${SORTED_OVERLAY_ID} .tm-item-rrp {
            width: 120px;
            text-align: right;
            color: #ccc;
            font-size: 12px;
            flex-shrink: 0;
            margin-right: 10px;
        }
        #${SORTED_OVERLAY_ID} .tm-item-rrp .tm-rrp-label {
            color: #777;
            font-size: 11px;
            margin-left: 3px;
        }

        /* Navigate button */
        #${SORTED_OVERLAY_ID} .tm-item-goto {
            margin-left: 8px;
            padding: 3px 10px;
            background: #333;
            border: 1px solid #444;
            border-radius: 3px;
            color: #acea00;
            font-size: 11px;
            font-weight: 700;
            cursor: pointer;
            flex-shrink: 0;
            white-space: nowrap;
        }
        #${SORTED_OVERLAY_ID} .tm-item-goto:hover {
            background: #444;
            color: #c4ff33;
        }

        .tm-flash-highlight {
            animation: tm-flash 1.5s ease-out;
        }
        @keyframes tm-flash {
            0% { outline: 3px solid #acea00; outline-offset: 2px; background: rgba(172,234,0,0.25); }
            100% { outline: 3px solid transparent; outline-offset: 2px; background: transparent; }
        }
    `;
    document.head.appendChild(style);

    function createButton(label, sortType) {
        const btn = document.createElement('button');
        btn.className = 'tm-sort-btn';
        btn.textContent = label + (sortType ? ' ↓' : '');
        if (sortType) btn.dataset.sortType = sortType;
        btn.dataset.order = 'desc';
        return btn;
    }

    const PANEL_POS_KEY = 'tm-sort-panel-pos';

    function makePanelDraggable(panel, handle) {
        let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;

        function onPointerDown(e) {
            // Only left mouse / touch
            if (e.button !== undefined && e.button !== 0) return;
            dragging = true;
            const rect = panel.getBoundingClientRect();
            // Switch to left/top positioning so we can drag freely
            panel.style.left = rect.left + 'px';
            panel.style.top = rect.top + 'px';
            panel.style.right = 'auto';
            panel.style.bottom = 'auto';
            startLeft = rect.left;
            startTop = rect.top;
            startX = e.clientX;
            startY = e.clientY;
            panel.classList.add('tm-dragging');
            handle.setPointerCapture?.(e.pointerId);
            e.preventDefault();
        }
        function onPointerMove(e) {
            if (!dragging) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            const maxLeft = window.innerWidth - panel.offsetWidth;
            const maxTop = window.innerHeight - panel.offsetHeight;
            const newLeft = Math.max(0, Math.min(maxLeft, startLeft + dx));
            const newTop = Math.max(0, Math.min(maxTop, startTop + dy));
            panel.style.left = newLeft + 'px';
            panel.style.top = newTop + 'px';
        }
        function onPointerUp(e) {
            if (!dragging) return;
            dragging = false;
            panel.classList.remove('tm-dragging');
            try {
                localStorage.setItem(PANEL_POS_KEY, JSON.stringify({
                    left: panel.style.left, top: panel.style.top
                }));
            } catch (err) {}
        }
        handle.addEventListener('pointerdown', onPointerDown);
        window.addEventListener('pointermove', onPointerMove);
        window.addEventListener('pointerup', onPointerUp);
    }

    function restorePanelPosition(panel) {
        try {
            const saved = JSON.parse(localStorage.getItem(PANEL_POS_KEY) || 'null');
            if (saved && saved.left && saved.top) {
                panel.style.left = saved.left;
                panel.style.top = saved.top;
                panel.style.right = 'auto';
                panel.style.bottom = 'auto';
            }
        } catch (e) {}
    }

    function ensurePanel() {
        let panel = document.getElementById(BUTTON_CONTAINER_ID);
        if (!panel) {
            panel = document.createElement('div');
            panel.id = BUTTON_CONTAINER_ID;

            const handle = document.createElement('div');
            handle.className = 'tm-drag-handle';
            handle.textContent = 'Item sorting';
            handle.title = 'Drag to move';
            panel.appendChild(handle);

            const buttonsRow = document.createElement('div');
            buttonsRow.className = 'tm-buttons-row';
            panel.appendChild(buttonsRow);

            const total = document.createElement('span');
            total.id = TOTAL_ID;
            total.className = 'tm-total';
            total.textContent = TOTAL_PLACEHOLDER;
            panel.appendChild(total);

            const status = document.createElement('span');
            status.className = 'tm-status';
            status.id = 'tm-status';
            panel.appendChild(status);

            const forceLabel = document.createElement('label');
            forceLabel.className = 'tm-checkbox';
            forceLabel.title = 'When checked, sorting first switches to the "All" items tab so every item is included. Uncheck to sort only the currently visible category.';
            const forceCb = document.createElement('input');
            forceCb.type = 'checkbox';
            forceCb.id = 'tm-force-all';
            // Default: checked. Persisted across sessions only if user unchecks it.
            const savedForce = (() => { try { return localStorage.getItem(FORCE_ALL_KEY); } catch (e) { return null; } })();
            forceCb.checked = savedForce !== '0';
            forceCb.addEventListener('change', () => {
                try { localStorage.setItem(FORCE_ALL_KEY, forceCb.checked ? '1' : '0'); } catch (e) {}
                // Reset cached market items so a category switch is reflected on next sort
                marketCollectedItems = [];
                marketCollectedScopeKey = '';
                hasSorted = false;
                updateTotalDisplay();
            });
            forceLabel.appendChild(forceCb);
            forceLabel.appendChild(document.createTextNode('Force "All items" tab'));
            panel.appendChild(forceLabel);

            // Hide Equipped checkbox (inventory only — but harmless on market)
            const hideLabel = document.createElement('label');
            hideLabel.className = 'tm-checkbox';
            hideLabel.title = 'Hide equipped items (highlighted green) from the inventory list.';
            const hideCb = document.createElement('input');
            hideCb.type = 'checkbox';
            hideCb.id = 'tm-hide-equipped';
            try { hideCb.checked = localStorage.getItem(HIDE_EQUIPPED_KEY) === '1'; } catch (e) { hideCb.checked = false; }
            hideEquipped = hideCb.checked;
            hideCb.addEventListener('change', () => {
                hideEquipped = hideCb.checked;
                try { localStorage.setItem(HIDE_EQUIPPED_KEY, hideCb.checked ? '1' : '0'); } catch (e) {}
                applyGreenVisibility(document);
            });
            hideLabel.appendChild(hideCb);
            hideLabel.appendChild(document.createTextNode('Hide equipped items'));
            panel.appendChild(hideLabel);

            document.body.appendChild(panel);
            restorePanelPosition(panel);
            makePanelDraggable(panel, handle);
        }
        return panel;
    }

    function getButtonsRow() {
        const panel = ensurePanel();
        let row = panel.querySelector('.tm-buttons-row');
        if (!row) {
            row = document.createElement('div');
            row.className = 'tm-buttons-row';
            panel.insertBefore(row, panel.querySelector('#' + TOTAL_ID));
        }
        return row;
    }

    function getTotalNode() {
        ensurePanel();
        return document.getElementById(TOTAL_ID);
    }

    function setStatus(msg) {
        ensurePanel();
        const el = document.getElementById('tm-status');
        if (el) el.textContent = msg;
    }

    function showLoadingOverlay(msg, sub) {
        if (!loadingOverlay) {
            loadingOverlay = document.createElement('div');
            loadingOverlay.className = 'tm-loading-overlay';
            document.body.appendChild(loadingOverlay);
        }
        loadingOverlay.innerHTML = `<div>${msg || 'Loading…'}</div>` +
            (sub ? `<div class="tm-loading-sub">${sub}</div>` : '');
        loadingOverlay.style.display = 'flex';
    }
    function hideLoadingOverlay() {
        if (loadingOverlay) loadingOverlay.style.display = 'none';
    }

    function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
    function nextFrame() { return new Promise(resolve => requestAnimationFrame(() => resolve())); }

    // =============================================
    // MARKET: Collect items by scrolling
    // =============================================
    function getVirtualListContainer() {
        const allPanelList = getVisibleAllItemsPanel()?.querySelector('[class*="virtualList"]');
        if (allPanelList) return allPanelList;
        return Array.from(document.querySelectorAll('[class*="virtualList"]')).find(el => {
            const style = window.getComputedStyle(el);
            return style.display !== 'none' && style.visibility !== 'hidden';
        }) || document.querySelector('[class*="virtualList"]');
    }

    function getActiveMarketScopeKey() {
        if (isForceAllChecked()) return 'force:all-items';
        const activeTab = document.querySelector('li.ui-tabs-active, li.ui-state-active, li[aria-selected="true"]');
        const tabType = activeTab?.getAttribute('data-type') || activeTab?.id || activeTab?.textContent?.trim();
        const panelId = activeTab?.querySelector('a[href^="#"]')?.getAttribute('href') || location.hash;
        return `tab:${tabType || panelId || 'visible'}:${panelId || ''}`;
    }

    function parseMarketPriceNode(priceEl) {
        if (!priceEl) return 0;
        const ariaLabel = priceEl.getAttribute('aria-label') || '';
        let match = ariaLabel.match(/\$([\d,]+)/);
        if (!match) {
            match = (priceEl.textContent || '').match(/\$([\d,]+)/);
        }
        return match ? parseInt(match[1].replace(/,/g, ''), 10) : 0;
    }

    function getMarketListingName(listingEl) {
        return listingEl.querySelector('[class*="name___"]')?.textContent?.trim() || '';
    }

    function getGroupedPriceCacheKey(listingEl, toggleBtn) {
        const imgEl = listingEl.querySelector('img');
        const imgSrc = imgEl?.getAttribute('src') || imgEl?.src || '';
        const imgMatch = imgSrc.match(/\/images\/items\/(\d+)\//i);
        if (imgMatch) return `item:${imgMatch[1]}`;

        const panelId = toggleBtn?.getAttribute('aria-controls') || '';
        const panelMatch = panelId.match(/itemInfo-(\d+)-/i);
        if (panelMatch) return `item:${panelMatch[1]}`;

        const name = getMarketListingName(listingEl);
        return name ? `name:${name}` : '';
    }

    function getGroupedItemDebugName(listingEl, toggleBtn) {
        const name = getMarketListingName(listingEl);
        if (name) return name;

        const ariaLabel = toggleBtn?.getAttribute('aria-label') || '';
        const match = ariaLabel.match(/Expand\s+\d+\s+(.+?)\s+items?/i);
        return match?.[1]?.trim() || ariaLabel || 'unknown item';
    }

    function getGroupedInfoScopes(listingEl) {
        return [
            listingEl,
            listingEl.nextElementSibling,
            listingEl.previousElementSibling,
            listingEl.parentElement,
            listingEl.parentElement?.nextElementSibling,
            listingEl.parentElement?.previousElementSibling,
            listingEl.parentElement?.parentElement,
            document.body,
        ].filter((scope, index, arr) => scope && arr.indexOf(scope) === index);
    }

    function getGroupedInfoButton(listingEl) {
        return listingEl.querySelector('button[aria-controls][aria-expanded], [class*="viewInfoButton"][aria-controls]');
    }

    function getGroupedInfoPanel(listingEl, toggleBtn) {
        const panelId = toggleBtn?.getAttribute('aria-controls');
        if (panelId) {
            const byId = document.getElementById(panelId);
            if (byId) return byId;
            if (window.CSS && typeof window.CSS.escape === 'function') {
                for (const scope of getGroupedInfoScopes(listingEl)) {
                    const scoped = scope?.querySelector?.(`#${window.CSS.escape(panelId)}`);
                    if (scoped) return scoped;
                }
            }
        }

        for (const scope of getGroupedInfoScopes(listingEl)) {
            if (!scope?.querySelector) continue;
            const panel = scope.querySelector('[id*="itemInfo"], [class*="additionalInfo"]');
            if (panel) return panel;
        }

        return null;
    }

    function getGroupedInfoPrice(listingEl, toggleBtn) {
        const panel = getGroupedInfoPanel(listingEl, toggleBtn);
        if (panel) {
            const priceNodes = panel.querySelectorAll('[class*="price___"], [aria-label*="Recommended Retail Price"]');
            for (const node of priceNodes) {
                const price = parseMarketPriceNode(node);
                if (price) return price;
            }
        }

        for (const scope of getGroupedInfoScopes(listingEl)) {
            if (!scope?.querySelector) continue;
            const info = scope.querySelector('[class*="additionalInfo"], [id*="itemInfo"]');
            if (info) {
                const price = parseMarketPriceNode(info.querySelector('[class*="price___"], [aria-label*="Recommended Retail Price"]'));
                if (price) return price;
            }
        }

        return 0;
    }

    async function waitForGroupedInfoPrice(listingEl, toggleBtn, timeout = 1500) {
        const immediatePrice = getGroupedInfoPrice(listingEl, toggleBtn);
        if (immediatePrice) return immediatePrice;

        // Also try finding the panel by aria-controls ID directly on document
        const panelId = toggleBtn?.getAttribute('aria-controls');

        const start = performance.now();
        while (performance.now() - start < timeout) {
            // Check via normal scopes
            const price = getGroupedInfoPrice(listingEl, toggleBtn);
            if (price) return price;

            // Direct panel lookup by ID on document
            if (panelId) {
                const panel = document.getElementById(panelId);
                if (panel) {
                    const priceNode = panel.querySelector('[class*="price___"], [aria-label*="Recommended Retail Price"]');
                    const p = parseMarketPriceNode(priceNode);
                    if (p) return p;
                }
            }

            if (!toggleBtn?.isConnected) break;
            // Use setTimeout polling (50ms intervals) instead of rAF to avoid CPU burn
            await sleep(50);
        }

        console.log('[TM Sort Items] Timeout waiting for price on:', getGroupedItemDebugName(listingEl, toggleBtn));
        return 0;
    }

    async function getGroupedMarketPrice(listingEl, qty) {
        const toggleBtn = getGroupedInfoButton(listingEl);
        if (!toggleBtn || qty <= 1) return 0;

        const cacheKey = getGroupedPriceCacheKey(listingEl, toggleBtn);
        if (cacheKey && groupedPriceCache.has(cacheKey)) {
            return groupedPriceCache.get(cacheKey) || 0;
        }

        const existingPrice = getGroupedInfoPrice(listingEl, toggleBtn);
        if (existingPrice) {
            if (cacheKey) groupedPriceCache.set(cacheKey, existingPrice);
            return existingPrice;
        }

        const wasExpanded = toggleBtn.getAttribute('aria-expanded') === 'true';
        if (!wasExpanded) {
            toggleBtn.click();
            await sleep(300); // Give Torn time to render the expanded panel
        }

        const hiddenPrice = await waitForGroupedInfoPrice(listingEl, toggleBtn);
        if (hiddenPrice && cacheKey) {
            groupedPriceCache.set(cacheKey, hiddenPrice);
        }

        // Collapse it back
        if (!wasExpanded && toggleBtn.isConnected && toggleBtn.getAttribute('aria-expanded') === 'true') {
            toggleBtn.click();
            await sleep(100);
        }

        return hiddenPrice;
    }

    async function parseMarketListing(listingEl) {
        const name = getMarketListingName(listingEl);

        let qty = 1;
        const titleEl = listingEl.querySelector('[class*="title___"]');
        if (titleEl) {
            const titleText = titleEl.textContent || '';
            const m = titleText.match(/x\s*(\d+)/i);
            if (m) qty = parseInt(m[1], 10);
        }

        const directPrice = parseMarketPriceNode(listingEl.querySelector('[class*="price___"]'));
        const hiddenPrice = (!directPrice && qty > 1) ? await getGroupedMarketPrice(listingEl, qty) : 0;

        const singleRrp = directPrice || hiddenPrice || 0;
        const totalRrp = singleRrp > 0 ? singleRrp * qty : 0;

        const rrp = singleRrp;
        const hasPrice = singleRrp > 0 || totalRrp > 0;

        let imgSrc = '';
        const imgEl = listingEl.querySelector('img');
        if (imgEl) {
            imgSrc = imgEl.src || imgEl.getAttribute('src') || '';
        }

        const dataIndex = parseInt(listingEl.dataset.index || '0', 10);

        const transformMatch = (listingEl.style.transform || '').match(/translateY\(([\d.]+)px\)/);
        const translateY = transformMatch ? parseFloat(transformMatch[1]) : 0;

        return { name, qty, rrp, singleRrp, totalRrp, hasPrice, imgSrc, dataIndex, translateY };
    }

    function fireMouseEvent(el, type) {
        if (!el) return;
        try {
            const evt = new MouseEvent(type, { bubbles: true, cancelable: true, view: window, button: 0 });
            el.dispatchEvent(evt);
        } catch (e) { /* ignore */ }
    }

    function getVisibleAllItemsPanel() {
        const panel = document.querySelector('#all-items');
        if (!panel) return null;
        const style = window.getComputedStyle(panel);
        return style.display !== 'none' && style.visibility !== 'hidden' ? panel : null;
    }

    function runJqueryAllItemsTab(allTab) {
        try {
            const $ = window.jQuery || window.$;
            if (!$ || !$.fn || !$.fn.tabs) return false;
            const tabsRoot = $(allTab).closest('.ui-tabs');
            const tabIndex = $(allTab).index();
            if (!tabsRoot.length || tabIndex < 0) return false;
            tabsRoot.tabs('option', 'active', tabIndex);
            if (typeof tabsRoot.tabs('load') === 'function') tabsRoot.tabs('load', tabIndex);
            return true;
        } catch (e) {
            console.log('[TM Sort Items] jQuery tab activation failed:', e);
            return false;
        }
    }

    function activateAllItemsDomFallback(allTab) {
        const anchor = allTab.querySelector('a[href="#all-items"]') || allTab.querySelector('a') || allTab;
        const panel = document.querySelector('#all-items');
        const tabsRoot = allTab.closest('.ui-tabs') || document;

        Array.from(tabsRoot.querySelectorAll('li[role="tab"], li.ui-tabs-tab, li[id*="categoriesItem"], li[data-type]')).forEach(tab => {
            const selected = tab === allTab;
            tab.classList.toggle('ui-tabs-active', selected);
            tab.classList.toggle('ui-state-active', selected);
            tab.setAttribute('aria-selected', selected ? 'true' : 'false');
            tab.setAttribute('tabindex', selected ? '0' : '-1');
        });

        if (anchor?.setAttribute) anchor.setAttribute('aria-selected', 'true');
        if (panel) {
            Array.from(tabsRoot.querySelectorAll('[role="tabpanel"], .ui-tabs-panel')).forEach(p => {
                const selected = p.id === 'all-items';
                p.hidden = !selected;
                p.setAttribute('aria-hidden', selected ? 'false' : 'true');
                p.style.display = selected ? '' : 'none';
            });
            panel.hidden = false;
            panel.setAttribute('aria-hidden', 'false');
            panel.style.display = '';
        }
    }

    async function clickAllItemsTab() {
        // Ensure the "All" category tab is active so every item is included.
        // Torn uses jQuery UI tabs which often need full mouse events, not just .click().
        const allTab = document.querySelector('#categoriesItem[data-type="All"]')
            || document.querySelector('li[data-type="All"]')
            || document.querySelector('a[href="#all-items"]')?.closest('li');
        if (!allTab) {
            console.log('[TM Sort Items] Could not find All items tab');
            setStatus('All tab not found');
            return false;
        }

        const visibleAllList = getVisibleAllItemsPanel()?.querySelector('[class*="virtualList"]');
        const isActive = allTab.classList.contains('ui-tabs-active')
            || allTab.getAttribute('aria-selected') === 'true';
        const isLoaded = allTab.getAttribute('data-loaded') === '1' || !!visibleAllList;
        console.log('[TM Sort Items] All tab state before click:', {
            active: isActive,
            loaded: isLoaded,
            dataLoaded: allTab.getAttribute('data-loaded'),
            className: allTab.className
        });
        if ((isActive && isLoaded) || visibleAllList) return false;

        const anchor = allTab.querySelector('a') || allTab;
        runJqueryAllItemsTab(allTab);
        if (anchor.hash === '#all-items' && location.hash !== '#all-items') {
            try { history.replaceState(null, '', location.pathname + location.search + '#all-items'); } catch (e) { location.hash = 'all-items'; }
            try { window.dispatchEvent(new HashChangeEvent('hashchange')); } catch (e) {}
        }
        // Fire a full mouse event sequence on both anchor and li
        ['mouseover', 'mouseenter', 'mousemove', 'mousedown', 'mouseup', 'click'].forEach(t => {
            fireMouseEvent(anchor, t);
            fireMouseEvent(allTab, t);
        });
        try { anchor.click(); } catch (e) {}
        try { allTab.click(); } catch (e) {}

        // Wait only as long as needed for the visible list to appear.
        for (let i = 0; i < 10; i++) {
            if (getVisibleAllItemsPanel()?.querySelector('[class*="virtualList"]')) break;
            await sleep(25);
            const activeNow = allTab.classList.contains('ui-tabs-active') || allTab.getAttribute('aria-selected') === 'true';
            const loadedNow = allTab.getAttribute('data-loaded') === '1' || !!getVisibleAllItemsPanel()?.querySelector('[class*="virtualList"]');
            if (activeNow && loadedNow) break;
        }
        if (!getVisibleAllItemsPanel()) activateAllItemsDomFallback(allTab);
        return true;
    }

    async function collectAllMarketItems() {
        const scopeKey = getActiveMarketScopeKey();
        if (marketCacheByScope.has(scopeKey)) {
            marketCollectedScopeKey = scopeKey;
            return marketCacheByScope.get(scopeKey) || [];
        }

        if (isForceAllChecked()) await clickAllItemsTab();
        const activeScopeKey = getActiveMarketScopeKey();
        if (marketCacheByScope.has(activeScopeKey)) {
            marketCollectedScopeKey = activeScopeKey;
            return marketCacheByScope.get(activeScopeKey) || [];
        }

        const virtualList = getVirtualListContainer();
        if (!virtualList) return [];

        showLoadingOverlay('Loading items, please wait…');

        const collected = new Map();
        const processedKeys = new Set();
        const listHeight = parseInt(virtualList.style.height) || 6800;
        const scrollStep = 300;
        const maxSteps = Math.ceil(listHeight / scrollStep) + 20;

        window.scrollTo(0, 0);
        await sleep(100);

        let stableCount = 0, lastCollectedCount = 0;

        for (let step = 0; step < maxSteps; step++) {
            const listings = Array.from(virtualList.querySelectorAll('[class*="virtualListing"]'));
            for (const el of listings) {
                const rawIndex = el.dataset.index || '';
                const transformKey = el.style.transform || '';
                const rowKey = rawIndex || transformKey || ((el.querySelector('[class*="name___"]')?.textContent || '').trim() + '|' + (el.querySelector('[class*="title___"]')?.textContent || '').trim());
                if (!rowKey || processedKeys.has(rowKey)) continue;
                processedKeys.add(rowKey);

                const parsed = await parseMarketListing(el);
                if (parsed.name && !collected.has(parsed.dataIndex)) {
                    collected.set(parsed.dataIndex, parsed);
                }
            }

            showLoadingOverlay('Loading items, please wait…');

            if (collected.size === lastCollectedCount) {
                stableCount++;
                if (stableCount > 10) break;
            } else {
                stableCount = 0;
                lastCollectedCount = collected.size;
            }

            window.scrollBy(0, scrollStep);
            await sleep(150);
        }

        window.scrollTo(0, 0);
        hideLoadingOverlay();

        const items = Array.from(collected.values()).sort((a, b) => a.dataIndex - b.dataIndex);
        marketCollectedScopeKey = activeScopeKey;
        marketCacheByScope.set(activeScopeKey, items);
        console.log(`[TM Sort Items] Collected ${items.length} market items`);
        setStatus(`Collected ${items.length} items`);
        return items;
    }

    // =============================================
    // MARKET: Show sorted overlay & navigate to real item on click
    // =============================================
    function scrollToMarketItem(item) {
        closeSortedOverlay();

        const virtualList = getVirtualListContainer();
        if (!virtualList) return;

        // Estimate row height from the virtual list (default ~32px per row)
        const allRows = virtualList.querySelectorAll('[class*="virtualListing"]');
        let rowHeight = 32;
        if (allRows.length >= 2) {
            const t1 = parseFloat((allRows[0].style.transform || '').match(/translateY\(([\d.]+)/)?.[1] || '0');
            const t2 = parseFloat((allRows[1].style.transform || '').match(/translateY\(([\d.]+)/)?.[1] || '0');
            if (t2 > t1) rowHeight = t2 - t1;
        }

        // Calculate approximate scroll position from data-index
        const virtualListRect = virtualList.getBoundingClientRect();
        const listTop = window.scrollY + virtualListRect.top;
        const estimatedY = listTop + (item.dataIndex * rowHeight);
        const scrollTarget = Math.max(0, estimatedY - window.innerHeight / 2);
        window.scrollTo({ top: scrollTarget, behavior: 'smooth' });

        // Wait for scroll + virtual list to re-render, then find and highlight
        function findAndHighlight(attempts = 0) {
            if (attempts > 15) return;
            const realNode = virtualList.querySelector(`[data-index="${item.dataIndex}"]`);
            if (realNode) {
                realNode.scrollIntoView({ behavior: 'smooth', block: 'center' });
                realNode.classList.remove('tm-flash-highlight');
                void realNode.offsetWidth;
                realNode.classList.add('tm-flash-highlight');
                return;
            }
            // Also try matching by name
            const listings = virtualList.querySelectorAll('[class*="virtualListing"]');
            for (const el of listings) {
                const nameEl = el.querySelector('[class*="name___"]');
                if (nameEl && nameEl.textContent.trim() === item.name) {
                    el.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    el.classList.remove('tm-flash-highlight');
                    void el.offsetWidth;
                    el.classList.add('tm-flash-highlight');
                    return;
                }
            }
            setTimeout(() => findAndHighlight(attempts + 1), 200);
        }

        setTimeout(() => findAndHighlight(), 500);
    }

    function fmtPrice(n) {
        return '$' + n.toLocaleString('en-US');
    }

    function showSortedOverlay(sortedItems, sortLabel) {
        closeSortedOverlay();

        const overlay = document.createElement('div');
        overlay.id = SORTED_OVERLAY_ID;

        // Header
        const header = document.createElement('div');
        header.className = 'tm-overlay-header';

        const title = document.createElement('span');
        title.className = 'tm-overlay-title';
        title.textContent = `Sorted by ${sortLabel} — click any row to jump to it`;
        header.appendChild(title);

        const closeBtn = document.createElement('button');
        closeBtn.className = 'tm-overlay-close';
        closeBtn.textContent = '✕ Close';
        closeBtn.addEventListener('click', closeSortedOverlay);
        header.appendChild(closeBtn);

        overlay.appendChild(header);

        // Item rows — matching Torn's native layout
        sortedItems.forEach((item) => {
            const row = document.createElement('div');
            row.className = 'tm-item-row';

            // Item image
            if (item.imgSrc) {
                const img = document.createElement('img');
                img.className = 'tm-item-img';
                img.src = item.imgSrc;
                img.alt = item.name;
                row.appendChild(img);
            } else {
                const ph = document.createElement('div');
                ph.className = 'tm-item-img-placeholder';
                row.appendChild(ph);
            }

            // Item name with quantity
            const nameSpan = document.createElement('span');
            nameSpan.className = 'tm-item-name';
            nameSpan.textContent = item.qty > 1 ? `${item.name} x${item.qty}` : item.name;
            row.appendChild(nameSpan);

            // Sorted value display
            const rrpSpan = document.createElement('span');
            rrpSpan.className = 'tm-item-rrp';
            if (item.val) {
                rrpSpan.textContent = fmtPrice(item.val);
            } else if (item.hasPrice) {
                rrpSpan.textContent = fmtPrice(item.rrp);
            } else {
                rrpSpan.textContent = '';
            }
            row.appendChild(rrpSpan);

            // Go to item button
            const goBtn = document.createElement('span');
            goBtn.className = 'tm-item-goto';
            goBtn.textContent = 'Go →';
            row.appendChild(goBtn);

            row.addEventListener('click', () => scrollToMarketItem(item));
            overlay.appendChild(row);
        });

        // Close on Escape
        overlay.addEventListener('keydown', e => { if (e.key === 'Escape') closeSortedOverlay(); });
        overlay.tabIndex = -1;

        // Add backdrop
        const backdrop = document.createElement('div');
        backdrop.id = SORTED_OVERLAY_ID + '-backdrop';
        backdrop.addEventListener('click', closeSortedOverlay);
        document.body.appendChild(backdrop);

        document.body.appendChild(overlay);
        overlay.focus();
    }

    function closeSortedOverlay() {
        const el = document.getElementById(SORTED_OVERLAY_ID);
        if (el) el.remove();
        const bd = document.getElementById(SORTED_OVERLAY_ID + '-backdrop');
        if (bd) bd.remove();
    }

    // =============================================
    // INVENTORY: Original logic
    // =============================================
    async function loadAllItemsInventory() {
        if (itemsFullyLoaded) return;
        showLoadingOverlay('Loading items, please wait…');
        const start = Date.now(), MAX_MS = 25000;
        return new Promise(resolve => {
            let lastHeight = 0, sameHeightCount = 0, attempts = 0, maxAttempts = 120;
            function finish() { itemsFullyLoaded = true; hideLoadingOverlay(); resolve(); }
            function scrollStep() {
                attempts++;
                window.scrollTo(0, document.body.scrollHeight);
                const newHeight = document.body.scrollHeight;
                if (newHeight === lastHeight) {
                    sameHeightCount++;
                    if (sameHeightCount > 5 || attempts > maxAttempts || (Date.now() - start) > MAX_MS) return finish();
                } else { sameHeightCount = 0; lastHeight = newHeight; }
                setTimeout(scrollStep, 300);
            }
            scrollStep();
        });
    }

    function parsePriceElem(priceElem) {
        if (!priceElem) return { single: 0, total: 0, qty: 0 };
        const rawText = priceElem.textContent.replace(/\u00A0/g, ' ').trim();
        if (/N\/A/i.test(rawText)) return { single: 0, total: 0, qty: 0 };
        const numTokens = (rawText.match(/\d[\d,]*/g) || []).map(s => parseInt(s.replace(/,/g, ''), 10));
        let qty = 0;
        const qtySpan = priceElem.querySelector('.tt-item-quantity');
        const qtyMatch = (qtySpan && qtySpan.textContent.match(/(\d+)/)) || rawText.match(/(\d+)\s*x/i);
        if (qtyMatch) qty = parseInt(qtyMatch[1], 10);
        if (qty) {
            const total = numTokens[numTokens.length - 1] || 0;
            let single = numTokens.find(n => n !== qty && n !== total) || 0;
            if (!single && total && qty) single = Math.round(total / qty);
            return { single, total, qty };
        }
        if (!numTokens.length) return { single: 0, total: 0, qty: 0 };
        if (numTokens.length === 1) return { single: numTokens[0], total: numTokens[0], qty: 0 };
        const max = Math.max(...numTokens);
        const min = Math.min(...numTokens);
        if (max % min === 0 && (max / min) <= 1000) return { single: min, total: max, qty: Math.round(max / min) };
        return { single: numTokens[0], total: max, qty: 0 };
    }

    function isVisible(el) { return !!(el && (el.offsetWidth || el.offsetHeight || el.getClientRects().length)); }

    function getVisibleItemContainers() {
        let containers = Array.from(document.querySelectorAll('.items-cont, .itemsList, ul.items-cont, ul.itemsList')).filter(isVisible);
        if (!containers.length) {
            containers = Array.from(document.querySelectorAll('ul')).filter(u => /-items$/.test(u.id) && isVisible(u));
        }
        return containers;
    }

    async function getCirculationMap() {
        if (circulationMap) return circulationMap;
        if (!API_KEY) {
            setStatus('Circulation: no API key set in script');
            console.warn('[TM Sort] Circulation requires API_KEY constant at top of script.');
            return null;
        }
        try {
            setStatus('Fetching circulation data…');
            const res = await fetch(`https://api.torn.com/torn/?selections=items&key=${API_KEY}`);
            const data = await res.json();
            if (data.error) {
                setStatus(`Circulation API error: ${data.error.error || data.error}`);
                console.warn('[TM Sort] Torn API error:', data.error);
                return null;
            }
            const map = {};
            if (data.items) Object.values(data.items).forEach(item => { if (item?.name) map[item.name] = item.circulation || 0; });
            circulationMap = map;
            console.log('[TM Sort] Circulation map loaded:', Object.keys(map).length, 'items');
            return map;
        } catch (e) {
            setStatus('Circulation fetch failed (network)');
            console.warn('[TM Sort] Circulation fetch failed:', e);
            return null;
        }
    }

    async function sortInventoryLists(sortType, order) {
        const containers = getVisibleItemContainers();
        let cMap = null;
        if (sortType === 'circulation') {
            cMap = await getCirculationMap();
            if (!cMap) return false;
        }

        containers.forEach(container => {
            const items = Array.from(container.children).filter(el => el.tagName === 'LI');
            if (!items.length) return;
            const pairs = items.map((li, i) => {
                let val = 0;
                if (sortType === 'single' || sortType === 'total') {
                    const priceElem = li.querySelector('.tt-item-price');
                    const parsed = parsePriceElem(priceElem);
                    val = sortType === 'single' ? parsed.single : parsed.total;
                } else if (sortType === 'circulation') {
                    const nameElem = li.querySelector('.tt-item-name') || li.querySelector('.name-wrap .name') || li.querySelector('.title-wrap .name') || li.querySelector('.name');
                    const itemName = nameElem ? nameElem.textContent.trim() : '';
                    val = (itemName && cMap && Object.prototype.hasOwnProperty.call(cMap, itemName)) ? cMap[itemName] : 0;
                }
                return { li, val, index: i };
            });
            pairs.sort((a, b) => {
                if (a.val === b.val) return a.index - b.index;
                return order === 'desc' ? b.val - a.val : a.val - b.val;
            });
            pairs.forEach(p => container.appendChild(p.li));
        });
    }

    // === Sort for MARKET ===
    async function sortMarket(sortType, order) {
        const scopeKey = getActiveMarketScopeKey();
        if (isForceAllChecked() && marketCacheByScope.has(scopeKey)) clickAllItemsTab();
        if (marketCollectedScopeKey !== scopeKey) {
            marketCollectedItems = marketCacheByScope.get(scopeKey) || [];
            if (marketCollectedItems.length) marketCollectedScopeKey = scopeKey;
        }
        if (!marketCollectedItems.length) {
            marketCollectedItems = await collectAllMarketItems();
        }
        if (!marketCollectedItems.length) { setStatus('No items found!'); return; }

        let cMap = null;
        if (sortType === 'circulation') {
            cMap = await getCirculationMap();
            if (!cMap) return;
        }

        const sortable = marketCollectedItems.map((item, i) => {
            let val = 0;
            if (sortType === 'rrp' || sortType === 'single') val = item.singleRrp || item.rrp;
            else if (sortType === 'total') val = item.totalRrp || ((item.singleRrp || item.rrp) * item.qty);
            else if (sortType === 'qty') val = item.qty;
            else if (sortType === 'circulation') val = (item.name && cMap && cMap[item.name]) ? cMap[item.name] : 0;
            return { ...item, val, origIndex: i };
        });

        sortable.sort((a, b) => {
            if (a.val === b.val) return a.origIndex - b.origIndex;
            return order === 'desc' ? b.val - a.val : a.val - b.val;
        });

        const labelMap = { total: 'Total Value', single: 'Single Value', circulation: 'Circulation', qty: 'Quantity' };
        const sortLabel = labelMap[sortType] || sortType;
        showSortedOverlay(sortable, sortLabel);
        setStatus(`Sorted ${sortable.length} items by ${sortLabel} (${order})`);
    }

    async function sortVisibleLists(sortType, order) {
        if (isMarketPage()) await sortMarket(sortType, order);
        else await sortInventoryLists(sortType, order);
    }

    function formatNum(n) { return n.toLocaleString('en-US'); }

    function isFactionItem(li) {
        return !!li.querySelector('.option-return-to-faction, .return, [data-action="return"], [data-type="armoury"], [data-armoryid]');
    }

    function computeTotalValue() {
        if (isMarketPage()) {
            let sum = 0;
            marketCollectedItems.forEach(item => { if (item.hasPrice) sum += item.totalRrp || ((item.singleRrp || item.rrp) * item.qty); });
            return sum;
        }
        const containers = getVisibleItemContainers();
        let sum = 0;
        containers.forEach(container => {
            Array.from(container.children).filter(el =>
                el.tagName === 'LI' && isVisible(el) && !el.classList.contains('tm-hidden-green') && !isFactionItem(el)
            ).forEach(li => {
                const { total } = parsePriceElem(li.querySelector('.tt-item-price'));
                if (Number.isFinite(total)) sum += total || 0;
            });
        });
        return sum;
    }

    function updateTotalDisplay() {
        const node = getTotalNode();
        if (!node) return;
        if (!hasSorted) { node.textContent = TOTAL_PLACEHOLDER; return; }
        node.textContent = `Total value: $${formatNum(computeTotalValue())}`;
    }

    function applyGreenVisibility(scope = document) {
        suppressGreenObserver = true;
        scope.querySelectorAll('li.bg-green').forEach(el => el.classList.toggle('tm-hidden-green', hideEquipped));
        suppressGreenObserver = false;
        updateTotalDisplay();
    }

    function addSortButtonsOnce() {
        const panel = ensurePanel();
        const row = getButtonsRow();
        if (row.dataset.tmButtonsAdded === '1') return;

        const onMarket = isMarketPage();

        const btnSortA = createButton('Total Value', 'total');
        const btnSortB = createButton('Single Value', 'single');
        const btnCirc = createButton('Circulation', 'circulation');
        const sortButtons = [btnSortA, btnSortB, btnCirc];

        if (onMarket) sortButtons.push(createButton('Quantity', 'qty'));

        async function onSortClick(clickedBtn) {
            const sortType = clickedBtn.dataset.sortType;
            const currentOrder = clickedBtn.dataset.order || 'desc';
            sortButtons.forEach(btn => {
                if (btn === clickedBtn) return;
                btn.dataset.order = 'desc';
                btn.textContent = btn.textContent.replace(/↓|↑/, '↓');
            });
            if (isForceAllChecked() && !onMarket) await clickAllItemsTab();
            if (!onMarket) await loadAllItemsInventory();
            await sortVisibleLists(sortType, currentOrder);
            hasSorted = true;
            updateTotalDisplay();
            const newOrder = currentOrder === 'desc' ? 'asc' : 'desc';
            clickedBtn.dataset.order = newOrder;
            clickedBtn.textContent = clickedBtn.textContent.replace(/↓|↑/, newOrder === 'desc' ? '↓' : '↑');
            if (!onMarket) window.scrollTo(0, 0);
        }

        sortButtons.forEach(btn => btn.addEventListener('click', () => onSortClick(btn)));
        sortButtons.forEach(btn => row.appendChild(btn));
        row.dataset.tmButtonsAdded = '1';
        updateTotalDisplay();
    }

    // Observers
    let totalRaf = 0;
    const main = document.querySelector('#mainContainer') || document.documentElement;

    new MutationObserver(mutations => {
        if (mutations.every(m => {
            const tgt = m.target instanceof Element ? m.target : null;
            const panel = tgt && tgt.closest && tgt.closest('#' + BUTTON_CONTAINER_ID);
            return panel || (tgt && tgt.id === TOTAL_ID);
        })) return;
        if (totalRaf) return;
        totalRaf = requestAnimationFrame(() => { totalRaf = 0; updateTotalDisplay(); });
    }).observe(main, { childList: true, subtree: true, characterData: true });

    new MutationObserver(() => addSortButtonsOnce()).observe(main, { childList: true, subtree: true });

    new MutationObserver(mutations => {
        if (suppressGreenObserver || !hideEquipped) return;
        for (const m of mutations) {
            if (!m.addedNodes?.length) continue;
            m.addedNodes.forEach(node => {
                if (!(node instanceof Element)) return;
                if (node.matches?.('li.bg-green')) applyGreenVisibility(node.parentElement || document);
                else if (node.querySelectorAll?.('li.bg-green').length) applyGreenVisibility(node);
            });
        }
    }).observe(main, { childList: true, subtree: true });

    addSortButtonsOnce();
})();