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);
})();