Torn Trade Reputation

Torn PDA / Tampermonkey script for viewing and voting on shared trader reputation in Torn

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

// ==UserScript==
// @name         Torn Trade Reputation
// @namespace    https://greatest.deepsurf.us/users/1590579
// @version      1.3.0.6
// @description  Torn PDA / Tampermonkey script for viewing and voting on shared trader reputation in Torn
// @author       Vreebn [4149405]
// @match        https://www.torn.com/*
// @match        https://torn.com/*
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @connect      torn-trade-rep.ebnoreza.workers.dev
// @connect      tornexchange.com
// @run-at       document-idle
// @license      MIT
// @supportURL   https://greatest.deepsurf.us/en/scripts/573609-torn-trade-reputation/feedback
// @homepageURL  https://greatest.deepsurf.us/en/scripts/573609-torn-trade-reputation
// ==/UserScript==

(function () {
    'use strict';

    if (window.__TTR_ACTIVE__) return;
    window.__TTR_ACTIVE__ = true;

    const SCRIPT_VERSION = '1.2.0.6';
    const WORKER_BASE = 'https://torn-trade-rep.ebnoreza.workers.dev';
    const PDA_KEY = '###PDA-APIKEY###';

    const STORAGE_KEY_TORN = 'ttr_torn_api_key';
    const STORAGE_KEY_TE = 'ttr_te_api_key';
    const STORAGE_KEY_TE_ENABLED = 'ttr_te_enabled';
    const STORAGE_KEY_PANEL_STATE = 'ttr_panel_state_v1';

    const STORAGE_KEY_LAST_EVENTS_HEAD = 'ttr_last_events_head_ref_v1';
    const STORAGE_KEY_SYNCED_EVENT_REFS = 'ttr_synced_event_refs_v1';

    const ROUTE_POLL_MS = 1200;
    const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
    const EVENT_SYNC_DEBOUNCE_MS = 2500;
    const MAX_SYNCED_EVENT_REFS = 1200;
    const SUSPICIOUS_MUG_LINK_WINDOW_SEC = 10 * 60;
    const NINETY_DAYS_SEC = 90 * 24 * 60 * 60;

    const CACHE_KEYS = {
        SELF_PROFILE: 'ttr_cache_self_profile_v1',
        PRIVATE_PROFILE: 'ttr_cache_private_profile_v1',
        SUPPLEMENTAL: 'ttr_cache_supplemental_v1',
        RESOLVE_USER: 'ttr_cache_resolve_user_v1'
    };

    const MUG_SUBTYPES = {
        after_market_purchase: 'After market purchase',
        after_cancelled_trade: 'After cancelled trade',
        other_trade_related_mug: 'Other trade-related mug'
    };

    let currentRouteKey = '';
    let currentTargetId = null;
    let currentTargetName = '';
    let currentTradeId = null;
    let currentContextType = null;
    let selfProfileCache = null;

    let currentViewerVote = null;
    let currentViewerMugReport = null;

    let rootEl = null;
    let bodyEl = null;
    let collapsedSummaryEl = null;
    let mugRowEl = null;

    let nameEl = null;
    let scoreEl = null;
    let myVoteEl = null;
    let mugReportsEl = null;
    let mugTopReasonEl = null;
    let statusEl = null;
    let btnUp = null;
    let btnDown = null;
    let actionsEl = null;
    let friendEnemyEl = null;
    let teEl = null;
    let tradeHistoryEl = null;
    let settingsBtn = null;
    let selfVotesBtn = null;
    let riskLabelEl = null;
    let currentBountyEl = null;
    let mug90dEl = null;
    let collapseBtn = null;

    let lastEventsSyncAt = 0;
    let syncedEventRefs = loadSyncedEventRefs();
    let panelState = loadPanelState();

    init().catch((err) => {
        console.error('[TTR] Init error:', err);
    });

    async function init() {
        injectStyles();
        installRouteHooks();

        if (!getEffectiveTornApiKey()) {
            openTornKeyPrompt();
        }

        await refreshForCurrentRoute();
        scheduleEventsSync(true);

        setInterval(() => {
            refreshForCurrentRoute().catch((err) => {
                console.error('[TTR] Route refresh error:', err);
            });
            scheduleEventsSync(false);
        }, ROUTE_POLL_MS);
    }

    function injectStyles() {
        if (document.getElementById('ttr-style')) return;

        const style = document.createElement('style');
        style.id = 'ttr-style';
        style.textContent = `
            .ttr-inline-card {
                display: block !important;
                width: 100% !important;
                min-width: 100% !important;
                max-width: 100% !important;
                box-sizing: border-box !important;
                clear: both !important;
                margin: 6px 0 10px 0 !important;
                padding: 10px 12px !important;
                border: 1px solid rgba(255,255,255,0.10) !important;
                border-radius: 4px !important;
                background: rgba(0,0,0,0.18) !important;
                font-size: 12px !important;
                line-height: 1.35 !important;
                overflow: hidden !important;
            }

            .ttr-inline-card.ttr-tone-safe {
                background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(14,64,28,0.48) 100%) !important;
                border-color: rgba(111,226,111,0.18) !important;
            }
            .ttr-inline-card.ttr-tone-neutral {
                background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(78,62,18,0.42) 100%) !important;
                border-color: rgba(255,215,90,0.16) !important;
            }
            .ttr-inline-card.ttr-tone-risky {
                background: linear-gradient(180deg, rgba(0,0,0,0.25) 0%, rgba(88,18,18,0.50) 100%) !important;
                border-color: rgba(255,123,123,0.18) !important;
            }

            .ttr-inline-card.collapsed {
                padding: 8px 10px !important;
            }
            .ttr-inline-card.collapsed #ttr-header {
                grid-template-columns: minmax(0,1fr) auto !important;
                gap: 4px !important;
                align-items: center !important;
            }
            .ttr-inline-card.collapsed .ttr-first-left,
            .ttr-inline-card.collapsed .ttr-first-center {
                display: none !important;
            }
            .ttr-inline-card.collapsed .ttr-first-right {
                width: auto !important;
                justify-self: end !important;
            }
            .ttr-inline-card.collapsed #ttr-settings {
                display: none !important;
            }
            .ttr-inline-card.collapsed #ttr-body {
                display: none !important;
            }
            .ttr-inline-card.collapsed #ttr-collapsed-summary {
                display: block !important;
                margin-top: 6px !important;
            }

            .ttr-mount-row {
                display: block !important;
                width: 100% !important;
                min-width: 100% !important;
                max-width: 100% !important;
                box-sizing: border-box !important;
                clear: both !important;
            }

            .ttr-inline-main {
                display: grid !important;
                grid-template-columns: minmax(0,1fr) auto minmax(0,1fr) !important;
                align-items: center !important;
                gap: 8px !important;
                width: 100% !important;
                min-width: 0 !important;
            }

            .ttr-first-left {
                text-align: left !important;
                min-width: 0 !important;
                overflow: hidden !important;
                white-space: nowrap !important;
                text-overflow: ellipsis !important;
                justify-self: start !important;
            }

            .ttr-first-center {
                text-align: center !important;
                min-width: 0 !important;
                white-space: nowrap !important;
                font-weight: 700 !important;
                opacity: 0.96 !important;
                overflow: hidden !important;
                text-overflow: ellipsis !important;
                justify-self: center !important;
            }

            .ttr-first-right {
                text-align: right !important;
                min-width: 0 !important;
                justify-self: end !important;
                width: 100% !important;
            }

            .ttr-inline-top-right {
                display: inline-flex !important;
                width: auto !important;
                justify-content: flex-end !important;
                align-items: center !important;
                gap: 2px !important;
                margin-left: auto !important;
            }

            .ttr-inline-reason-row {
                display: flex !important;
                justify-content: center !important;
                align-items: center !important;
                gap: 8px !important;
                width: 100% !important;
                text-align: center !important;
                margin-top: 8px !important;
                opacity: 0.88 !important;
                font-weight: 700 !important;
                min-height: 16px !important;
                flex-wrap: wrap !important;
            }

            .ttr-inline-stats-row {
                display: grid !important;
                grid-template-columns: repeat(4, minmax(0, 1fr)) !important;
                align-items: center !important;
                gap: 6px !important;
                width: 100% !important;
                margin-top: 8px !important;
            }

            .ttr-stat-cell {
                text-align: center !important;
                min-width: 0 !important;
                white-space: nowrap !important;
                overflow: hidden !important;
                text-overflow: ellipsis !important;
            }

            .ttr-inline-label,
            .ttr-risk-label {
                opacity: 0.78 !important;
                margin-right: 4px !important;
            }

            .ttr-inline-name,
            .ttr-inline-score,
            .ttr-inline-vote,
            .ttr-inline-meta-value,
            .ttr-inline-history-value,
            .ttr-risk-value {
                font-weight: 700 !important;
            }

            .ttr-inline-name {
                display: inline-block !important;
                max-width: 100% !important;
                overflow: hidden !important;
                text-overflow: ellipsis !important;
                vertical-align: bottom !important;
            }

            .ttr-inline-score.pos,
            .ttr-inline-vote.pos,
            .ttr-inline-meta-value.pos,
            .ttr-inline-history-value.pos,
            .ttr-risk-value.pos,
            .ttr-collapsed-value.pos {
                color: #6fe26f !important;
            }

            .ttr-inline-score.neg,
            .ttr-inline-vote.neg,
            .ttr-inline-meta-value.neg,
            .ttr-inline-history-value.neg,
            .ttr-risk-value.neg,
            .ttr-collapsed-value.neg {
                color: #ff7b7b !important;
            }

            .ttr-inline-score.neutral,
            .ttr-inline-vote.neutral,
            .ttr-inline-meta-value.neutral,
            .ttr-inline-history-value.neutral,
            .ttr-risk-value.neutral,
            .ttr-collapsed-value.neutral {
                color: #f3f4f6 !important;
                opacity: 0.92 !important;
            }

            .ttr-gear {
                appearance: none !important;
                -webkit-appearance: none !important;
                border: 1px solid rgba(255,255,255,0.20) !important;
                background: rgba(255,255,255,0.04) !important;
                color: #fff !important;
                border-radius: 4px !important;
                padding: 3px 7px !important;
                font-size: 12px !important;
                cursor: pointer !important;
                line-height: 1 !important;
                box-sizing: border-box !important;
            }

            .ttr-collapsed-summary {
                display: none !important;
                width: 100% !important;
                white-space: normal !important;
                overflow: visible !important;
                text-overflow: unset !important;
                word-break: break-word !important;
                line-height: 1.45 !important;
                font-size: 11px !important;
                font-weight: 700 !important;
                opacity: 0.96 !important;
            }
            .ttr-collapsed-piece {
                display: inline !important;
            }
            .ttr-collapsed-sep {
                opacity: 0.65 !important;
                color: #fff !important;
            }

            .ttr-inline-status {
                display: block !important;
                opacity: 0.72 !important;
                width: 100% !important;
                margin-top: 8px !important;
            }

            .ttr-inline-status.centered {
                text-align: center !important;
            }

            .ttr-inline-actions {
                display: grid !important;
                grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
                gap: 8px !important;
                width: 100% !important;
                margin-top: 10px !important;
            }

            .ttr-inline-actions.hidden {
                display: none !important;
            }

            .ttr-inline-self-row {
                display: none !important;
                width: 100% !important;
                margin-top: 8px !important;
                justify-content: center !important;
                align-items: center !important;
                text-align: center !important;
            }

            .ttr-inline-self-row.show {
                display: flex !important;
            }

            .ttr-btn,
            .ttr-btn:visited,
            .ttr-btn:hover,
            .ttr-btn:active,
            .ttr-btn:focus,
            .ttr-self-btn,
            .ttr-link-btn,
            .ttr-tab-btn,
            .ttr-modal-save-top {
                appearance: none !important;
                -webkit-appearance: none !important;
                border-radius: 4px !important;
                padding: 7px 8px !important;
                font-size: 12px !important;
                font-weight: 700 !important;
                cursor: pointer !important;
                width: 100% !important;
                min-width: 0 !important;
                border-width: 1px !important;
                border-style: solid !important;
                box-shadow: none !important;
                text-shadow: none !important;
                background-image: none !important;
                outline: none !important;
                text-align: center !important;
                box-sizing: border-box !important;
            }

            .ttr-self-btn {
                width: auto !important;
                min-width: 150px !important;
                margin: 0 auto !important;
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
            }

            .ttr-btn-up,
            .ttr-btn-up:visited,
            .ttr-btn-up:hover,
            .ttr-btn-up:active,
            .ttr-btn-up:focus {
                color: #6fe26f !important;
                background: rgba(255,255,255,0.04) !important;
                border-color: rgba(111,226,111,0.45) !important;
            }

            .ttr-btn-down,
            .ttr-btn-down:visited,
            .ttr-btn-down:hover,
            .ttr-btn-down:active,
            .ttr-btn-down:focus {
                color: #ff7b7b !important;
                background: rgba(255,255,255,0.04) !important;
                border-color: rgba(255,123,123,0.45) !important;
            }

            .ttr-btn-up.active,
            .ttr-btn-up.active:visited,
            .ttr-btn-up.active:hover,
            .ttr-btn-up.active:active,
            .ttr-btn-up.active:focus {
                background: #2f9e44 !important;
                border-color: #2f9e44 !important;
                color: #fff !important;
            }

            .ttr-btn-down.active,
            .ttr-btn-down.active:visited,
            .ttr-btn-down.active:hover,
            .ttr-btn-down.active:active,
            .ttr-btn-down.active:focus {
                background: #d9485f !important;
                border-color: #d9485f !important;
                color: #fff !important;
            }

            .ttr-link-btn,
            .ttr-link-btn:visited,
            .ttr-link-btn:hover,
            .ttr-link-btn:active,
            .ttr-link-btn:focus,
            .ttr-self-btn,
            .ttr-modal-save-top,
            .ttr-tab-btn {
                color: #fff !important;
                background: rgba(255,255,255,0.04) !important;
                border-color: rgba(255,255,255,0.22) !important;
                text-decoration: none !important;
                display: inline-flex !important;
                align-items: center !important;
                justify-content: center !important;
            }

            .ttr-modal-save-top {
                border-color: rgba(111,226,111,0.45) !important;
                color: #6fe26f !important;
                width: auto !important;
                flex: 0 0 auto !important;
                padding: 7px 10px !important;
            }

            .ttr-btn:disabled,
            .ttr-self-btn:disabled {
                opacity: 0.55 !important;
                cursor: not-allowed !important;
            }

            .ttr-modal-backdrop {
                position: fixed !important;
                inset: 0 !important;
                background: rgba(0,0,0,0.60) !important;
                z-index: 2147483646 !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
                padding: 12px !important;
            }

            .ttr-modal {
                position: relative !important;
                width: min(560px, 100%) !important;
                max-height: min(88vh, 820px) !important;
                overflow: auto !important;
                background: #111827 !important;
                color: #f3f4f6 !important;
                border: 1px solid #374151 !important;
                border-radius: 10px !important;
                padding: 16px !important;
                box-sizing: border-box !important;
                box-shadow: 0 10px 30px rgba(0,0,0,0.35) !important;
            }

            .ttr-modal h3 { margin: 0 0 10px 0 !important; font-size: 16px !important; }
            .ttr-modal p { margin: 0 0 10px 0 !important; line-height: 1.45 !important; opacity: 0.92 !important; }

            .ttr-modal input[type="text"] {
                width: 100% !important;
                box-sizing: border-box !important;
                padding: 8px 10px !important;
                border-radius: 6px !important;
                border: 1px solid #4b5563 !important;
                background: #0b1220 !important;
                color: #f3f4f6 !important;
                margin: 8px 0 12px 0 !important;
                font-size: 12px !important;
            }

            .ttr-modal .ttr-modal-note { opacity: 0.72 !important; font-size: 12px !important; }

            .ttr-modal label {
                display: flex !important;
                align-items: center !important;
                gap: 8px !important;
                margin: 8px 0 !important;
            }

            .ttr-modal-topbar {
                display: flex !important;
                align-items: center !important;
                justify-content: space-between !important;
                gap: 8px !important;
                margin-bottom: 8px !important;
            }

            .ttr-modal-topbar h3 {
                margin: 0 !important;
                flex: 1 1 auto !important;
                min-width: 0 !important;
            }

            .ttr-modal-tabs {
                display: flex !important;
                gap: 8px !important;
                flex-wrap: wrap !important;
                margin: 10px 0 14px 0 !important;
            }

            .ttr-tab-btn.active {
                border-color: rgba(111,226,111,0.45) !important;
                color: #6fe26f !important;
            }

            .ttr-tab-panel { display: none !important; }
            .ttr-tab-panel.active { display: block !important; }

            .ttr-mug-option {
                appearance: none !important;
                -webkit-appearance: none !important;
                width: 100% !important;
                text-align: left !important;
                border: 1px solid #374151 !important;
                background: #0b1220 !important;
                color: #f3f4f6 !important;
                border-radius: 8px !important;
                padding: 10px 12px !important;
                margin-top: 8px !important;
                cursor: pointer !important;
                font-size: 13px !important;
                box-sizing: border-box !important;
            }

            .ttr-mug-option:hover { filter: brightness(1.06) !important; }

            .ttr-mug-actions-grid {
                display: grid !important;
                grid-template-columns: 1fr !important;
                gap: 8px !important;
                margin-top: 12px !important;
            }

            .ttr-votes-tools {
                display: grid !important;
                grid-template-columns: minmax(0, 1fr) !important;
                gap: 8px !important;
                margin: 8px 0 10px 0 !important;
            }

            .ttr-votes-search {
                width: 100% !important;
                box-sizing: border-box !important;
                padding: 8px 10px !important;
                border-radius: 6px !important;
                border: 1px solid #4b5563 !important;
                background: #0b1220 !important;
                color: #f3f4f6 !important;
                margin: 0 !important;
                font-size: 12px !important;
            }

            .ttr-votes-list {
                margin-top: 10px !important;
                border: 1px solid #374151 !important;
                border-radius: 8px !important;
                overflow: hidden !important;
            }

            .ttr-votes-head, .ttr-votes-row {
                display: grid !important;
                grid-template-columns: minmax(0, 1.6fr) 110px 110px !important;
                gap: 10px !important;
                align-items: center !important;
                padding: 10px 12px !important;
            }

            .ttr-votes-head { background: rgba(255,255,255,0.05) !important; font-weight: 700 !important; }
            .ttr-votes-row { border-top: 1px solid rgba(255,255,255,0.08) !important; }
            .ttr-votes-empty { padding: 14px 12px !important; opacity: 0.8 !important; }

            .ttr-votes-link {
                color: #93c5fd !important;
                text-decoration: none !important;
                overflow: hidden !important;
                text-overflow: ellipsis !important;
                white-space: nowrap !important;
                display: block !important;
            }

            .ttr-vote-up { color: #6fe26f !important; font-weight: 700 !important; }
            .ttr-vote-down { color: #ff7b7b !important; font-weight: 700 !important; }
            .ttr-votes-row.hidden { display: none !important; }

            .ttr-contact-text {
                margin-bottom: 10px !important;
                opacity: 0.92 !important;
                line-height: 1.45 !important;
            }

            .ttr-perm-list {
                display: grid !important;
                grid-template-columns: 1fr !important;
                gap: 6px !important;
                margin: 8px 0 12px 0 !important;
            }

            .ttr-perm-item {
                display: block !important;
                padding: 8px 10px !important;
                border-radius: 6px !important;
                background: rgba(255,255,255,0.03) !important;
                border: 1px solid rgba(255,255,255,0.08) !important;
                font-size: 13px !important;
                word-break: break-word !important;
            }

            .ttr-button-row {
                display: grid !important;
                grid-template-columns: 1fr !important;
                gap: 10px !important;
                width: 100% !important;
                margin-top: 10px !important;
            }

            @media (min-width: 640px) {
                .ttr-button-row.cols-2 {
                    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
                }
            }

            @media (min-width: 760px) {
                .ttr-button-row.cols-3 {
                    grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
                }
            }

            /* Mobile / narrow profile layout: 4 rows × 2 columns */
            @media (max-width: 900px) {
                .ttr-inline-card { padding: 9px 10px !important; }

                .ttr-inline-main {
                    grid-template-columns: minmax(0,1fr) auto minmax(0,1fr) !important;
                    gap: 6px !important;
                    font-size: 11px !important;
                }

                .ttr-inline-stats-row {
                    grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
                    gap: 8px !important;
                }

                .ttr-stat-cell {
                    white-space: normal !important;
                    overflow: visible !important;
                    text-overflow: unset !important;
                    line-height: 1.4 !important;
                }

                .ttr-inline-actions { gap: 6px !important; }

                .ttr-btn,
                .ttr-btn:visited,
                .ttr-btn:hover,
                .ttr-btn:active,
                .ttr-btn:focus,
                .ttr-self-btn,
                .ttr-link-btn,
                .ttr-tab-btn,
                .ttr-modal-save-top {
                    padding: 8px 4px !important;
                    font-size: 11px !important;
                }

                .ttr-votes-head, .ttr-votes-row {
                    grid-template-columns: minmax(0, 1.4fr) 80px 80px !important;
                    gap: 8px !important;
                    font-size: 12px !important;
                }
            }

            @media (max-width: 560px) {
                .ttr-modal {
                    padding: 14px !important;
                }

                .ttr-modal-topbar {
                    flex-direction: row !important;
                    align-items: center !important;
                    justify-content: space-between !important;
                    gap: 8px !important;
                    flex-wrap: nowrap !important;
                }

                .ttr-modal-topbar h3 {
                    flex: 1 1 auto !important;
                    min-width: 0 !important;
                }

                .ttr-modal-save-top {
                    width: auto !important;
                    flex: 0 0 auto !important;
                }

                .ttr-modal-tabs {
                    display: grid !important;
                    grid-template-columns: 1fr !important;
                }

                .ttr-tab-btn {
                    width: 100% !important;
                }
            }

            @media (max-width: 430px) {
                .ttr-inline-reason-row {
                    flex-direction: column !important;
                    gap: 2px !important;
                }

                .ttr-inline-actions {
                    grid-template-columns: 1fr !important;
                }
            }
        `;
        document.head.appendChild(style);
    }

    function loadPanelState() {
        try {
            const raw = getPersistentValue(STORAGE_KEY_PANEL_STATE);
            if (!raw) return { collapsed: false };
            const parsed = JSON.parse(raw);
            return { collapsed: !!parsed?.collapsed };
        } catch {
            return { collapsed: false };
        }
    }

    async function persistPanelState() {
        try {
            setPersistentValue(STORAGE_KEY_PANEL_STATE, JSON.stringify(panelState));
        } catch {}
    }

    function installRouteHooks() {
        const trigger = () => {
            setTimeout(() => {
                refreshForCurrentRoute().catch((err) => {
                    console.error('[TTR] Route refresh error:', err);
                });
                scheduleEventsSync(true);
            }, 150);
        };

        window.addEventListener('hashchange', trigger);
        window.addEventListener('popstate', trigger);

        const wrapHistory = (fnName) => {
            const original = history[fnName];
            if (typeof original !== 'function') return;
            history[fnName] = function (...args) {
                const out = original.apply(this, args);
                trigger();
                return out;
            };
        };

        wrapHistory('pushState');
        wrapHistory('replaceState');
    }

    function getInjectedPdaApiKey() {
        const raw = String(PDA_KEY ?? '').trim();
        if (!raw) return null;
        if (raw.charAt(0) === '#') return null;
        return raw;
    }

    function getStoredTornApiKey() {
        return safeStorageGet(STORAGE_KEY_TORN) || '';
    }

    function setStoredTornApiKey(key) {
        const oldKey = getStoredTornApiKey();
        const cleaned = String(key || '').trim();

        if (cleaned) safeStorageSet(STORAGE_KEY_TORN, cleaned);
        else safeStorageRemove(STORAGE_KEY_TORN);

        if (cleaned !== oldKey) {
            selfProfileCache = null;
            clearAllTtrCaches();
        }
    }

    function getEffectiveTornApiKey() {
        return getStoredTornApiKey() || getInjectedPdaApiKey() || null;
    }

    function getStoredTeApiKey() {
        return safeStorageGet(STORAGE_KEY_TE) || '';
    }

    function isTeEnabled() {
        const raw = safeStorageGet(STORAGE_KEY_TE_ENABLED);
        return raw !== '0';
    }

    function setStoredTeApiKey(key) {
        const oldKey = getStoredTeApiKey();
        const cleaned = String(key || '').trim();

        if (cleaned) safeStorageSet(STORAGE_KEY_TE, cleaned);
        else safeStorageRemove(STORAGE_KEY_TE);

        if (cleaned !== oldKey) {
            clearSupplementalCache();
        }
    }

    function setTeEnabled(enabled) {
        const old = isTeEnabled();
        safeStorageSet(STORAGE_KEY_TE_ENABLED, enabled ? '1' : '0');
        if (old !== enabled) {
            clearSupplementalCache();
        }
    }

    function safeStorageGet(key) {
        try { return localStorage.getItem(key); } catch { return null; }
    }

    function safeStorageSet(key, value) {
        try { localStorage.setItem(key, value); } catch {}
    }

    function safeStorageRemove(key) {
        try { localStorage.removeItem(key); } catch {}
    }

    function safeSessionGet(key) {
        try { return sessionStorage.getItem(key); } catch { return null; }
    }

    function safeSessionSet(key, value) {
        try { sessionStorage.setItem(key, value); } catch {}
    }

    function getPersistentValue(key) {
        return safeStorageGet(key) ?? safeSessionGet(key) ?? null;
    }

    function setPersistentValue(key, value) {
        let wrote = false;
        try {
            localStorage.setItem(key, value);
            wrote = true;
        } catch {}
        if (!wrote) {
            try { sessionStorage.setItem(key, value); } catch {}
        }
    }

    function safeReadJson(key, fallback) {
        try {
            const raw = localStorage.getItem(key);
            if (!raw) return fallback;
            return JSON.parse(raw);
        } catch {
            try {
                const raw = sessionStorage.getItem(key);
                if (!raw) return fallback;
                return JSON.parse(raw);
            } catch {
                return fallback;
            }
        }
    }

    function safeWriteJson(key, value) {
        const raw = JSON.stringify(value);
        let wrote = false;
        try {
            localStorage.setItem(key, raw);
            wrote = true;
        } catch {}
        if (!wrote) {
            try { sessionStorage.setItem(key, raw); } catch {}
        }
    }

    function loadSyncedEventRefs() {
        try {
            const raw = getPersistentValue(STORAGE_KEY_SYNCED_EVENT_REFS);
            if (!raw) return new Set();
            const arr = JSON.parse(raw);
            if (!Array.isArray(arr)) return new Set();
            return new Set(arr.filter(Boolean));
        } catch {
            return new Set();
        }
    }

    function saveSyncedEventRefs(set) {
        try {
            const arr = Array.from(set).slice(-MAX_SYNCED_EVENT_REFS);
            setPersistentValue(STORAGE_KEY_SYNCED_EVENT_REFS, JSON.stringify(arr));
        } catch {}
    }

    function getNowMs() {
        return Date.now();
    }

    function makeCacheEntry(data, ttlMs = CACHE_TTL_MS) {
        return {
            savedAt: getNowMs(),
            expiresAt: getNowMs() + ttlMs,
            data
        };
    }

    function isCacheEntryValid(entry) {
        return !!entry && typeof entry === 'object' && Number(entry.expiresAt || 0) > getNowMs();
    }

    function readCacheBucket(bucketKey) {
        const raw = safeReadJson(bucketKey, {});
        return raw && typeof raw === 'object' ? raw : {};
    }

    function writeCacheBucket(bucketKey, bucket) {
        safeWriteJson(bucketKey, bucket || {});
    }

    function getCacheValue(bucketKey, itemKey) {
        const bucket = readCacheBucket(bucketKey);
        const entry = bucket[itemKey];
        if (!isCacheEntryValid(entry)) {
            if (entry) {
                delete bucket[itemKey];
                writeCacheBucket(bucketKey, bucket);
            }
            return null;
        }
        return entry.data ?? null;
    }

    function setCacheValue(bucketKey, itemKey, data, ttlMs = CACHE_TTL_MS) {
        const bucket = readCacheBucket(bucketKey);
        bucket[itemKey] = makeCacheEntry(data, ttlMs);
        writeCacheBucket(bucketKey, bucket);
    }

    function deleteCacheValue(bucketKey, itemKey) {
        const bucket = readCacheBucket(bucketKey);
        if (Object.prototype.hasOwnProperty.call(bucket, itemKey)) {
            delete bucket[itemKey];
            writeCacheBucket(bucketKey, bucket);
        }
    }

    function pruneBucket(bucketKey, maxEntries = 300) {
        const bucket = readCacheBucket(bucketKey);
        const now = getNowMs();
        let changed = false;

        for (const key of Object.keys(bucket)) {
            const entry = bucket[key];
            if (!entry || Number(entry.expiresAt || 0) <= now) {
                delete bucket[key];
                changed = true;
            }
        }

        const keys = Object.keys(bucket);
        if (keys.length > maxEntries) {
            keys.sort((a, b) => Number(bucket[b]?.savedAt || 0) - Number(bucket[a]?.savedAt || 0));
            const keep = new Set(keys.slice(0, maxEntries));
            for (const key of keys) {
                if (!keep.has(key)) {
                    delete bucket[key];
                    changed = true;
                }
            }
        }

        if (changed) writeCacheBucket(bucketKey, bucket);
    }

    function clearSupplementalCache() {
        safeStorageRemove(CACHE_KEYS.SUPPLEMENTAL);
        try { sessionStorage.removeItem(CACHE_KEYS.SUPPLEMENTAL); } catch {}
    }

    function clearAllTtrCaches() {
        for (const key of Object.values(CACHE_KEYS)) {
            safeStorageRemove(key);
            try { sessionStorage.removeItem(key); } catch {}
        }
    }

    function getViewerScopedProfileCacheKey(viewerId, targetId) {
        return `${viewerId}:${targetId}`;
    }

    function getSelfProfileCacheKey() {
        return 'self';
    }

    function getSupplementalCacheKey(targetId) {
        return String(targetId || '');
    }

    function getResolveUserCacheKey(name) {
        return normalizeName(name || '');
    }

    function isProfilePage() {
        return location.pathname === '/profiles.php' && !!new URL(location.href).searchParams.get('XID');
    }

    function isAnyTradePage() {
        return location.pathname === '/trade.php';
    }

    function isTradeRelevantPage() {
        if (!isAnyTradePage()) return false;
        const step = getHashStep().toLowerCase();
        return ['view', 'initiatetrade', 'add', 'confirm'].includes(step) || !!document.querySelector('.trade-cont.m-top10');
    }

    function isEventsPage() {
        const url = new URL(location.href);
        return location.pathname === '/page.php' && url.searchParams.get('sid') === 'events';
    }

    function getHashStep() {
        const hash = String(location.hash || '');
        const m1 = hash.match(/[#/]step=([^&]+)/i);
        if (m1) return m1[1];
        const m2 = hash.match(/step=([^&]+)/i);
        if (m2) return m2[1];
        return '';
    }

    function getTradeIdFromHash() {
        const hash = String(location.hash || '');
        const m = hash.match(/(?:^|[&#])ID=(\d+)/i);
        return m ? String(m[1]) : null;
    }

    function getProfileTargetId() {
        const xid = Number(new URL(location.href).searchParams.get('XID'));
        return Number.isInteger(xid) && xid > 0 ? xid : null;
    }

    function getProfileTargetName() {
        const title =
            document.querySelector('h4') ||
            document.querySelector('.content-title h4') ||
            document.querySelector('.profile-wrapper h4');

        const txt = String(title?.textContent || '').trim();
        if (!txt) return '';
        return txt.replace(/'s Profile$/i, '').trim();
    }

    function getTradeTargetNameFromDom() {
        const tradeNames = extractTradeNamesFromPage();
        const selfNameNorm = normalizeName(selfProfileCache?.name || '');

        if (tradeNames.length) {
            const other = tradeNames.find(n => normalizeName(n) !== selfNameNorm) || tradeNames[0];
            return String(other || '').trim();
        }

        const rightTitle = document.querySelector('.trade-cont.m-top10 .user.right .title-black, .trade-cont.m-top10 .user.right.tt-modified .title-black');
        return String(rightTitle?.textContent || '').trim();
    }

    function normalizeName(name) {
        return String(name || '').replace(/\s+/g, ' ').trim().toLowerCase();
    }

    function gmRequest(details) {
        return new Promise((resolve, reject) => {
            if (typeof GM_xmlhttpRequest !== 'function') {
                reject(new Error('GM_xmlhttpRequest unavailable'));
                return;
            }

            GM_xmlhttpRequest({
                timeout: 30000,
                ...details,
                onload: (res) => resolve(res),
                onerror: (err) => reject(err || new Error('Network error')),
                ontimeout: () => reject(new Error('Timeout'))
            });
        });
    }

    async function gmGetJson(url, extraHeaders = {}) {
        const res = await gmRequest({
            method: 'GET',
            url,
            headers: {
                Accept: 'application/json',
                ...extraHeaders
            }
        });

        let data = null;
        try {
            data = JSON.parse(res.responseText);
        } catch {
            throw new Error(`Invalid JSON response (${res.status})`);
        }

        return {
            status: res.status,
            ok: res.status >= 200 && res.status < 300,
            data
        };
    }

    async function gmPostJson(url, body, extraHeaders = {}) {
        const res = await gmRequest({
            method: 'POST',
            url,
            headers: {
                'Content-Type': 'application/json',
                Accept: 'application/json',
                ...extraHeaders
            },
            data: JSON.stringify(body)
        });

        let data = null;
        try {
            data = JSON.parse(res.responseText);
        } catch {}

        return {
            status: res.status,
            ok: res.status >= 200 && res.status < 300,
            data
        };
    }

    async function getSelfProfile() {
        if (selfProfileCache?.id) return selfProfileCache;

        const cached = getCacheValue(CACHE_KEYS.SELF_PROFILE, getSelfProfileCacheKey());
        if (cached?.id) {
            selfProfileCache = cached;
            return selfProfileCache;
        }

        const apiKey = getEffectiveTornApiKey();
        if (!apiKey) return null;

        const url = new URL('https://api.torn.com/v2/user/basic');
        url.searchParams.set('striptags', 'true');
        url.searchParams.set('key', apiKey);

        const res = await gmGetJson(url.toString());
        if (!res.ok || !res.data?.profile?.id) {
            throw new Error(res.data?.error?.error || `Failed to fetch self profile (${res.status})`);
        }

        selfProfileCache = res.data.profile;
        setCacheValue(CACHE_KEYS.SELF_PROFILE, getSelfProfileCacheKey(), selfProfileCache);
        pruneBucket(CACHE_KEYS.SELF_PROFILE, 3);
        return selfProfileCache;
    }

    function isSelfTarget() {
        const selfId = Number(selfProfileCache?.id || 0);
        return !!selfId && !!currentTargetId && selfId === currentTargetId;
    }

    async function resolveUserByName(name) {
        const cacheKey = getResolveUserCacheKey(name);
        if (!cacheKey) return null;

        const cached = getCacheValue(CACHE_KEYS.RESOLVE_USER, cacheKey);
        if (cached?.id) return cached;

        const res = await gmPostJson(`${WORKER_BASE}/resolve-user`, { name });
        if (!res.ok || !res.data?.ok) return null;

        const user = res.data.user || null;
        if (user?.id) {
            setCacheValue(CACHE_KEYS.RESOLVE_USER, cacheKey, user);
            pruneBucket(CACHE_KEYS.RESOLVE_USER, 500);
        }
        return user;
    }

    function extractTradeNamesFromPage() {
        const names = new Set();

        const tradeTitle = document.querySelector('.trade-cont.m-top10 .t-title.title-black');
        const titleText = String(tradeTitle?.textContent || '').trim();
        const m = titleText.match(/Trade between\s+(.+?)\s*&\s*(.+)$/i);
        if (m) {
            if (m[1]) names.add(m[1].trim());
            if (m[2]) names.add(m[2].trim());
        }

        const rightTitle = document.querySelector('.trade-cont.m-top10 .user.right .title-black, .trade-cont.m-top10 .user.right.tt-modified .title-black');
        const rightText = String(rightTitle?.textContent || '').trim();
        if (rightText) names.add(rightText);

        const leftTitle = document.querySelector('.trade-cont.m-top10 .user.left .title-black, .trade-cont.m-top10 .user.left.tt-modified .title-black');
        const leftText = String(leftTitle?.textContent || '').trim();
        if (leftText) names.add(leftText);

        return [...names].filter(Boolean);
    }

    function extractTradeTargetIdFromAnchors(selfId) {
        const scope = document.querySelector('.trade-cont.m-top10') || document;
        const anchors = Array.from(scope.querySelectorAll('a[href*="profiles.php?XID="], a[href*="/profiles.php?XID="]'));

        const ids = [...new Set(
            anchors
                .map(a => {
                    const m = a.href.match(/XID=(\d+)/);
                    return m ? Number(m[1]) : null;
                })
                .filter(id => Number.isInteger(id) && id > 0)
        )];

        if (!ids.length) return null;
        if (ids.length === 1) return ids[0];

        const other = ids.find(id => id !== selfId);
        return other || ids[0];
    }

    async function getTradeTargetId() {
        const tradeContainer = document.querySelector('.trade-cont.m-top10');
        if (!tradeContainer) return null;

        const self = await getSelfProfile().catch(() => null);
        const selfId = Number(self?.id || 0);
        const selfNameNorm = normalizeName(self?.name || '');

        const anchorId = extractTradeTargetIdFromAnchors(selfId);
        if (anchorId && anchorId !== selfId) return anchorId;

        const tradeNames = extractTradeNamesFromPage();
        if (tradeNames.length) {
            const otherName = tradeNames.find(name => normalizeName(name) !== selfNameNorm) || tradeNames[0];
            if (otherName) {
                const resolved = await resolveUserByName(otherName).catch(() => null);
                if (resolved?.id) return resolved.id;
            }
        }

        const logAnchor = Array.from(document.querySelectorAll('.log a[href*="profiles.php?XID="], .log a[href*="/profiles.php?XID="]'))
            .find(a => normalizeName(a.textContent) !== selfNameNorm);

        if (logAnchor) {
            const m = logAnchor.href.match(/XID=(\d+)/);
            if (m) return Number(m[1]);
        }

        return null;
    }

    function createInlineCard() {
        const wrap = document.createElement('div');
        wrap.id = 'ttr-inline-root';
        wrap.className = 'ttr-inline-card ttr-tone-neutral';
        if (panelState.collapsed) wrap.classList.add('collapsed');

        wrap.innerHTML = `
            <div class="ttr-inline-main" id="ttr-header">
                <div class="ttr-first-left">
                    <span class="ttr-inline-label">User:</span><span id="ttr-name" class="ttr-inline-name">-</span>
                </div>
                <div class="ttr-first-center">
                    🎖️ Torn Trade Reputation 🎖️
                </div>
                <div class="ttr-first-right">
                    <div class="ttr-inline-top-right">
                        <button class="ttr-gear" id="ttr-collapse-btn" title="Collapse / Expand">${panelState.collapsed ? '▲' : '▼'}</button>
                        <button id="ttr-settings" class="ttr-gear" type="button" title="Setting">⚙</button>
                    </div>
                </div>
            </div>

            <div id="ttr-collapsed-summary" class="ttr-collapsed-summary"></div>

            <div id="ttr-body">
                <div class="ttr-inline-reason-row" id="ttr-mug-row">
                    <span><span class="ttr-inline-label">Mug Reports:</span><span id="ttr-mugreports" class="ttr-inline-vote neutral">0</span></span>
                    <span><span class="ttr-inline-label">Reason:</span><span id="ttr-mug-top-reason" class="ttr-inline-vote neutral">-</span></span>
                </div>

                <div class="ttr-inline-stats-row">
                    <div class="ttr-stat-cell">
                        <span class="ttr-risk-label">Risk Label:</span>
                        <span id="ttr-risk-label" class="ttr-risk-value neutral">Neutral</span>
                    </div>
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">Current Bounty:</span>
                        <span id="ttr-current-bounty" class="ttr-inline-meta-value neg">$0</span>
                    </div>
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">90d Mugged:</span>
                        <span id="ttr-mug90d" class="ttr-inline-meta-value neutral">-</span>
                    </div>
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">Friend - Enemy:</span>
                        <span id="ttr-fe" class="ttr-inline-meta-value neutral">-</span>
                    </div>
                </div>

                <div class="ttr-inline-stats-row">
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">Your Vote:</span>
                        <span id="ttr-myvote" class="ttr-inline-vote neutral">0</span>
                    </div>
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">Score:</span>
                        <span id="ttr-score" class="ttr-inline-score neutral">-</span>
                    </div>
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">Trades Seen:</span>
                        <span id="ttr-trade-history" class="ttr-inline-history-value neutral">0</span>
                    </div>
                    <div class="ttr-stat-cell">
                        <span class="ttr-inline-label">Torn Exchange:</span>
                        <span id="ttr-te" class="ttr-inline-meta-value neutral">-</span>
                    </div>
                </div>

                <div><span id="ttr-status" class="ttr-inline-status"></span></div>

                <div id="ttr-self-row" class="ttr-inline-self-row">
                    <button id="ttr-self-votes-btn" class="ttr-self-btn" type="button">People I Voted</button>
                </div>

                <div id="ttr-actions" class="ttr-inline-actions">
                    <button id="ttr-up" class="ttr-btn ttr-btn-up" type="button">Upvote</button>
                    <button id="ttr-down" class="ttr-btn ttr-btn-down" type="button">Downvote</button>
                </div>
            </div>
        `;

        rootEl = wrap;
        bodyEl = wrap.querySelector('#ttr-body');
        collapsedSummaryEl = wrap.querySelector('#ttr-collapsed-summary');
        mugRowEl = wrap.querySelector('#ttr-mug-row');

        nameEl = wrap.querySelector('#ttr-name');
        scoreEl = wrap.querySelector('#ttr-score');
        myVoteEl = wrap.querySelector('#ttr-myvote');
        mugReportsEl = wrap.querySelector('#ttr-mugreports');
        mugTopReasonEl = wrap.querySelector('#ttr-mug-top-reason');
        tradeHistoryEl = wrap.querySelector('#ttr-trade-history');
        statusEl = wrap.querySelector('#ttr-status');
        btnUp = wrap.querySelector('#ttr-up');
        btnDown = wrap.querySelector('#ttr-down');
        actionsEl = wrap.querySelector('#ttr-actions');
        friendEnemyEl = wrap.querySelector('#ttr-fe');
        teEl = wrap.querySelector('#ttr-te');
        settingsBtn = wrap.querySelector('#ttr-settings');
        selfVotesBtn = wrap.querySelector('#ttr-self-votes-btn');
        riskLabelEl = wrap.querySelector('#ttr-risk-label');
        currentBountyEl = wrap.querySelector('#ttr-current-bounty');
        mug90dEl = wrap.querySelector('#ttr-mug90d');
        collapseBtn = wrap.querySelector('#ttr-collapse-btn');

        btnUp.addEventListener('click', () => submitVote(1));
        btnDown.addEventListener('click', () => handleDownvoteFlow());
        settingsBtn.addEventListener('click', openSettingsModal);
        selfVotesBtn.addEventListener('click', openMyVotesModal);

        collapseBtn.addEventListener('click', async (e) => {
            e.stopPropagation();
            panelState.collapsed = !panelState.collapsed;
            wrap.classList.toggle('collapsed', panelState.collapsed);
            collapseBtn.textContent = panelState.collapsed ? '▲' : '▼';
            await persistPanelState();
            updateCollapsedSummary();
        });

        updateMugRowVisibility(0);
        updateCollapsedSummary();

        return wrap;
    }

    function getProfileMountPoint() {
        const ffInfo = document.getElementById('ff-scouter-run-once');
        if (ffInfo && ffInfo.parentNode) {
            return { parent: ffInfo.parentNode, before: ffInfo.nextSibling || null };
        }

        const contentTitle =
            document.querySelector('.content-title') ||
            document.querySelector('h4')?.parentNode ||
            document.querySelector('.profile-wrapper h4')?.parentNode;

        if (contentTitle && contentTitle.parentNode) {
            return { parent: contentTitle.parentNode, before: contentTitle.nextSibling };
        }

        const h4 = document.querySelector('h4');
        if (h4?.parentNode?.parentNode) {
            return { parent: h4.parentNode.parentNode, before: h4.parentNode.nextSibling };
        }

        return null;
    }

    function getTradeMountPoint() {
        const invoice = document.querySelector('.reza-invoice-wrap');
        if (invoice && invoice.parentNode) {
            return { parent: invoice.parentNode, before: invoice };
        }

        const tradeContainer = document.querySelector('.trade-cont.m-top10');
        if (tradeContainer && tradeContainer.parentNode) {
            return { parent: tradeContainer.parentNode, before: tradeContainer };
        }

        const warnBox = document.querySelector('.info-msg-cont.border-round.m-top10');
        if (warnBox && warnBox.parentNode) {
            return { parent: warnBox.parentNode, before: warnBox.nextSibling };
        }

        const title = document.querySelector('.trade-wrap, .trade-cont');
        if (title && title.parentNode) {
            return { parent: title.parentNode, before: title };
        }

        return null;
    }

    function ensureMounted(contextType) {
        const point = contextType === 'trade' ? getTradeMountPoint() : getProfileMountPoint();
        if (!point) return false;

        let existing = document.getElementById('ttr-mount-row');
        if (!existing) {
            existing = document.createElement('div');
            existing.id = 'ttr-mount-row';
            existing.className = 'ttr-mount-row';
        }

        const desiredBefore = point.before || null;

        if (existing.parentNode !== point.parent || existing.nextSibling !== desiredBefore) {
            point.parent.insertBefore(existing, desiredBefore);
        }

        let card = document.getElementById('ttr-inline-root');
        if (!card) {
            card = createInlineCard();
            existing.appendChild(card);
        } else if (card.parentNode !== existing) {
            existing.appendChild(card);
        }

        rootEl = card;
        return true;
    }

    function setStatus(text, centered = false) {
        if (!statusEl) return;
        statusEl.textContent = text || '';
        statusEl.classList.toggle('centered', !!centered);
        updateCollapsedSummary();
    }

    function setDisabled(disabled) {
        if (btnUp) btnUp.disabled = !!disabled;
        if (btnDown) btnDown.disabled = !!disabled;
        if (selfVotesBtn) selfVotesBtn.disabled = !!disabled;
    }

    function setActionsVisible(visible) {
        if (!actionsEl) return;
        actionsEl.classList.toggle('hidden', !visible);
    }

    function setSelfVotesButtonVisible(visible) {
        const row = document.getElementById('ttr-self-row');
        if (!row) return;
        row.classList.toggle('show', !!visible);
    }

    function setScore(value) {
        if (!scoreEl) return;
        scoreEl.textContent = String(value ?? 0);
        scoreEl.classList.remove('pos', 'neg', 'neutral');

        const n = Number(value || 0);
        if (n > 0) scoreEl.classList.add('pos');
        else if (n < 0) scoreEl.classList.add('neg');
        else scoreEl.classList.add('neutral');

        updateCollapsedSummary();
    }

    function setVote(vote) {
        currentViewerVote = vote == null ? null : Number(vote);

        if (!myVoteEl) return;

        myVoteEl.classList.remove('pos', 'neg', 'neutral');
        btnUp?.classList.remove('active');
        btnDown?.classList.remove('active');

        if (vote === 1) {
            myVoteEl.textContent = '+1';
            myVoteEl.classList.add('pos');
            btnUp?.classList.add('active');
        } else if (vote === -1) {
            myVoteEl.textContent = '-1';
            myVoteEl.classList.add('neg');
            btnDown?.classList.add('active');
        } else {
            myVoteEl.textContent = '0';
            myVoteEl.classList.add('neutral');
        }

        updateCollapsedSummary();
    }

    function updateMugRowVisibility(value) {
        if (!mugRowEl) return;
        const shown = Number(value || 0);
        mugRowEl.style.display = shown > 0 ? 'flex' : 'none';
    }

    function setMugReports(value) {
        if (!mugReportsEl) return;
        const n = toNullableInt(value);
        const shown = n === null ? 0 : n;

        mugReportsEl.textContent = String(shown);
        mugReportsEl.classList.remove('pos', 'neg', 'neutral');

        if (shown > 0) mugReportsEl.classList.add('neg');
        else mugReportsEl.classList.add('neutral');

        updateMugRowVisibility(shown);
    }

    function setMugTopReason(topReason) {
        if (!mugTopReasonEl) return;

        mugTopReasonEl.classList.remove('neg', 'neutral');

        const label = topReason?.label || '-';
        mugTopReasonEl.textContent = label;

        if (label && label !== '-') mugTopReasonEl.classList.add('neg');
        else mugTopReasonEl.classList.add('neutral');
    }

    function setFriendEnemy(friendEnemy) {
        if (!friendEnemyEl) return;

        friendEnemyEl.classList.remove('pos', 'neg', 'neutral');

        const diff = toNullableInt(friendEnemy?.diff);
        if (diff === null) {
            friendEnemyEl.textContent = '-';
            friendEnemyEl.classList.add('neutral');
            updateCollapsedSummary();
            return;
        }

        friendEnemyEl.textContent = diff > 0 ? `+${diff}` : String(diff);
        if (diff > 0) friendEnemyEl.classList.add('pos');
        else if (diff < 0) friendEnemyEl.classList.add('neg');
        else friendEnemyEl.classList.add('neutral');

        updateCollapsedSummary();
    }

    function setTe(te) {
        if (!teEl) return;

        teEl.classList.remove('pos', 'neg', 'neutral');

        const votes = toNullableInt(te?.votes);
        if (votes === null) {
            teEl.textContent = '-';
            teEl.classList.add('neutral');
            updateCollapsedSummary();
            return;
        }

        teEl.textContent = votes > 0 ? `+${votes}` : String(votes);
        if (votes > 0) teEl.classList.add('pos');
        else if (votes < 0) teEl.classList.add('neg');
        else teEl.classList.add('neutral');

        updateCollapsedSummary();
    }

    function setTradeHistorySeen(value) {
        if (!tradeHistoryEl) return;

        const count = toNullableInt(value);
        const shown = count === null ? 0 : count;

        tradeHistoryEl.textContent = String(shown);
        tradeHistoryEl.classList.remove('pos', 'neg', 'neutral');

        if (shown > 0) tradeHistoryEl.classList.add('pos');
        else tradeHistoryEl.classList.add('neutral');

        updateCollapsedSummary();
    }

    function formatMoneyCompact(value) {
        const n = Number(value || 0);
        if (!Number.isFinite(n)) return '-';
        if (n <= 0) return '$0';
        if (n >= 1000000000) return `$${(n / 1000000000).toFixed(2)}B`;
        if (n >= 1000000) return `$${(n / 1000000).toFixed(2)}M`;
        if (n >= 1000) return `$${(n / 1000).toFixed(2)}K`;
        return `$${Math.round(n)}`;
    }

    function setCurrentBountyTotal(value) {
        if (!currentBountyEl) return;
        const n = Number(value || 0);

        currentBountyEl.classList.remove('pos', 'neg', 'neutral');
        currentBountyEl.textContent = formatMoneyCompact(n);
        currentBountyEl.classList.add('neg');

        updateCollapsedSummary();
    }

    function setMuggedLast90d(value) {
        if (!mug90dEl) return;
        const n = Number(value || 0);

        mug90dEl.classList.remove('pos', 'neg', 'neutral');

        if (!Number.isFinite(n) || n < 0) {
            mug90dEl.textContent = '-';
            mug90dEl.classList.add('neutral');
            updateCollapsedSummary();
            return;
        }

        mug90dEl.textContent = formatMoneyCompact(n);
        if (n >= 250000000) mug90dEl.classList.add('neg');
        else mug90dEl.classList.add('neutral');

        updateCollapsedSummary();
    }

    function applyRiskTone(tone) {
        if (!rootEl) return;
        rootEl.classList.remove('ttr-tone-safe', 'ttr-tone-neutral', 'ttr-tone-risky');

        if (tone === 'safe') rootEl.classList.add('ttr-tone-safe');
        else if (tone === 'risky') rootEl.classList.add('ttr-tone-risky');
        else rootEl.classList.add('ttr-tone-neutral');
    }

    function getRiskAssessment(data) {
        const viewerVote = Number(data?.viewerVote || 0);
        const score = Number(data?.score || 0);
        const mugReports = Number(data?.mugReports || 0);

        const friends = Number(data?.friendEnemy?.friends || 0);
        const enemies = Number(data?.friendEnemy?.enemies || 0);
        const diff = Number(
            data?.friendEnemy?.diff ??
            ((Number.isFinite(friends) && Number.isFinite(enemies)) ? (friends - enemies) : 0)
        );

        const currentBounty = Number(data?.currentBountyTotal || 0);
        const mugged90d = Number(data?.muggedLast90d || 0);
        const enemiesDominate = enemies > friends;

        if (viewerVote === 1) {
            if (!enemiesDominate) return { key: 'safe', label: 'Safe', tone: 'safe' };
            if (mugReports > 0 || currentBounty > 0 || mugged90d >= 1000000000) {
                return { key: 'neutral', label: 'Neutral', tone: 'neutral' };
            }
            return { key: 'safe', label: 'Safe', tone: 'safe' };
        }

        if (viewerVote === -1) {
            return { key: 'risky', label: 'Likely Buy-Mugger', tone: 'risky' };
        }

        if (score >= 3) {
            if (!enemiesDominate) return { key: 'safe', label: 'Safe', tone: 'safe' };
            return { key: 'neutral', label: 'Neutral', tone: 'neutral' };
        }

        if (score <= -3) {
            return { key: 'risky', label: 'Likely Buy-Mugger', tone: 'risky' };
        }

        if (!enemiesDominate) {
            if (diff > 0) return { key: 'safe', label: 'Safe', tone: 'safe' };
            return { key: 'neutral', label: 'Neutral', tone: 'neutral' };
        }

        let danger = 0;
        if (mugReports > 0) danger += 2;
        if (currentBounty > 0) danger += 1;
        if (mugged90d >= 1000000000) danger += 2;
        else if (mugged90d >= 250000000) danger += 1;

        if (diff <= -500) danger += 2;
        else if (diff < 0) danger += 1;

        if (danger >= 4) {
            return { key: 'risky', label: 'Likely Buy-Mugger', tone: 'risky' };
        }

        return { key: 'neutral', label: 'Neutral', tone: 'neutral' };
    }

    function setRiskLabel(data) {
        if (!riskLabelEl) return;

        const risk = getRiskAssessment(data);

        riskLabelEl.textContent = risk.label;
        riskLabelEl.classList.remove('pos', 'neg', 'neutral');

        if (risk.tone === 'safe') riskLabelEl.classList.add('pos');
        else if (risk.tone === 'risky') riskLabelEl.classList.add('neg');
        else riskLabelEl.classList.add('neutral');

        applyRiskTone(risk.tone);
        updateCollapsedSummary();
    }

    function getValueClass(el) {
        if (!el) return 'neutral';
        if (el.classList.contains('pos')) return 'pos';
        if (el.classList.contains('neg')) return 'neg';
        return 'neutral';
    }

    function makeCollapsedPiece(label, el, fallback) {
        const value = String(el?.textContent || fallback || '-').trim() || '-';
        const cls = getValueClass(el);
        return `<span class="ttr-collapsed-piece">${escapeHtml(label)}: <span class="ttr-collapsed-value ${cls}">${escapeHtml(value)}</span></span>`;
    }

    function updateCollapsedSummary() {
        if (!collapsedSummaryEl) return;

        collapsedSummaryEl.innerHTML = [
            makeCollapsedPiece('Risk Label', riskLabelEl, 'Neutral'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('Current Bounty', currentBountyEl, '$0'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('90d Mugged', mug90dEl, '$0'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('Friend - Enemy', friendEnemyEl, '-'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('Your Vote', myVoteEl, '0'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('Score', scoreEl, '0'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('Trades Seen', tradeHistoryEl, '0'),
            '<span class="ttr-collapsed-sep"> | </span>',
            makeCollapsedPiece('Torn Exchange', teEl, '-')
        ].join('');
    }

    function openTornKeyPrompt() {
        if (getEffectiveTornApiKey()) return;
        const existing = document.getElementById('ttr-torn-key-backdrop');
        if (existing) return;

        const backdrop = document.createElement('div');
        backdrop.id = 'ttr-torn-key-backdrop';
        backdrop.className = 'ttr-modal-backdrop';

        backdrop.innerHTML = `
            <div class="ttr-modal">
                <h3>Torn API Key Required</h3>
                <p>To use Torn Trade Reputation outside Torn PDA auto-injection, please save your Torn <strong>public</strong> API key here.</p>
                <p class="ttr-modal-note">This key is stored locally in your browser only and is used so the script can identify you and load profile / trade reputation correctly.</p>
                <input id="ttr-torn-key-input" type="text" placeholder="Paste your Torn public API key here" value="" />
                <div class="ttr-button-row">
                    <button id="ttr-torn-key-save" class="ttr-modal-save-top" type="button">Save and close</button>
                </div>
            </div>
        `;

        document.body.appendChild(backdrop);

        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) backdrop.remove();
        });

        backdrop.querySelector('#ttr-torn-key-save')?.addEventListener('click', async () => {
            const input = backdrop.querySelector('#ttr-torn-key-input');
            const value = String(input?.value || '').trim();
            if (!value) return;

            setStoredTornApiKey(value);
            backdrop.remove();

            if (currentTargetId) {
                try {
                    await refreshProfileCard({ preferCache: false });
                    await refreshSupplementalData(currentTargetId, { preferCache: false });
                } catch (err) {
                    console.warn('[TTR] Refresh after Torn key save warning:', err);
                }
            }

            scheduleEventsSync(true);
        });
    }

    function activateSettingsTab(backdrop, tabName) {
        backdrop.querySelectorAll('.ttr-tab-btn').forEach(btn => {
            btn.classList.toggle('active', btn.dataset.tab === tabName);
        });
        backdrop.querySelectorAll('.ttr-tab-panel').forEach(panel => {
            panel.classList.toggle('active', panel.dataset.tab === tabName);
        });
    }

    function openSettingsModal() {
        const existing = document.getElementById('ttr-modal-backdrop');
        if (existing) existing.remove();

        const backdrop = document.createElement('div');
        backdrop.id = 'ttr-modal-backdrop';
        backdrop.className = 'ttr-modal-backdrop';

        const teKey = getStoredTeApiKey();
        const tornKey = getStoredTornApiKey();

        backdrop.innerHTML = `
            <div class="ttr-modal">
                <div class="ttr-modal-topbar">
                    <h3>Setting</h3>
                    <button id="ttr-modal-save-top" class="ttr-modal-save-top" type="button">Save and close</button>
                </div>

                <div class="ttr-modal-tabs">
                    <button class="ttr-tab-btn active" data-tab="te" type="button">Torn Exchange key</button>
                    <button class="ttr-tab-btn" data-tab="api" type="button">API key</button>
                    <button class="ttr-tab-btn" data-tab="contact" type="button">Contact</button>
                </div>

                <div class="ttr-tab-panel active" data-tab="te">
                    <p>Enter your Torn Exchange API key here if you want the script to fetch TE trader reputation for the current target.</p>
                    <p class="ttr-modal-note">Your TE key is stored locally in your browser / PDA only. The script sends TE result data to the reputation server as a backup snapshot, but it does not send your TE key itself.</p>
                    <label>
                        <input id="ttr-te-enabled" type="checkbox" ${isTeEnabled() ? 'checked' : ''} />
                        Enable TE lookup
                    </label>
                    <input id="ttr-te-key" type="text" placeholder="Paste your TE API key here" value="${escapeHtmlAttr(teKey)}" />
                    <div class="ttr-modal-note">Use the same TE API key you use to sign in to Torn Exchange.</div>
                </div>

                <div class="ttr-tab-panel" data-tab="api">
                    <p>If you do not want to use your main Torn public API key, you can create a custom public key for Torn Trade Reputation with only the permissions this script uses:</p>
                    <div class="ttr-perm-list">
                        <div class="ttr-perm-item"><code>[/user/basic]</code></div>
                        <div class="ttr-perm-item"><code>[/user/profile]</code></div>
                        <div class="ttr-perm-item"><code>[/user/faction]</code></div>
                        <div class="ttr-perm-item"><code>[/user/bounties]</code></div>
                        <div class="ttr-perm-item"><code>[/user/personalstats]</code></div>
                    </div>
                    <input id="ttr-torn-key" type="text" placeholder="Paste your Torn public / custom API key here" value="${escapeHtmlAttr(tornKey)}" />
                    <div class="ttr-button-row cols-2">
                        <a class="ttr-link-btn" href="https://www.torn.com/preferences.php#tab=api?&step=addNewKey&title=Torn%20Trade%20Reputation&type=1" target="_blank" rel="noopener noreferrer">Create public key</a>
                        <a class="ttr-link-btn" href="https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=Torn%20Trade%20Reputation&user=basic,profile,faction,bounties,personalstats" target="_blank" rel="noopener noreferrer">Create custom key</a>
                    </div>
                </div>

                <div class="ttr-tab-panel" data-tab="contact">
                    <div class="ttr-contact-text">
                        Report bugs, send ideas, or contact Vreebn [4149405] here.
                    </div>
                    <div class="ttr-button-row cols-3">
                        <a class="ttr-link-btn" href="https://www.torn.com/profiles.php?XID=4149405" target="_blank" rel="noopener noreferrer">Profile</a>
                        <a class="ttr-link-btn" href="https://www.torn.com/messages.php#/p=compose&XID=4149405" target="_blank" rel="noopener noreferrer">Message</a>
                        <a class="ttr-link-btn" href="https://www.torn.com/forums.php#/p=threads&f=67&t=16555319&b=0&a=0" target="_blank" rel="noopener noreferrer">Forum</a>
                    </div>
                    <div class="ttr-modal-note" style="margin-top:10px;">Using subject: <code>TTR:</code> helps keep messages organized.</div>
                </div>
            </div>
        `;

        document.body.appendChild(backdrop);

        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) backdrop.remove();
        });

        backdrop.querySelectorAll('.ttr-tab-btn').forEach(btn => {
            btn.addEventListener('click', () => activateSettingsTab(backdrop, btn.dataset.tab));
        });

        backdrop.querySelector('#ttr-modal-save-top')?.addEventListener('click', async () => {
            const teInput = backdrop.querySelector('#ttr-te-key');
            const teEnabled = !!backdrop.querySelector('#ttr-te-enabled')?.checked;
            const teValue = String(teInput?.value || '').trim();

            const tornInput = backdrop.querySelector('#ttr-torn-key');
            const tornValue = String(tornInput?.value || '').trim();

            setStoredTeApiKey(teValue);
            setTeEnabled(teEnabled);
            setStoredTornApiKey(tornValue);

            backdrop.remove();
            setStatus('Settings saved');

            if (currentTargetId) {
                try {
                    await refreshProfileCard({ preferCache: false });
                    await refreshSupplementalData(currentTargetId, { preferCache: false });
                } catch (err) {
                    console.warn('[TTR] Refresh after settings save warning:', err);
                }
            }

            scheduleEventsSync(true);
        });
    }

    function closeAnyModalById(id) {
        const existing = document.getElementById(id);
        if (existing) existing.remove();
    }

    function closeAnyMugModal() {
        closeAnyModalById('ttr-mug-backdrop');
    }

    function openMugDecisionModal() {
        closeAnyMugModal();

        const backdrop = document.createElement('div');
        backdrop.id = 'ttr-mug-backdrop';
        backdrop.className = 'ttr-modal-backdrop';

        backdrop.innerHTML = `
            <div class="ttr-modal">
                <h3>Report mug?</h3>
                <p>Do you want to add a mug report for this player too?</p>
                <div class="ttr-mug-actions-grid">
                    <button id="ttr-mug-yes" class="ttr-btn ttr-btn-down" type="button">Yes</button>
                    <button id="ttr-mug-no" class="ttr-link-btn" type="button">No</button>
                    <button id="ttr-mug-cancel" class="ttr-link-btn" type="button">Cancel</button>
                </div>
            </div>
        `;

        document.body.appendChild(backdrop);

        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) backdrop.remove();
        });

        backdrop.querySelector('#ttr-mug-cancel')?.addEventListener('click', () => {
            backdrop.remove();
        });

        backdrop.querySelector('#ttr-mug-no')?.addEventListener('click', async () => {
            backdrop.remove();
            await submitVote(-1);
        });

        backdrop.querySelector('#ttr-mug-yes')?.addEventListener('click', () => {
            backdrop.remove();
            openMugSubtypeModal();
        });
    }

    function openMugSubtypeModal() {
        closeAnyMugModal();

        const backdrop = document.createElement('div');
        backdrop.id = 'ttr-mug-backdrop';
        backdrop.className = 'ttr-modal-backdrop';

        backdrop.innerHTML = `
            <div class="ttr-modal">
                <h3>Mug report reason</h3>
                <p>Pick the subtype you want to save for this mug report.</p>
                <button class="ttr-mug-option" data-subtype="after_market_purchase" type="button">${MUG_SUBTYPES.after_market_purchase}</button>
                <button class="ttr-mug-option" data-subtype="after_cancelled_trade" type="button">${MUG_SUBTYPES.after_cancelled_trade}</button>
                <button class="ttr-mug-option" data-subtype="other_trade_related_mug" type="button">${MUG_SUBTYPES.other_trade_related_mug}</button>
                <div class="ttr-mug-actions-grid">
                    <button id="ttr-mug-back" class="ttr-link-btn" type="button">Back</button>
                    <button id="ttr-mug-cancel" class="ttr-link-btn" type="button">Cancel</button>
                </div>
            </div>
        `;

        document.body.appendChild(backdrop);

        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) backdrop.remove();
        });

        backdrop.querySelector('#ttr-mug-cancel')?.addEventListener('click', () => {
            backdrop.remove();
        });

        backdrop.querySelector('#ttr-mug-back')?.addEventListener('click', () => {
            backdrop.remove();
            openMugDecisionModal();
        });

        backdrop.querySelectorAll('.ttr-mug-option').forEach((btn) => {
            btn.addEventListener('click', async () => {
                const subtype = String(btn.getAttribute('data-subtype') || '').trim();
                backdrop.remove();
                await submitVote(-1, { mugReportSubtype: subtype });
            });
        });
    }

    async function openMyVotesModal() {
        const existing = document.getElementById('ttr-myvotes-backdrop');
        if (existing) existing.remove();

        const backdrop = document.createElement('div');
        backdrop.id = 'ttr-myvotes-backdrop';
        backdrop.className = 'ttr-modal-backdrop';

        backdrop.innerHTML = `
            <div class="ttr-modal">
                <div class="ttr-modal-topbar">
                    <button id="ttr-myvotes-close-top" class="ttr-link-btn" type="button" style="width:auto !important;">Close</button>
                    <h3>People I Voted</h3>
                    <div></div>
                </div>
                <div class="ttr-votes-tools">
                    <input id="ttr-myvotes-search" class="ttr-votes-search" type="text" placeholder="Search by name or ID..." />
                </div>
                <div id="ttr-myvotes-content"></div>
            </div>
        `;

        document.body.appendChild(backdrop);

        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) backdrop.remove();
        });

        backdrop.querySelector('#ttr-myvotes-close-top')?.addEventListener('click', () => {
            backdrop.remove();
        });

        const content = backdrop.querySelector('#ttr-myvotes-content');
        const searchInput = backdrop.querySelector('#ttr-myvotes-search');

        try {
            const apiKey = getEffectiveTornApiKey();
            if (!apiKey) throw new Error('Missing Torn API key');

            const res = await gmPostJson(`${WORKER_BASE}/private/my-votes`, {
                apiKey,
                scriptVersion: SCRIPT_VERSION
            });

            if (!res.ok || !res.data?.ok) {
                throw new Error(res.data?.error || `Load failed (${res.status})`);
            }

            const votes = Array.isArray(res.data?.votes) ? res.data.votes : [];

            if (!votes.length) {
                content.innerHTML = `<div class="ttr-votes-empty">You have not voted anyone yet.</div>`;
                return;
            }

            content.innerHTML = `
                <div class="ttr-votes-list">
                    <div class="ttr-votes-head">
                        <div>Player</div>
                        <div>Vote</div>
                        <div>Score</div>
                    </div>
                    ${votes.map((row) => {
                        const voteTxt = Number(row.vote) === 1 ? 'Upvote' : 'Downvote';
                        const voteClass = Number(row.vote) === 1 ? 'ttr-vote-up' : 'ttr-vote-down';
                        const targetId = Number(row.targetId || 0);
                        const targetName = escapeHtml(row.targetName || `User ${targetId}`);
                        const score = Number(row.targetScore || 0);
                        const scoreText = score > 0 ? `+${score}` : String(score);
                        const searchBlob = `${String(targetName).toLowerCase()} ${String(targetId)}`;
                        return `
                            <div class="ttr-votes-row" data-search="${escapeHtmlAttr(searchBlob)}">
                                <div>
                                    <a class="ttr-votes-link" href="https://www.torn.com/profiles.php?XID=${targetId}" target="_blank" rel="noopener noreferrer">${targetName} [${targetId}]</a>
                                </div>
                                <div class="${voteClass}">${voteTxt}</div>
                                <div>${escapeHtml(scoreText)}</div>
                            </div>
                        `;
                    }).join('')}
                </div>
            `;

            searchInput.addEventListener('input', () => {
                const q = String(searchInput.value || '').trim().toLowerCase();
                const rows = Array.from(content.querySelectorAll('.ttr-votes-row'));
                let shown = 0;

                rows.forEach((row) => {
                    const hay = String(row.getAttribute('data-search') || '').toLowerCase();
                    const match = !q || hay.includes(q);
                    row.classList.toggle('hidden', !match);
                    if (match) shown++;
                });

                let empty = content.querySelector('.ttr-votes-empty');
                if (!shown) {
                    if (!empty) {
                        empty = document.createElement('div');
                        empty.className = 'ttr-votes-empty';
                        empty.textContent = 'No matching results.';
                        content.appendChild(empty);
                    } else {
                        empty.textContent = 'No matching results.';
                    }
                } else if (empty) {
                    empty.remove();
                }
            });
        } catch (err) {
            content.innerHTML = `<div class="ttr-votes-empty">Failed to load: ${escapeHtml(err?.message || 'Unknown error')}</div>`;
        }
    }

    async function handleDownvoteFlow() {
        if (!currentTargetId) return;

        await getSelfProfile().catch(() => null);
        if (isSelfTarget()) {
            setActionsVisible(false);
            setSelfVotesButtonVisible(true);
            setDisabled(false);
            return;
        }

        const shownVote = currentViewerVote == null ? 0 : Number(currentViewerVote);
        if (shownVote === -1) {
            await submitVote(-1);
            return;
        }

        openMugDecisionModal();
    }

    async function fetchUserProfileById(userId) {
        const apiKey = getEffectiveTornApiKey();
        if (!apiKey || !userId) return null;

        const url = new URL(`https://api.torn.com/v2/user/${userId}/profile`);
        url.searchParams.set('striptags', 'true');
        url.searchParams.set('key', apiKey);

        const res = await gmGetJson(url.toString());
        if (!res.ok || !res.data?.profile?.id) {
            throw new Error(res.data?.error?.error || `Failed to fetch target profile (${res.status})`);
        }

        return res.data.profile;
    }

    async function fetchCurrentBountyTotalById(userId) {
        const apiKey = getEffectiveTornApiKey();
        if (!apiKey || !userId) return null;

        const url = new URL(`https://api.torn.com/v2/user/${userId}/bounties`);
        url.searchParams.set('key', apiKey);

        const res = await gmGetJson(url.toString());
        if (!res.ok) {
            throw new Error(res.data?.error?.error || `Failed to fetch bounties (${res.status})`);
        }

        const arr = Array.isArray(res.data?.bounties) ? res.data.bounties : [];
        return arr.reduce((sum, row) => {
            const reward = Number(row?.reward || 0);
            const qty = Number(row?.quantity || 0);
            return sum + (reward * qty);
        }, 0);
    }

    async function fetchMoneyMuggedCurrentById(userId) {
        const apiKey = getEffectiveTornApiKey();
        if (!apiKey || !userId) return null;

        const url = new URL(`https://api.torn.com/v2/user/${userId}/personalstats`);
        url.searchParams.set('cat', 'all');
        url.searchParams.set('key', apiKey);

        const res = await gmGetJson(url.toString());
        if (!res.ok) {
            throw new Error(res.data?.error?.error || `Failed to fetch current personalstats (${res.status})`);
        }

        return Number(
            res.data?.personalstats?.attacking?.networth?.money_mugged ??
            res.data?.personalstats?.moneymugged ??
            0
        );
    }

    async function fetchMoneyMuggedHistoricalById(userId) {
        const apiKey = getEffectiveTornApiKey();
        if (!apiKey || !userId) return null;

        const ts = Math.floor(Date.now() / 1000) - NINETY_DAYS_SEC;

        const url = new URL(`https://api.torn.com/v2/user/${userId}/personalstats`);
        url.searchParams.set('stat', 'moneymugged');
        url.searchParams.set('timestamp', String(ts));
        url.searchParams.set('key', apiKey);

        const res = await gmGetJson(url.toString());
        if (!res.ok) {
            throw new Error(res.data?.error?.error || `Failed to fetch historical personalstats (${res.status})`);
        }

        const ps = res.data?.personalstats;

        if (Array.isArray(ps)) {
            const found = ps.find(x => String(x?.name || '').toLowerCase() === 'moneymugged');
            return Number(found?.value || 0);
        }

        return Number(
            ps?.attacking?.networth?.money_mugged ??
            ps?.moneymugged ??
            0
        );
    }

    async function fetchMuggedLast90dById(userId) {
        const [currentVal, historicalVal] = await Promise.all([
            fetchMoneyMuggedCurrentById(userId),
            fetchMoneyMuggedHistoricalById(userId)
        ]);

        if (currentVal == null || historicalVal == null) return null;
        return Math.max(0, Number(currentVal) - Number(historicalVal));
    }

    async function fetchTeProfile(targetId) {
        const teKey = getStoredTeApiKey();
        if (!teKey || !isTeEnabled() || !targetId) return null;

        const url = new URL('https://tornexchange.com/api/profile');
        url.searchParams.set('key', teKey);
        url.searchParams.set('user_id', String(targetId));

        const res = await gmGetJson(url.toString());
        if (!res.ok || res.data?.status !== 'success' || !res.data?.data) {
            throw new Error(res.data?.message || `Failed to fetch TE profile (${res.status})`);
        }

        const d = res.data.data;
        return {
            votes: toNullableInt(d.votes),
            name: d.name || null,
            updatedAt: d.updated_at || null
        };
    }

    async function pushSnapshotUpdate(targetId, targetName, friendEnemy, te, currentBountyTotal, muggedLast90d) {
        const apiKey = getEffectiveTornApiKey();
        if (!apiKey || !targetId) return;

        const payload = {
            apiKey,
            targetId,
            targetName: targetName || '',
            scriptVersion: SCRIPT_VERSION
        };

        if (friendEnemy) payload.friendEnemy = friendEnemy;
        if (te) payload.te = te;
        if (typeof currentBountyTotal === 'number') payload.currentBountyTotal = currentBountyTotal;
        if (typeof muggedLast90d === 'number') payload.muggedLast90d = muggedLast90d;

        const res = await gmPostJson(`${WORKER_BASE}/snapshot/update`, payload);
        if (!res.ok || !res.data?.ok) {
            throw new Error(res.data?.error || `Snapshot update failed (${res.status})`);
        }
    }

    async function refreshSupplementalData(targetId, options = {}) {
        if (!targetId) return;

        const preferCache = options.preferCache !== false;
        const cacheKey = getSupplementalCacheKey(targetId);

        if (preferCache) {
            const cached = getCacheValue(CACHE_KEYS.SUPPLEMENTAL, cacheKey);
            if (cached) {
                if (cached.friendEnemy) setFriendEnemy(cached.friendEnemy);
                if (cached.te) setTe(cached.te);
                if (typeof cached.currentBountyTotal === 'number') setCurrentBountyTotal(cached.currentBountyTotal);
                if (typeof cached.muggedLast90d === 'number') setMuggedLast90d(cached.muggedLast90d);
                return cached;
            }
        }

        let liveFriendEnemy = null;
        let liveTe = null;
        let liveCurrentBountyTotal = null;
        let liveMuggedLast90d = null;

        try {
            const targetProfile = await fetchUserProfileById(targetId);
            if (targetProfile) {
                const friends = toNullableInt(targetProfile.friends);
                const enemies = toNullableInt(targetProfile.enemies);
                const diff = (friends !== null && enemies !== null) ? friends - enemies : null;

                liveFriendEnemy = {
                    friends,
                    enemies,
                    diff,
                    fetchedAt: Math.floor(Date.now() / 1000)
                };

                setFriendEnemy(liveFriendEnemy);
            }
        } catch (err) {
            console.warn('[TTR] Friend/enemy fetch warning:', err);
        }

        try {
            const te = await fetchTeProfile(targetId);
            if (te) {
                liveTe = te;
                setTe(liveTe);
            }
        } catch (err) {
            console.warn('[TTR] TE fetch warning:', err);
        }

        try {
            const bountyTotal = await fetchCurrentBountyTotalById(targetId);
            if (typeof bountyTotal === 'number') {
                liveCurrentBountyTotal = bountyTotal;
                setCurrentBountyTotal(liveCurrentBountyTotal);
            }
        } catch (err) {
            console.warn('[TTR] Bounty fetch warning:', err);
        }

        try {
            const mugged90d = await fetchMuggedLast90dById(targetId);
            if (typeof mugged90d === 'number') {
                liveMuggedLast90d = mugged90d;
                setMuggedLast90d(liveMuggedLast90d);
            }
        } catch (err) {
            console.warn('[TTR] 90d mugged fetch warning:', err);
        }

        const merged = {
            friendEnemy: liveFriendEnemy,
            te: liveTe,
            currentBountyTotal: typeof liveCurrentBountyTotal === 'number' ? liveCurrentBountyTotal : null,
            muggedLast90d: typeof liveMuggedLast90d === 'number' ? liveMuggedLast90d : null
        };

        if (!liveFriendEnemy && !liveTe && liveCurrentBountyTotal == null && liveMuggedLast90d == null) {
            return null;
        }

        setCacheValue(CACHE_KEYS.SUPPLEMENTAL, cacheKey, merged);
        pruneBucket(CACHE_KEYS.SUPPLEMENTAL, 1000);

        try {
            await pushSnapshotUpdate(
                targetId,
                currentTargetName,
                liveFriendEnemy,
                liveTe,
                typeof liveCurrentBountyTotal === 'number' ? liveCurrentBountyTotal : undefined,
                typeof liveMuggedLast90d === 'number' ? liveMuggedLast90d : undefined
            );
        } catch (err) {
            console.warn('[TTR] Snapshot push warning:', err);
        }

        return merged;
    }

    function updatePrivateProfileCacheFromResponse(viewerId, targetId, responseData) {
        if (!viewerId || !targetId || !responseData) return;
        const cacheKey = getViewerScopedProfileCacheKey(viewerId, targetId);
        setCacheValue(CACHE_KEYS.PRIVATE_PROFILE, cacheKey, responseData);
        pruneBucket(CACHE_KEYS.PRIVATE_PROFILE, 1500);
    }

    function readPrivateProfileCache(viewerId, targetId) {
        if (!viewerId || !targetId) return null;
        return getCacheValue(CACHE_KEYS.PRIVATE_PROFILE, getViewerScopedProfileCacheKey(viewerId, targetId));
    }

    function invalidatePrivateProfileCache(viewerId, targetId) {
        if (!viewerId || !targetId) return;
        deleteCacheValue(CACHE_KEYS.PRIVATE_PROFILE, getViewerScopedProfileCacheKey(viewerId, targetId));
    }

    async function refreshProfileCard(options = {}) {
        if (!currentTargetId) return;

        const preferCache = options.preferCache !== false;

        setStatus('');
        setDisabled(true);

        try {
            const apiKey = getEffectiveTornApiKey();
            if (!apiKey) {
                setDisabled(true);
                openTornKeyPrompt();
                return;
            }

            const self = await getSelfProfile().catch(() => null);
            const viewerId = Number(self?.id || 0);

            if (preferCache && viewerId) {
                const cached = readPrivateProfileCache(viewerId, currentTargetId);
                if (cached) {
                    renderCard(cached);

                    if (isSelfTarget()) {
                        setActionsVisible(false);
                        setSelfVotesButtonVisible(true);
                    } else {
                        setActionsVisible(true);
                        setSelfVotesButtonVisible(false);
                    }

                    setStatus('', false);
                    setDisabled(false);
                    return;
                }
            }

            const res = await gmPostJson(`${WORKER_BASE}/private/profile`, {
                apiKey,
                targetId: currentTargetId,
                targetName: currentTargetName || '',
                scriptVersion: SCRIPT_VERSION
            });

            if (!res.ok || !res.data?.ok) {
                throw new Error(res.data?.error || `Profile load failed (${res.status})`);
            }

            renderCard(res.data);
            if (viewerId) {
                updatePrivateProfileCacheFromResponse(viewerId, currentTargetId, res.data);
            }

            if (isSelfTarget()) {
                setActionsVisible(false);
                setSelfVotesButtonVisible(true);
                setDisabled(false);
            } else {
                setActionsVisible(true);
                setSelfVotesButtonVisible(false);
                setStatus('', false);
                setDisabled(false);
            }
        } catch (err) {
            console.error('[TTR] Profile error:', err);
            setStatus('Load failed', false);
            setActionsVisible(true);
            setSelfVotesButtonVisible(false);
            setDisabled(true);
        }
    }

    function renderCard(data) {
        const resolvedName = String(data?.target?.name || '').trim() || currentTargetName || `#${currentTargetId}`;
        currentTargetName = resolvedName;
        currentViewerVote = data?.viewerVote ?? null;
        currentViewerMugReport = data?.viewerMugReport ?? null;

        if (nameEl) nameEl.textContent = resolvedName;

        setScore(data?.score ?? 0);
        setVote(data?.viewerVote ?? null);
        setMugReports(data?.mugReports ?? 0);
        setMugTopReason(data?.mugReportTopSubtype ?? null);
        setFriendEnemy(data?.friendEnemy ?? null);
        setTe(data?.te ?? null);
        setTradeHistorySeen(data?.tradeHistorySeen ?? 0);
        setCurrentBountyTotal(Number(data?.currentBountyTotal || 0));
        setMuggedLast90d(data?.muggedLast90d == null ? null : Number(data?.muggedLast90d || 0));
        setRiskLabel(data);
        updateCollapsedSummary();
    }

    async function submitVote(vote, options = {}) {
        if (!currentTargetId) return;

        const self = await getSelfProfile().catch(() => null);
        const viewerId = Number(self?.id || 0);

        if (isSelfTarget()) {
            setActionsVisible(false);
            setSelfVotesButtonVisible(true);
            setDisabled(false);
            return;
        }

        const mugReportSubtype = String(options?.mugReportSubtype || '').trim();
        const shownVote = currentViewerVote == null ? 0 : Number(currentViewerVote);

        if (shownVote === vote && !mugReportSubtype) {
            vote = 0;
        }

        setDisabled(true);

        try {
            const apiKey = getEffectiveTornApiKey();
            if (!apiKey) {
                openTornKeyPrompt();
                return;
            }

            const res = await gmPostJson(`${WORKER_BASE}/vote`, {
                apiKey,
                targetId: currentTargetId,
                targetName: currentTargetName || '',
                vote,
                scriptVersion: SCRIPT_VERSION
            });

            if (!res.ok || !res.data?.ok) {
                throw new Error(res.data?.error || `Vote failed (${res.status})`);
            }

            setScore(res.data?.score ?? 0);
            setVote(res.data?.viewerVote ?? null);
            if (res.data?.friendEnemy) setFriendEnemy(res.data.friendEnemy);
            if (res.data?.te) setTe(res.data.te);
            if (typeof res.data?.mugReports !== 'undefined') setMugReports(res.data.mugReports);
            if (typeof res.data?.mugReportTopSubtype !== 'undefined') setMugTopReason(res.data.mugReportTopSubtype);
            if (typeof res.data?.tradeHistorySeen !== 'undefined') setTradeHistorySeen(res.data.tradeHistorySeen);
            if (typeof res.data?.currentBountyTotal === 'number') setCurrentBountyTotal(res.data.currentBountyTotal);
            if (typeof res.data?.muggedLast90d === 'number') setMuggedLast90d(res.data.muggedLast90d);

            let cachePayload = {
                ok: true,
                viewer: self ? {
                    id: self.id,
                    name: self.name,
                    level: self.level ?? null,
                    status: self.status ?? null
                } : null,
                target: {
                    id: currentTargetId,
                    name: currentTargetName || `#${currentTargetId}`
                },
                score: res.data?.score ?? 0,
                upvotes: res.data?.upvotes ?? null,
                downvotes: res.data?.downvotes ?? null,
                viewerVote: res.data?.viewerVote ?? null,
                mugReports: res.data?.mugReports ?? 0,
                viewerMugReport: res.data?.viewerMugReport ?? null,
                mugReportTopSubtype: res.data?.mugReportTopSubtype ?? null,
                friendEnemy: res.data?.friendEnemy ?? null,
                te: res.data?.te ?? null,
                faction: res.data?.faction ?? null,
                protection: res.data?.protection ?? null,
                tradeHistorySeen: res.data?.tradeHistorySeen ?? 0,
                currentBountyTotal: res.data?.currentBountyTotal ?? 0,
                muggedLast90d: res.data?.muggedLast90d ?? null
            };

            if (mugReportSubtype && vote === -1) {
                const mugRes = await gmPostJson(`${WORKER_BASE}/mug-report`, {
                    apiKey,
                    targetId: currentTargetId,
                    targetName: currentTargetName || '',
                    subtype: mugReportSubtype,
                    scriptVersion: SCRIPT_VERSION
                });

                if (!mugRes.ok || !mugRes.data?.ok) {
                    throw new Error(mugRes.data?.error || `Mug report failed (${mugRes.status})`);
                }

                currentViewerMugReport = mugRes.data?.viewerMugReport ?? null;
                if (typeof mugRes.data?.mugReports !== 'undefined') setMugReports(mugRes.data.mugReports);
                if (typeof mugRes.data?.mugReportTopSubtype !== 'undefined') setMugTopReason(mugRes.data.mugReportTopSubtype);

                cachePayload.viewerMugReport = mugRes.data?.viewerMugReport ?? null;
                cachePayload.mugReports = mugRes.data?.mugReports ?? cachePayload.mugReports;
                cachePayload.mugReportTopSubtype = mugRes.data?.mugReportTopSubtype ?? cachePayload.mugReportTopSubtype;
            } else if (typeof res.data?.viewerMugReport !== 'undefined') {
                cachePayload.viewerMugReport = res.data?.viewerMugReport ?? null;
            }

            setRiskLabel(cachePayload);

            if (viewerId) {
                updatePrivateProfileCacheFromResponse(viewerId, currentTargetId, cachePayload);
            }

            setStatus('');
            updateCollapsedSummary();
        } catch (err) {
            console.error('[TTR] Vote error:', err);
            setStatus(err?.message || 'Vote failed', true);
            if (viewerId) invalidatePrivateProfileCache(viewerId, currentTargetId);
        } finally {
            if (!isSelfTarget()) setDisabled(false);
        }
    }

    async function refreshForCurrentRoute() {
        const profile = isProfilePage();
        const trade = isTradeRelevantPage();

        if (!profile && !trade) {
            removeCard();
            currentRouteKey = '';
            currentTargetId = null;
            currentTargetName = '';
            currentTradeId = null;
            currentContextType = null;
            currentViewerVote = null;
            currentViewerMugReport = null;
            return;
        }

        let contextType = null;
        let targetId = null;
        let targetName = '';
        let tradeId = null;

        if (profile) {
            contextType = 'profile';
            targetId = getProfileTargetId();
            targetName = getProfileTargetName();
        } else if (trade) {
            contextType = 'trade';
            targetId = await getTradeTargetId().catch((err) => {
                console.error('[TTR] Trade target detect error:', err);
                return null;
            });
            targetName = getTradeTargetNameFromDom();
            tradeId = getTradeIdFromHash();
        }

        if (!targetId) return;
        if (!ensureMounted(contextType)) return;

        const routeKey = `${contextType}:${targetId}:${tradeId || ''}`;
        const changed = routeKey !== currentRouteKey || currentContextType !== contextType;

        currentRouteKey = routeKey;
        currentTargetId = targetId;
        currentTargetName = String(targetName || '').trim();
        currentTradeId = tradeId || null;
        currentContextType = contextType;

        if (!changed) return;

        currentViewerVote = null;
        currentViewerMugReport = null;

        if (nameEl) nameEl.textContent = currentTargetName || `#${targetId}`;
        setScore('-');
        setVote(0);
        setMugReports(0);
        setMugTopReason(null);
        setFriendEnemy(null);
        setTe(null);
        setTradeHistorySeen(0);
        setCurrentBountyTotal(0);
        setMuggedLast90d(null);
        setStatus('', false);
        setActionsVisible(true);
        setSelfVotesButtonVisible(false);
        applyRiskTone('neutral');

        if (riskLabelEl) {
            riskLabelEl.textContent = 'Neutral';
            riskLabelEl.classList.remove('pos', 'neg', 'neutral');
            riskLabelEl.classList.add('neutral');
        }

        updateCollapsedSummary();

        await refreshProfileCard({ preferCache: true });

        const supplemental = await refreshSupplementalData(targetId, { preferCache: true }).catch((err) => {
            console.warn('[TTR] Supplemental data refresh warning:', err);
            return null;
        });

        if (supplemental) {
            const cached = readPrivateProfileCache(Number(selfProfileCache?.id || 0), targetId);
            const merged = {
                ...(cached || {}),
                friendEnemy: supplemental.friendEnemy ?? cached?.friendEnemy ?? null,
                te: supplemental.te ?? cached?.te ?? null,
                currentBountyTotal: supplemental.currentBountyTotal ?? cached?.currentBountyTotal ?? 0,
                muggedLast90d: supplemental.muggedLast90d ?? cached?.muggedLast90d ?? null,
                viewerVote: currentViewerVote
            };
            setRiskLabel(merged);
            updateCollapsedSummary();
        }
    }

    function removeCard() {
        const existing = document.getElementById('ttr-mount-row');
        if (existing) existing.remove();

        closeAnyModalById('ttr-modal-backdrop');
        closeAnyModalById('ttr-torn-key-backdrop');
        closeAnyModalById('ttr-mug-backdrop');
        closeAnyModalById('ttr-myvotes-backdrop');

        rootEl = null;
        bodyEl = null;
        collapsedSummaryEl = null;
        mugRowEl = null;

        nameEl = null;
        scoreEl = null;
        myVoteEl = null;
        mugReportsEl = null;
        mugTopReasonEl = null;
        tradeHistoryEl = null;
        statusEl = null;
        btnUp = null;
        btnDown = null;
        actionsEl = null;
        friendEnemyEl = null;
        teEl = null;
        settingsBtn = null;
        selfVotesBtn = null;
        riskLabelEl = null;
        currentBountyEl = null;
        mug90dEl = null;
        collapseBtn = null;
    }

    function scheduleEventsSync(force) {
        if (!isEventsPage()) return;

        const headRef = getEventsHeadRef();
        if (!headRef) return;

        const prevHeadRef = getPersistentValue(STORAGE_KEY_LAST_EVENTS_HEAD) || '';
        const now = Date.now();

        if (!force) {
            if (headRef === prevHeadRef) return;
            if (now - lastEventsSyncAt < EVENT_SYNC_DEBOUNCE_MS) return;
        }

        lastEventsSyncAt = now;
        setPersistentValue(STORAGE_KEY_LAST_EVENTS_HEAD, headRef);

        syncEventsPageEvidence().catch((err) => {
            console.warn('[TTR] Events sync warning:', err);
        });
    }

    async function syncEventsPageEvidence() {
        if (!isEventsPage()) return;

        const apiKey = getEffectiveTornApiKey();
        if (!apiKey) return;

        const self = await getSelfProfile().catch(() => null);
        const viewerId = Number(self?.id || 0);
        if (!viewerId) return;

        const parsedEvents = await parseEventsPageEvidence(viewerId);
        if (!parsedEvents.length) return;

        const unsynced = parsedEvents.filter(ev => ev?.eventRef && !syncedEventRefs.has(ev.eventRef));
        if (!unsynced.length) return;

        const res = await gmPostJson(`${WORKER_BASE}/event-evidence/batch`, {
            apiKey,
            scriptVersion: SCRIPT_VERSION,
            events: unsynced
        });

        if (!res.ok || !res.data?.ok) {
            throw new Error(res.data?.error || `Event evidence sync failed (${res.status})`);
        }

        unsynced.forEach(ev => syncedEventRefs.add(ev.eventRef));

        if (syncedEventRefs.size > MAX_SYNCED_EVENT_REFS) {
            const arr = Array.from(syncedEventRefs).slice(-800);
            syncedEventRefs = new Set(arr);
        }

        saveSyncedEventRefs(syncedEventRefs);
    }

    function getEventsHeadRef() {
        const firstItem = document.querySelector('ul[class*="eventsList"] li[class*="listItemWrapper"]');
        if (!firstItem) return '';

        const msgEl = firstItem.querySelector('p[class*="message"]');
        const timeEl = firstItem.querySelector('time');

        const msg = normalizeSpaces(msgEl?.textContent || '');
        const timeText = extractTimeTextFromTimeElement(timeEl);

        return `${timeText}||${msg}`.trim();
    }

    async function parseEventsPageEvidence(viewerId) {
        const items = Array.from(document.querySelectorAll('ul[class*="eventsList"] li[class*="listItemWrapper"]'));
        if (!items.length) return [];

        const out = [];
        const mugEvents = [];
        const saleCandidates = [];

        for (const item of items) {
            const msgEl = item.querySelector('p[class*="message"]');
            const timeEl = item.querySelector('time');
            if (!msgEl || !timeEl) continue;

            const fullText = normalizeSpaces(msgEl.textContent);
            const eventAt = parseEventTimestampToUnix(timeEl);
            if (!eventAt) continue;

            const anchors = Array.from(msgEl.querySelectorAll('a[href]'));
            const profileAnchors = anchors
                .map(a => ({
                    href: String(a.getAttribute('href') || ''),
                    text: normalizeSpaces(a.textContent),
                    id: extractXidFromHref(String(a.getAttribute('href') || ''))
                }))
                .filter(a => Number.isInteger(a.id) && a.id > 0);

            if (/has initiated a trade titled/i.test(fullText)) {
                const tradeAnchor = anchors.find(a => /trade\.php/i.test(String(a.getAttribute('href') || '')));
                const tradeId = extractTradeIdFromHref(String(tradeAnchor?.getAttribute('href') || ''));
                const target = profileAnchors[0];

                if (target && target.id !== viewerId) {
                    const eventRef = tradeId
                        ? `trade_initiated:${target.id}:${tradeId}`
                        : `trade_initiated:${target.id}:${eventAt}`;

                    out.push({
                        kind: 'trade_initiated',
                        targetId: target.id,
                        targetName: target.text || '',
                        eventAt,
                        tradeId: tradeId || null,
                        attackLogId: null,
                        eventRef
                    });
                }
                continue;
            }

            const saleCandidate = await parseSaleBuyerCandidate(fullText, eventAt);
            if (saleCandidate) {
                saleCandidates.push(saleCandidate);
                continue;
            }

            if (/mugged you\b/i.test(fullText)) {
                const attackAnchor = anchors.find(a => /page\.php\?sid=attackLog/i.test(String(a.getAttribute('href') || '')));
                const attackLogId = extractAttackLogIdFromHref(String(attackAnchor?.getAttribute('href') || ''));
                const muggerAnchors = profileAnchors.filter(a => a.id !== viewerId);

                mugEvents.push({
                    eventAt,
                    attackLogId: attackLogId || null,
                    directMuggerId: muggerAnchors.length ? muggerAnchors[0].id : null,
                    directMuggerName: muggerAnchors.length ? (muggerAnchors[0].text || '') : ''
                });
            }
        }

        for (const mug of mugEvents) {
            const nearbySales = saleCandidates.filter(c =>
                mug.eventAt >= c.eventAt &&
                (mug.eventAt - c.eventAt) <= SUSPICIOUS_MUG_LINK_WINDOW_SEC
            );

            if (!nearbySales.length) continue;

            let matchedCandidate = null;

            if (mug.directMuggerId) {
                const exactMatches = nearbySales.filter(c => c.targetId === mug.directMuggerId);
                if (exactMatches.length !== 1) continue;
                matchedCandidate = exactMatches[0];
            } else {
                const uniqueIds = [...new Set(nearbySales.map(c => c.targetId))];
                if (uniqueIds.length !== 1) continue;
                matchedCandidate = nearbySales
                    .filter(c => c.targetId === uniqueIds[0])
                    .sort((a, b) => b.eventAt - a.eventAt)[0];
            }

            const eventRef = mug.attackLogId
                ? `mugged_you_linked:${matchedCandidate.targetId}:${mug.attackLogId}`
                : `mugged_you_linked:${matchedCandidate.targetId}:${mug.eventAt}`;

            out.push({
                kind: 'mugged_you',
                targetId: matchedCandidate.targetId,
                targetName: matchedCandidate.targetName || mug.directMuggerName || '',
                eventAt: mug.eventAt,
                tradeId: null,
                attackLogId: mug.attackLogId || null,
                eventRef
            });
        }

        return dedupeEvents(out);
    }

    async function parseSaleBuyerCandidate(fullText, eventAt) {
        let m = fullText.match(/on the Item Market to\s+([A-Za-z0-9_.\-\[\]\s]+?)\s+for\s+\$/i);
        if (m) {
            const targetName = normalizeSpaces(m[1]);
            const resolved = await resolveUserByName(targetName).catch(() => null);
            if (resolved?.id) {
                return {
                    kind: 'sale_candidate_market',
                    targetId: Number(resolved.id),
                    targetName: resolved.name || targetName,
                    eventAt
                };
            }
        }

        m = fullText.match(/^(.+?)\s+bought\s+.+?\s+from your bazaar\b/i);
        if (m) {
            const targetName = normalizeSpaces(m[1]);
            const resolved = await resolveUserByName(targetName).catch(() => null);
            if (resolved?.id) {
                return {
                    kind: 'sale_candidate_bazaar',
                    targetId: Number(resolved.id),
                    targetName: resolved.name || targetName,
                    eventAt
                };
            }
        }

        return null;
    }

    function dedupeEvents(events) {
        const seen = new Set();
        const out = [];

        for (const ev of events) {
            const key = String(ev?.eventRef || '');
            if (!key || seen.has(key)) continue;
            seen.add(key);
            out.push(ev);
        }

        return out;
    }

    function normalizeSpaces(value) {
        return String(value || '').replace(/\s+/g, ' ').trim();
    }

    function extractXidFromHref(href) {
        const m = String(href || '').match(/XID=(\d+)/i);
        return m ? Number(m[1]) : null;
    }

    function extractTradeIdFromHref(href) {
        const m = String(href || '').match(/(?:^|[?#&])ID=(\d+)/i);
        return m ? String(m[1]) : null;
    }

    function extractAttackLogIdFromHref(href) {
        const s = String(href || '');
        let m = s.match(/attackLog&ID=([A-Za-z0-9]+)/i);
        if (m) return String(m[1]);
        m = s.match(/attackLog.*?[?&]ID=([A-Za-z0-9]+)/i);
        if (m) return String(m[1]);
        m = s.match(/[?&]ID=([A-Za-z0-9]+)/i);
        return m ? String(m[1]) : null;
    }

    function extractTimeTextFromTimeElement(timeEl) {
        if (!timeEl) return '';

        const direct =
            timeEl.getAttribute?.('datetime') ||
            timeEl.getAttribute?.('title') ||
            timeEl.getAttribute?.('aria-label') ||
            '';

        if (direct && /(\d{1,2}):(\d{2}):(\d{2}).*?(\d{1,2})\/(\d{1,2})\/(\d{2,4})/.test(direct)) {
            return direct;
        }

        const cloned = timeEl.cloneNode(true);
        const text = normalizeSpaces(cloned.textContent || '');
        if (text) return text;

        return normalizeSpaces(timeEl.innerText || '');
    }

    function getCurrentTctUnixApprox() {
        const bodyText = normalizeSpaces(document.body?.innerText || document.body?.textContent || '');
        const m = bodyText.match(/(\d{1,2}):(\d{2}):(\d{2})\s*-\s*(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
        if (m) {
            let yy = Number(m[6]);
            if (yy < 100) yy += 2000;
            const utcMs = Date.UTC(
                yy,
                Number(m[5]) - 1,
                Number(m[4]),
                Number(m[1]),
                Number(m[2]),
                Number(m[3])
            );
            if (Number.isFinite(utcMs)) {
                return Math.floor(utcMs / 1000);
            }
        }

        return Math.floor(Date.now() / 1000);
    }

    function parseEventTimestampToUnix(timeEl) {
        const text = extractTimeTextFromTimeElement(timeEl);
        if (!text) return null;

        const m = text.match(/(\d{1,2}):(\d{2}):(\d{2})\s*(?:-\s*)?(\d{1,2})\/(\d{1,2})\/(\d{2,4})/);
        if (!m) return null;

        let yy = Number(m[6]);
        if (yy < 100) yy += 2000;

        const rawUtc = Math.floor(Date.UTC(
            yy,
            Number(m[5]) - 1,
            Number(m[4]),
            Number(m[1]),
            Number(m[2]),
            Number(m[3])
        ) / 1000);

        const approxNowTct = getCurrentTctUnixApprox();
        const approxNowReal = Math.floor(Date.now() / 1000);
        const offset = approxNowReal - approxNowTct;

        return rawUtc + offset;
    }

    function toNullableInt(v) {
        if (v === null || v === undefined || v === '') return null;
        const n = Number(v);
        return Number.isInteger(n) ? n : null;
    }

    function escapeHtmlAttr(value) {
        return String(value ?? '')
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    function escapeHtml(value) {
        return String(value ?? '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    pruneBucket(CACHE_KEYS.SELF_PROFILE, 3);
    pruneBucket(CACHE_KEYS.PRIVATE_PROFILE, 1500);
    pruneBucket(CACHE_KEYS.SUPPLEMENTAL, 1000);
    pruneBucket(CACHE_KEYS.RESOLVE_USER, 500);
})();