TornW3B Trading Companion

Calculates the total value of items in a trade on Torn.com.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         TornW3B Trading Companion
// @namespace    http://tampermonkey.net/
// @version      1.31
// @description  Calculates the total value of items in a trade on Torn.com.
// @match        https://www.torn.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      weav3r.dev
// @connect      api.torn.com
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (typeof GM_addStyle === 'undefined') {
        window.GM_addStyle = function(css) {
            const style = document.createElement('style');
            style.textContent = css;
            (document.head || document.documentElement).appendChild(style);
        };
    }
    if (typeof GM_getValue === 'undefined') {
        window.GM_getValue = function(key, defaultValue) {
            try {
                const item = localStorage.getItem('GM_' + key);
                return item !== null ? JSON.parse(item) : defaultValue;
            } catch (e) {
                return defaultValue;
            }
        };
    }
    if (typeof GM_setValue === 'undefined') {
        window.GM_setValue = function(key, value) {
            try {
                if (value === undefined || value === null) {
                    localStorage.removeItem('GM_' + key);
                } else {
                    localStorage.setItem('GM_' + key, JSON.stringify(value));
                }
            } catch (e) {
                if (e.name === 'QuotaExceededError') {
                    const keys = Object.keys(localStorage).filter(k => k.startsWith('GM_'));
                    if (keys.length > 0) localStorage.removeItem(keys[0]);
                    localStorage.setItem('GM_' + key, JSON.stringify(value));
                }
            }
        };
    }
    if (typeof GM_xmlhttpRequest === 'undefined') {
        window.GM_xmlhttpRequest = function(options) {
            const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
            let aborted = false;
            const fetchOptions = {
                method: options.method || 'GET',
                headers: options.headers || {}
            };
            if (controller) fetchOptions.signal = controller.signal;
            if (options.data && fetchOptions.method !== 'GET') {
                fetchOptions.body = options.data;
                if (!fetchOptions.headers['Content-Type']) {
                    fetchOptions.headers['Content-Type'] = 'application/json';
                }
            }
            fetch(options.url, fetchOptions).then(response => {
                if (aborted) return;
                return response.text().then(text => {
                    if (aborted) return;
                    const result = {
                        status: response.status,
                        statusText: response.statusText,
                        responseText: text,
                        response: text,
                        readyState: 4,
                        responseHeaders: Object.fromEntries(response.headers.entries())
                    };
                    if (response.ok) {
                        options.onload && options.onload(result);
                    } else {
                        options.onerror && options.onerror(result);
                    }
                });
            }).catch(error => {
                if (!aborted) {
                    options.onerror && options.onerror({
                        status: 0,
                        statusText: error.message,
                        responseText: '',
                        response: '',
                        readyState: 4
                    });
                }
            });
            if (options.timeout) {
                setTimeout(() => {
                    if (!aborted) {
                        aborted = true;
                        if (controller) controller.abort();
                        options.ontimeout && options.ontimeout({
                            status: 0,
                            statusText: 'Request timeout',
                            responseText: '',
                            response: '',
                            readyState: 4
                        });
                    }
                }, options.timeout);
            }
            return { abort: function() { aborted = true; if (controller) controller.abort(); } };
        };
    }

    const CONFIG = {
        API_BASE: 'https://weav3r.dev/api/',
        TORN_API: 'https://api.torn.com/',
        CACHE_DURATION: 3600000,
        MAX_CACHE_SIZE: 100,
        SEVEN_HOURS: 7 * 60 * 60 * 1000,
        POLL_INTERVAL: 30000,
        POLL_BUFFER: 29000
    };

    const TORN_API_ERRORS = {
        0: 'Unknown error',
        1: 'API key is empty',
        2: 'Incorrect API key',
        3: 'Wrong type',
        4: 'Wrong fields',
        5: 'Too many requests (max 100 per minute)',
        6: 'Incorrect ID',
        7: 'Incorrect ID-entity relation',
        8: 'IP blocked due to abuse',
        9: 'API system is currently disabled',
        10: 'Key owner is in federal jail',
        11: 'Key change error (once every 60 seconds)',
        12: 'Key read error',
        13: 'Key disabled due to owner inactivity',
        14: 'Daily read limit reached',
        15: 'Temporary error',
        16: 'Access level of this key is not high enough',
        17: 'Backend error, please try again',
        18: 'API key has been paused by the owner',
        19: 'Must be migrated to crimes 2.0',
        20: 'Race not yet finished',
        21: 'Incorrect category',
        22: 'This selection is only available in API v1',
        23: 'This selection is only available in API v2',
        24: 'Temporarily closed'
    };

    const handleTornApiError = (data) => {
        if (!data?.error) return null;
        const code = data.error.code;
        const message = TORN_API_ERRORS[code] || data.error.error || 'Unknown error';
        return { code, message };
    };

    const handleHttpError = (status) => {
        if (status >= 200 && status < 300) return null;
        if (status === 400) return 'Bad request';
        if (status === 401) return 'Unauthorized';
        if (status === 403) return 'Forbidden';
        if (status === 404) return 'Not found';
        if (status === 429) return 'Too many requests';
        if (status >= 500) return 'Server error';
        return `HTTP error ${status}`;
    };

    const TRADE_EVENTS = {
        INITIATED: 4401,
        CANCELLED: 4411,
        DECLINED: 4413,
        EXPIRED: 4420,
        COMPLETED: 4430,
        ACCEPTED: 4431,
        OTHER_MONEY_ADD: 4480,
        OTHER_MONEY_REMOVE: 4481,
        OTHER_ITEMS_ADD: 4482,
        OTHER_ITEMS_REMOVE: 4483,
        OTHER_PROPERTY_ADD: 4484,
        OTHER_PROPERTY_REMOVE: 4494,
        OTHER_FACTION_ADD: 4490,
        OTHER_FACTION_REMOVE: 4491,
        OTHER_COMPANY_ADD: 4492,
        OTHER_COMPANY_REMOVE: 4493
    };

    const TERMINAL_EVENTS = [TRADE_EVENTS.COMPLETED, TRADE_EVENTS.EXPIRED, TRADE_EVENTS.CANCELLED];

    const OTHER_ADD_EVENTS = [
        TRADE_EVENTS.OTHER_ITEMS_ADD, TRADE_EVENTS.OTHER_MONEY_ADD, TRADE_EVENTS.OTHER_PROPERTY_ADD,
        TRADE_EVENTS.OTHER_FACTION_ADD, TRADE_EVENTS.OTHER_COMPANY_ADD
    ];

    const OTHER_REMOVE_EVENTS = [
        TRADE_EVENTS.OTHER_ITEMS_REMOVE, TRADE_EVENTS.OTHER_MONEY_REMOVE, TRADE_EVENTS.OTHER_PROPERTY_REMOVE,
        TRADE_EVENTS.OTHER_FACTION_REMOVE, TRADE_EVENTS.OTHER_COMPANY_REMOVE
    ];

    const TRACKED_EVENTS = [...OTHER_ADD_EVENTS, ...OTHER_REMOVE_EVENTS, TRADE_EVENTS.ACCEPTED, TRADE_EVENTS.DECLINED];

    const TRADE_STATES = {
        empty: { text: 'Empty', color: '#6b7280' },
        'needs-money': { text: 'Items Received', color: '#f59e0b' },
        'too-low': { text: 'Too Low', color: '#ef4444' },
        'too-high': { text: 'Too High', color: '#ef4444' },
        ready: { text: 'Ready', color: '#10b981' },
        accepted: { text: 'Accepted', color: '#8b5cf6' },
        completed: { text: 'Completed', color: '#22c55e' },
        expired: { text: 'Expired', color: '#6b7280' },
        cancelled: { text: 'Cancelled', color: '#ef4444' },
        declined: { text: 'Declined', color: '#ef4444' },
        'items-received': { text: 'Items Received', color: '#f59e0b' },
        'items-removed': { text: 'Items Removed', color: '#f59e0b' },
        'money-received': { text: 'Money Received', color: '#10b981' },
        'money-removed': { text: 'Money Removed', color: '#f59e0b' },
        'property-received': { text: 'Property Received', color: '#8b5cf6' },
        'property-removed': { text: 'Property Removed', color: '#f59e0b' },
        'faction-received': { text: 'Faction Received', color: '#06b6d4' },
        'faction-removed': { text: 'Faction Removed', color: '#f59e0b' },
        'company-received': { text: 'Company Received', color: '#14b8a6' },
        'company-removed': { text: 'Company Removed', color: '#f59e0b' },
        'assets-added': { text: 'Assets Added', color: '#3b82f6' },
        'assets-removed': { text: 'Assets Removed', color: '#f59e0b' },
        unknown: { text: 'Unknown', color: '#6b7280' }
    };

    const EVENT_TO_STATE = {
        [TRADE_EVENTS.OTHER_ITEMS_ADD]: 'items-received',
        [TRADE_EVENTS.OTHER_ITEMS_REMOVE]: 'items-removed',
        [TRADE_EVENTS.OTHER_MONEY_ADD]: 'money-received',
        [TRADE_EVENTS.OTHER_MONEY_REMOVE]: 'money-removed',
        [TRADE_EVENTS.OTHER_PROPERTY_ADD]: 'property-received',
        [TRADE_EVENTS.OTHER_PROPERTY_REMOVE]: 'property-removed',
        [TRADE_EVENTS.OTHER_FACTION_ADD]: 'faction-received',
        [TRADE_EVENTS.OTHER_FACTION_REMOVE]: 'faction-removed',
        [TRADE_EVENTS.OTHER_COMPANY_ADD]: 'company-received',
        [TRADE_EVENTS.OTHER_COMPANY_REMOVE]: 'company-removed',
        [TRADE_EVENTS.ACCEPTED]: 'accepted',
        [TRADE_EVENTS.DECLINED]: 'declined'
    };

    const SELECTORS = {
        TRADE_LIST: '.trades-cont.current:not(.m-bottom10)',
        TRADE_LIST_ITEM: '.trades-cont.current:not(.m-bottom10) li',
        TRADE_LINK: '.view a[href*="ID="]',
        STRIPE_CONTAINER: '.stripe-container',
        TRADE_CONTAINER: '#trade-container',
        INFO_MSG_GREEN: '.info-msg-cont.green',
        ACCEPT_BTN: '.btn.accept.torn-btn.green',
        USER_LEFT_MONEY: '.user.left .cont li.color1 ul.desc li .name.left',
        USER_RIGHT_ITEMS: '.user.right .cont li.color2 ul.desc li',
        ADD_MONEY_LINK: '.user.left .cont li.color1 .add a[href*="step=addmoney"]'
    };

    const SVG_ICONS = {
        receipt: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>',
        back: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 13S14.22 4.41 6.42 4.41V1L0 6.7l6.42 5.9V8.75c4.24 0 7.37.38 9.58 4.25"/></svg>',
        dashboard: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z"/></svg>',
        calculate: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-2h2v2zm0-4H7v-2h2v2zm0-4H7V7h2v2zm4 8h-2v-2h2v2zm0-4h-2v-2h2v2zm0-4h-2V7h2v2zm4 8h-2v-2h2v2zm0-4h-2v-2h2v2zm0-4h-2V7h2v2z"/></svg>',
        money: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1.41 16.09V20h-2.67v-1.93c-1.71-.36-3.16-1.46-3.27-3.4h1.96c.1 1.05.82 1.87 2.65 1.87 1.96 0 2.4-.98 2.4-1.59 0-.83-.44-1.61-2.67-2.14-2.48-.6-4.18-1.62-4.18-3.67 0-1.72 1.39-2.84 3.11-3.21V4h2.67v1.95c1.86.45 2.79 1.86 2.85 3.39H14.3c-.05-1.11-.64-1.87-2.22-1.87-1.5 0-2.4.68-2.4 1.64 0 .84.65 1.39 2.67 1.91s4.18 1.39 4.18 3.91c-.01 1.83-1.38 2.83-3.12 3.16z"/></svg>',
        add: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>',
        check: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>',
        paste: '<svg class="button-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>'
    };

    const isTradePage = location.pathname.startsWith('/trade.php');
    const checkIsMobile = () => window.innerWidth <= 784;
    let isMobile = checkIsMobile();

    if (isTradePage) {
        GM_addStyle(`
:root {--bg-dark:#1a1a1a;--bg-darker:#222;--bg-med:#333;--bg-light:#444;--bg-lighter:#555;--border:#444;--text:#fff;--text-muted:#e2e8f0;--success:#28a745;--success-hover:#218838;--error:#f66;--transition:all .3s ease;--shadow:0 4px 8px rgba(0,0,0,.2);--radius:6px}
.title-red + .warning-cont, .title-red + .warning-cont + hr {display:none !important}
.title-red:has(+ .warning-cont) {display:none !important}
.trade-status-indicator {font-weight:600;font-size:10px;margin-left:8px;padding:3px 7px;border-radius:3px;white-space:nowrap;display:inline-block}
.stripe-container {width:100%;background:var(--bg-med);margin:0;border-radius:8px;display:flex;justify-content:center;align-items:center;padding:15px 30px;border:1px solid var(--border);box-shadow:var(--shadow);transition:var(--transition);box-sizing:border-box;gap:10px;-webkit-tap-highlight-color:transparent;touch-action:manipulation}
.stripe-container.expanded {justify-content:space-around}
.calculate-button,.total-value-container,.receipt-url-container,.copy-url-button,.view-edit-receipt-button,.accept-trade-button {padding:10px 20px;background:linear-gradient(145deg,var(--bg-light),var(--bg-lighter));color:var(--text);border:1px solid var(--bg-med);border-radius:var(--radius);cursor:pointer;font:14px 'Segoe UI',Tahoma,sans-serif;margin:0;transition:var(--transition),transform .2s;text-align:center;white-space:nowrap;min-width:150px;flex:0 1 auto;-webkit-tap-highlight-color:transparent;touch-action:manipulation;user-select:none;-webkit-user-select:none}
.calculate-button:hover,.total-value-container:hover,.receipt-url-container:hover,.copy-url-button:hover,.view-edit-receipt-button:hover,.accept-trade-button:hover:not(:disabled),.calculate-button:active,.total-value-container:active,.receipt-url-container:active,.copy-url-button:active,.view-edit-receipt-button:active,.accept-trade-button:active:not(:disabled) {background:linear-gradient(145deg,var(--bg-lighter),var(--bg-light));transform:translateY(-2px)}
.accept-trade-button:disabled {opacity:.5;cursor:not-allowed;background:linear-gradient(145deg,var(--bg-med),var(--bg-light))}
.accept-trade-button.enabled {background:linear-gradient(145deg,var(--success),var(--success-hover))}
.accept-trade-button.enabled:hover {background:linear-gradient(145deg,var(--success-hover),var(--success))}
.accept-trade-button.button-accepted {background:linear-gradient(145deg,#8b5cf6,#7c3aed)}
.accept-trade-button.button-error {background:linear-gradient(145deg,#dc2626,#b91c1c)}
.button-icon {display:none}
.button-text {display:inline}
.value-container {display:none;align-items:center;justify-content:space-evenly;flex-grow:1}
.value-container.visible {display:flex}
.hidden {display:none}
.content-hidden {display:none}
.content-visible {display:block}
.copy-confirmation {color:#6b6 !important;transition:color .3s;pointer-events:none}
.error-button {background-color:var(--error) !important;border-color:#c00 !important}
a {color:#e0e0e0;text-decoration:none;cursor:pointer;transition:color .3s}
a:hover {color:var(--text)}
.receipt-modal-overlay {position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:1000;display:flex;justify-content:center;align-items:center;opacity:0;visibility:hidden;transition:opacity .3s,visibility .3s;backdrop-filter:blur(2px)}
.receipt-modal-overlay.visible {opacity:1;visibility:visible}
.receipt-modal {background:var(--bg-dark);border-radius:12px;width:90%;max-width:700px;max-height:85vh;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,.7);border:1px solid var(--border);transform:translateY(20px);opacity:0;transition:transform .3s,opacity .3s}
.receipt-modal-overlay.visible .receipt-modal {transform:translateY(0);opacity:1}
.modal-header {background:var(--bg-darker);padding:12px 20px;border-bottom:1px solid var(--border);display:flex;justify-content:flex-end;align-items:center;border-radius:12px 12px 0 0}
.close-modal {background:none;border:none;color:var(--text);font-size:24px;cursor:pointer;padding:0;opacity:.8;transition:opacity .2s;line-height:1}
.close-modal:hover {opacity:1}
.modal-content {padding:20px;background:var(--bg-dark);flex:1;overflow-y:auto;border-radius:0 0 12px 12px}
.receipt-content {height:100%;display:flex;flex-direction:column;gap:16px}
.loading-spinner {width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin:20px auto}
@keyframes spin {to {transform:rotate(360deg)}}
.items-table {width:100%;border-collapse:separate;border-spacing:0;margin:0;background:var(--bg-dark);font-size:14px;border:1px solid var(--border);border-radius:6px;overflow:hidden}
.items-table th {background:var(--bg-med);font:600 13px/1 'Segoe UI';text-transform:uppercase;letter-spacing:.5px;padding:14px 12px;color:var(--text);text-align:center;border-bottom:2px solid var(--border)}
.items-table td {background:var(--bg-darker);border-bottom:1px solid var(--border);padding:8px 12px !important;text-align:center;color:var(--text-muted);font-family:'Segoe UI',sans-serif;transition:background .2s;vertical-align:middle}
.items-table tbody tr:last-child td {border-bottom:none}
.items-table tbody tr:hover td {background:var(--bg-light)}
.price-editable {cursor:pointer;position:relative;padding-right:24px;transition:background .2s}
.price-editable:hover {background:rgba(59,130,246,0.1)}
.price-editable::after {content:'✎';position:absolute;right:8px;top:50%;transform:translateY(-50%);opacity:0;transition:opacity .2s;font-size:13px;color:#4a9eff}
.price-editable:hover::after {opacity:.8}
.price-edited {position:relative}
.price-edited::after {content:'*';color:#fbbf24;position:absolute;right:4px;top:2px;font-size:16px;font-weight:bold}
.save-changes-button {transition:var(--transition);position:relative;overflow:hidden;padding:10px 20px;background:linear-gradient(145deg,var(--bg-light),var(--bg-lighter));color:var(--text);border:1px solid var(--border);border-radius:6px;cursor:pointer;font:500 14px 'Segoe UI';width:auto;margin-top:12px;display:inline-block}
.save-changes-button:hover {background:linear-gradient(145deg,var(--bg-lighter),var(--bg-light))}
.save-changes-button.saving {background:#666;cursor:wait}
.save-changes-button.saving::after {content:'Saving...';position:absolute;inset:0;background:#666;display:flex;align-items:center;justify-content:center}
.save-changes-button.saved {background:linear-gradient(145deg,#10b981,#059669)}
.save-changes-button.error {background:var(--error);animation:shake .5s}
.save-changes-button.save-button-hidden {display:none}
.receipt-url-copy {margin-bottom:12px;padding:14px;background:var(--bg-darker);border-radius:6px;border:1px solid var(--border)}
.url-display {display:flex;align-items:center;justify-content:space-between;gap:12px}
.url-text {color:var(--text-muted);font:12px 'Courier New',monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
.items-table input[type="text"] {background:var(--bg-med);border:2px solid #4a9eff;color:var(--text);border-radius:4px;font:14px 'Segoe UI',sans-serif;width:100%;padding:8px;box-shadow:0 0 0 3px rgba(74,158,255,0.1);outline:none;text-align:center}
.items-table input[type="text"]:focus {border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,0.2)}
.editable-input {width:100px;padding:6px;text-align:center;font-size:15px}
.copy-url-button,.view-edit-receipt-button {background:linear-gradient(145deg,var(--bg-light),var(--bg-lighter));border:1px solid var(--border);border-radius:6px;cursor:pointer;font-weight:600;transition:var(--transition)}
.copy-url-button:hover,.view-edit-receipt-button:hover {background:linear-gradient(145deg,var(--bg-lighter),var(--bg-light));transform:translateY(-1px)}
@media (max-width:784px) {.editable-input {width:80px;font-size:14px}}
.calculate-button.calculating {opacity:.7;cursor:not-allowed}
.calculate-button.error {animation:shake .5s;background-color:#f44}
@keyframes shake {0%,100% {transform:translateX(0)}25% {transform:translateX(-5px)}75% {transform:translateX(5px)}}
@keyframes flashNew {0%,100% {background:transparent}50% {background:rgba(59,130,246,0.1)}}
.flash-new {animation:flashNew 1s ease-in-out}
.flash-update {animation:flashNew 0.5s ease-in-out}
.fade-out {transition:opacity 0.3s,transform 0.3s;opacity:0;transform:translateX(-20px)}
.item-image {max-width:50px;max-height:50px;display:block;margin:0 auto}
.item-name-colored {transition:color 0.3s ease}
.mobile-input-font {font-size:16px}
.info-hidden {display:none}
.msg.right-round button.api-key-button,.title-black button.api-key-button {font-size:10px !important;padding:4px 8px !important;margin-left:8px !important;background:rgba(255,255,255,0.5) !important;border:1px solid rgba(0,0,0,0.15) !important;border-radius:3px !important;color:#555 !important;cursor:pointer !important;vertical-align:middle !important;display:inline-block !important}
body.dark-mode .msg.right-round button.api-key-button,body.dark-mode .title-black button.api-key-button {background:rgba(0,0,0,0.35) !important;border:1px solid rgba(255,255,255,0.1) !important;color:#999 !important}
@media only screen and (max-width:784px), only screen and (max-device-width:784px) {
.stripe-container {display:flex !important;flex-direction:row !important;flex-wrap:wrap !important;gap:8px !important;padding:15px !important;align-items:stretch !important;justify-content:space-between !important}
.stripe-container>.value-container,.stripe-container>.value-container.visible {display:contents !important;flex-direction:unset !important}
.stripe-container>.value-container>.total-value-container,.stripe-container .total-value-container {width:100% !important;max-width:100% !important;min-width:100% !important;flex:0 0 100% !important;margin:0 !important;padding:12px 20px !important;font-size:16px !important;box-shadow:0 2px 4px rgba(0,0,0,.2) !important;order:-3 !important;box-sizing:border-box !important}
.stripe-container>.calculate-button {width:calc(33.333% - 5.33px) !important;max-width:calc(33.333% - 5.33px) !important;min-width:0 !important;flex:0 0 calc(33.333% - 5.33px) !important;margin:0 !important;padding:12px 6px !important;font-size:12px !important;box-shadow:0 2px 4px rgba(0,0,0,.2) !important;order:-2 !important;white-space:nowrap !important;overflow:hidden !important;box-sizing:border-box !important;text-overflow:clip !important}
.stripe-container>.value-container>.receipt-url-container,.stripe-container .receipt-url-container {width:calc(33.333% - 5.33px) !important;max-width:calc(33.333% - 5.33px) !important;min-width:0 !important;flex:0 0 calc(33.333% - 5.33px) !important;margin:0 !important;padding:12px 6px !important;font-size:12px !important;box-shadow:0 2px 4px rgba(0,0,0,.2) !important;order:-1 !important;white-space:nowrap !important;overflow:hidden !important;box-sizing:border-box !important;text-overflow:clip !important}
.stripe-container>.accept-trade-button {width:calc(33.333% - 5.33px) !important;max-width:calc(33.333% - 5.33px) !important;min-width:0 !important;flex:0 0 calc(33.333% - 5.33px) !important;margin:0 !important;padding:12px 6px !important;font-size:12px !important;box-shadow:0 2px 4px rgba(0,0,0,.2) !important;order:0 !important;white-space:nowrap !important;overflow:hidden !important;box-sizing:border-box !important;text-overflow:clip !important}
.stripe-container .button-icon {display:inline-block !important;width:18px !important;height:18px !important;vertical-align:middle !important}
.stripe-container .button-text {display:none !important}
.copy-url-button,.view-edit-receipt-button {width:auto;padding:10px 16px;touch-action:manipulation;font-size:13px}
.receipt-modal {width:95%;max-width:95%;margin:10px;max-height:90vh}
.modal-header {padding:14px 18px}
.modal-header h2 {font-size:1.1em}
.close-modal {font-size:20px;padding:3px 8px}
.modal-content {padding:16px}
.items-table {font-size:13px}
.items-table th {padding:10px 8px;font-size:13px}
.items-table td {padding:8px 8px !important;font-size:13px}
.url-display {flex-direction:row;gap:8px;flex-wrap:wrap}
.url-text {font-size:11px}
}
@media only screen and (max-width:480px), only screen and (max-device-width:480px) {
.stripe-container {display:flex !important;flex-direction:row !important;flex-wrap:wrap !important;gap:6px !important;padding:12px !important;align-items:stretch !important;justify-content:space-between !important}
.stripe-container>.value-container,.stripe-container>.value-container.visible {display:contents !important;flex-direction:unset !important}
.stripe-container>.value-container>.total-value-container,.stripe-container .total-value-container {width:100% !important;max-width:100% !important;min-width:100% !important;flex:0 0 100% !important;margin:0 !important;padding:14px !important;font-size:14px !important;order:-3 !important;box-sizing:border-box !important}
.stripe-container>.calculate-button {width:calc(33.333% - 4px) !important;max-width:calc(33.333% - 4px) !important;min-width:0 !important;flex:0 0 calc(33.333% - 4px) !important;margin:0 !important;padding:12px 3px !important;font-size:11px !important;order:-2 !important;white-space:nowrap !important;overflow:hidden !important;box-sizing:border-box !important;text-overflow:clip !important}
.stripe-container>.value-container>.receipt-url-container,.stripe-container .receipt-url-container {width:calc(33.333% - 4px) !important;max-width:calc(33.333% - 4px) !important;min-width:0 !important;flex:0 0 calc(33.333% - 4px) !important;margin:0 !important;padding:12px 3px !important;font-size:11px !important;order:-1 !important;white-space:nowrap !important;overflow:hidden !important;box-sizing:border-box !important;text-overflow:clip !important}
.stripe-container>.accept-trade-button {width:calc(33.333% - 4px) !important;max-width:calc(33.333% - 4px) !important;min-width:0 !important;flex:0 0 calc(33.333% - 4px) !important;margin:0 !important;padding:12px 3px !important;font-size:11px !important;order:0 !important;white-space:nowrap !important;overflow:hidden !important;box-sizing:border-box !important;text-overflow:clip !important}
.stripe-container .button-icon {display:inline-block !important;width:16px !important;height:16px !important;vertical-align:middle !important}
.receipt-modal {width:100%;max-width:100%;margin:5px;max-height:95vh}
.modal-header {padding:12px 16px}
.modal-header h2 {font-size:1em}
.close-modal {font-size:18px;padding:2px 7px}
.modal-content {padding:14px}
.items-table {font-size:12px}
.items-table th {padding:9px 6px;font-size:12px}
.items-table td {padding:8px 6px !important;font-size:12px}
.save-changes-button {width:100%;margin:12px 0;touch-action:manipulation}
.copy-url-button,.view-edit-receipt-button {width:auto;padding:10px 14px;touch-action:manipulation;font-size:12px}
.url-display {flex-direction:row;gap:6px;flex-wrap:wrap}
.url-text {font-size:10px}
.editable-input {width:70px;font-size:13px;padding:5px}
}
.new-trade-toggle-header{cursor:pointer;position:relative}
.new-trade-toggle-header::after{content:'▼';font-size:11px;position:absolute;right:10px;top:50%;transform:translateY(-50%);opacity:.7;transition:transform .2s}
.new-trade-toggle-header.expanded::after{transform:translateY(-50%) rotate(-180deg)}
`);
    }

    if (isTradePage) {
        if (!document.querySelector('meta[name="viewport"]')) {
            const viewport = document.createElement('meta');
            viewport.name = 'viewport';
            viewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
            document.head.appendChild(viewport);
        }
    }

    const storage = {
        get: (keys) => Promise.resolve((Array.isArray(keys) ? keys : [keys]).reduce((acc, key) => {
            const val = GM_getValue(key);
            if (val !== undefined) acc[key] = val;
            return acc;
        }, {})),
        set: (items) => Promise.resolve(Object.entries(items).forEach(([k, v]) => GM_setValue(k, v)))
    };

    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    const retry = async (fn, maxAttempts = 5, delay = 100) => {
        for (let attempt = 1; attempt <= maxAttempts; attempt++) {
            try {
                const result = await fn();
                if (result !== false && result !== null && result !== undefined) {
                    return result;
                }
            } catch (error) {
                if (attempt === maxAttempts) throw error;
            }
            if (attempt < maxAttempts) await sleep(delay);
        }
        return null;
    };

    const debounce = (func, wait) => {
        const actualWait = isMobile ? wait * 1.5 : wait;
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func(...args), actualWait);
        };
    };

    const updateMobileStatus = () => {
        const wasMobile = isMobile;
        isMobile = checkIsMobile();
        if (wasMobile !== isMobile) console.log('Mobile status changed:', isMobile);
    };

    window.addEventListener('resize', debounce(updateMobileStatus, 250));

    const userProfileCache = new Map();
    let storageBatch = {};
    let storageBatchTimeout = null;

    const flushStorageBatch = () => {
        if (Object.keys(storageBatch).length > 0) {
            storage.set(storageBatch);
            storageBatch = {};
        }
    };

    const batchedStorage = {
        set: (key, value) => {
            storageBatch[key] = value;
            clearTimeout(storageBatchTimeout);
            storageBatchTimeout = setTimeout(flushStorageBatch, 50);
        },
        flush: flushStorageBatch
    };

    const apiKey = {
        get: async () => (await storage.get(['torn_api_key'])).torn_api_key || null,
        set: (key) => storage.set({ torn_api_key: key }),
        remove: () => storage.set({ torn_api_key: undefined }),
        ensure: async () => {
            let key = await apiKey.get();
            if (!key) {
                key = prompt('Please enter your Torn API key for real-time trade updates:');
                if (key) await apiKey.set(key);
            }
            return key;
        }
    };

    const fetchUserProfile = async (userID) => {
        const cached = userProfileCache.get(userID);
        if (cached && Date.now() - cached.timestamp < CONFIG.CACHE_DURATION) return cached.data;

        const key = await apiKey.get();
        if (!key) return null;

        try {
            const response = await fetch(`${CONFIG.TORN_API}user/${userID}?key=${key}&comment=AutoTrade&selections=profile`);
            const httpError = handleHttpError(response.status);
            if (httpError) return null;

            const data = await response.json();
            const tornError = handleTornApiError(data);
            if (tornError) {
                if (tornError.code === 2) {
                    await storage.set({ torn_api_key: undefined });
                }
                return null;
            }

            if (userProfileCache.size >= CONFIG.MAX_CACHE_SIZE) {
                userProfileCache.delete(userProfileCache.keys().next().value);
            }
            userProfileCache.set(userID, { data, timestamp: Date.now() });
            return data;
        } catch {
            return null;
        }
    };

    const addHandler = (elem, handler, preventDisabled = false) => {
        elem._clickHandler = handler;
        elem.addEventListener('click', handler);
        if (isMobile) {
            elem._touchHandler = (e) => {
                if (!preventDisabled || !elem.disabled) {
                    e.preventDefault();
                    handler(e);
                }
            };
            elem.addEventListener('touchend', elem._touchHandler, { passive: false });
        }
    };

    const removeHandler = (elem) => {
        if (elem._clickHandler) {
            elem.removeEventListener('click', elem._clickHandler);
            if (isMobile && elem._touchHandler) {
                elem.removeEventListener('touchend', elem._touchHandler);
            }
        }
    };

    const el = (tag, { classes, text, html, attributes, style, onClick, children } = {}) => {
        const e = document.createElement(tag);
        if (classes) e.classList.add(...(Array.isArray(classes) ? classes : [classes]));
        if (text) e.textContent = text;
        if (html) e.innerHTML = html;
        if (attributes) Object.entries(attributes).forEach(([k, v]) => e.setAttribute(k, v));
        if (style) Object.assign(e.style, style);
        if (onClick) addHandler(e, onClick, true);
        if (children) children.forEach(c => c && e.appendChild(c));
        return e;
    };

    const q = (sel) => document.querySelector(sel);
    const qa = (sel) => document.querySelectorAll(sel);
    const getParam = (name) => new URLSearchParams(location.hash.substring(1)).get(name);
    const getTradeID = () => getParam('ID');
    const getStep = () => getParam('step');

    let lastProcessedEventID = null;
    let tradePollingInterval = null;
    let timeRefreshInterval = null;
    let isFirstPoll = true;
    let lastPollTime = 0;
    let apiTradeData = {};
    let currentReceipt = null;
    let currentReceiptURL = null;

    const initTradeData = () => ({
        items: [],
        moneyTotal: 0,
        hasItems: false,
        hasMoney: false,
        hasProperty: false,
        hasFaction: false,
        hasCompany: false,
        accepted: false,
        declined: false,
        lastEventType: null,
        lastEventState: null
    });

    const getUsername = () => q('.user.right .title-black')?.childNodes[0]?.nodeValue?.trim() || null;

    const getCurrentUserID = () => {
        try {
            const tornUserInput = q('#torn-user');
            if (tornUserInput?.value) {
                const userData = JSON.parse(tornUserInput.value);
                if (userData?.id) {
                    return userData.id;
                }
            }
        } catch (e) { console.error('Failed to get user ID:', e); }
        return null;
    };

    const getMoneyInTrade = () => {
        const match = q(SELECTORS.USER_LEFT_MONEY)?.textContent?.trim()?.match(/\$([0-9,]+)/);
        return match ? parseInt(match[1].replace(/,/g, ''), 10) : 0;
    };

    const moneyMatches = () => currentReceipt?.total_value && getMoneyInTrade() === currentReceipt.total_value;

    const parseItems = () => {
        const items = [];
        qa(SELECTORS.USER_RIGHT_ITEMS).forEach(row => {
            const name = row.querySelector('.name.left')?.childNodes[0]?.nodeValue?.trim()?.match(/^(.+?)(?:\s+x(\d+))?$/);
            if (name) items.push({ name: name[1].trim(), quantity: parseInt(name[2] || 1, 10) });
        });
        return items;
    };

    const parseValidItems = () => parseItems().filter(i => i.name.toLowerCase() !== "no items in trade");

    const itemsMatch = (items1, items2) => {
        if (items1.length !== items2.length) return false;
        const sort = arr => [...arr].sort((a, b) => a.name.localeCompare(b.name));
        const [s1, s2] = [sort(items1), sort(items2)];
        return s1.every((item, i) => item.name === s2[i].name && item.quantity === s2[i].quantity);
    };

    const getStoredData = async (tradeID) => (await storage.get([`trade_${tradeID}`]))[`trade_${tradeID}`] || null;

    const storeData = async (tradeID, items, receipt, receiptURL) => {
        const existing = await getStoredData(tradeID);
        const manual_prices = existing?.manual_prices || {};
        return storage.set({
            [`trade_${tradeID}`]: { items, receipt, receiptURL, timestamp: Date.now(), total_value: receipt.total_value, manual_prices }
        });
    };

    const getTradeState = async (tradeID) => {
        const key = `trade_state_${tradeID}`;
        const data = (await storage.get([key]))[key];
        if (!data) return null;
        if (Date.now() - data.timestamp > CONFIG.SEVEN_HOURS) {
            await storage.set({ [key]: undefined });
            return null;
        }
        return data;
    };

    const setTradeState = async (tradeID, state, additionalData = {}) => {
        batchedStorage.set(`trade_state_${tradeID}`, { state, timestamp: Date.now(), ...additionalData });
    };

    const formatTimeAgo = (timestamp) => {
        const s = Math.floor((Date.now() - timestamp) / 1000);
        if (s < 60) return `${s}s ago`;
        const m = Math.floor(s / 60);
        if (m < 60) return `${m}m ago`;
        const h = Math.floor(m / 60);
        return h < 24 ? `${h}h ago` : `${Math.floor(h / 24)}d ago`;
    };

    const calculateTimeRemaining = (timestamp) => {
        const remaining = (timestamp * 1000 + 6 * 60 * 60 * 1000) - Date.now();
        if (remaining <= 0) return '0 mins';
        const hours = Math.floor(remaining / (60 * 60 * 1000));
        const minutes = Math.floor((remaining % (60 * 60 * 1000)) / (60 * 1000));
        return hours > 0 ? `${hours} hrs ${minutes} mins` : `${minutes} mins`;
    };

    const getReceiptIdFromURL = (receiptURL) => {
        if (!receiptURL) return null;
        try {
            const url = new URL(receiptURL);
            const pathParts = url.pathname.split('/');
            const receiptIndex = pathParts.indexOf('receipt');
            if (receiptIndex !== -1 && receiptIndex < pathParts.length - 1) {
                return pathParts[receiptIndex + 1];
            }
        } catch (e) { console.error('Failed to parse receipt URL:', e); }
        return null;
    };

    const apiCall = (endpoint, data) => new Promise((resolve) => {
        const defaultResponse = endpoint.startsWith('pricelist/') ? { receipt: null, receiptURL: null, error: null } : { success: false, error: null };
        GM_xmlhttpRequest({
            method: 'POST',
            url: `${CONFIG.API_BASE}${endpoint}`,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify(data),
            onload: (r) => {
                const httpError = handleHttpError(r.status);
                if (httpError) {
                    resolve({ ...defaultResponse, error: httpError });
                    return;
                }
                try {
                    const parsed = JSON.parse(r.responseText);
                    if (parsed.error) {
                        resolve({ ...defaultResponse, error: parsed.error });
                        return;
                    }
                    resolve(parsed);
                } catch {
                    resolve({ ...defaultResponse, error: 'Invalid response' });
                }
            },
            onerror: () => resolve({ ...defaultResponse, error: 'Network error' }),
            ontimeout: () => resolve({ ...defaultResponse, error: 'Request timeout' })
        });
    });

    const showMsg = async (element, message, duration = 1000, className = 'copy-confirmation') => {
        const span = element.querySelector('.button-text');
        const orig = span ? span.textContent : (element.children.length ? element.innerHTML : element.textContent);
        const prop = element.children.length ? 'innerHTML' : 'textContent';

        span ? (span.textContent = message) : (element[prop] = message);
        element.classList.add(className);

        await sleep(duration);
        span ? (span.textContent = orig) : (element[prop] = orig);
        element.classList.remove(className);
    };

    const createButton = (classes, iconSvg, text, onClick) =>
        el('button', { classes, html: `${iconSvg}<span class="button-text">${text}</span>`, onClick });

    const setButtonState = (btn, { icon, text, disabled, enabled, background, onClick }) => {
        const iconEl = btn.querySelector('.button-icon');
        const textEl = btn.querySelector('.button-text');

        if (iconEl && icon) iconEl.outerHTML = icon;
        if (textEl && text) textEl.textContent = text;
        if (!iconEl || !textEl) btn.innerHTML = `${icon}<span class="button-text">${text}</span>`;

        btn.disabled = disabled;
        btn.classList.toggle('enabled', enabled);
        btn.classList.toggle('button-accepted', background && background.includes('8b5cf6'));
        btn.classList.toggle('button-error', background && background.includes('dc2626'));
        btn.style.background = background || '';

        removeHandler(btn);
        if (onClick) addHandler(btn, onClick, true);
    };

    const determineTradeState = () => {
        const items = parseValidItems();
        const hasItems = items.length > 0;
        const moneyInTrade = getMoneyInTrade();
        const expectedValue = currentReceipt?.total_value || 0;
        const acceptButtonExists = !!q(SELECTORS.ACCEPT_BTN);
        const tradeID = getTradeID();
        const tradeData = tradeID ? apiTradeData[tradeID] : null;
        const hasAnyAssets = hasItems || tradeData?.hasProperty || tradeData?.hasFaction || tradeData?.hasCompany;

        if (!hasAnyAssets) return 'empty';
        if (moneyInTrade === 0) return 'needs-money';
        if (expectedValue > 0 && moneyInTrade < expectedValue) return 'too-low';
        if (expectedValue > 0 && moneyInTrade > expectedValue) return 'too-high';
        if (moneyInTrade > 0 && !acceptButtonExists) return 'accepted';
        if (moneyInTrade === expectedValue && acceptButtonExists) return 'ready';
        return 'unknown';
    };

    const inferStateFromAPI = async (tradeID) => {
        const apiData = apiTradeData[tradeID];
        if (!apiData) return null;

        let inferredState = null;
        const hasAnyAssets = apiData.hasItems || apiData.hasProperty || apiData.hasFaction || apiData.hasCompany;

        if (apiData.declined && apiData.lastEventType === TRADE_EVENTS.DECLINED) {
            inferredState = 'declined';
        } else if (apiData.accepted) {
            inferredState = 'accepted';
        } else if (apiData.lastEventState && EVENT_TO_STATE[apiData.lastEventType]) {
            inferredState = EVENT_TO_STATE[apiData.lastEventType];
        } else if (!hasAnyAssets && !apiData.hasMoney) {
            inferredState = 'empty';
        } else if (hasAnyAssets && !apiData.hasMoney) {
            inferredState = 'needs-money';
        } else if (hasAnyAssets && apiData.hasMoney) {
            inferredState = 'ready';
        }

        if (inferredState) {
            const existingState = await getTradeState(tradeID);
            const stateChanged = !existingState || existingState.state !== inferredState;

            const additionalData = {
                totalValue: apiData.moneyTotal || 0,
                eventTimestamp: stateChanged ? apiData.lastEventTime : (existingState?.eventTimestamp || apiData.lastEventTime),
                lastEventType: apiData.lastEventType
            };

            if (apiData.accepted && apiData.acceptedAt) {
                additionalData.acceptedAt = stateChanged ? apiData.acceptedAt : (existingState?.acceptedAt || apiData.acceptedAt);
            }
            if (apiData.declined && apiData.declinedAt) {
                additionalData.declinedAt = stateChanged ? apiData.declinedAt : (existingState?.declinedAt || apiData.declinedAt);
            }

            await setTradeState(tradeID, inferredState, additionalData);
        }

        return inferredState;
    };

    const getStateDisplay = async (stateData, tradeID = null) => {
        if (!stateData && tradeID && apiTradeData[tradeID]) {
            const inferred = await inferStateFromAPI(tradeID);
            if (inferred) stateData = await getTradeState(tradeID);
        }

        if (!stateData) {
            if (tradeID && !apiTradeData[tradeID]) return null;
            return { text: 'Not opened', color: '#6b7280' };
        }

        const state = stateData.state;
        const baseDisplay = TRADE_STATES[state] || { text: 'Not opened', color: '#6b7280' };
        const timestampToUse = (state === 'accepted' && stateData.acceptedAt) ||
                               (state === 'declined' && stateData.declinedAt) ||
                               stateData.eventTimestamp || stateData.timestamp;
        const skipValueStates = ['empty', 'completed', 'expired', 'cancelled', 'declined'];

        let displayText = baseDisplay.text;
        if (timestampToUse) displayText += ` ${formatTimeAgo(timestampToUse)}`;
        if (stateData.totalValue && !skipValueStates.includes(state)) {
            displayText += ` ($${stateData.totalValue.toLocaleString()})`;
        }

        return { text: displayText, color: baseDisplay.color };
    };

    const updateTradeState = async () => {
        const tradeID = getTradeID();
        if (!tradeID || getStep() !== 'view') return;

        const state = determineTradeState();
        const existing = await getTradeState(tradeID);
        const additionalData = { totalValue: currentReceipt?.total_value || 0 };

        if (state === 'accepted' || existing?.acceptedAt) {
            additionalData.acceptedAt = existing?.acceptedAt || Date.now();
        }

        await setTradeState(tradeID, state, additionalData);
    };

    const cleanupOldStates = async () => {
        const allKeys = await storage.get([]);
        const now = Date.now();
        const toDelete = Object.keys(allKeys)
            .filter(k => k.startsWith('trade_state_') && allKeys[k] && now - allKeys[k].timestamp > CONFIG.SEVEN_HOURS)
            .reduce((obj, k) => ({ ...obj, [k]: undefined }), {});

        if (Object.keys(toDelete).length) await storage.set(toDelete);
    };

    const displayTradeStates = async () => {
        if (!isTradePage || getStep()) return;

        ensureTradeListExists();
        const tradeListItems = qa(SELECTORS.TRADE_LIST_ITEM);
        if (!tradeListItems.length) return;

        await Promise.all(Array.from(tradeListItems).map(async (li) => {
            if (li.querySelector('.trade-status-indicator')) return;

            const viewLink = li.querySelector(SELECTORS.TRADE_LINK);
            const tradeID = viewLink?.href.match(/ID=(\d+)/)?.[1];
            if (!tradeID) return;

            const stateData = await getTradeState(tradeID);
            const display = await getStateDisplay(stateData, tradeID);
            if (!display) return;

            const statusSpan = el('span', {
                classes: 'trade-status-indicator',
                text: display.text,
                attributes: { 'data-trade-id': tradeID },
                style: {
                    color: display.color,
                    backgroundColor: `${display.color}20`,
                    border: `1px solid ${display.color}40`
                }
            });

            const descP = li.querySelector('.desc p');
            if (descP) descP.appendChild(statusSpan);
        }));
    };

    const debouncedDisplayStates = debounce(displayTradeStates, 100);

    const refreshTradeStatusTimes = async () => {
        if (!isTradePage || getStep()) return;

        const statusBadges = qa('.trade-status-indicator[data-trade-id]');
        if (!statusBadges.length) return;

        await Promise.all(Array.from(statusBadges).map(async (badge) => {
            const tradeID = badge.getAttribute('data-trade-id');
            if (!tradeID) return;

            const display = await getStateDisplay(await getTradeState(tradeID), tradeID);
            if (badge.textContent !== display.text) {
                badge.textContent = display.text;
                Object.assign(badge.style, {
                    color: display.color,
                    backgroundColor: `${display.color}20`,
                    border: `1px solid ${display.color}40`
                });
            }
        }));
    };

    const createTradeRow = async (tradeID, userID, description, timestamp) => {
        if (!isTradePage) return;
        
        const tradeList = q(SELECTORS.TRADE_LIST) || ensureTradeListExists();
        if (!tradeList) return;

        const profile = await fetchUserProfile(userID);
        const userName = profile?.name || `User [${userID}]`;
        const honor = profile?.honor || 0;
        const timeRemaining = calculateTimeRemaining(timestamp);
        const userNameChars = userName.split('').map(c => `<span data-char="${c}"></span>`).join('');
        const honorImageBase = `/images/honors/${honor}/`;

        const li = el('li');
        li.innerHTML = `
            <div class="namet t-blue h">
                <a class="user name t-hide" data-placeholder="${userName} [${userID}]" href="/profiles.php?XID=${userID}" title="${userName} [${userID}]">
                    <div class="honor-text-wrap default" style="--arrow-width: 20px;">
                        <img srcset="${honorImageBase}f.png 1x, ${honorImageBase}[email protected] 2x, ${honorImageBase}[email protected] 3x, ${honorImageBase}[email protected] 4x" src="${honorImageBase}f.png" border="0" alt="${userName} [${userID}]">
                        <span class="honor-text honor-text-svg">${userNameChars}</span>
                        <span class="honor-text">${userName}</span>
                    </div>
                </a>
                <a class="user name t-show" data-placeholder="${userName} [${userID}]" href="/profiles.php?XID=${userID}" title="${userName} [${userID}]">
                    <div class="honor-text-wrap default" style="--arrow-width: 20px;">
                        <img srcset="${honorImageBase}h.png 1x, ${honorImageBase}[email protected] 2x, ${honorImageBase}[email protected] 3x, ${honorImageBase}[email protected] 4x" src="${honorImageBase}h.png" border="0" alt="${userName} [${userID}]">
                        <span class="honor-text honor-text-svg">${userNameChars}</span>
                        <span class="honor-text">${userName}</span>
                    </div>
                </a>
            </div>
            <div class="desc">
                <span class="desk">Expires in -</span>
                <span class="mob">Exp:</span>
                <span class="time">${timeRemaining} </span>
                <p>Description: ${description}</p>
            </div>
            <div class="view">
                <a href="trade.php#step=view&ID=${tradeID}" area-label="view">
                    <i class="view-icon"></i>
                </a>
            </div>
        `;

        const lastLi = tradeList.querySelector('li.last');
        if (lastLi) lastLi.classList.remove('last');

        tradeList.appendChild(li);
        li.classList.add('last');
        debouncedDisplayStates();

        li.classList.add('flash-new');
    };

    const updateTradeStatusBadge = async (tradeID) => {
        if (!isTradePage) return;
        
        const allTrades = qa(SELECTORS.TRADE_LIST_ITEM);
        for (const li of allTrades) {
            const link = li.querySelector(SELECTORS.TRADE_LINK);
            if (link && link.href.includes(`ID=${tradeID}`)) {
                const statusIndicator = li.querySelector('.trade-status-indicator');
                if (statusIndicator) {
                    statusIndicator.remove();
                    debouncedDisplayStates();
                }
                li.classList.add('flash-update');
                break;
            }
        }
    };

    const removeTradeFromList = (tradeID) => {
        if (!isTradePage) return;
        
        for (const li of qa(SELECTORS.TRADE_LIST_ITEM)) {
            const link = li.querySelector(SELECTORS.TRADE_LINK);
            if (link?.href.match(/ID=(\d+)/)?.[1] === String(tradeID)) {
                li.classList.add('fade-out');
                sleep(300).then(() => li.remove());
                return;
            }
        }
    };

    const processActivityLog = async (eventsData) => {
        if (!eventsData || typeof eventsData !== 'object') return;

        const step = getStep();
        const isMainTradePage = isTradePage && !step;
        const tradeList = isMainTradePage ? (q(SELECTORS.TRADE_LIST) || ensureTradeListExists()) : null;

        const events = Object.entries(eventsData)
            .map(([id, event]) => ({ ...event, ID: id }))
            .sort((a, b) => b.timestamp - a.timestamp);

        const tradeStatuses = {};
        const tradesToCreate = [];

        for (const event of events) {
            if (lastProcessedEventID && event.ID === lastProcessedEventID) break;
            if (event.category !== 'Trades') continue;

            const tradeIDMatch = event.data?.trade_id?.match(/ID=(\d+)/);
            if (!tradeIDMatch) continue;

            const tradeID = tradeIDMatch[1];
            const eventLog = Number(event.log);

            if (!tradeStatuses[tradeID]) tradeStatuses[tradeID] = eventLog;

            if (eventLog === TRADE_EVENTS.INITIATED) {
                tradesToCreate.push({
                    tradeID,
                    userID: event.data?.user,
                    description: event.data?.description || 'Trade',
                    timestamp: event.timestamp,
                    eventID: event.ID
                });
            }
        }

        const validTrades = tradesToCreate
            .filter(trade => !TERMINAL_EVENTS.includes(tradeStatuses[trade.tradeID]))
            .sort((a, b) => a.timestamp - b.timestamp);

        for (const trade of validTrades) {
            if (!apiTradeData[trade.tradeID]) {
                apiTradeData[trade.tradeID] = initTradeData();
            }
            apiTradeData[trade.tradeID].userID = trade.userID;
            apiTradeData[trade.tradeID].lastEventTime = trade.timestamp * 1000;
        }

        if (tradeList) {
            for (const trade of validTrades) {
                const allTrades = Array.from(qa(SELECTORS.TRADE_LIST_ITEM));
                const existingTrade = allTrades.find(li => {
                    const link = li.querySelector(SELECTORS.TRADE_LINK);
                    return link && link.href.includes(`ID=${trade.tradeID}`);
                });
                if (!existingTrade) await createTradeRow(trade.tradeID, trade.userID, trade.description, trade.timestamp);
            }
        }

        for (const event of events) {
            if (lastProcessedEventID && event.ID === lastProcessedEventID) break;
            if (event.category !== 'Trades') continue;

            const tradeIDMatch = event.data?.trade_id?.match(/ID=(\d+)/);
            if (!tradeIDMatch) continue;

            const tradeID = tradeIDMatch[1];

            if (!apiTradeData[tradeID]) {
                apiTradeData[tradeID] = initTradeData();
            }

            const data = apiTradeData[tradeID];

            const eventLog = Number(event.log);

            if (TERMINAL_EVENTS.includes(eventLog)) {
                if (isMainTradePage) removeTradeFromList(tradeID);
                delete apiTradeData[tradeID];
                batchedStorage.set(`trade_state_${tradeID}`, undefined);
                continue;
            }

            const isTrackedEvent = TRACKED_EVENTS.includes(eventLog);
            if (isTrackedEvent) {
                data.lastEventTime = event.timestamp * 1000;
                data.lastEventType = eventLog;
                data.lastEventState = EVENT_TO_STATE[eventLog] || null;
            }

            if (eventLog === TRADE_EVENTS.ACCEPTED) {
                data.accepted = true;
                data.acceptedAt = event.timestamp * 1000;
            }

            if (eventLog === TRADE_EVENTS.DECLINED) {
                data.declined = true;
                data.declinedAt = event.timestamp * 1000;
            }

            if ([TRADE_EVENTS.OTHER_ITEMS_ADD].includes(eventLog)) {
                if (event.data?.items) data.items = event.data.items;
                data.hasItems = true;
            } else if (eventLog === TRADE_EVENTS.OTHER_ITEMS_REMOVE) {
                data.hasItems = false;
            }

            if ([TRADE_EVENTS.OTHER_MONEY_ADD].includes(eventLog)) {
                data.moneyTotal = event.data?.total || event.data?.money || 0;
                data.hasMoney = data.moneyTotal > 0;
            } else if (eventLog === TRADE_EVENTS.OTHER_MONEY_REMOVE) {
                data.moneyTotal = event.data?.total || 0;
                data.hasMoney = data.moneyTotal > 0;
            }

            if ([TRADE_EVENTS.OTHER_PROPERTY_ADD].includes(eventLog)) {
                data.hasProperty = true;
            } else if (eventLog === TRADE_EVENTS.OTHER_PROPERTY_REMOVE) {
                data.hasProperty = false;
            }

            if ([TRADE_EVENTS.OTHER_FACTION_ADD].includes(eventLog)) {
                data.hasFaction = true;
            } else if (eventLog === TRADE_EVENTS.OTHER_FACTION_REMOVE) {
                data.hasFaction = false;
            }

            if ([TRADE_EVENTS.OTHER_COMPANY_ADD].includes(eventLog)) {
                data.hasCompany = true;
            } else if (eventLog === TRADE_EVENTS.OTHER_COMPANY_REMOVE) {
                data.hasCompany = false;
            }

            if (isTrackedEvent) {
                await inferStateFromAPI(tradeID);
                if (isMainTradePage) updateTradeStatusBadge(tradeID);
            }
        }

        await Promise.all(Object.keys(apiTradeData).map(async (tradeID) => {
            const existingState = await getTradeState(tradeID);
            if (!existingState || (apiTradeData[tradeID].lastEventTime && existingState.eventTimestamp !== apiTradeData[tradeID].lastEventTime)) {
                await inferStateFromAPI(tradeID);
            }
        }));

        if (events.length > 0) lastProcessedEventID = events[0].ID;
        if (isFirstPoll) isFirstPoll = false;
    };

    const pollTradeEvents = async () => {
        const now = Date.now();
        if (now - lastPollTime < CONFIG.POLL_BUFFER) return;
        lastPollTime = now;

        const key = await apiKey.get();
        if (!key) return;

        const fiveMinutesAgo = Math.floor((Date.now() - 300000) / 1000);

        try {
            const response = await fetch(`${CONFIG.TORN_API}user/?key=${key}&cat=94&from=${fiveMinutesAgo}&selections=log`);
            const httpError = handleHttpError(response.status);
            if (httpError) return;

            const data = await response.json();
            const tornError = handleTornApiError(data);
            if (tornError) {
                if ([1, 2, 10, 13, 18].includes(tornError.code)) {
                    await storage.set({ torn_api_key: undefined });
                    stopTradePolling();
                }
                return;
            }

            if (data?.log && typeof data.log === 'object') {
                await processActivityLog(data.log);
                if (isTradePage && !getStep()) {
                    await sleep(100);
                    debouncedDisplayStates();
                }
            }
        } catch {
            return;
        }
    };

    const startTradePolling = async () => {
        if (tradePollingInterval) return;

        const key = await apiKey.get();
        if (!key) return;

        isFirstPoll = true;
        await pollTradeEvents();
        tradePollingInterval = setInterval(pollTradeEvents, CONFIG.POLL_INTERVAL);

        if (!timeRefreshInterval) {
            timeRefreshInterval = setInterval(refreshTradeStatusTimes, 1000);
        }
    };

    const stopTradePolling = () => {
        if (tradePollingInterval) {
            clearInterval(tradePollingInterval);
            tradePollingInterval = null;
        }
        if (timeRefreshInterval) {
            clearInterval(timeRefreshInterval);
            timeRefreshInterval = null;
        }
    };

    const colorCodeItemsByValue = () => {
        if (!isTradePage || !currentReceipt?.items?.length) return;

        const items = currentReceipt.items;
        const validItems = items.filter(i => i.totalValue && i.totalValue > 0);
        const values = validItems.map(i => i.totalValue);
        const maxValue = Math.max(...values);
        const minValue = Math.min(...values);

        const colorStops = [
            { pos: 0, r: 127, g: 29, b: 29 },
            { pos: 0.15, r: 239, g: 68, b: 68 },
            { pos: 0.35, r: 251, g: 146, b: 60 },
            { pos: 0.5, r: 251, g: 191, b: 36 },
            { pos: 0.7, r: 163, g: 230, b: 53 },
            { pos: 0.85, r: 101, g: 217, b: 101 },
            { pos: 1, r: 74, g: 222, b: 128 }
        ];

        const getColor = (value, hasPrice) => {
            if (!hasPrice || !value) return '#7f1d1d';
            if (validItems.length === 0) return '#7f1d1d';

            let normalizedValue = maxValue > minValue ? (value - minValue) / (maxValue - minValue) : (value === maxValue ? 1 : 0);
            normalizedValue = Math.max(0, Math.min(1, normalizedValue));

            let color1, color2;
            for (let i = 0; i < colorStops.length - 1; i++) {
                if (normalizedValue <= colorStops[i + 1].pos) {
                    color1 = colorStops[i];
                    color2 = colorStops[i + 1];
                    break;
                }
            }

            if (color1 && color2) {
                const ratio = (normalizedValue - color1.pos) / (color2.pos - color1.pos);
                const r = Math.round(color1.r + (color2.r - color1.r) * ratio);
                const g = Math.round(color1.g + (color2.g - color1.g) * ratio);
                const b = Math.round(color1.b + (color2.b - color1.b) * ratio);
                return `rgb(${r}, ${g}, ${b})`;
            }
            return '#7f1d1d';
        };

        qa(SELECTORS.USER_RIGHT_ITEMS).forEach(element => {
            const nameElement = element.querySelector('.name.left');
            if (!nameElement) return;

            const textNode = nameElement.childNodes[0]?.nodeValue?.trim();
            if (!textNode) return;
            const match = textNode.match(/^(.+?)(?:\s+x\d+)?$/);
            const itemName = match ? match[1].trim() : textNode;
            if (!itemName) return;

            const receiptItem = items.find(i => i.name === itemName);
            if (!receiptItem) return;

            const hasPrice = receiptItem.priceUsed !== undefined && receiptItem.priceUsed !== null && receiptItem.priceUsed !== 0 && !isNaN(receiptItem.priceUsed);
            nameElement.classList.add('item-name-colored');
            nameElement.style.color = getColor(receiptItem.totalValue, hasPrice);
        });
    };

    const updateAcceptBtn = (btn) => {
        if (!isTradePage) return;
        if (!btn) btn = q('.accept-trade-button');
        if (!btn) return;

        const moneyInTrade = getMoneyInTrade();
        const acceptButtonExists = !!q(SELECTORS.ACCEPT_BTN);
        const expectedValue = currentReceipt?.total_value || 0;
        const hasItems = currentReceipt?.items?.length > 0;
        const goToAddMoney = () => q(SELECTORS.ADD_MONEY_LINK)?.click();

        let newState;
        if (!hasItems) {
            newState = { icon: SVG_ICONS.add, text: 'No Items', disabled: true, enabled: false };
        } else if (!moneyInTrade) {
            newState = { icon: SVG_ICONS.add, text: 'Add Money', disabled: false, enabled: false, onClick: goToAddMoney };
        } else if (moneyInTrade > 0 && !acceptButtonExists) {
            newState = {
                icon: SVG_ICONS.check,
                text: 'Accepted',
                disabled: true,
                enabled: false,
                background: 'linear-gradient(145deg, #8b5cf6, #7c3aed)'
            };
        } else if (expectedValue > 0 && moneyInTrade !== expectedValue) {
            newState = {
                icon: SVG_ICONS.add,
                text: moneyInTrade < expectedValue ? 'Too Low' : 'Too High',
                disabled: false,
                enabled: false,
                background: 'linear-gradient(145deg, #dc2626, #b91c1c)',
                onClick: goToAddMoney
            };
        } else {
            const matches = moneyMatches();
            newState = {
                icon: SVG_ICONS.check,
                text: 'Accept Trade',
                disabled: !matches,
                enabled: matches,
                onClick: async () => {
                    if (!moneyMatches()) return;
                    const acceptBtn = q(SELECTORS.ACCEPT_BTN);
                    if (acceptBtn) {
                        const tradeID = getTradeID();
                        if (tradeID) {
                            await setTradeState(tradeID, 'accepted', {
                                acceptedAt: Date.now(),
                                totalValue: currentReceipt?.total_value || 0
                            });
                        }
                        acceptBtn.click();
                    }
                }
            };
        }

        const currentText = btn.querySelector('.button-text')?.textContent;
        const currentDisabled = btn.disabled;
        const currentEnabled = btn.classList.contains('enabled');
        const currentBackground = btn.style.background;

        if (currentText !== newState.text || 
            currentDisabled !== newState.disabled || 
            currentEnabled !== newState.enabled ||
            currentBackground !== (newState.background || '')) {
            setButtonState(btn, newState);
        }
    };

    const updateUI = () => {
        if (!isTradePage) return;
        
        const total = q('.total-value-container');
        const receipt = q('.receipt-url-container');
        const container = q('.value-container');
        const stripe = q('.stripe-container');

        if (total && currentReceipt?.total_value !== undefined) {
            total.textContent = `Total Value: $${currentReceipt.total_value.toLocaleString()}`;

            if (currentReceipt.items?.length > 0 && currentReceiptURL) {
                [total, receipt].forEach(e => e.classList.remove('hidden'));
                removeHandler(receipt);
                addHandler(receipt, showModal);
            } else {
                total.classList.remove('hidden');
                receipt.classList.add('hidden');
            }

            container.classList.add('visible');
            stripe.classList.add('expanded');

            removeHandler(total);
            addHandler(total, () => {
                navigator.clipboard.writeText(currentReceipt.total_value.toString());
                showMsg(total, 'Copied!', 1000);
            });

            updateAcceptBtn();

            if (!window.acceptBtnUpdateInterval) {
                window.acceptBtnUpdateInterval = setInterval(() => {
                    if (q('.accept-trade-button')) {
                        updateAcceptBtn();
                        updateTradeState();
                    }
                }, isMobile ? 750 : 500);
            }

            updateTradeState();
        }
    };

    const autoCalc = async () => {
        if (!isTradePage) return;
        
        const tradeID = getTradeID();
        if (!tradeID) return;

        const items = parseValidItems();

        if (!items.length) {
            currentReceipt = { items: [], total_value: 0 };
            currentReceiptURL = null;
            updateUI();
            return;
        }

        const totalEl = q('.total-value-container');
        if (totalEl && !currentReceipt) {
            totalEl.textContent = 'Calculating...';
            totalEl.classList.remove('hidden');
            q('.value-container')?.classList.add('visible');
        }

        const stored = await getStoredData(tradeID);
        if (stored && itemsMatch(stored.items, items)) {
            currentReceipt = stored.receipt;
            currentReceiptURL = stored.receiptURL;

            if (stored.manual_prices) {
                currentReceipt.items = currentReceipt.items.map(item =>
                    stored.manual_prices[item.name]
                        ? { ...item, priceUsed: stored.manual_prices[item.name], totalValue: stored.manual_prices[item.name] * item.quantity }
                        : item
                );
                currentReceipt.total_value = currentReceipt.items.reduce((sum, i) => sum + i.totalValue, 0);
            }
            updateUI();
            requestAnimationFrame(() => colorCodeItemsByValue());
            return;
        }

        const username = getUsername();
        const userID = getCurrentUserID();
        if (!userID) {
            console.error('Could not get current user ID');
            return;
        }

        const res = await apiCall(`pricelist/${userID}`, { items, username, tradeID });

        if (res?.error) {
            const totalEl = q('.total-value-container');
            if (totalEl) {
                totalEl.textContent = `Error: ${res.error}`;
                totalEl.classList.add('error-button');
                sleep(3000).then(() => totalEl.classList.remove('error-button'));
            }
            return;
        }

        if (res?.receipt?.total_value !== undefined && res.receiptURL) {
            currentReceipt = res.receipt;
            currentReceiptURL = res.receiptURL;
            storeData(tradeID, items, res.receipt, res.receiptURL);
            updateUI();
            requestAnimationFrame(() => colorCodeItemsByValue());
        }
    };

    const createTable = (items = []) => {
        const tbody = el('tbody');
        items.forEach(item => {
            const itemId = item.id || item.itemId || '';
            const imageUrl = itemId ? `https://www.torn.com/images/items/${itemId}/medium.png` : '';
            const row = el('tr', {
                html: `<td><img src="${imageUrl}" alt="Item" class="item-image"/></td><td>${item.name}</td><td>${item.quantity}</td><td>$${(item.priceUsed || 0).toLocaleString()}</td><td>$${(item.totalValue || 0).toLocaleString()}</td>`
            });
            row.dataset.itemId = itemId;
            tbody.appendChild(row);
        });
        return el('table', {
            classes: 'items-table',
            children: [
                el('thead', { html: '<tr><th>Image</th><th>Name</th><th>Quantity</th><th>Price</th><th>Total</th></tr>' }),
                tbody
            ]
        });
    };

    const createModal = () => {
        const closeModal = () => overlay.classList.remove('visible');
        const overlay = el('div', { classes: 'receipt-modal-overlay', onClick: (e) => e.target === overlay && closeModal() });
        const modal = el('div', {
            classes: 'receipt-modal',
            children: [
                el('div', {
                    classes: 'modal-header',
                    children: [
                        el('button', { classes: 'close-modal', html: '&times;', onClick: closeModal })
                    ]
                }),
                el('div', {
                    classes: 'modal-content',
                    children: [el('div', { classes: 'receipt-content', children: [el('div', { classes: 'loading-spinner', text: 'Loading...' })] })]
                })
            ]
        });
        overlay.appendChild(modal);
        document.body.appendChild(overlay);
        return overlay;
    };

    const makeEditable = (table, saveBtn) => {
        table.querySelectorAll('tbody tr td:nth-child(4)').forEach(cell => {
            cell.classList.add('price-editable');
            addHandler(cell, () => {
                if (cell.querySelector('input')) return;
                
                const price = cell.textContent.replace(/[^0-9.]/g, '');
                const formattedPrice = parseFloat(price).toLocaleString();
                const input = Object.assign(document.createElement('input'), { 
                    type: 'text', 
                    value: formattedPrice, 
                    className: 'editable-input'
                });
                cell.textContent = '';
                cell.appendChild(input);
                input.focus();
                
                input.select();

                const formatInput = () => {
                    const rawValue = input.value.replace(/[^0-9.]/g, '');
                    const numValue = parseFloat(rawValue);
                    if (!isNaN(numValue)) {
                        const cursorPos = input.selectionStart;
                        const oldLength = input.value.length;
                        input.value = numValue.toLocaleString();
                        const newLength = input.value.length;
                        const diff = newLength - oldLength;
                        input.setSelectionRange(cursorPos + diff, cursorPos + diff);
                    }
                };

                input.addEventListener('input', (e) => {
                    input.value = input.value.replace(/[^0-9.,]/g, '');
                });

                const done = () => {
                    const rawValue = input.value.replace(/[^0-9.]/g, '');
                    const newPrice = parseFloat(rawValue);
                    cell.textContent = isNaN(newPrice) ? `$${parseFloat(price).toLocaleString()}` : `$${newPrice.toLocaleString()}`;
                    if (!isNaN(newPrice)) {
                        cell.classList.add('price-edited');
                        updateTotals(table);
                        saveBtn.classList.remove('save-button-hidden');
                    }
                };
                input.addEventListener('blur', done, { passive: true });
                input.addEventListener('keypress', (e) => e.key === 'Enter' && input.blur());
            });
        });
    };

    const updateTotals = (table) => table.querySelectorAll('tbody tr').forEach(row => {
        const qty = parseInt(row.cells[2].textContent) || 0;
        const price = parseFloat(row.cells[3].textContent.replace(/[^0-9.]/g, '')) || 0;
        row.cells[4].textContent = `$${(qty * price).toLocaleString()}`;
    });

    const saveChanges = async (table, saveBtn) => {
        saveBtn.classList.add('saving');
        saveBtn.disabled = true;

        const updatedItems = [];
        const manualPrices = {};

        table.querySelectorAll('tbody tr').forEach(row => {
            const name = row.cells[1].textContent;
            const priceUsed = parseFloat(row.cells[3].textContent.replace(/[^0-9.]/g, '')) || 0;
            const wasEdited = row.cells[3].classList.contains('price-edited');
            const itemId = parseInt(row.dataset.itemId);

            if (wasEdited && itemId && !isNaN(itemId)) {
                manualPrices[name] = priceUsed;
                updatedItems.push({ itemId, price: priceUsed });
            }
        });

        const receiptId = getReceiptIdFromURL(currentReceiptURL);
        if (!receiptId) {
            saveBtn.classList.remove('saving');
            saveBtn.disabled = false;
            saveBtn.classList.add('error');
            sleep(2000).then(() => saveBtn.classList.remove('error'));
            return;
        }

        if (updatedItems.length === 0) {
            saveBtn.classList.remove('saving');
            saveBtn.disabled = false;
            return;
        }

        const res = await apiCall(`updateReceipt/${receiptId}`, { items: updatedItems });

        saveBtn.classList.remove('saving');
        saveBtn.disabled = false;

        if (res?.error || res?.success === false) {
            saveBtn.classList.add('error');
            saveBtn.textContent = res?.error || 'Save failed';
            sleep(2000).then(() => {
                saveBtn.classList.remove('error');
                saveBtn.textContent = 'Save Changes';
            });
        } else {
            const tradeID = getTradeID();

            if (currentReceipt?.items) {
                const priceMap = new Map(updatedItems.map(item => [item.itemId, item.price]));

                currentReceipt.items = currentReceipt.items.map(item => {
                    const itemId = item.id || item.itemId;
                    if (priceMap.has(itemId)) {
                        const newPrice = priceMap.get(itemId);
                        return { ...item, priceUsed: newPrice, totalValue: newPrice * (item.quantity || 1) };
                    }
                    return item;
                });

                currentReceipt.total_value = currentReceipt.items.reduce((sum, item) => sum + (item.totalValue || 0), 0);
            }

            if (tradeID) {
                const items = Array.from(table.querySelectorAll('tbody tr')).map(row => ({
                    name: row.cells[1].textContent,
                    quantity: parseInt(row.cells[2].textContent)
                }));

                const existing = await getStoredData(tradeID) || {};
                existing.items = items;
                existing.receipt = currentReceipt;
                existing.receiptURL = existing.receiptURL || currentReceiptURL;
                existing.total_value = currentReceipt?.total_value || 0;
                existing.timestamp = Date.now();
                existing.manual_prices = { ...existing.manual_prices, ...manualPrices };

                await storage.set({ [`trade_${tradeID}`]: existing });
            }

            const tbody = table.querySelector('tbody');
            if (tbody && currentReceipt?.items) {
                tbody.innerHTML = '';
                currentReceipt.items.forEach(item => {
                    const isEdited = manualPrices.hasOwnProperty(item.name);
                    const itemId = item.id || item.itemId || '';
                    const imageUrl = itemId ? `https://www.torn.com/images/items/${itemId}/medium.png` : '';

                    const row = el('tr', {
                        children: [
                            el('td', { children: [el('img', { attributes: { src: imageUrl, alt: 'Item' }, classes: 'item-image' })] }),
                            el('td', { text: item.name }),
                            el('td', { text: item.quantity.toString() }),
                            el('td', { text: `$${(item.priceUsed || 0).toLocaleString()}`, classes: isEdited ? ['price-edited'] : [] }),
                            el('td', { text: `$${(item.totalValue || 0).toLocaleString()}` })
                        ]
                    });
                    row.dataset.itemId = itemId;
                    tbody.appendChild(row);
                });
                makeEditable(table, saveBtn);
            }

            const total = q('.total-value-container');
            if (total) total.textContent = `Total Value: $${currentReceipt.total_value.toLocaleString()}`;
            updateAcceptBtn();

            saveBtn.classList.add('saved');
            sleep(2000).then(() => {
                saveBtn.classList.add('save-button-hidden');
                saveBtn.classList.remove('saved');
            });
        }
    };

    const showModal = async () => {
        if (!isTradePage || !currentReceipt || !currentReceiptURL) return;

        let modal = q('.receipt-modal-overlay') || createModal();
        const content = modal.querySelector('.receipt-content');
        content.innerHTML = '<div class="loading-spinner">Loading...</div>';
        modal.classList.add('visible');

        await sleep(200);
        content.innerHTML = '';

            const copyBtn = el('button', {
                classes: 'copy-url-button',
                text: 'Copy URL',
                onClick: () => navigator.clipboard.writeText(currentReceiptURL).then(() => showMsg(copyBtn, 'Copied!', 2000))
            });

            content.appendChild(el('div', {
                classes: 'receipt-url-copy',
                children: [el('div', { classes: 'url-display', children: [el('span', { classes: 'url-text', text: currentReceiptURL }), copyBtn] })]
            }));

            const table = createTable(currentReceipt.items);
            content.appendChild(table);

            const saveBtn = el('button', {
                classes: ['save-changes-button', 'save-button-hidden'],
                text: 'Save Changes',
                onClick: () => saveChanges(table, saveBtn)
            });
            content.appendChild(saveBtn);

            makeEditable(table, saveBtn);
            updateTotals(table);
    };

    const ensureTradeListExists = () => {
        if (!isTradePage || getStep()) return null;

        let tradeList = q(SELECTORS.TRADE_LIST);
        if (tradeList) return tradeList;

        const noTradesMsg = Array.from(qa('.info-msg-cont')).find(el => {
            const msg = el.querySelector('.msg.right-round');
            return msg && msg.textContent.trim() === 'You have no current trades.';
        });

        if (!noTradesMsg) return null;

        tradeList = el('ul', {
            classes: ['trades-cont', 'current', 'cont-gray', 'bottom-round']
        });

        noTradesMsg.parentNode.insertBefore(tradeList, noTradesMsg.nextSibling);
        return tradeList;
    };

    const initApiKeyButton = async (retryCount = 0) => {
        if (!isTradePage || getStep()) return;
        if (retryCount > 20) return; // Stop retrying after 2 seconds

        const currentTradeHeading = Array.from(qa('.title-black.top-round.m-top10')).find(el => 
            el.textContent.trim() === 'Current Trade'
        );

        const noTradesMsg = Array.from(qa('.info-msg-cont')).find(el => {
            const msg = el.querySelector('.msg.right-round');
            return msg && msg.textContent.trim() === 'You have no current trades.';
        })?.querySelector('.msg.right-round');

        // Try alternative selectors if the primary ones don't work
        const altHeading = q('.title-black.top-round') || q('.title-black.m-top10');
        const altMsg = Array.from(qa('.info-msg-cont')).find(el => {
            const msg = el.querySelector('.msg');
            return msg && msg.textContent.trim().includes('no current trades');
        })?.querySelector('.msg');
        
        const container = currentTradeHeading || noTradesMsg || altHeading || altMsg;
        if (!container) {
            sleep(100).then(() => initApiKeyButton(retryCount + 1));
            return;
        }
        if (container.querySelector('.api-key-button')) return;

        const updateButton = async () => {
            const existingBtn = container.querySelector('.api-key-button');
            if (existingBtn) existingBtn.remove();

            const hasKey = !!(await apiKey.get());
            
            const button = el('button', {
                classes: 'api-key-button',
                text: hasKey ? 'Remove key' : 'Set API key',
                onClick: async () => {
                    if (hasKey) {
                        await apiKey.remove();
                        stopTradePolling();
                        updateButton();
                    } else {
                        const key = prompt('Please enter your FULL access API key');
                        if (key) {
                            await apiKey.set(key);
                            updateButton();
                            startTradePolling();
                        }
                    }
                }
            });

            container.appendChild(button);
        };

        await updateButton();
    };

    const debouncedInitApiKeyButton = debounce(initApiKeyButton, 100);

    const initNewTradeCollapsible = (root = document) => {
        if (!isTradePage || getStep()) return;

        root.querySelectorAll('.init-trade.m-top10').forEach(container => {
            if (container.dataset.newTradeCollapsedInit === '1') return;

            const heading = container.querySelector('.title-black.top-round[role="heading"]');
            const content = container.querySelector('.cont-gray.bottom-round');
            if (!heading || !content) return;

            container.dataset.newTradeCollapsedInit = '1';
            content.style.display = 'none';
            heading.classList.add('new-trade-toggle-header');

            addHandler(heading, () => {
                const isHidden = content.style.display === 'none';
                content.style.display = isHidden ? '' : 'none';
                heading.classList.toggle('expanded', isHidden);
            }, true);
        });
    };

    const initStripe = () => {
        if (!isTradePage) return;
        
        const isAcceptPage = getStep() === 'accept2';
        let tc = q('.trade-cont.m-top10');
        const isAddMoneyPage = !!q('.init-trade.add-money.m-top10');

        if (isAcceptPage) {
            const confirmMsg = q('.info-msg-cont.green');
            if (confirmMsg && !q('.stripe-container')) {
                const total = el('div', {
                    classes: ['total-value-container'],
                    text: 'Total Value: $0',
                    onClick: () => currentReceipt?.total_value && (navigator.clipboard.writeText(currentReceipt.total_value.toString()), showMsg(total, 'Copied!', 1000))
                });

                const receipt = el('div', {
                    classes: ['receipt-url-container'],
                    html: `${SVG_ICONS.receipt}<span class="button-text">View/Edit Receipt</span>`,
                    onClick: showModal
                });

                const container = el('div', { classes: ['value-container'], children: [total, receipt] });
                const tradeID = getTradeID();

                const backToTradeBtn = createButton(['calculate-button'], SVG_ICONS.back, 'Back to Trade',
                    () => tradeID && (window.location.hash = `#step=view&ID=${tradeID}`)
                );

                const dashboardBtn = createButton(['accept-trade-button', 'enabled'], SVG_ICONS.dashboard, 'Dashboard',
                    () => window.location.hash = '#'
                );

                const stripe = el('div', { classes: ['stripe-container', 'expanded'], children: [backToTradeBtn, container, dashboardBtn] });
                confirmMsg.parentNode.insertBefore(stripe, confirmMsg.nextSibling);

                (async () => {
                    if (tradeID) {
                        const stored = await getStoredData(tradeID);
                        if (stored?.receipt) {
                            currentReceipt = stored.receipt;
                            currentReceiptURL = stored.receiptURL;
                            total.textContent = `Total Value: $${currentReceipt.total_value.toLocaleString()}`;

                            if (currentReceipt.items?.length > 0 && currentReceiptURL) {
                                receipt.classList.remove('hidden');
                                container.classList.add('visible');
                            } else {
                                receipt.classList.add('hidden');
                            }
                        }
                    }
                })();
            }
            return;
        }

        if (!tc && isAddMoneyPage) tc = q('.init-trade.add-money.m-top10');

        if (isAddMoneyPage && !q('.info-msg-cont')) {
            (async () => {
                let messageText = 'Add money to complete your side of the trade';
                const tradeID = getTradeID();
                if (tradeID) {
                    const stored = await getStoredData(tradeID);
                    if (stored?.total_value) {
                        messageText = `Add $${stored.total_value.toLocaleString()} to complete your side of the trade`;
                    }
                }

                const spacerMsg = el('div', {
                    classes: ['info-msg-cont', 'border-round', 'm-top10', 'r3210'],
                    html: `<div class="info-msg border-round"><i class="info-icon"></i><div class="delimiter"><div class="msg right-round" role="alert" aria-live="polite">${messageText}</div></div></div>`
                });

                const existingStripe = q('.stripe-container');
                if (existingStripe) {
                    existingStripe.parentNode.insertBefore(spacerMsg, existingStripe);
                } else if (tc) {
                    tc.parentNode.insertBefore(spacerMsg, tc);
                }
            })();
        }

        if (!tc || q('.stripe-container')) return;

        const total = el('div', {
            classes: ['hidden', 'total-value-container'],
            text: 'Total Value: $0',
            onClick: () => currentReceipt?.total_value && (navigator.clipboard.writeText(currentReceipt.total_value.toString()), showMsg(total, 'Copied!', 1000))
        });

        const receipt = el('div', {
            classes: ['hidden', 'receipt-url-container'],
            html: `${SVG_ICONS.receipt}<span class="button-text">View/Edit Receipt</span>`,
            onClick: showModal
        });

        const container = el('div', { classes: ['value-container'], children: [total, receipt] });
        let children = [];

        if (isAddMoneyPage) {
            const pasteBtn = createButton(['calculate-button'], SVG_ICONS.paste, 'Paste', async () => {
                const text = await navigator.clipboard.readText();
                qa('input.input-money').forEach(input => {
                    input.value = text;
                    input.dispatchEvent(new Event('input', { bubbles: true }));
                    input.dispatchEvent(new Event('change', { bubbles: true }));
                });
            });

            const placeholderContainer = el('div', {
                classes: ['value-container', 'visible'],
                children: [
                    createButton(['calculate-button'], SVG_ICONS.money, 'Placeholder 1', () => {}),
                    createButton(['calculate-button'], SVG_ICONS.money, 'Placeholder 2', () => {})
                ]
            });

            const changeBtn = createButton(['accept-trade-button', 'enabled'], SVG_ICONS.check, 'Change',
                () => q('input[type="submit"].torn-btn')?.click()
            );

            children = [pasteBtn, placeholderContainer, changeBtn];
        } else {
            const accept = createButton(['accept-trade-button'], SVG_ICONS.check, 'Accept Trade',
                () => moneyMatches() && q(SELECTORS.ACCEPT_BTN)?.click()
            );
            accept.disabled = true;

            const calculateHandler = async () => {
                btn.classList.add('calculating');
                btn.disabled = true;

                total.textContent = 'Calculating...';
                total.classList.remove('hidden');
                container.classList.add('visible');

                const items = parseValidItems();
                if (!items.length) {
                    showMsg(btn, 'No items found', 2000, 'error');
                    btn.classList.remove('calculating');
                    btn.disabled = false;
                    total.classList.add('hidden');
                    container.classList.remove('visible');
                    return;
                }

                const username = getUsername();
                const tradeID = getTradeID();
                const userID = getCurrentUserID();
                if (!userID) {
                    showMsg(btn, 'Could not get user ID', 2000, 'error');
                    btn.classList.remove('calculating');
                    btn.disabled = false;
                    return;
                }

                const res = await apiCall(`pricelist/${userID}`, { items, username, tradeID });

                btn.classList.remove('calculating');
                btn.disabled = false;

                if (res?.error) {
                    showMsg(btn, res.error, 3000, 'error-button');
                    total.textContent = `Error: ${res.error}`;
                    return;
                }

                if (res?.receipt?.total_value !== undefined && res.receiptURL) {
                    currentReceipt = res.receipt;
                    currentReceiptURL = res.receiptURL;
                    total.textContent = `Total Value: $${currentReceipt.total_value.toLocaleString()}`;
                    [total, receipt].forEach(e => e.classList.remove('hidden'));
                    container.classList.add('visible');
                    stripe.classList.add('expanded');
                    updateAcceptBtn(accept);
                    requestAnimationFrame(() => colorCodeItemsByValue());
                    storeData(tradeID, items, res.receipt, res.receiptURL);
                }
            };

            const btn = createButton(['calculate-button'], SVG_ICONS.calculate, 'Calculate Trade', calculateHandler);
            children = [btn, container, accept];
        }

        const stripe = el('div', { classes: ['stripe-container'], children });
        tc.parentNode.insertBefore(stripe, tc);
    };

    const autoFill = async () => {
        if (!isTradePage || getStep() !== 'addmoney') return;
        const stored = await getStoredData(getTradeID());
        if (!stored?.total_value) return;

            const fill = () => {
            const inputs = qa('input.input-money');
            if (!inputs.length) return false;
            inputs.forEach(i => {
                if (isMobile) i.classList.add('mobile-input-font');
                i.value = stored.total_value.toString();
                i.dispatchEvent(new Event('input', { bubbles: true }));
                i.dispatchEvent(new Event('change', { bubbles: true }));
            });
            return true;
        };

        if (fill()) return;
        const obs = new MutationObserver((_, o) => fill() && o.disconnect());
        obs.observe(document.body, { childList: true, subtree: true });
        sleep(isMobile ? 7000 : 5000).then(() => obs.disconnect());
    };

    const initializeScript = () => {
        if (!isTradePage) return;
        
        const obs = new MutationObserver((muts) => {
            for (const m of muts) {
                if (m.type === 'childList') {
                    const nodes = Array.from(m.addedNodes);

                    if (nodes.some(n => n.nodeType === 1 && n.classList?.contains('trade-cont'))) {
                        initStripe();
                        autoCalc();
                    }

                    if (nodes.some(n => n.nodeType === 1 && n.classList?.contains('add-money'))) {
                        initStripe();
                        autoFill();
                    }

                    if (nodes.some(n => n.nodeType === 1 && (n.classList?.contains('init-trade') || n.querySelector?.('.init-trade.m-top10')))) {
                        initNewTradeCollapsible();
                    }

                    if (nodes.some(n => n.nodeType === 1 && n.classList?.contains('trades-cont'))) {
                        debouncedDisplayStates();
                    }

                    if (nodes.some(n => n.nodeType === 1 && (n.classList?.contains('title-black') || n.querySelector?.('.title-black.top-round.m-top10')))) {
                        debouncedInitApiKeyButton();
                    }

                    if (nodes.some(n => n.nodeType === 1 && (n.classList?.contains('info-msg-cont') || n.querySelector?.('.info-msg-cont')))) {
                        ensureTradeListExists();
                        debouncedInitApiKeyButton();
                    }

                    for (const node of nodes) {
                        if (node.nodeType === 1 && node.classList?.contains('info-msg-cont') && node.classList?.contains('green')) {
                            const msgText = node.querySelector('.msg')?.textContent?.trim();

                            if (msgText?.includes('accepted the trade')) {
                                sleep(50).then(() => initStripe());
                            } else if (msgText?.includes('changed your money for the trade') && !msgText?.includes('accepted') && !msgText?.includes('complete')) {
                                node.classList.add('info-hidden');
                                const nextSibling = node.nextElementSibling;
                                requestAnimationFrame(() => {
                                    node.remove();
                                    if (nextSibling?.tagName === 'HR' && nextSibling.classList.contains('page-head-delimiter')) {
                                        nextSibling.remove();
                                    }
                                });
                            }
                        }
                    }
                }
            }
        });

        obs.observe(document.body, { childList: true, subtree: true });
        initNewTradeCollapsible();
        debouncedInitApiKeyButton();
        ensureTradeListExists();
        initStripe();
        autoCalc();
    };

    if (isTradePage) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initializeScript);
        } else {
            initializeScript();
        }
    }

    if (isTradePage && getStep() === 'addmoney') autoFill();
    if (isTradePage && getStep() === 'accept2') initStripe();

    if (window.requestIdleCallback) {
        requestIdleCallback(() => cleanupOldStates(), { timeout: 5000 });
    } else {
        sleep(100).then(() => cleanupOldStates());
    }

    startTradePolling();

    if (isTradePage && !getStep()) {
        ensureTradeListExists();
        displayTradeStates();
        initApiKeyButton(); // Also call directly in case MutationObserver misses it
        const tradeListObserver = new MutationObserver(debouncedDisplayStates);
        const tradeList = q(SELECTORS.TRADE_LIST);
        if (tradeList) tradeListObserver.observe(tradeList, { childList: true });
    }

    window.addEventListener('hashchange', () => {
        if (window.acceptBtnUpdateInterval) {
            clearInterval(window.acceptBtnUpdateInterval);
            window.acceptBtnUpdateInterval = null;
        }

        if (isTradePage) {
            if (getStep() === 'addmoney') autoFill();
            if (getStep() === 'accept2') sleep(50).then(() => initStripe());
            if (!getStep()) debouncedDisplayStates();
        }
    });

    window.addEventListener('beforeunload', () => {
        stopTradePolling();
        batchedStorage.flush();
    });
})();