CAT Script v3

CAT Script Beta - Fluffy Kittens

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         CAT Script v3
// @namespace    http://tampermonkey.net/
// @version      1.9.76
// @description  CAT Script Beta - Fluffy Kittens
// @author       JESUUS [2353554]
// @license      MIT
// @match        https://www.torn.com/*
// @grant        GM_xmlhttpRequest
// @connect      cat-script.com
// @connect      142.44.247.35
// @connect      localhost
// @connect      api.torn.com
// @connect      www.tornstats.com
// @connect      ffscouter.com
// @connect      www.torn.com
// @run-at       document-idle
// ==/UserScript==
//
// ===== Terms of Service & Privacy Policy =====
// Full version: https://cat-script.com/terms
//
// API Key Usage:
// - Your API key is stored locally in your browser (localStorage)
// - It is used to fetch the data listed below directly from api.torn.com
// - During registration, your key is sent to our server to verify your identity via the Torn API — it is never stored in our database
// - After registration, the server only receives a unique auth token for all subsequent requests
//
// Client-side API calls (from your browser, using your key):
// - user/self/basic — Your name, ID, faction
// - user/{id}/profile — Player profiles
// - faction/{id}/wars — Faction war data
// - faction/{id}/rankedwars — Ranked war detection
// - faction/{id}/basic,members — Faction member list
//
// Key Access Level: Public Access
//
// Update Notification:
// - The script checks for new versions and displays a banner when an update is available
// - No automatic update — you update manually via Greasyfork or TornPDA
// - No personal data is sent during the version check
//
// Page Data Interception (read-only):
// The script reads data from Torn's own requests on the war page.
// This avoids extra API calls and provides real-time updates.
// - WebSocket: updateStatus — Status changes (Hospital, Okay, Traveling, etc.)
// - Fetch: getwarusers — War member statuses
// - Fetch: getProcessBarRefreshData — War progress bar data
// - Fetch: getwardata — Ranked war data + online status
// - Fetch: factionsRankedWarProcessBarRefresh — Ranked war scores + timers
// - Fetch: chat/online-status — Online/idle/offline
// - XHR: attackData — Chain count + timer (attack page only)
// All interception is read-only. Original requests/responses are never modified.
// Active on the faction war page and attack page only.
// When the Torn tab is not actively viewed, all page data interception is paused.
//
// Data stored on server:
// - Player ID, name, faction ID (auto-deleted after 15 days of inactivity)
// - Auth token (auto-deleted after 90 days of inactivity, not your API key)
// - Call events (permanent, for leaderboards)
// - Member statuses (auto-deleted after 2 hours)
// - Activity logs (auto-deleted after 48 hours)
// - War data (permanent)
// - Error reports (auto-deleted after 7 days)
// - Rating score (permanent, linked to player_id to prevent duplicates)
// - Discord webhook config (permanent, webhook URL + notification settings per faction)
//
// Data sharing:
// - Faction members see calls.
// - Faction leaders/co-leaders can see calls, statuses, and leaderboard during wars
// - Faction leaders/co-leaders can access a read-only dashboard
// - Rating scores are displayed as aggregated stats only (average + count). Individual votes are never shown publicly.
// - We do NOT sell, share, or provide your data to any third party
// - Only the script developer (JESUUS [2353554]) has admin access
//
// Contact JESUUS [2353554] on Torn for data deletion requests.
// =================================================

(function () {
    'use strict';

    const state = {
        catBlocked: false,
        catOtherFaction: false,
        viewingFactionId: null,
        enhancer: null,
        updateAvailable: null,
        updateRequired: false,
    };
    /**
     * Normalize faction ID by removing 'faction-' prefix if present
     * Supports both formats: 'faction-54178' and '54178'
     */
    function normalizeFactionId(factionId) {
        if (!factionId)
            return null;
        if (typeof factionId !== 'string')
            return null;
        return factionId.replace(/^faction-/, '');
    }

    const VERSION = '1.9.76';
    const CONFIG = {
        selectors: {
            descWrap: '.desc-wrap',
            factionWar: '.desc-wrap .f-war-list',
            member: '.desc-wrap .member___fZiTx, .desc-wrap [class*="member___"]',
            level: '.desc-wrap .level___g3CWR, .desc-wrap [class*="level___"]',
            points: '.desc-wrap .points___TQbnu, .desc-wrap [class*="points___"]',
            status: '.desc-wrap [class*="status___"]',
            attackButton: '.desc-wrap .attack',
            callButton: '.desc-wrap .call-button',
            bspColumn: '#faction_war_list_id .bsp-column',
            bspStats: '#faction_war_list_id .iconStats',
            factionName: '.desc-wrap .faction-name, .desc-wrap [class*="name___"]',
            factionImage: '.desc-wrap .faction-image, .desc-wrap [class*="image___"]',
            memberRow: '.desc-wrap li[class*="member"]',
            factionBlock: '.desc-wrap .f-war-list'
        },
        colors: {
            primary: '#667eea',
            secondary: '#764ba2',
            accent: '#f093fb',
            success: '#4ecdc4',
            warning: '#ffe066',
            danger: '#ff6b6b',
            dark: '#1a1a2e',
            darkSecondary: '#16213e',
            light: '#ffffff',
            lightSecondary: '#f8f9fa',
            enemyFaction: '#FF794C',
            yourFaction: '#86B202',
            travelEta: '#FFB74D'
        },
        animations: {
            duration: '0.3s',
            easing: 'cubic-bezier(0.4, 0, 0.2, 1)'
        },
        areas: {
            1: 'Torn',
            2: 'Mexico',
            3: 'Hawaii',
            4: 'South Africa',
            5: 'Japan',
            6: 'China',
            7: 'Argentina',
            8: 'Switzerland',
            9: 'Canada',
            10: 'UK',
            11: 'UAE',
            12: 'Cayman'
        },
        travelTimes: {
            2: { standard: 1560, airstrip: 1080, wlt: 780, bct: 480 }, // Mexico
            3: { standard: 8040, airstrip: 5640, wlt: 4020, bct: 2400 }, // Hawaii
            4: { standard: 17820, airstrip: 12480, wlt: 8940, bct: 5340 }, // South Africa
            5: { standard: 13500, airstrip: 9480, wlt: 6780, bct: 4080 }, // Japan
            6: { standard: 14520, airstrip: 10140, wlt: 7260, bct: 4320 }, // China
            7: { standard: 10020, airstrip: 7020, wlt: 4980, bct: 3000 }, // Argentina
            8: { standard: 10500, airstrip: 7380, wlt: 5280, bct: 3180 }, // Switzerland
            9: { standard: 2460, airstrip: 1740, wlt: 1200, bct: 720 }, // Canada
            10: { standard: 9540, airstrip: 6660, wlt: 4800, bct: 2880 }, // UK
            11: { standard: 16260, airstrip: 11400, wlt: 8100, bct: 4860 }, // UAE
            12: { standard: 2100, airstrip: 1500, wlt: 1080, bct: 660 }, // Cayman
        }
    };

    function checkForUpdate() {
        const serverUrl = 'https://cat-script.com';
        /** Compare semver strings: returns -1 if a < b, 0 if equal, 1 if a > b */
        const cmpVer = (a, b) => {
            const pa = a.split('.').map(Number);
            const pb = b.split('.').map(Number);
            for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
                const na = pa[i] || 0;
                const nb = pb[i] || 0;
                if (na < nb)
                    return -1;
                if (na > nb)
                    return 1;
            }
            return 0;
        };
        const checkVersion = (responseText) => {
            try {
                const data = JSON.parse(responseText);
                if (!data.success)
                    return;
                // Check minimum required version (blocks functionality)
                if (data.minVersion && cmpVer(VERSION, data.minVersion) < 0) {
                    state.updateRequired = true;
                    state.updateAvailable = data.version || data.minVersion;
                    return;
                }
                // Check for available update (soft notification)
                if (data.version && cmpVer(VERSION, data.version) < 0) {
                    state.updateAvailable = data.version;
                }
            }
            catch (_e) { /* silent */ }
        };
        if (typeof GM_xmlhttpRequest !== 'undefined') {
            GM_xmlhttpRequest({
                method: 'GET',
                url: `${serverUrl}/api/script/version`,
                timeout: 5000,
                onload: (resp) => checkVersion(resp.responseText),
                onerror: () => { }
            });
        }
        else if (typeof PDA_httpGet === 'function') {
            PDA_httpGet(`${serverUrl}/api/script/version`).then(checkVersion).catch(() => { });
        }
        else if (typeof window !== 'undefined' && typeof window.customFetch === 'function') {
            window.customFetch(`${serverUrl}/api/script/version`).then((r) => r.text()).then(checkVersion).catch(() => { });
        }
    }

    let wasInactiveLastCheck = false;
    function checkUrl() {
        const currentHash = window.location.hash;
        const currentSearch = window.location.search;
        if (currentSearch.includes('step=rankreport')) {
            wasInactiveLastCheck = true;
            const styleElement = document.getElementById('faction-war-enhancer-styles');
            if (styleElement)
                styleElement.remove();
            return false;
        }
        if (currentSearch.includes('step=profile')) {
            const idMatch = currentSearch.match(/ID=(\d+)/);
            if (idMatch) {
                const pageFactionId = idMatch[1];
                const userFactionId = (localStorage.getItem('cat_user_faction_id') || '').replace(/"/g, '');
                state.viewingFactionId = pageFactionId;
                if (userFactionId && pageFactionId !== userFactionId) {
                    state.catOtherFaction = true;
                }
                else {
                    state.catOtherFaction = false;
                }
                // Auto-refresh faction data from Torn API when user visits faction page
                if (window.FactionWarEnhancer && window.FactionWarEnhancer.apiManager) {
                    window.FactionWarEnhancer.apiManager.ensureFactionFreshData().catch(err => {
                        console.log('[URL Checker] Faction refresh error:', err);
                    });
                }
            }
        }
        else {
            // Reset when not on step=profile
            state.catOtherFaction = false;
            state.viewingFactionId = null;
        }
        if (state.catOtherFaction) {
            document.documentElement.classList.add('cat-other-faction');
        }
        else {
            document.documentElement.classList.remove('cat-other-faction');
        }
        const isTerritoryWarProfile = currentSearch.includes('step=profile')
            && /^#\/war\/\d+$/.test(currentHash);
        const isArchivedWar = /^#\/war\/\d+$/.test(currentHash);
        const isAllowed = !isTerritoryWarProfile && !isArchivedWar && !currentHash.includes('tab=') && (currentHash.startsWith('#/war') || currentHash === '#/');
        if (!isAllowed) {
            if (!wasInactiveLastCheck && window.FactionWarEnhancer) {
                window.FactionWarEnhancer.pause();
            }
            wasInactiveLastCheck = true;
            const styleElement = document.getElementById('faction-war-enhancer-styles');
            if (styleElement) {
                styleElement.remove();
            }
        }
        else {
            if (wasInactiveLastCheck) {
                if (window.FactionWarEnhancer) {
                    if (window.FactionWarEnhancer.cssManager) {
                        const cssManager = window.FactionWarEnhancer.cssManager;
                        if (!document.getElementById('faction-war-enhancer-styles')) {
                            cssManager.createStyleElement();
                        }
                        cssManager.injectCSS();
                    }
                    // Restart timers and DOM observer after SPA navigation back to war page
                    window.FactionWarEnhancer.resume();
                    window.FactionWarEnhancer.startDomObserver();
                    // Re-check faction membership in case user changed faction
                    if (window.FactionWarEnhancer.apiManager) {
                        window.FactionWarEnhancer.apiManager.fetchUserInfoFromTornAPI().catch(err => {
                            console.log('[URL Checker] Faction refresh error:', err);
                        });
                    }
                }
            }
            wasInactiveLastCheck = false;
        }
        return isAllowed;
    }
    function setupUrlListeners() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                setTimeout(() => {
                    checkUrl();
                    window.addEventListener('hashchange', checkUrl);
                }, 50);
            });
        }
        else {
            setTimeout(() => {
                checkUrl();
                window.addEventListener('hashchange', checkUrl);
            }, 50);
        }
    }

    const StorageUtil = {
        get(key, defaultValue = null) {
            try {
                const value = localStorage.getItem(key);
                if (value === null)
                    return defaultValue;
                try {
                    return JSON.parse(value);
                }
                catch (e) {
                    return value;
                }
            }
            catch (e) {
                console.log(`Error reading from localStorage [${key}]:`, e);
                return defaultValue;
            }
        },
        set(key, value) {
            try {
                if (typeof value === 'string') {
                    localStorage.setItem(key, value);
                }
                else if (value === null || value === undefined) {
                    localStorage.removeItem(key);
                }
                else {
                    localStorage.setItem(key, JSON.stringify(value));
                }
                return true;
            }
            catch (e) {
                console.log(`Error writing to localStorage [${key}]:`, e);
                return false;
            }
        },
        remove(key) {
            try {
                localStorage.removeItem(key);
                return true;
            }
            catch (e) {
                console.log(`Error removing from localStorage [${key}]:`, e);
                return false;
            }
        }
    };

    function pageFocus() {
        return document.visibilityState !== 'hidden';
    }
    const _w = [];
    const _wi = [];
    let _r = false;
    // Members confirmed "Okay" by Torn directly (warDesc or WS) — server Hospital ignored for these
    const tornConfirmedOkay = new Set();
    function _h(enhancer, su) {
        if (!su.status)
            return;
        const id = String(su.userId);
        const status = su.status;
        const statusText = (status.text || '').trim();
        const isHospital = statusText === 'Hospital' || statusText.toLowerCase().includes('hospital');
        // Only trigger hosp scan for hospital/non-hospital transitions — not every WS update
        if (isHospital || enhancer.hospTime[id])
            enhancer._hospScanNeeded = true;
        if (isHospital) {
            tornConfirmedOkay.delete(id);
            // Only mark as abroad if the WS explicitly says area !== 1
            // Don't use stale travelData — the player may have returned to Torn
            if (status.area && status.area !== 1) {
                enhancer.previousStatus[id] = { status: 'Abroad', area: status.area };
            }
            else if (!status.area) {
                // No area info in WS = player is at Torn, clear any stale abroad status
                delete enhancer.previousStatus[id];
            }
            delete enhancer.travelData[id];
            const endTime = status.until || status.updateAt || su.until;
            if (endTime) {
                const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                if (endMs > Date.now()) {
                    enhancer.hospTime[id] = endTime;
                }
            }
            // Auto-cancel when target goes Hospital is handled server-side via /api/status-update
        }
        else if (statusText === 'Okay' || statusText === 'Traveling' || statusText === 'Abroad' || statusText === 'Jail' || statusText === 'Federal' || statusText === 'Fallen') {
            tornConfirmedOkay.add(id);
            delete enhancer.hospTime[id];
            if ((statusText === 'Traveling' || statusText === 'Abroad') && status.area !== undefined) {
                const prevArea = enhancer.travelData[id]?.area || enhancer.previousStatus[id]?.area;
                enhancer.travelData[id] = {
                    area: status.area,
                    status: statusText,
                    updateAt: status.until || status.updateAt || enhancer.travelData[id]?.updateAt,
                    departedAt: statusText === 'Traveling' ? Date.now() : enhancer.travelData[id]?.departedAt,
                    fromArea: status.area === 1 ? prevArea : undefined
                };
                delete enhancer.previousStatus[id];
            }
            else if ((statusText === 'Traveling' || statusText === 'Abroad') && enhancer.previousStatus[id]) {
                enhancer.travelData[id] = {
                    area: enhancer.previousStatus[id].area,
                    status: statusText,
                    updateAt: status.until || status.updateAt || enhancer.travelData[id]?.updateAt,
                    departedAt: statusText === 'Traveling' ? Date.now() : enhancer.travelData[id]?.departedAt,
                    fromArea: enhancer.previousStatus[id].area === 1 ? undefined : enhancer.previousStatus[id].area
                };
                delete enhancer.previousStatus[id];
            }
            else if (statusText !== 'Traveling' && statusText !== 'Abroad') {
                delete enhancer.travelData[id];
                delete enhancer.previousStatus[id];
            }
        }
        if (enhancer.pollingManager) {
            const prevAbroad = enhancer.previousStatus[id];
            const update = {
                status: statusText,
                details: status.details || null,
                until: status.until || status.updateAt || null,
                previousStatus: prevAbroad ? prevAbroad.status : null,
                previousArea: prevAbroad ? prevAbroad.area : (status.area != null ? status.area : null),
                departedAt: enhancer.travelData[id]?.departedAt || null
            };
            enhancer.pollingManager.queueStatusUpdate(id, update);
        }
        // Don't call updateHospTimers() synchronously — the 1s timer loop already handles it.
        // Only schedule a single deferred scan per hospital event (collapse storm of 50ms timeouts).
        if (isHospital) {
            if (!enhancer._hospScanPending) {
                enhancer._hospScanPending = true;
                setTimeout(() => {
                    enhancer._hospScanPending = false;
                    enhancer._hospScanNeeded = true;
                    enhancer.scanHospitalizedMembers();
                    enhancer.updateHospTimers();
                    if (enhancer.enhancementManager)
                        enhancer.enhancementManager.restoreSavedSort(true);
                }, 50);
            }
        }
    }
    /**
     * Parse updateIcons WS message to extract hospital timer and abroad location.
     * icon82 = Hospital (contains data-time='seconds'), icon71 = Abroad (title has location).
     */
    function _parseIcons(enhancer, iconsUpdate) {
        const id = String(iconsUpdate.userId);
        const html = iconsUpdate.icons;
        // Use receivedAt timestamp if message was buffered (PDA background), else now
        const receivedAt = iconsUpdate._receivedAt || Date.now();
        // Extract hospital timer: look for any icon with Hospital title + data-time
        // Icon IDs vary (icon15, icon82, etc.) so match by Hospital keyword instead
        const hospMatch = html.match(/Hospital[^>]*data-time=(?:'|&#039;|&quot;|")(\d+)(?:'|&#039;|&quot;|")/);
        if (hospMatch) {
            const seconds = parseInt(hospMatch[1], 10);
            if (seconds > 0) {
                const untilMs = receivedAt + (seconds * 1000);
                tornConfirmedOkay.delete(id);
                enhancer.hospTime[id] = untilMs;
                enhancer._hospScanNeeded = true;
                // Extract abroad location from any icon with "Abroad in" title
                const abroadMatch = html.match(/Abroad in ([^&"<>]+)/);
                if (abroadMatch) {
                    const locationName = abroadMatch[1].trim();
                    const areaEntries = Object.entries(CONFIG.areas);
                    const areaEntry = areaEntries.find(([, name]) => name === locationName);
                    if (areaEntry) {
                        enhancer.previousStatus[id] = { status: 'Abroad', area: Number(areaEntry[0]) };
                    }
                }
                // Queue status update to server with the timer
                if (enhancer.pollingManager) {
                    const prevAbroad = enhancer.previousStatus[id];
                    enhancer.pollingManager.queueStatusUpdate(id, {
                        status: 'Hospital',
                        details: null,
                        until: Math.floor(untilMs / 1000),
                        previousStatus: prevAbroad ? prevAbroad.status : null,
                        previousArea: prevAbroad ? prevAbroad.area : null,
                        departedAt: null
                    });
                }
                // Deferred scan to pick up the node
                setTimeout(() => {
                    enhancer._hospScanNeeded = true;
                    enhancer.scanHospitalizedMembers();
                    enhancer.updateHospTimers();
                }, 50);
            }
        }
        else if (!enhancer.hospTime[id]) {
            tornConfirmedOkay.add(id);
        }
    }
    /**
     * Process a member's status from a bulk Torn API response (fetch intercepts).
     * Handles hospital, traveling, abroad, and other statuses uniformly.
     * Updates travelData, previousStatus, hospTime, and queues status update to server.
     */
    function processBulkMemberStatus(enhancer, userId, status) {
        const statusText = status.text;
        if (!statusText)
            return;
        if (statusText === 'Hospital') {
            tornConfirmedOkay.delete(userId);
            // Preserve abroad info for blue hospital timer coloring
            if (enhancer.travelData[userId] && enhancer.travelData[userId].area !== 1) {
                enhancer.previousStatus[userId] = { status: 'Abroad', area: enhancer.travelData[userId].area };
            }
            else if (status.area !== undefined && status.area !== 1) {
                enhancer.previousStatus[userId] = { status: 'Abroad', area: status.area };
            }
            if (status.updateAt)
                enhancer.hospTime[userId] = status.updateAt;
            delete enhancer.travelData[userId];
        }
        else {
            tornConfirmedOkay.add(userId);
            delete enhancer.hospTime[userId];
            if ((statusText === 'Traveling' || statusText === 'Abroad') && status.area !== undefined) {
                const existing = enhancer.travelData[userId];
                enhancer.travelData[userId] = {
                    area: status.area,
                    status: statusText,
                    updateAt: status.updateAt ?? existing?.updateAt,
                    departedAt: existing?.departedAt,
                    fromArea: existing?.fromArea
                };
                delete enhancer.previousStatus[userId];
            }
            else if ((statusText === 'Traveling' || statusText === 'Abroad') && enhancer.previousStatus[userId]) {
                const existing = enhancer.travelData[userId];
                enhancer.travelData[userId] = {
                    area: enhancer.previousStatus[userId].area,
                    status: statusText,
                    updateAt: status.updateAt ?? existing?.updateAt,
                    departedAt: existing?.departedAt,
                    fromArea: existing?.fromArea
                };
                delete enhancer.previousStatus[userId];
            }
            else if (statusText !== 'Traveling' && statusText !== 'Abroad') {
                delete enhancer.travelData[userId];
                delete enhancer.previousStatus[userId];
            }
        }
        // Queue status update to server
        if (enhancer.pollingManager) {
            const prevAbroad = enhancer.previousStatus[userId];
            enhancer.pollingManager.queueStatusUpdate(userId, {
                status: statusText,
                details: status.details || null,
                until: status.updateAt || null,
                previousStatus: prevAbroad ? prevAbroad.status : null,
                previousArea: prevAbroad ? prevAbroad.area : (status.area != null ? status.area : null),
                departedAt: enhancer.travelData[userId]?.departedAt || null
            });
        }
    }
    function installInterceptors() {
        const targetWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
        const oldWebSocket = targetWindow.WebSocket;
        // Intentional monkey-patch: replace WebSocket constructor to intercept messages
        // @ts-expect-error — function-to-constructor replacement is inherent to monkey-patching
        targetWindow.WebSocket = function (...args) {
            const socket = new oldWebSocket(...args);
            socket.addEventListener('message', (event) => {
                try {
                    if (!state.enhancer)
                        return;
                    const raw = event.data;
                    if (typeof raw !== 'string' || raw[0] !== '{')
                        return;
                    // Handle concatenated JSON objects (e.g., {...}{...})
                    let json;
                    try {
                        json = JSON.parse(raw);
                    }
                    catch (parseErr) {
                        // Try to extract position from error and parse first object only
                        const match = String(parseErr).match(/position (\d+)/);
                        if (match) {
                            json = JSON.parse(raw.substring(0, parseInt(match[1], 10)));
                        }
                        else {
                            return; // Silently ignore unparseable messages
                        }
                    }
                    const respUser = json?.push?.pub?.data?.message?.namespaces?.users;
                    const statusUpdate = respUser?.actions?.updateStatus;
                    const iconsUpdate = respUser?.actions?.updateIcons;
                    if (statusUpdate) {
                        if (_r || pageFocus()) {
                            _h(state.enhancer, statusUpdate);
                        }
                        else {
                            // Buffer for DOM update on focus, but still send to server now
                            _w.push(statusUpdate);
                            if (statusUpdate.status && state.enhancer.pollingManager) {
                                const sid = String(statusUpdate.userId);
                                state.enhancer.pollingManager.queueStatusUpdate(sid, {
                                    status: statusUpdate.status.text || '',
                                    details: statusUpdate.status.details || null,
                                    until: statusUpdate.status.until || statusUpdate.status.updateAt || null,
                                    previousStatus: null,
                                    previousArea: statusUpdate.status.area != null ? statusUpdate.status.area : null,
                                    departedAt: null
                                });
                            }
                        }
                    }
                    // Extract hospital timer + abroad info from updateIcons HTML
                    if (iconsUpdate && iconsUpdate.icons && state.enhancer) {
                        if (_r || pageFocus()) {
                            _parseIcons(state.enhancer, iconsUpdate);
                        }
                        else {
                            _wi.push({ ...iconsUpdate, _receivedAt: Date.now() });
                        }
                    }
                    if (!statusUpdate && !iconsUpdate) {
                        if (_r || pageFocus())
                            state.enhancer.updateHospTimers();
                    }
                }
                catch (e) {
                    if (state.enhancer)
                        state.enhancer.apiManager.reportError('wsStatusIntercept', e);
                }
            });
            return socket;
        };
        Object.defineProperties(targetWindow.WebSocket, {
            prototype: { value: oldWebSocket.prototype },
            CONNECTING: { value: oldWebSocket.CONNECTING },
            OPEN: { value: oldWebSocket.OPEN },
            CLOSING: { value: oldWebSocket.CLOSING },
            CLOSED: { value: oldWebSocket.CLOSED },
        });
        // Extension mode: attach message handlers to WebSockets captured by early-inject.js
        const earlyWsSockets = targetWindow._catWsSockets;
        if (earlyWsSockets && earlyWsSockets.length > 0) {
            for (const socket of earlyWsSockets) {
                socket.addEventListener('message', (event) => {
                    try {
                        if (!state.enhancer)
                            return;
                        const raw = event.data;
                        if (typeof raw !== 'string' || raw[0] !== '{')
                            return;
                        let json;
                        try {
                            json = JSON.parse(raw);
                        }
                        catch (parseErr) {
                            const match = String(parseErr).match(/position (\d+)/);
                            if (match) {
                                json = JSON.parse(raw.substring(0, parseInt(match[1], 10)));
                            }
                            else {
                                return;
                            }
                        }
                        const respUser = json?.push?.pub?.data?.message?.namespaces?.users;
                        const statusUpdate = respUser?.actions?.updateStatus;
                        const iconsUpdate = respUser?.actions?.updateIcons;
                        if (statusUpdate) {
                            if (_r || pageFocus()) {
                                _h(state.enhancer, statusUpdate);
                            }
                            else {
                                _w.push(statusUpdate);
                                if (statusUpdate.status && state.enhancer.pollingManager) {
                                    const sid = String(statusUpdate.userId);
                                    state.enhancer.pollingManager.queueStatusUpdate(sid, {
                                        status: statusUpdate.status.text || '',
                                        details: statusUpdate.status.details || null,
                                        until: statusUpdate.status.until || statusUpdate.status.updateAt || null,
                                        previousStatus: null,
                                        previousArea: statusUpdate.status.area != null ? statusUpdate.status.area : null,
                                        departedAt: null
                                    });
                                }
                            }
                        }
                        if (iconsUpdate && iconsUpdate.icons && state.enhancer) {
                            if (_r || pageFocus()) {
                                _parseIcons(state.enhancer, iconsUpdate);
                            }
                            else {
                                _wi.push({ ...iconsUpdate, _receivedAt: Date.now() });
                            }
                        }
                        if (!statusUpdate && !iconsUpdate) {
                            if (_r || pageFocus())
                                state.enhancer.updateHospTimers();
                        }
                    }
                    catch (e) {
                        if (state.enhancer)
                            state.enhancer.apiManager.reportError('wsStatusIntercept', e);
                    }
                });
            }
            console.log(`[CAT] Attached WS handlers to ${earlyWsSockets.length} early sockets`);
        }
        let _lastFocusLost = Date.now();
        targetWindow.addEventListener('blur', () => { _lastFocusLost = Date.now(); });
        targetWindow.addEventListener('focus', () => {
            if (!state.enhancer)
                return;
            const awayMs = Date.now() - _lastFocusLost;
            if (awayMs > 60000) {
                // Away for > 1 min — DOM updates are stale, but still apply hospTime updates
                // so that ipecac/syrup taken while away are reflected correctly
                if (_w.length) {
                    const pending = _w.splice(0);
                    for (const su of pending) {
                        if (!su.status)
                            continue;
                        const sid = String(su.userId);
                        const st = (su.status.text || '').trim();
                        const endTime = su.status.until || su.status.updateAt || su.until;
                        if (st === 'Hospital' || st.toLowerCase().includes('hospital')) {
                            if (endTime) {
                                const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                                if (endMs > Date.now())
                                    state.enhancer.hospTime[sid] = endTime;
                            }
                            tornConfirmedOkay.delete(sid);
                        }
                        else {
                            delete state.enhancer.hospTime[sid];
                            tornConfirmedOkay.add(sid);
                        }
                    }
                }
                if (_wi.length) {
                    const pendingIcons = _wi.splice(0);
                    for (const iu of pendingIcons) {
                        if (!iu.icons)
                            continue;
                        const sid = String(iu.userId);
                        const receivedAt = iu._receivedAt || Date.now();
                        const hospMatch = iu.icons.match(/Hospital[^>]*data-time=(?:'|&#039;|&quot;|")(\d+)(?:'|&#039;|&quot;|")/);
                        if (hospMatch) {
                            const seconds = parseInt(hospMatch[1], 10);
                            if (seconds > 0) {
                                const untilMs = receivedAt + (seconds * 1000);
                                if (untilMs > Date.now()) {
                                    state.enhancer.hospTime[sid] = untilMs;
                                    tornConfirmedOkay.delete(sid);
                                }
                            }
                        }
                        else if (!state.enhancer.hospTime[sid]) {
                            tornConfirmedOkay.add(sid);
                        }
                    }
                }
            }
            else {
                // Short absence — process buffered messages normally
                if (_w.length) {
                    const pending = _w.splice(0);
                    for (const su of pending) {
                        _h(state.enhancer, su);
                    }
                }
                if (_wi.length) {
                    const pendingIcons = _wi.splice(0);
                    for (const iu of pendingIcons) {
                        _parseIcons(state.enhancer, iu);
                    }
                }
            }
            // requestCalls() is already handled by resume() in lifecycle.ts (via tooltips visibilitychange)
        });
        targetWindow._p = (v) => { _r = v !== undefined ? v : !_r; return _r; };
        const oldFetch = targetWindow.fetch;
        targetWindow._catOldFetch = oldFetch;
        targetWindow.fetch = async (...args) => {
            let url;
            try {
                const firstArg = args[0];
                url = (typeof firstArg === 'object' && firstArg !== null && 'url' in firstArg)
                    ? firstArg.url
                    : (typeof firstArg === 'string' ? firstArg : undefined);
            }
            catch (_) { /* ignore */ }
            const response = await oldFetch(...args);
            try {
                if (state.enhancer && typeof url === 'string' && pageFocus()) {
                    const enhancer = state.enhancer;
                    if (url.includes('step=getwarusers') || url.includes('step=getProcessBarRefreshData')) {
                        const clone = response.clone();
                        clone.text().then(text => {
                            if (!text || text[0] !== '{')
                                return;
                            const json = JSON.parse(text);
                            let members = null;
                            if (json.warDesc?.members)
                                members = json.warDesc.members;
                            else if (json.userStatuses)
                                members = json.userStatuses;
                            if (!members)
                                return;
                            Object.keys(members).forEach((id) => {
                                const member = members[id];
                                const status = member.status || member;
                                processBulkMemberStatus(enhancer, String(member.userID || id), status);
                            });
                            enhancer.updateTravelingStatus();
                            setTimeout(() => {
                                enhancer._hospScanNeeded = true;
                                enhancer.scanHospitalizedMembers();
                                enhancer.updateHospTimers();
                                if (enhancer.enhancementManager)
                                    enhancer.enhancementManager.restoreSavedSort(true);
                            }, 100);
                        }).catch(err => { enhancer.apiManager.reportError('interceptWarUsers', err); });
                    }
                    if (url.includes('step=getwardata')) {
                        const clone2 = response.clone();
                        clone2.text().then(text => {
                            if (!text || text[0] !== '{')
                                return;
                            const json = JSON.parse(text);
                            if (json.rankedWarMembers) {
                                if (!enhancer.onlineStatuses)
                                    enhancer.onlineStatuses = {};
                                let _onlineChanged = false;
                                Object.entries(json.rankedWarMembers).forEach(([id, data]) => {
                                    const st = data?.onlineStatus?.status;
                                    if (st && enhancer.onlineStatuses[id] !== st) {
                                        enhancer.onlineStatuses[id] = st;
                                        _onlineChanged = true;
                                    }
                                    if (data?.status?.text) {
                                        processBulkMemberStatus(enhancer, id, data.status);
                                    }
                                });
                                if (_onlineChanged)
                                    enhancer._onlineStatusVersion = (enhancer._onlineStatusVersion || 0) + 1;
                                // Build id→row map from cached rows (avoids one querySelector per member)
                                const cachedRows = (enhancer._memberRowsValid && enhancer._cachedMemberRows)
                                    ? enhancer._cachedMemberRows : null;
                                const rowById = {};
                                if (cachedRows) {
                                    for (let _i = 0; _i < cachedRows.length; _i++) {
                                        const _r = cachedRows[_i];
                                        const _uid = _r.dataset.catUid;
                                        if (_uid)
                                            rowById[_uid] = _r;
                                    }
                                }
                                Object.entries(json.rankedWarMembers).forEach(([id, data]) => {
                                    const st = data?.onlineStatus?.status || 'offline';
                                    let memberRow = rowById[id] || null;
                                    if (!memberRow) {
                                        const a = document.querySelector(`.desc-wrap a[href*="profiles.php?XID=${id}"]`);
                                        memberRow = a?.closest('li') || null;
                                        if (memberRow)
                                            memberRow.dataset.catUid = id;
                                    }
                                    if (memberRow) {
                                        let nameEl = memberRow._catNameEl || null;
                                        if (nameEl && (!nameEl.isConnected || (!nameEl.className.includes('honor-text-wrap') && !nameEl.className.includes('honorWrap___'))))
                                            nameEl = null;
                                        if (!nameEl) {
                                            nameEl = memberRow.querySelector('.honor-text-wrap') ||
                                                memberRow.querySelector('[class*="honorWrap___"]');
                                            if (nameEl)
                                                memberRow._catNameEl = nameEl;
                                        }
                                        if (nameEl)
                                            nameEl.dataset.onlineStatus = st;
                                        memberRow.dataset.catStatus = st;
                                    }
                                });
                                StorageUtil.set('cat_online_statuses', enhancer.onlineStatuses);
                            }
                            if (json.members && Array.isArray(json.members)) {
                                json.members.forEach((member) => {
                                    if (member.status?.text) {
                                        processBulkMemberStatus(enhancer, String(member.userID), member.status);
                                    }
                                });
                            }
                            // Relay chain timer to server for Discord chain monitor
                            if (json.wars && Array.isArray(json.wars)) {
                                const chainEntry = json.wars.find(w => w.key === 'chain' || w.type === 'chain');
                                const chainEnd = chainEntry?.data?.chain?.end; // unix timestamp in ms
                                const chainCount = chainEntry?.data?.chainBar?.chain || chainEntry?.data?.chain?.chain || 0;
                                const chainStats = chainEntry?.data?.stats;
                                if (chainEnd && chainEnd > 0) {
                                    const remaining = Math.round((chainEnd - Date.now()) / 1000);
                                    void remaining;
                                }
                                if (chainEnd && chainEnd > 0) {
                                    const factionId = json.factionID || StorageUtil.get('cat_user_faction_id', null);
                                    if (factionId) {
                                        enhancer.apiManager.apiRequest(`${enhancer.apiManager.serverUrl}/api/chain-timer`, {
                                            method: 'POST',
                                            headers: {
                                                'Content-Type': 'application/json',
                                                'Authorization': `Bearer ${enhancer.apiManager.authToken}`
                                            },
                                            body: JSON.stringify({
                                                factionId: String(factionId),
                                                chainEnd,
                                                chainCount,
                                                respect: chainStats?.respect || null,
                                                attackers: chainStats?.members || null
                                            })
                                        }).catch(() => { });
                                    }
                                }
                            }
                            enhancer.updateTravelingStatus();
                            enhancer.updateHospTimers();
                        }).catch(err => { enhancer.apiManager.reportError('interceptWarData', err); });
                    }
                    if (url.includes('sid=factionsRankedWarProcessBarRefresh')) {
                        const clonePB = response.clone();
                        clonePB.text().then(text => {
                            if (!text || text[0] !== '{')
                                return;
                            const json = JSON.parse(text);
                            if (!json.users)
                                return;
                            const userEntries = Object.entries(json.users);
                            userEntries.forEach(([id, userData]) => {
                                if (userData.status?.text) {
                                    processBulkMemberStatus(enhancer, String(id), userData.status);
                                }
                                // Extract hospital timer from icons HTML (same as WS updateIcons)
                                if (userData.icons && typeof userData.icons === 'string') {
                                    _parseIcons(enhancer, { userId: id, icons: userData.icons });
                                }
                            });
                            enhancer.updateTravelingStatus();
                            setTimeout(() => {
                                enhancer._hospScanNeeded = true;
                                enhancer.scanHospitalizedMembers();
                                enhancer.updateHospTimers();
                            }, 100);
                        }).catch(err => { enhancer.apiManager.reportError('interceptProcessBarRefresh', err); });
                    }
                    if (url.includes('chat/online-status')) {
                        const clone3 = response.clone();
                        clone3.text().then(text => {
                            if (!text || text[0] !== '{')
                                return;
                            const json = JSON.parse(text);
                            if (!enhancer.onlineStatuses)
                                enhancer.onlineStatuses = {};
                            let changed = false;
                            Object.entries(json).forEach(([id, status]) => {
                                if (enhancer.onlineStatuses[id] !== status) {
                                    enhancer.onlineStatuses[id] = status;
                                    changed = true;
                                }
                            });
                            if (changed) {
                                enhancer._onlineStatusVersion = (enhancer._onlineStatusVersion || 0) + 1;
                                // Build id→row map from cached rows — avoids one querySelector per member
                                const cachedRows2 = (enhancer._memberRowsValid && enhancer._cachedMemberRows)
                                    ? enhancer._cachedMemberRows : null;
                                const rowById2 = {};
                                if (cachedRows2) {
                                    for (let _i = 0; _i < cachedRows2.length; _i++) {
                                        const _r = cachedRows2[_i];
                                        const _uid = _r.dataset.catUid;
                                        if (_uid)
                                            rowById2[_uid] = _r;
                                    }
                                }
                                Object.entries(json).forEach(([id, st]) => {
                                    // Color the player name based on online status via data attribute
                                    let memberRow = rowById2[id] || null;
                                    if (!memberRow) {
                                        const a = document.querySelector(`.desc-wrap a[href*="profiles.php?XID=${id}"]`);
                                        memberRow = a?.closest('li') || null;
                                        if (memberRow)
                                            memberRow.dataset.catUid = id;
                                    }
                                    if (memberRow) {
                                        let nameEl = memberRow._catNameEl || null;
                                        if (nameEl && (!nameEl.isConnected || (!nameEl.className.includes('honor-text-wrap') && !nameEl.className.includes('honorWrap___'))))
                                            nameEl = null;
                                        if (!nameEl) {
                                            nameEl = memberRow.querySelector('.honor-text-wrap') ||
                                                memberRow.querySelector('[class*="honorWrap___"]');
                                            if (nameEl)
                                                memberRow._catNameEl = nameEl;
                                        }
                                        if (nameEl)
                                            nameEl.dataset.onlineStatus = st;
                                        memberRow.dataset.catStatus = st;
                                    }
                                });
                                // Cache online statuses for instant display on next load
                                StorageUtil.set('cat_online_statuses', enhancer.onlineStatuses);
                                // Invalidate call button states and immediately re-render to avoid visual flash
                                document.querySelectorAll('.call-button.other-call').forEach(btn => {
                                    btn.dataset.callState = '';
                                });
                                if (enhancer.currentCalls?.length > 0) {
                                    enhancer._lastCallsHash = undefined;
                                    enhancer.updateCallButtons(enhancer.currentCalls);
                                }
                                if (enhancer.enhancementManager)
                                    enhancer.enhancementManager.restoreSavedSort(true);
                            }
                        }).catch(err => { enhancer.apiManager.reportError('interceptOnlineStatus', err); });
                    }
                }
            }
            catch (_) { /* never block Torn's fetch */ }
            return response;
        };
    }

    function getModalStyles() {
        return `
                /* API Key Modal */
                .torn-api-modal {
                    position: fixed;
                    top: 0;
                    left: 0;
                    right: 0;
                    bottom: 0;
                    background: rgba(0, 0, 0, 0.7);
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    z-index: 10000;
                    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                }

                .torn-api-modal-content {
                    background: linear-gradient(135deg, #1a1a2e, #16213e);
                    border-radius: 16px;
                    padding: 40px;
                    max-width: 500px;
                    width: 90%;
                    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
                    border: 2px solid #667eea;
                    animation: slideUp 0.3s ease;
                }

                @keyframes slideUp {
                    from {
                        opacity: 0;
                        transform: translateY(20px);
                    }
                    to {
                        opacity: 1;
                        transform: translateY(0);
                    }
                }

                .torn-api-modal-title {
                    font-size: 24px;
                    font-weight: bold;
                    color: #667eea;
                    margin-bottom: 15px;
                    text-align: center;
                }

                .torn-api-modal-subtitle {
                    font-size: 14px;
                    color: #b0b0b0;
                    text-align: center;
                    margin-bottom: 25px;
                    line-height: 1.6;
                }

                .torn-api-modal-input-group {
                    margin-bottom: 20px;
                }

                .torn-api-modal-label {
                    display: block;
                    font-size: 12px;
                    font-weight: 600;
                    color: #f0f0f0;
                    margin-bottom: 8px;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }

                .torn-api-modal-input {
                    width: 100%;
                    padding: 12px 15px;
                    border: 2px solid #667eea;
                    border-radius: 8px;
                    background: rgba(255, 255, 255, 0.05);
                    color: #ffffff;
                    font-size: 14px;
                    font-family: 'Monaco', 'Menlo', monospace;
                    box-sizing: border-box;
                    transition: all 0.2s ease;
                }

                .torn-api-modal-input:focus {
                    outline: none;
                    border-color: #f093fb;
                    background: rgba(255, 255, 255, 0.1);
                    box-shadow: 0 0 10px rgba(240, 147, 251, 0.3);
                }

                .torn-api-modal-link {
                    color: #667eea;
                    text-decoration: none;
                    font-weight: 600;
                    transition: color 0.2s ease;
                }

                .torn-api-modal-link:hover {
                    color: #f093fb;
                }

                .torn-api-modal-buttons {
                    display: flex;
                    gap: 10px;
                    margin-top: 25px;
                }

                .torn-api-modal-btn {
                    flex: 1;
                    padding: 12px 20px;
                    border: none;
                    border-radius: 8px;
                    font-weight: 600;
                    font-size: 14px;
                    cursor: pointer;
                    transition: all 0.2s ease;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                }

                .torn-api-modal-btn-confirm {
                    background: linear-gradient(135deg, #667eea, #764ba2);
                    color: white;
                }

                .torn-api-modal-btn-confirm:hover {
                    transform: translateY(-2px);
                    box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
                }

                .torn-api-modal-btn-confirm:active {
                    transform: translateY(0);
                }

                .torn-api-modal-btn-cancel {
                    background: rgba(255, 255, 255, 0.1);
                    color: #b0b0b0;
                    border: 1px solid rgba(255, 255, 255, 0.2);
                }

                .torn-api-modal-btn-cancel:hover {
                    background: rgba(255, 255, 255, 0.15);
                    color: #ffffff;
                }

                .torn-api-modal-error {
                    color: #ff6b6b;
                    font-size: 12px;
                    margin-top: 8px;
                    display: none;
                }

                .torn-api-modal-error.show {
                    display: block;
                }
    `;
    }

    function getFactionLayoutStyles() {
        return `
                /* Main call buttons styling */
                .desc-wrap .call-button {
                    font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif !important;
                    background: linear-gradient(135deg, ${CONFIG.colors.dark}, ${CONFIG.colors.darkSecondary}) !important;
                    border-radius: 16px !important;
                    box-shadow:
                        0 20px 40px rgba(0,0,0,0.3),
                        0 8px 16px rgba(0,0,0,0.2),
                        inset 0 1px 0 rgba(255,255,255,0.1) !important;
                    border: none !important;
                    padding: 20px !important;
                    margin: 2px 0 !important;
                    position: relative !important;
                    transition: all ${CONFIG.animations.duration} ${CONFIG.animations.easing} !important;
                }

                .desc-wrap .f-war-list::before {
                    content: '' !important;
                    position: absolute !important;
                    top: 0 !important;
                    left: 0 !important;
                    right: 0 !important;
                    height: 2px !important;
                    background: linear-gradient(90deg, ${CONFIG.colors.primary}, ${CONFIG.colors.accent}) !important;
                }


                /* Enemy faction names */
                .desc-wrap .f-war-list .faction-name,
                .desc-wrap .f-war-list [class*="name___"] {
                    color: ${CONFIG.colors.enemyFaction} !important;
                    text-shadow: 0 0 8px rgba(255, 121, 76, 0.4) !important;
                    font-size: 1.5em !important;
                    font-weight: 700 !important;
                    margin-bottom: 16px !important;
                }

                /* Your faction names */
                .your-faction .faction-name,
                .your-faction [class*="name___"],
                [class*="your-faction"] .faction-name,
                [class*="your-faction"] [class*="name___"] {
                    color: ${CONFIG.colors.yourFaction} !important;
                    text-shadow: 0 0 8px rgba(134, 178, 2, 0.4) !important;
                    font-size: 1.5em !important;
                    font-weight: 700 !important;
                    margin-bottom: 16px !important;
                }

                /* Member list styling - ONLY in desc-wrap */
                .desc-wrap li.member___fZiTx, .desc-wrap li[class*="member___"] {
                    background: rgba(255,255,255,0.05) !important;
                    border: none !important;
                    border-radius: 12px !important;
                    padding: 12px !important;
                    margin: 6px 0 !important;
                    transition: all ${CONFIG.animations.duration} ${CONFIG.animations.easing} !important;
                    backdrop-filter: blur(10px) !important;
                    position: relative !important;
                    display: flex;
                    align-items: center !important;
                    justify-content: space-between !important;
                    flex-wrap: nowrap !important;
                    min-height: 50px !important;
                }

                .desc-wrap li.member___fZiTx:hover, .desc-wrap li[class*="member___"]:hover {
                    background: rgba(255,255,255,0.08) !important;
                    border-color: ${CONFIG.colors.primary} !important;
                    transform: translateY(-1px) !important;
                    box-shadow: 0 6px 20px rgba(102, 126, 234, 0.15) !important;
                }

                /* Level styling - ONLY in desc-wrap - minimalist design */
                .desc-wrap .level___g3CWR, .desc-wrap [class*="level___"] {
                    padding: 2px 4px !important;
                    border-radius: 4px !important;
                    font-size: 1em !important;
                    font-weight: 500 !important;
                    display: inline-block !important;
                    margin-right: 3px !important;
                    border: none !important;
                    min-width: 22px !important;
                    max-width: 30px !important;
                    text-align: center !important;
                    font-family: 'Monaco', 'Menlo', monospace !important;
                    flex-shrink: 0 !important;
                    color: #fff !important;
                }
                body:not(.dark-mode) .desc-wrap .level___g3CWR,
                body:not(.dark-mode) .desc-wrap [class*="level___"] {
                    color: #333 !important;
                }

                /* Points/Score styling - ONLY in desc-wrap */
                .desc-wrap .points___TQbnu, .desc-wrap [class*="points___"] {
                    margin-top: -0.2em !important;
                    font-weight: 600 !important;
                    font-size: 1em !important;
                }

                /* Score column width in enemy faction */
                .enemy-faction [class*="points___"]:not(.tab___UztMc),
                .enemy-faction .points___TQbnu:not(.tab___UztMc),
                .desc-wrap .points___TQbnu:not(.tab___UztMc),
                .desc-wrap [class*="points___"]:not(.tab___UztMc),
                li.enemy___uiAJH > .points___TQbnu,
                li[class*="enemy___"] > .points___TQbnu,
                li.enemy > .points___TQbnu {
                    max-width: 50px !important;
                    width: 50px !important;
                    flex: 0 0 50px !important;
                    overflow: hidden !important;
                    text-overflow: ellipsis !important;
                    padding: 0 !important;
                    box-sizing: border-box !important;
                }

                /* Status column width in enemy faction */
                .enemy-faction [class*="status___"]:not(.tab___UztMc),
                .desc-wrap [class*="status___"]:not(.tab___UztMc),
                li.enemy___uiAJH > [class*="status___"],
                li[class*="enemy___"] > [class*="status___"],
                li.enemy > [class*="status___"] {
                    max-width: 60px !important;
                    width: 60px !important;
                    flex: 0 0 60px !important;
                    overflow: hidden !important;
                    text-overflow: ellipsis !important;
                    padding: 0 !important;
                    margin-left: 5px !important;
                    box-sizing: border-box !important;
                }

                /* Status indicators - ONLY in desc-wrap */
                .desc-wrap [class*="status___"], .desc-wrap .status {
                    border-radius: 6px !important;
                    font-size: 0.85em !important;
                    font-weight: 500 !important;
                    text-transform: uppercase !important;
                    letter-spacing: 0.3px !important;
                    display: inline-block !important;
                }

                /* Status colors - ONLY in desc-wrap */
                .desc-wrap [class*="status___"]:contains("Online"), .desc-wrap .status:contains("Online") {
                    background: ${CONFIG.colors.success} !important;
                    color: ${CONFIG.colors.dark} !important;
                }

                .desc-wrap [class*="status___"]:contains("Offline"), .desc-wrap .status:contains("Offline") {
                    background: rgba(255,255,255,0.15) !important;
                    color: ${CONFIG.colors.lightSecondary} !important;
                }

                .desc-wrap [class*="status___"]:contains("Hospital"), .desc-wrap .status:contains("Hospital") {
                    background: ${CONFIG.colors.danger} !important;
                    color: ${CONFIG.colors.light} !important;
                }

                .desc-wrap [class*="status___"]:contains("Traveling"), .desc-wrap .status:contains("Traveling") {
                    background: ${CONFIG.colors.warning} !important;
                    color: ${CONFIG.colors.dark} !important;
                }

                .desc-wrap [class*="status___"]:contains("Idle"), .desc-wrap .status:contains("Idle") {
                    background: #ffa726 !important;
                    color: ${CONFIG.colors.dark} !important;
                }

                .desc-wrap [class*="status___"]:contains("Abroad"), .desc-wrap .status:contains("Abroad") {
                    background: #ab47bc !important;
                    color: ${CONFIG.colors.light} !important;
                }

                li[data-abroad-hosp] [class*="status___"],
                li[data-abroad-hosp] .status.left {
                    color: #4FC3F7 !important;
                }
                li[data-abroad-hosp] [class*="status___"]::after,
                li[data-abroad-hosp] .status.left::after {
                    content: " \\2192 " attr(data-abroad-dest);
                    font-size: 0.85em;
                    opacity: 0.9;
                }

                .desc-wrap [class*="status___"]:contains("Jail"), .desc-wrap .status:contains("Jail") {
                    background: #8d6e63 !important;
                    color: ${CONFIG.colors.light} !important;
                }

                /* Attack buttons - ONLY in desc-wrap */



                /* Attack container - ensure proper spacing for both buttons */
                .desc-wrap li[class*="member___"] .attack:has(.call-button),
                .desc-wrap li[class*="member___"] .attack,
                .desc-wrap .call-attack-container {
                    flex-wrap: nowrap !important;
                    overflow: visible !important;
                    display: flex !important;
                    align-items: center !important;
                    gap: 4px !important;
                    justify-content: flex-start !important;
                    min-width: 100px !important;
                    width: 100px !important;
                    flex: 0 0 100px !important;
                }



                /* Hide attack column + header on other factions' pages
                   Specificity must beat .desc-wrap li[class*="member___"] .attack (0,3,1) */
                html.cat-other-faction .desc-wrap li .attack:not(:has(.call-button)),
                html.cat-other-faction .desc-wrap [class*="attack___"]:not(:has(.call-button)),
                html.cat-other-faction .attack:not(:has(.call-button)),
                html.cat-other-faction [class*="attack___"]:not(:has(.call-button)) {
                    display: none !important;
                    width: 0 !important;
                    min-width: 0 !important;
                    padding: 0 !important;
                    overflow: hidden !important;
                    flex: 0 0 0px !important;
                }

                /* Attack column header styling */
                .desc-wrap .attack___wBWp2.tab___UztMc,
                .desc-wrap [class*="attack___"].tab___UztMc,
                .desc-wrap .attack.tab,
                .desc-wrap .attack-header {
                    min-width: 90px !important;
                    width: 90px !important;
                    flex: 0 0 90px !important;
                    text-align: center !important;
                }

                /* Your faction rows — flex layout for vertical centering */
                .your-faction li[class*="your___"],
                .your-faction li.your,
                [class*="your-faction"] li[class*="your___"],
                [class*="your-faction"] li.your {
                    display: flex;
                    align-items: center !important;
                    flex-wrap: nowrap !important;
                    position: relative !important;
                }

                /* CD column vertical centering */
                .your-faction .cd-column,
                [class*="your-faction"] .cd-column {
                    align-self: center !important;
                }

                /* Reduce Status column width in your-faction only */
                .your-faction [class*="status___"],
                [class*="your-faction"] [class*="status___"] {
                    flex: 0 0 auto !important;
                    max-width: 60px !important;
                    width: 60px !important;
                    overflow: hidden !important;
                    text-overflow: ellipsis !important;
                }

                /* Uniform font for all header columns */
                .white-grad > div[class*="tab___"],
                .white-grad > [class*="member___"],
                .white-grad > [class*="level___"],
                .white-grad > [class*="points___"],
                .white-grad > [class*="status___"],
                .white-grad > [class*="attack___"],
                .white-grad > .bsp-header {
                    font-size: 11px !important;
                    font-weight: 600 !important;
                    text-transform: capitalize !important;
                }

                /* Active sort header highlight */
                .white-grad > [data-sort] {
                    color: #74C0FC !important;
                }

                /* Header hover */
                .white-grad > [data-sort-enabled]:hover,
                .white-grad > .bsp-header:hover {
                    color: rgba(116, 192, 252, 0.35) !important;
                }

                /* Header column sizing — match data row widths */
                .white-grad > [class*="points___"] {
                    max-width: 50px !important;
                    width: 50px !important;
                    flex: 0 0 50px !important;
                    text-align: center !important;
                }

                .enemy-faction .white-grad > [class*="status___"] {
                    max-width: 60px !important;
                    width: 60px !important;
                    flex: 0 0 60px !important;
                    margin-left: 5px !important;
                    translate: -10px !important;
                }

                /* Hide sort arrows in header */
                .white-grad > [class*="points___"] > i,
                .white-grad > div[class*="points___"] > i,
                .white-grad [class*="sortIcon___"] {
                    display: none !important;
                }

                /* Remove all borders from faction war tables */
                .desc-wrap * {
                    border: none !important;
                }

                .desc-wrap .f-war-list,
                .desc-wrap .f-war-list *,
                .desc-wrap ul,
                .desc-wrap li,
                .desc-wrap div {
                    border: none !important;
                    border-top: none !important;
                    border-bottom: none !important;
                    border-left: none !important;
                    border-right: none !important;
                }

/* Enemy faction container max width */
                .enemy-faction.left[class*="tabMenuCont"] {
                    max-width: 390px !important;
                }

                /* Your faction container width */
                .your-faction.right[class*="tabMenuCont"] {
                    width: 410px !important;
                    max-width: 410px !important;
                }

                /* Force enemy faction header to single line */
                .enemy-faction .white-grad {
                    display: flex !important;
                    flex-wrap: nowrap !important;
                    align-items: center !important;
                    overflow: hidden !important;
                }

                /* War table header gradient (Torn native style) — must come AFTER border resets */
                #faction_war_list_id div.white-grad,
                .desc-wrap div.white-grad.c-pointer,
                .desc-wrap div.white-grad {
                    display: flex !important;
                    align-items: center !important;
                    background: linear-gradient(180deg, #666 0%, #333 100%) !important;
                    background-image: linear-gradient(180deg, #666 0%, #333 100%) !important;
                    border-top: 1px solid #555 !important;
                    border-bottom: 1px solid #222 !important;
                    border-left: none !important;
                    border-right: none !important;
                    box-shadow: 0 0 2px rgba(0,0,0,0.25) !important;
                    text-shadow: 0 0 2px #000 !important;
                    color: #fff !important;
                    padding: 0 !important;
                    min-height: 0 !important;
                    height: 26px !important;
                }

                /* Attack header styling only for enemy factions (not your-faction) */
                .white-grad > [class*="attack___"]:not(.your-faction *):not([class*="your-faction"] *) {
                    text-align: center !important;
                }

                /* Hide Attack header in your-faction */
                .your-faction .white-grad > [class*="attack___"],
                [class*="your-faction"] .white-grad > [class*="attack___"] {
                    display: none !important;
                }

                /* Hide TornTools duplicate button in option menu */
                #TDup_buttonInOptionMenu {
                    display: none !important;
                }


    `;
    }

    function getBspStyles() {
        return `
                /* BSP column styling - clean minimal design */
                #faction_war_list_id .bsp-column {
                    color: ${CONFIG.colors.light} !important;
                    padding: 2px 4px !important;
                    font-size: 1em !important;
                    font-weight: 700 !important;
                    display: inline-block !important;
                    min-width: 32px !important;
                    max-width: 32px !important;
                    text-align: center !important;
                    font-family: 'Monaco', 'Menlo', monospace !important;
                    flex-shrink: 0 !important;
                }
                #faction_war_list_id .enemy-faction .bsp-column {
                    margin-top: 10px !important;
                }

                /* BSP column header - inherits from uniform header styling above, with specific overrides */
                #faction_war_list_id .bsp-header {
                    min-width: 32px !important;
                    width: 32px !important;
                    flex: 0 0 32px !important;
                    margin-right: 2px !important;
                    background: none !important;
                    border: none !important;
                    cursor: pointer !important;
                    transition: color 0.2s ease !important;
                    margin-top: 2px !important;
                }



                /* BSP value styling - inherit from column */
                #faction_war_list_id .bsp-value {
                    font-weight: 600 !important;
                    display: inline !important;
                    font-family: var(--cat-bsp-font, inherit) !important;
                }

                /* BSP value colors - all official BSP colors */
                #faction_war_list_id .bsp-value.bsp-red {
                    color: #FF0000 !important; /* BSP red - highest threat */
                }

                #faction_war_list_id .bsp-value.bsp-orange {
                    color: #FFB30F !important; /* BSP orange - high threat */
                }

                #faction_war_list_id .bsp-value.bsp-blue {
                    color: #47A6FF !important; /* BSP blue - medium threat */
                }

                #faction_war_list_id .bsp-value.bsp-green {
                    color: #73DF5D !important; /* BSP green - low threat */
                }

                #faction_war_list_id .bsp-value.bsp-white {
                    color: #FFFFFF !important; /* BSP white - very low threat */
                }

                #faction_war_list_id .bsp-value.bsp-gray {
                    color: #949494 !important; /* BSP gray - minimal threat */
                }

                #faction_war_list_id .bsp-value.bsp-default {
                    color: ${CONFIG.colors.light} !important; /* Default white */
                }

                #faction_war_list_id .bsp-column .bsp-value.bsp-wait {
                    color: #888 !important; /* Wait - BSP not loaded yet */
                    font-style: italic !important;
                }

                /* Hide original BSP elements and their containers */
                #faction_war_list_id .iconStats {
                    z-index: -999 !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                    display: none !important;
                }

                /* Hide BSP parent containers */
                div[style*="position: absolute"][style*="z-index: 100"] {
                    z-index: -999 !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                    display: none !important;
                }

                /* More specific targeting for BSP containers */
                .TDup_ColoredStatsInjectionDiv {
                    z-index: -999 !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                    display: none !important;
                }
    `;
    }

    function getCallButtonStyles() {
        return `
                /* Call column - ONLY in desc-wrap for enemy faction */
                .desc-wrap .call-column {
                    display: inline-block !important;
                    width: 50px !important;
                    text-align: center !important;
                    vertical-align: middle !important;
                    margin-right: 8px !important;
                    flex-shrink: 0 !important;
                    order: -1 !important;
                    position: relative !important;
                }

                .desc-wrap .call-button {
                    background: linear-gradient(180deg, #111 0%, #555 25%, #333 60%, #333 78%, #111 100%) !important;
                    color: #eee !important;
                    border: 1px solid #111 !important;
                    padding: 4px 2px !important;
                    border-radius: 3px !important;
                    line-height: normal !important;
                    display: inline-block !important;
                    height: 22px !important;
                    box-sizing: border-box !important;
                    font-weight: 600 !important;
                    cursor: pointer !important;
                    transition: background 0.15s ease !important;
                    box-shadow: none !important;
                    text-shadow: 0 0 5px #000 !important;
                    text-transform: uppercase !important;
                    letter-spacing: 0.3px !important;
                    font-size: 0.65em !important;
                    width: auto !important;
                    min-width: 28px !important;
                    max-width: 40px !important;
                    white-space: nowrap !important;
                    overflow: hidden !important;
                    text-overflow: ellipsis !important;
                    text-align: center !important;
                    flex: 0 0 auto !important;
                    order: -1 !important;
                    margin-left: 8px !important;
                    margin-right: 4px !important;
                    vertical-align: middle !important;
                    z-index: 1000 !important;
                    position: relative !important;
                }

                .desc-wrap .call-button:hover {
                    background: linear-gradient(180deg, #333 0%, #777 25%, #333 59%, #666 78%, #333 100%) !important;
                    color: #fff !important;
                    text-shadow: 0 0 5px rgba(255, 255, 255, 0.25) !important;
                }

                .desc-wrap .call-button:active {
                    background: linear-gradient(180deg, #000 0%, #333 100%) !important;
                    box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.07) !important;
                }

                /* Locked state: player already has an active call */
                .desc-wrap .call-button.call-locked {
                    background: linear-gradient(180deg, #000 0%, #333 100%) !important;
                    color: #555 !important;
                    cursor: not-allowed !important;
                    pointer-events: auto !important;
                    text-shadow: none !important;
                    box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.07) !important;
                    border-color: #000 !important;
                }

                .desc-wrap .call-button.call-locked:hover {
                    background: linear-gradient(180deg, #000 0%, #333 100%) !important;
                }

                /* Call button color states - MUST be more specific than .call-button */
                .desc-wrap .call-button[class*="my-call"],
                .desc-wrap .call-button.my-call {
                    background: linear-gradient(180deg, #1e4400 0%, #5a9a00 25%, #4a7a00 60%, #4a7a00 78%, #1e4400 100%) !important;
                    color: #fff !important;
                    border-color: #1a3000 !important;
                    opacity: 1 !important;
                }

                .desc-wrap .call-button[class*="my-call"]:hover,
                .desc-wrap .call-button.my-call:hover {
                    background: linear-gradient(180deg, #2e5500 0%, #6aaa00 25%, #4a7a00 59%, #5a9a00 78%, #2e5500 100%) !important;
                    color: #fff !important;
                }

                .desc-wrap .call-button[class*="other-call"],
                .desc-wrap .call-button.other-call {
                    background: linear-gradient(180deg, #2e1008 0%, #6b2e1a 25%, #4d2010 60%, #4d2010 78%, #2e1008 100%) !important;
                    color: rgba(243, 117, 75, 0.9) !important;
                    border-color: #1a0800 !important;
                    cursor: not-allowed !important;
                    pointer-events: auto !important;
                }

                .desc-wrap .call-button[class*="other-call"]:hover,
                .desc-wrap .call-button.other-call:hover {
                    background: linear-gradient(180deg, #2e1008 0%, #6b2e1a 25%, #4d2010 60%, #4d2010 78%, #2e1008 100%) !important;
                }

                @keyframes callFlash {
                    0% { background: rgba(255, 255, 255, 0.3) !important; transform: scale(0.92); }
                    50% { transform: scale(1.05); }
                    100% { background: transparent !important; transform: scale(1); }
                }

                /* Custom tooltip for call buttons */
                .desc-wrap .call-button:hover::after {
                    content: attr(data-tooltip) !important;
                    position: absolute !important;
                    background: #333 !important;
                    color: #fff !important;
                    padding: 6px 10px !important;
                    border-radius: 4px !important;
                    font-size: 0.8em !important;
                    font-weight: 500 !important;
                    white-space: nowrap !important;
                    bottom: 130% !important;
                    left: 50% !important;
                    transform: translateX(-50%) !important;
                    z-index: 2000 !important;
                    pointer-events: none !important;
                    opacity: 1 !important;
                    animation: tooltipFade 0.2s ease-in !important;
                }

                .desc-wrap .call-button:hover::before {
                    content: '' !important;
                    position: absolute !important;
                    bottom: 120% !important;
                    left: 50% !important;
                    transform: translateX(-50%) !important;
                    border: 4px solid transparent !important;
                    border-top-color: #333 !important;
                    z-index: 2000 !important;
                    pointer-events: none !important;
                }

                @keyframes tooltipFade {
                    from {
                        opacity: 0;
                        transform: translateX(-50%) translateY(5px);
                    }
                    to {
                        opacity: 1;
                        transform: translateX(-50%) translateY(0);
                    }
                }

                /* Rally button styling */
                .desc-wrap .rally-button {
                    background: linear-gradient(180deg, #111 0%, #555 25%, #333 60%, #333 78%, #111 100%) !important;
                    color: #eee !important;
                    border: 1px solid #111 !important;
                    padding: 4px 2px !important;
                    border-radius: 3px !important;
                    height: 22px !important;
                    box-sizing: border-box !important;
                    cursor: pointer !important;
                    transition: background 0.15s ease, max-width 0.2s ease !important;
                    box-shadow: none !important;
                    display: inline-flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                    gap: 3px !important;
                    vertical-align: middle !important;
                    flex-shrink: 0 !important;
                    max-width: 18px !important;
                    margin-right: 4px !important;
                    overflow: hidden !important;
                    position: relative !important;
                }

                .desc-wrap .rally-button:hover {
                    background: linear-gradient(180deg, #333 0%, #777 25%, #333 59%, #666 78%, #333 100%) !important;
                }

                .desc-wrap .rally-button:active {
                    background: linear-gradient(180deg, #000 0%, #333 100%) !important;
                    box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.07) !important;
                }

                /* Integrated count label inside the button */
                .desc-wrap .rally-button .rally-count {
                    font-size: 9px !important;
                    font-weight: 700 !important;
                    color: rgba(255, 255, 255, 0.9) !important;
                    line-height: 1 !important;
                    white-space: nowrap !important;
                }

                /* Rally button expands when people have joined */
                .desc-wrap .rally-button.has-ralliers {
                    max-width: 34px !important;
                    padding: 4px 5px 4px 2px !important;
                }

                /* Player is in this rally */
                .desc-wrap .rally-button.rally-joined {
                    background: linear-gradient(180deg, #1e4400 0%, #5a9a00 25%, #4a7a00 60%, #4a7a00 78%, #1e4400 100%) !important;
                    border-color: #1a3000 !important;
                }

                .desc-wrap .rally-button.rally-joined:hover {
                    background: linear-gradient(180deg, #2e5500 0%, #6aaa00 25%, #4a7a00 59%, #5a9a00 78%, #2e5500 100%) !important;
                }

                /* Rally disabled (player in another rally) */
                .desc-wrap .rally-button.rally-disabled {
                    background: linear-gradient(180deg, #000 0%, #333 100%) !important;
                    color: #555 !important;
                    cursor: not-allowed !important;
                    pointer-events: auto !important;
                    border-color: #000 !important;
                }

                .desc-wrap .rally-button.rally-disabled img {
                    opacity: 0.3 !important;
                }

                /* ===== FLAT BUTTON STYLE (toggle via body.cat-btn-flat) ===== */

                .cat-btn-flat .desc-wrap .call-button {
                    background: #333 !important;
                    text-shadow: none !important;
                    box-shadow: none !important;
                }
                .cat-btn-flat .desc-wrap .call-button:hover {
                    background: #444 !important;
                    text-shadow: none !important;
                }
                .cat-btn-flat .desc-wrap .call-button:active {
                    background: #222 !important;
                    box-shadow: none !important;
                }
                .cat-btn-flat .desc-wrap .call-button.call-locked {
                    background: #1a1a1a !important;
                    box-shadow: none !important;
                }
                .cat-btn-flat .desc-wrap .call-button.call-locked:hover {
                    background: #1a1a1a !important;
                }
                .cat-btn-flat .desc-wrap .call-button[class*="my-call"],
                .cat-btn-flat .desc-wrap .call-button.my-call {
                    background: #4a7a00 !important;
                }
                .cat-btn-flat .desc-wrap .call-button[class*="my-call"]:hover,
                .cat-btn-flat .desc-wrap .call-button.my-call:hover {
                    background: #5a9a00 !important;
                }
                .cat-btn-flat .desc-wrap .call-button[class*="other-call"],
                .cat-btn-flat .desc-wrap .call-button.other-call {
                    background: #4d2010 !important;
                }
                .cat-btn-flat .desc-wrap .call-button[class*="other-call"]:hover,
                .cat-btn-flat .desc-wrap .call-button.other-call:hover {
                    background: #4d2010 !important;
                }

                .cat-btn-flat .desc-wrap .rally-button {
                    background: #333 !important;
                    box-shadow: none !important;
                }
                .cat-btn-flat .desc-wrap .rally-button:hover {
                    background: #444 !important;
                }
                .cat-btn-flat .desc-wrap .rally-button:active {
                    background: #222 !important;
                    box-shadow: none !important;
                }
                .cat-btn-flat .desc-wrap .rally-button.rally-joined {
                    background: #4a7a00 !important;
                }
                .cat-btn-flat .desc-wrap .rally-button.rally-joined:hover {
                    background: #5a9a00 !important;
                }
                .cat-btn-flat .desc-wrap .rally-button.rally-disabled {
                    background: #1a1a1a !important;
                }

                /* Hide call/rally buttons if API key not configured or update required */
                body.hide-call-buttons .call-button,
                body.hide-call-buttons .rally-button {
                    display: none !important;
                }

                /* Pistol icon (attacking indicator) */
                .cat-pistol-icon {
                    display: inline-block !important;
                    width: 28px !important;
                    height: 28px !important;
                    vertical-align: middle !important;
                    margin-left: 4px !important;
                }

                .cat-pistol-icon svg,
                .cat-pistol-icon img {
                    display: block !important;
                    width: 28px !important;
                    height: 28px !important;
                }

                /* Tactical marker icon (smoke/tear/both/kill) */
                .cat-tactical-marker {
                    display: inline-block !important;
                    height: 28px !important;
                    vertical-align: middle !important;
                    margin-left: 4px !important;
                    margin-right: 4px !important;
                }

                .cat-tactical-marker img {
                    display: block !important;
                    height: 28px !important;
                    width: 28px !important;
                    object-fit: contain !important;
                }

                /* Soft uncall badge (countdown timer in status column) */
                .cat-soft-uncall-badge {
                    display: inline-block !important;
                    padding: 1px 3px !important;
                    max-height: 20px !important;
                    line-height: 18px !important;
                    box-sizing: border-box !important;
                    margin-top: -2px !important;
                    border: 1px solid #FF794C !important;
                    border-radius: 3px !important;
                    color: #FF794C !important;
                    font-size: 12px !important;
                    font-weight: 600 !important;
                    font-family: monospace !important;
                    white-space: nowrap !important;
                    vertical-align: middle !important;
                    margin-left: 4px !important;
                    animation: catSoftUncallPulse 1.5s ease-in-out infinite !important;
                }
                @keyframes catSoftUncallPulse {
                    0%, 100% { border-color: #FF794C; color: #FF794C; }
                    50% { border-color: #FFB090; color: #FFB090; }
                }
                .cat-soft-uncall-badge.cat-hosp-uncall {
                    border-color: #82C91E !important;
                    color: #82C91E !important;
                    animation: catHospUncallPulse 1.5s ease-in-out infinite !important;
                }
                @keyframes catHospUncallPulse {
                    0%, 100% { border-color: #82C91E; color: #82C91E; }
                    50% { border-color: #A3D944; color: #A3D944; }
                }


                /* Chain bonus assignment — golden glow on call button */
                .call-button.chain-bonus-assigned {
                    border: 1.5px solid #FFD700 !important;
                    animation: catBonusGlow 1.5s ease-in-out infinite !important;
                }
                @keyframes catBonusGlow {
                    0%, 100% { box-shadow: 0 0 4px rgba(255, 215, 0, 0.4); }
                    50% { box-shadow: 0 0 10px rgba(255, 215, 0, 0.8); }
                }

                /* Chain bonus badge (HIT BONUS text in attack column) */
                .cat-bonus-badge {
                    display: inline-flex !important;
                    flex-direction: column !important;
                    align-items: center !important;
                    vertical-align: middle !important;
                    line-height: 1 !important;
                    font-weight: 950 !important;
                    text-transform: uppercase !important;
                    letter-spacing: 0.5px !important;
                    margin-left: 4px !important;
                    color: #FFD700 !important;
                    text-shadow: 0 0 6px rgba(255, 215, 0, 0.5) !important;
                    -webkit-text-fill-color: #FFD700 !important;
                }
                .cat-bonus-badge .bonus-hit {
                    font-size: 9px !important;
                    letter-spacing: 0.5px !important;
                }
                .cat-bonus-badge .bonus-label {
                    font-size: 5.5px !important;
                    letter-spacing: 0px !important;
                }
    `;
    }

    function getMemberStyles() {
        return `
                /* Faction images - ONLY in desc-wrap */
                .desc-wrap .faction-image, .desc-wrap [class*="image___"] {
                    border-radius: 12px !important;
                    box-shadow: 0 8px 24px rgba(0,0,0,0.3) !important;
                    border: none !important;
                    transition: all ${CONFIG.animations.duration} ${CONFIG.animations.easing} !important;
                }

                .desc-wrap .faction-image:hover, .desc-wrap [class*="image___"]:hover {
                    transform: scale(1.05) !important;
                    box-shadow: 0 12px 32px rgba(0,0,0,0.4) !important;
                }

                /* Honor badges enhancement - ONLY in desc-wrap */
                .desc-wrap img[src*="honor"] {
                    filter: drop-shadow(0 0 8px rgba(255, 215, 0, 0.6)) !important;
                    transition: filter ${CONFIG.animations.duration} !important;
                }

                .desc-wrap img[src*="honor"]:hover {
                    filter: drop-shadow(0 0 12px rgba(255, 215, 0, 0.8)) !important;
                }

                /* Hide faction logo - scalable approach */
                .factionWrap___GhZMa.flexCenter___bV1QP.customBlockWrap___AtrOa,
                [class*="factionWrap___"][class*="flexCenter___"][class*="customBlockWrap___"],
                .faction-logo-wrap {
                    display: none !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                }

                /* Hide honor image only - not the whole wrap - use opacity to preserve layout */
                /* Exception: .ff-scouter-arrow and .tt-ff-scouter-arrow are kept visible */
                .honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa .honor-text-wrap img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                [class*="honorWrap___"][class*="flexCenter___"][class*="customBlockWrap___"] img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .honor-wrap img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .honor-image-wrap img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow) {
                    opacity: 0 !important;
                    pointer-events: none !important;
                }

                /* Scalable hiding classes */
                .hide-faction-logo,
                .hide-honor-image,
                .logo-hidden,
                .honor-hidden {
                    display: none !important;
                }

                /* Ensure content visibility - ONLY in desc-wrap */
                .desc-wrap * {
                    opacity: 1 !important;
                    visibility: visible !important;
                }

                /* Override any hiding styles - ONLY in desc-wrap */
                .desc-wrap .f-war-list * {
                    display: revert !important;
                }

                /* Exception: keep logo and honor images hidden even in desc-wrap */
                .desc-wrap .factionWrap___GhZMa.flexCenter___bV1QP.customBlockWrap___AtrOa,
                .desc-wrap [class*="factionWrap___"][class*="flexCenter___"][class*="customBlockWrap___"],
                .desc-wrap .faction-logo-wrap,
                .desc-wrap .hide-faction-logo,
                .desc-wrap .logo-hidden {
                    display: none !important;
                    visibility: hidden !important;
                    opacity: 0 !important;
                }

                /* Honor images in desc-wrap - use opacity to preserve layout */
                /* Exception: .ff-scouter-arrow and .tt-ff-scouter-arrow are kept visible */
                .desc-wrap .honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .desc-wrap .honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa .honor-text-wrap img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .desc-wrap [class*="honorWrap___"][class*="flexCenter___"][class*="customBlockWrap___"] img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .desc-wrap .honor-wrap img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .desc-wrap .honor-image-wrap img:not(.ff-scouter-arrow):not(.tt-ff-scouter-arrow),
                .desc-wrap .hide-honor-image,
                .desc-wrap .honor-hidden {
                    opacity: 0 !important;
                    pointer-events: none !important;
                }

                /* Honor text styling - improved layout and typography */
                .desc-wrap .honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa,
                .desc-wrap [class*="honorWrap___"][class*="flexCenter___"][class*="customBlockWrap___"] {
                    justify-content: flex-start !important;
                    text-align: left !important;
                    padding: 0 !important;
                    margin: 0 !important;
                    min-width: auto !important;
                    width: auto !important;
                    flex-shrink: 1 !important;
                }

                /* Force style ALL honor text elements with stronger selectors */
                .desc-wrap .honorWrap___BHau4 *,
                .desc-wrap [class*="honorWrap___"] *,
                .desc-wrap .honorWrap___BHau4 .honor-text-wrap *,
                .desc-wrap [class*="honorWrap___"] .honor-text-wrap *,
                .desc-wrap .honorWrap___BHau4 .honor-text,
                .desc-wrap .honorWrap___BHau4 .honor-text-svg,
                .desc-wrap [class*="honorWrap___"] .honor-text,
                .desc-wrap [class*="honorWrap___"] .honor-text-svg,
                .desc-wrap .honorTextSymbol___PGzDa,
                .desc-wrap [class*="honorTextSymbol___"] {
                    font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif !important;
                    font-size: 11px !important;
                    font-weight: 600 !important;
                    color: ${CONFIG.colors.light} !important;
                    text-align: left !important;
                    line-height: 1.2 !important;
                }

                /* Hide SVG symbols version and keep only plain text */
                .desc-wrap .honorWrap___BHau4 .honor-text-svg,
                .desc-wrap [class*="honorWrap___"] .honor-text-svg {
                    display: none !important;
                }

                /* Hide online status icon, faction image, and level column */
                .desc-wrap .userStatusWrap___ljSJG,
                .desc-wrap [class*="userStatusWrap___"],
                .desc-wrap .factionWrap___GhZMa,
                .desc-wrap [class*="factionWrap___"],
                .desc-wrap .level___g3CWR:not(.__warhelper),
                .desc-wrap [class*="level___"]:not(.__warhelper),
                .desc-wrap .level___g3CWR.tab___UztMc:not(.__warhelper),
                .desc-wrap [class*="level___"].tab___UztMc:not(.__warhelper) {
                    display: none !important;
                }

                /* Re-show native online status dot when name colors are disabled */
                body.cat-no-name-colors .desc-wrap .userStatusWrap___ljSJG,
                body.cat-no-name-colors .desc-wrap [class*="userStatusWrap___"] {
                    display: inline-flex !important;
                    align-items: center !important;
                    align-self: center !important;
                    vertical-align: middle !important;
                }

                /* Honor text wrap as flex column for name + level */
                .desc-wrap .honor-text-wrap {
                    display: flex !important;
                    flex-direction: column !important;
                    align-items: flex-start !important;
                    justify-content: center !important;
                    line-height: 1 !important;
                    gap: 0 !important;
                }

                /* Level indicator under player name */
                .desc-wrap .cat-level-indicator,
                .desc-wrap a .cat-level-indicator,
                .desc-wrap .honorWrap___BHau4 .cat-level-indicator,
                .desc-wrap [class*="honorWrap___"] .cat-level-indicator,
                .desc-wrap .honor-text-wrap .cat-level-indicator,
                .desc-wrap a:hover .cat-level-indicator {
                    display: block !important;
                    font-size: 7px !important;
                    color: #fff !important;
                    margin: 0 !important;
                    padding: 0 !important;
                    line-height: 1 !important;
                    font-weight: 400 !important;
                    text-decoration: none !important;
                    border-bottom: none !important;
                    background: none !important;
                    -webkit-background-clip: unset !important;
                    background-clip: unset !important;
                }

                /* Custom font on player names (war tables only) */
                .desc-wrap .honor-text:not(.honor-text-svg) {
                    font-family: var(--cat-name-font, inherit) !important;
                }

                /* Online status gradient colors on player names */
                body:not(.cat-no-name-colors) .desc-wrap .honor-text-wrap[data-online-status="online"] .honor-text:not(.honor-text-svg),
                body:not(.cat-no-name-colors) .desc-wrap [data-online-status="online"] > .honor-text:not(.honor-text-svg) {
                    background: linear-gradient(to bottom, #A3DA00, #648600) !important;
                    -webkit-background-clip: text !important;
                    background-clip: text !important;
                    color: transparent !important;
                }
                body:not(.cat-no-name-colors) .desc-wrap .honor-text-wrap[data-online-status="idle"] .honor-text:not(.honor-text-svg),
                body:not(.cat-no-name-colors) .desc-wrap [data-online-status="idle"] > .honor-text:not(.honor-text-svg) {
                    background: linear-gradient(to bottom, #FBB904, #B65F01) !important;
                    -webkit-background-clip: text !important;
                    background-clip: text !important;
                    color: transparent !important;
                }
                body:not(.cat-no-name-colors) .desc-wrap .honor-text-wrap[data-online-status="offline"] .honor-text:not(.honor-text-svg),
                body:not(.cat-no-name-colors) .desc-wrap [data-online-status="offline"] > .honor-text:not(.honor-text-svg) {
                    background: linear-gradient(to bottom, #C6C6C6, #737373) !important;
                    -webkit-background-clip: text !important;
                    background-clip: text !important;
                    color: transparent !important;
                }

                /* Light mode - subtle shadow on player names for better readability */
                body:not(.dark-mode) .desc-wrap .honor-text-wrap .honor-text:not(.honor-text-svg) {
                    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.15) !important;
                }

                /* REMOVED PROBLEMATIC STYLES - keeping original layout */

                /* Member name column - reduce width and add truncation */
                .desc-wrap li[class*="member___"] {
                    display: flex;
                    align-items: center !important;
                    justify-content: space-between !important;
                    gap: 8px !important;
                }

                /* Member info container - limit width and add truncation */
                .desc-wrap li[class*="member___"] > *:first-child,
                .desc-wrap li[class*="member___"] .userInfoBox___LRjPl {
                    flex: 0 1 90px !important;
                    min-width: 80px !important;
                    max-width: 90px !important;
                    overflow: hidden !important;
                    white-space: nowrap !important;
                    text-overflow: ellipsis !important;
                }

                /* Honor wrap inside member - further limit width */
                .desc-wrap li[class*="member___"] .honorWrap___BHau4,
                .desc-wrap li[class*="member___"] [class*="honorWrap___"] {
                    max-width: 80px !important;
                    overflow: hidden !important;
                    text-overflow: ellipsis !important;
                }

                /* FF Scouter: allow overflow when ff-scouter-indicator or tt-ff-scouter-indicator is present */
                .desc-wrap li[class*="member___"] .honorWrap___BHau4:has(.ff-scouter-indicator),
                .desc-wrap li[class*="member___"] [class*="honorWrap___"]:has(.ff-scouter-indicator),
                .desc-wrap li[class*="member___"] .honorWrap___BHau4:has(.tt-ff-scouter-indicator),
                .desc-wrap li[class*="member___"] [class*="honorWrap___"]:has(.tt-ff-scouter-indicator) {
                    overflow: visible !important;
                }


                /* FF Scouter arrow - ensure visibility and proper positioning */
                /* Torn Tools version (.tt-ff-scouter-arrow) takes priority */
                .ff-scouter-arrow,
                .tt-ff-scouter-arrow {
                    opacity: 1 !important;
                    visibility: visible !important;
                    pointer-events: auto !important;
                    display: inline-block !important;
                }

                /* If both FF Scouter types exist, hide the standalone one */
                .honor-text-wrap:has(.tt-ff-scouter-arrow) .ff-scouter-arrow:not(.tt-ff-scouter-arrow) {
                    display: none !important;
                }

                /* Torn Tools FF Scouter column - header and values */
                .desc-wrap .tt-ff-scouter-faction-list-header,
                .desc-wrap .tt-ff-scouter-faction-list-value {
                    display: inline-block !important;
                    visibility: visible !important;
                    opacity: 1 !important;
                    font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif !important;
                    font-weight: 600 !important;
                    text-align: center !important;
                    min-width: 28px !important;
                    flex-shrink: 0 !important;
                }

                /* FF column header styling */
                .desc-wrap .white-grad .tt-ff-scouter-faction-list-header {
                    font-size: 0.75em !important;
                    text-transform: uppercase !important;
                    letter-spacing: 0.3px !important;
                    color: ${CONFIG.colors.light} !important;
                }

                body:not(.dark-mode) .desc-wrap .white-grad .tt-ff-scouter-faction-list-header {
                    color: #333 !important;
                }

                /* FF column value styling */
                .desc-wrap .tt-ff-scouter-faction-list-value {
                    font-size: 0.8em !important;
                    padding: 2px 4px !important;
                    border-radius: 3px !important;
                }

                /* Ensure other elements don't shrink */
                .desc-wrap li[class*="member___"] > *:not(:first-child) {
                    flex-shrink: 0 !important;
                }

                /* Header column - Members column width */
                .desc-wrap .member___fZiTx.tab___UztMc,
                .desc-wrap [class*="member___"].tab___UztMc {
                    width: 90px !important;
                    max-width: 90px !important;
                    min-width: 90px !important;
                    flex: 0 0 90px !important;
                }

                /* Data rows - Members column width  */
                ul.members-list.membersCont___USwcq li.enemy___uiAJH div.member.icons.left.member___fZiTx,
                .desc-wrap ul li div.member___fZiTx,
                .desc-wrap .member___fZiTx:not(.tab___UztMc),
                .desc-wrap [class*="member___"]:not(.tab___UztMc),
                div.member.icons.left.member___fZiTx,
                .member___fZiTx.icons.left,
                .member___fZiTx {
                    width: 90px !important;
                    max-width: 90px !important;
                    min-width: 90px !important;
                    flex: 0 0 90px !important;
                    overflow: hidden !important;
                    white-space: nowrap !important;
                    text-overflow: ellipsis !important;
                    box-sizing: border-box !important;
                }

                /* Force apply to any element with member class - ultimate fallback */
                [class*="member___fZiTx"] {
                    width: 90px !important;
                    max-width: 90px !important;
                    min-width: 90px !important;
                    flex: 0 0 90px !important;
                }

                /* Force parent containers to not override */
                .desc-wrap *:not(#custom-tabs-menu):not(.custom-tab-btn) {
                    flex-grow: 0 !important;
                }

                .desc-wrap ul li div {
                    flex-basis: auto !important;
                }

                /* No BSP column - wider member column (140px instead of 90px)
                   Only when War Helper BS is also hidden (cat-hide-warhelper-bs on body)
                   Only at screen width > 400px (responsive override at ≤400px in responsive-styles.ts) */
                @media (min-width: 401px) {
                /* Header column */
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .member___fZiTx.tab___UztMc,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) [class*="member___"].tab___UztMc {
                    width: 140px !important;
                    max-width: 140px !important;
                    min-width: 140px !important;
                    flex: 0 0 140px !important;
                }

                /* Member info container */
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] > *:first-child,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] .userInfoBox___LRjPl {
                    flex: 0 1 140px !important;
                    max-width: 140px !important;
                }

                /* Honor wrap */
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] .honorWrap___BHau4,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] [class*="honorWrap___"],
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] [class*="honorContainer___"] {
                    max-width: 120px !important;
                }

                /* FF Scouter: allow overflow in no-bsp mode too */
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] .honorWrap___BHau4:has(.ff-scouter-indicator),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] [class*="honorWrap___"]:has(.ff-scouter-indicator),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] .honorWrap___BHau4:has(.tt-ff-scouter-indicator),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) li[class*="member___"] [class*="honorWrap___"]:has(.tt-ff-scouter-indicator) {
                    overflow: visible !important;
                }

                /* Data rows - mirror all high-specificity selectors */
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) ul.members-list.membersCont___USwcq li.enemy___uiAJH div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) ul.members-list.membersCont___USwcq li div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .member___fZiTx.icons.left,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .desc-wrap ul li div.member___fZiTx,
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .desc-wrap .member___fZiTx:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .desc-wrap [class*="member___"]:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .member___fZiTx:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) [class*="member___"]:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) .member___fZiTx {
                    width: 140px !important;
                    max-width: 140px !important;
                    min-width: 140px !important;
                    flex: 0 0 140px !important;
                }

                /* Ultimate fallback */
                body.cat-hide-warhelper-bs .no-bsp:not(.has-ff) [class*="member___fZiTx"] {
                    width: 140px !important;
                    max-width: 140px !important;
                    min-width: 140px !important;
                    flex: 0 0 140px !important;
                }
                } /* end @media (min-width: 401px) */

                /* No BSP + screen ≤ 400px + enemy faction only */
                @media (max-width: 400px) {
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) ul.members-list.membersCont___USwcq li.enemy___uiAJH div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) ul.members-list.membersCont___USwcq li div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) .member___fZiTx.icons.left,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) .desc-wrap ul li div.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) .desc-wrap .member___fZiTx:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) .desc-wrap [class*="member___"]:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) .member___fZiTx:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) [class*="member___"]:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) .member___fZiTx {
                    width: 80px !important;
                    max-width: 80px !important;
                    min-width: 0 !important;
                    flex: 0 0 80px !important;
                }
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) [class*="member___fZiTx"] {
                    width: 80px !important;
                    max-width: 80px !important;
                    min-width: 0 !important;
                    flex: 0 0 80px !important;
                }
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) li[class*="member___"] > *:first-child,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) li[class*="member___"] .userInfoBox___LRjPl,
                body.cat-hide-warhelper-bs .enemy-faction.no-bsp:not(.has-ff) li[class*="member___"] [class*="userInfoBox___"] {
                    flex: 0 1 80px !important;
                    max-width: 80px !important;
                }
                } /* end @media (max-width: 400px) no-bsp */

                /* WITH BSP + screen ≤ 400px + enemy faction only */
                @media (max-width: 400px) {
                body.cat-hide-warhelper-bs .enemy-faction .member___fZiTx.tab___UztMc,
                body.cat-hide-warhelper-bs .enemy-faction [class*="member___"].tab___UztMc {
                    width: 50px !important;
                    max-width: 50px !important;
                    min-width: 0 !important;
                    flex: 0 0 50px !important;
                }
                body.cat-hide-warhelper-bs .enemy-faction ul.members-list.membersCont___USwcq li.enemy___uiAJH div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction ul.members-list.membersCont___USwcq li div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction div.member.icons.left.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction .member___fZiTx.icons.left,
                body.cat-hide-warhelper-bs .enemy-faction .desc-wrap ul li div.member___fZiTx,
                body.cat-hide-warhelper-bs .enemy-faction .desc-wrap .member___fZiTx:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction .desc-wrap [class*="member___"]:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction .member___fZiTx:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction [class*="member___"]:not(.tab___UztMc),
                body.cat-hide-warhelper-bs .enemy-faction .member___fZiTx {
                    width: 40px !important;
                    max-width: 40px !important;
                    min-width: 0 !important;
                    flex: 0 0 40px !important;
                }
                body.cat-hide-warhelper-bs .enemy-faction [class*="member___fZiTx"] {
                    width: 40px !important;
                    max-width: 40px !important;
                    min-width: 0 !important;
                    flex: 0 0 40px !important;
                }
                body.cat-hide-warhelper-bs .enemy-faction li[class*="member___"] > *:first-child,
                body.cat-hide-warhelper-bs .enemy-faction li[class*="member___"] .userInfoBox___LRjPl,
                body.cat-hide-warhelper-bs .enemy-faction li[class*="member___"] [class*="userInfoBox___"] {
                    flex: 0 1 40px !important;
                    max-width: 40px !important;
                }
                } /* end @media (max-width: 400px) with-bsp */

                /* ── FF Scouter Column ── */
                .ff-column {
                    min-width: 35px !important;
                    max-width: 42px !important;
                    text-align: center !important;
                    margin-right: 3px !important;
                    margin-top: 12px !important;
                    padding: 2px 4px !important;
                    font-size: 1em !important;
                    font-weight: 700 !important;
                    font-family: 'Monaco', 'Menlo', monospace !important;
                    display: flex !important;
                    align-items: center !important;
                    justify-content: center !important;
                }
                .ff-value {
                    font-size: 0.85em !important;
                }

                /* Hide TornTools stats estimate */
                .tt-stats-estimate {
                    display: none !important;
                }

                /* Enemy chain + timer layout in score bar */
                .bottomBox___ui4Jg,
                [class*="bottomBox___"] {
                    display: flex !important;
                    align-items: center !important;
                    white-space: nowrap !important;
                }

                /* View graph button always on top */
                .graphIcon___LuL62,
                [class*="graphIcon___"] {
                    z-index: 99999 !important;
                    position: relative !important;
                }

                /* Hide country label from blue timer (traveling/abroad) - now shown in level indicator */
                .status[data-abroad-dest]::after {
                    display: none !important;
                }

                /* ── Row Style: Status Colors ── */
                .cat-row-colors .members-list {
                    background-image: none !important;
                }
                .cat-row-colors .desc-wrap li[data-cat-status="online"] {
                    background: linear-gradient(90deg, rgba(50, 140, 0, 0.28), rgba(50, 140, 0, 0.06)) !important;
                }
                .cat-row-colors .desc-wrap li[data-cat-status="idle"] {
                    background: linear-gradient(90deg, rgba(180, 130, 0, 0.22), rgba(180, 130, 0, 0.05)) !important;
                }
                .cat-row-colors .desc-wrap li[data-cat-status="offline"] {
                    background: linear-gradient(90deg, rgba(100, 100, 100, 0.15), rgba(100, 100, 100, 0.03)) !important;
                }

                /* ── Row Style: Vertical Bar ── */
                .cat-row-bar .desc-wrap li[data-cat-status] {
                    border-left: 3px solid transparent !important;
                }
                .cat-row-bar .desc-wrap li[data-cat-status="online"] {
                    border-left-color: #A3DA00 !important;
                }
                .cat-row-bar .desc-wrap li[data-cat-status="idle"] {
                    border-left-color: #FBB904 !important;
                }
                .cat-row-bar .desc-wrap li[data-cat-status="offline"] {
                    border-left-color: #737373 !important;
                }


                /* ── Row Style: Contrast ── */
                .cat-row-contrast .members-list {
                    background-image: none !important;
                }
                .cat-row-contrast .desc-wrap li[data-cat-status="online"] {
                    background: rgba(60, 180, 0, 0.38) !important;
                    border-left: 3px solid #5FE000 !important;
                }
                .cat-row-contrast .desc-wrap li[data-cat-status="idle"] {
                    background: rgba(200, 140, 0, 0.32) !important;
                    border-left: 3px solid #FBB904 !important;
                }
                .cat-row-contrast .desc-wrap li[data-cat-status="offline"] {
                    background: rgba(80, 80, 80, 0.22) !important;
                    border-left: 3px solid #777 !important;
                }

    `;
    }

    function getResponsiveStyles() {
        return `
                /* Responsive design - ONLY in desc-wrap */
                @media (max-width: 768px) {
                    .cat-click-here {
                        display: none !important;
                    }

                    .desc-wrap .f-war-list {
                        padding: 16px !important;
                        margin: 2px 0 !important;
                    }

                    .desc-wrap li[class*="member___"] {
                        padding: 12px !important;
                    }

                    .desc-wrap .faction-name, .desc-wrap [class*="name___"] {
                        font-size: 1.2em !important;
                    }
                }

                /* No BSP + screen ≤ 400px: reduce member column to 60px */
                @media (max-width: 400px) {
                    /* Header only (tab___ distinguishes header from data rows) */
                    .desc-wrap .no-bsp:not(.has-ff) div.member.left[class*="member___"][class*="tab___"] {
                        width: 90px !important;
                        max-width: 90px !important;
                        flex: 0 0 90px !important;
                        min-width: 0 !important;
                        overflow: hidden !important;
                    }
                    /* Data rows */
                    .desc-wrap .no-bsp:not(.has-ff) ul li div.member.icons.left.member___fZiTx,
                    .desc-wrap .no-bsp:not(.has-ff) ul li div.member.icons.left[class*="member___"],
                    .desc-wrap .no-bsp:not(.has-ff) li div.member.icons.left[class*="member___"] {
                        width: 80px !important;
                        max-width: 80px !important;
                        flex: 0 0 80px !important;
                        min-width: 0 !important;
                        overflow: hidden !important;
                    }
                    /* Honor text wrap + any honorContainer inside member cell */
                    .desc-wrap .no-bsp:not(.has-ff) .honor-text-wrap,
                    .desc-wrap .no-bsp:not(.has-ff) [class*="honorContainer___"] {
                        max-width: 80px !important;
                        min-width: 0 !important;
                        overflow: hidden !important;
                    }
                    /* Player info box (name+level) */
                    .desc-wrap li [class*="userInfoBox___"],
                    .desc-wrap li [class*="rowSection___"] {
                        width: 80px !important;
                        max-width: 80px !important;
                        min-width: 0 !important;
                        overflow: hidden !important;
                    }
                }

                /* TornPDA specific - smaller attack column, larger status */
                @media (max-width: 500px) {
                    /* Attack column - reduce from 90px to 60px */
                    .desc-wrap .attack___wBWp2.tab___UztMc,
                    .desc-wrap [class*="attack___"].tab___UztMc,
                    .desc-wrap .attack.tab,
                    .desc-wrap .attack-header {
                        min-width: 60px !important;
                        width: 60px !important;
                        flex: 0 0 60px !important;
                    }

                    /* Attack container in member rows */
                    .desc-wrap li[class*="member___"] .attack:has(.call-button),
                    .desc-wrap li[class*="member___"] .attack,
                    .desc-wrap .call-attack-container {
                        min-width: 70px !important;
                        width: 70px !important;
                        flex: 0 0 70px !important;
                    }

                    /* Status column - increase from 60px to 75px */
                    .enemy-faction [class*="status___"]:not(.tab___UztMc),
                    .desc-wrap [class*="status___"]:not(.tab___UztMc),
                    li.enemy___uiAJH > [class*="status___"],
                    li[class*="enemy___"] > [class*="status___"],
                    li.enemy > [class*="status___"] {
                        max-width: 75px !important;
                        width: 75px !important;
                        flex: 0 0 75px !important;
                    }

                    /* Call button - slightly smaller */
                    .desc-wrap .call-button {
                        min-width: 30px !important;
                        max-width: 38px !important;
                        padding: 3px 4px !important;
                        font-size: 0.7em !important;
                    }
                }

                /* Dark mode enhancements - ONLY in desc-wrap */
                @media (prefers-color-scheme: dark) {
                    .desc-wrap .f-war-list {
                        box-shadow:
                            0 20px 40px rgba(0,0,0,0.5),
                            0 8px 16px rgba(0,0,0,0.3),
                            inset 0 1px 0 rgba(255,255,255,0.05) !important;
                    }
                }

                /* Light mode overrides - name & level text in dark grey */
                body:not(.dark-mode) .desc-wrap .level___g3CWR,
                body:not(.dark-mode) .desc-wrap [class*="level___"] {
                    color: #333 !important;
                    background: rgba(0,0,0,0.08) !important;
                }

                /* Light mode - level indicator under player name */
                body:not(.dark-mode) .desc-wrap .cat-level-indicator {
                    color: #333 !important;
                }

                body:not(.dark-mode) .desc-wrap .honorWrap___BHau4 *:not([data-online-status] .honor-text),
                body:not(.dark-mode) .desc-wrap [class*="honorWrap___"] *:not([data-online-status] .honor-text),
                body:not(.dark-mode) .desc-wrap .honorWrap___BHau4 .honor-text-wrap:not([data-online-status]) .honor-text,
                body:not(.dark-mode) .desc-wrap [class*="honorWrap___"] .honor-text-wrap:not([data-online-status]) .honor-text,
                body:not(.dark-mode) .desc-wrap .honorTextSymbol___PGzDa,
                body:not(.dark-mode) .desc-wrap [class*="honorTextSymbol___"] {
                    color: #333 !important;
                }

/* Light mode overrides - custom tabs */
                body:not(.dark-mode) #custom-tabs-menu {
                    background: linear-gradient(to bottom, #e8e8e8, #d8d8d8) !important;
                    border-bottom-color: #ccc !important;
                }
                body:not(.dark-mode) .custom-tab-btn {
                    background: linear-gradient(to bottom, #FFFFFF, #CECECE) !important;
                    color: #333 !important;
                    text-shadow: none !important;
                }
                body:not(.dark-mode) .custom-tab-btn:not(.blinking):not(.active) {
                    background: linear-gradient(to bottom, #FFFFFF, #CECECE) !important;
                }
                body:not(.dark-mode) .custom-tab-btn:hover:not(.active) {
                    background: linear-gradient(to bottom, #F0F0F0, #C0C0C0) !important;
                }
                body:not(.dark-mode) .custom-tab-btn.active {
                    background: linear-gradient(to bottom, #E8E8E8, #B8B8B8) !important;
                    color: #222 !important;
                }
                body:not(.dark-mode) .custom-tab-btn:not(:last-child) {
                    border-right-color: rgba(0, 0, 0, 0.15) !important;
                }
                body:not(.dark-mode) .custom-tab-btn.blinking {
                    animation: blinking-light 0.8s ease-in-out infinite !important;
                }
                @keyframes blinking-light {
                    0%, 100% { background: linear-gradient(to bottom, #FFFFFF, #CECECE); }
                    50% { background: linear-gradient(135deg, #FFD700, #FFA500); }
                }

                /* Light mode - tab content background */
                body:not(.dark-mode) .custom-tab-content.active {
                    background: #f5f5f5 !important;
                    color: #333 !important;
                }

                /* Light mode - muted grey text in tabs */
                body:not(.dark-mode) .custom-tab-content [style*="color: #a0aec0"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #cbd5e0"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #718096"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #e0e0e0"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #e2e8f0"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #7c8a9a"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #b0b0b0"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #999"] {
                    color: #555 !important;
                }
                /* Light mode - help tab titles (#ddd) */
                body:not(.dark-mode) .custom-tab-content [style*="color: #ddd"] {
                    color: #222 !important;
                }

                /* Light mode - blue labels in tabs */
                body:not(.dark-mode) .custom-tab-content [style*="color: #90caf9"] {
                    color: #1e5aa8 !important;
                }

                /* Light mode - green/teal leaderboard (#9ae6b4) */
                body:not(.dark-mode) .custom-tab-content [style*="color: #9ae6b4"] {
                    color: #1e7a45 !important;
                }

                /* Light mode - low contrast accent colors */
                body:not(.dark-mode) .custom-tab-content [style*="color: #ffc107"] {
                    color: #b8860b !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="color: #4ecdc4"] {
                    color: #2a9d8f !important;
                }

                /* Faction card - dark mode defaults */
                .cat-fc-box {
                    padding: 8px 10px;
                    background: rgba(0,0,0,0.2);
                    border: 1px solid rgba(255,255,255,0.06);
                    border-radius: 4px;
                    margin-bottom: 8px;
                }
                .cat-fc-label {
                    font-size: 10px;
                    color: #888;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                    margin-bottom: 3px;
                }
                .cat-fc-value {
                    font-size: 11px;
                    color: #ccc;
                }
                .cat-fc-accent {
                    /* color set inline via style attr */
                }

                /* Faction card - light mode */
                body:not(.dark-mode) .cat-fc-box {
                    background: rgba(0,0,0,0.06) !important;
                    border-color: rgba(0,0,0,0.1) !important;
                }
                body:not(.dark-mode) .cat-fc-label {
                    color: #555 !important;
                }
                body:not(.dark-mode) .cat-fc-value {
                    color: #333 !important;
                }
                body:not(.dark-mode) .cat-fc-accent[style*="#ACEA01"] {
                    color: #4a7a00 !important;
                }
                body:not(.dark-mode) .cat-fc-accent[style*="#FF794C"] {
                    color: #d9552a !important;
                }

                /* Light mode - leaderboard progress bar */
                body:not(.dark-mode) .custom-tab-content [style*="background: rgba(255,255,255,0.1)"] {
                    background: rgba(0,0,0,0.1) !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="background: #9ae6b4"] {
                    background: #1e7a45 !important;
                }

                /* Light mode - dark inner backgrounds in tabs (Plan, Help, etc.) */
                body:not(.dark-mode) .custom-tab-content [style*="background:rgba(0,0,0,0.2)"],
                body:not(.dark-mode) .custom-tab-content [style*="background: rgba(0,0,0,0.2)"] {
                    background: #f0f0f0 !important;
                    border-color: #ccc !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="background:rgba(0,0,0,0.3)"],
                body:not(.dark-mode) .custom-tab-content [style*="background: rgba(0,0,0,0.3)"] {
                    background: #e8e8e8 !important;
                    border-color: #ccc !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="background:rgba(255,255,255,0.03)"],
                body:not(.dark-mode) .custom-tab-content [style*="background: rgba(255,255,255,0.03)"] {
                    background: #f0f0f0 !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="border: 1px solid #444"],
                body:not(.dark-mode) .custom-tab-content [style*="border:1px solid #444"] {
                    border-color: #ccc !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="border:1px solid rgba(255,255,255,0.06)"],
                body:not(.dark-mode) .custom-tab-content [style*="border: 1px solid rgba(255,255,255,0.06)"] {
                    border-color: #ccc !important;
                }
                /* Light mode - text colors in tabs */
                body:not(.dark-mode) .custom-tab-content [style*="color:#ccc"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #ccc"] {
                    color: #333 !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="color:#ddd"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #ddd"] {
                    color: #222 !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="color:#888"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #888"] {
                    color: #666 !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="color:#bbb"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #bbb"] {
                    color: #555 !important;
                }
                /* Light mode - Plan tab buttons */
                body:not(.dark-mode) .custom-tab-content [style*="background:rgba(255,255,255,0.1)"],
                body:not(.dark-mode) .custom-tab-content [style*="background: rgba(255,255,255,0.1)"] {
                    background: #e0e0e0 !important;
                    color: #333 !important;
                    border-color: #bbb !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="background:rgba(255,255,255,0.06)"],
                body:not(.dark-mode) .custom-tab-content [style*="background: rgba(255,255,255,0.06)"] {
                    background: #e8e8e8 !important;
                    color: #333 !important;
                    border-color: #bbb !important;
                }
                /* Light mode - ACEA01 green accent */
                body:not(.dark-mode) .custom-tab-content [style*="color:#ACEA01"],
                body:not(.dark-mode) .custom-tab-content [style*="color: #ACEA01"] {
                    color: #4a7a00 !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="background: #2a3f5f"] {
                    background: #ddd !important;
                }
                body:not(.dark-mode) .custom-tab-content [style*="border-top: 2px solid #3a4556"] {
                    border-top-color: #ccc !important;
                }

                /* Light mode - Support tab */
                body:not(.dark-mode) .cat-contact-card {
                    background: rgba(0,0,0,0.04) !important;
                    border-color: #ccc !important;
                }
                body:not(.dark-mode) .cat-contact-card:hover {
                    background: rgba(0,0,0,0.08) !important;
                    border-color: #aaa !important;
                }
                body:not(.dark-mode) .cat-contact-card [style*="color:#ccc"] {
                    color: #333 !important;
                }
                body:not(.dark-mode) .cat-contact-card [style*="color:#888"] {
                    color: #666 !important;
                }
                body:not(.dark-mode) .cat-contact-card svg[fill="#aaa"] {
                    fill: #666 !important;
                }
                body:not(.dark-mode) #cat-debug-card {
                    background: #f0f0f0 !important;
                    border-color: #ccc !important;
                }
                body:not(.dark-mode) #cat-support-columns [style*="color:#666"] {
                    color: #888 !important;
                }
                body:not(.dark-mode) #cat-copy-debug {
                    background: #e8e8e8 !important;
                    border-color: #bbb !important;
                    color: #555 !important;
                }

                /* Light mode - What's New tab */
                body:not(.dark-mode) .cat-wn-title {
                    color: #222 !important;
                }
                body:not(.dark-mode) .cat-wn-body {
                    color: #444 !important;
                }
                body:not(.dark-mode) .cat-wn-body [style*="color: #FF6B6B"] {
                    color: #cc3333 !important;
                }
                body:not(.dark-mode) .cat-wn-body [style*="color: #4FC3F7"] {
                    color: #1976D2 !important;
                }

                /* Light mode - settings input */
                body:not(.dark-mode) #tab-setting-torn-apikey {
                    background: #fff !important;
                    color: #333 !important;
                    border-color: #ccc !important;
                }
                body:not(.dark-mode) .api-key-input::placeholder {
                    color: #999 !important;
                }

                /* Light mode - links in tab content */
                body:not(.dark-mode) .custom-tab-content a {
                    color: #2563eb !important;
                }

                /* Light mode - BSP values: dark versions for readability */
                body:not(.dark-mode) .bsp-value.bsp-red,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-red {
                    color: #CC0000 !important;
                }
                body:not(.dark-mode) .bsp-value.bsp-orange,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-orange {
                    color: #CC8800 !important;
                }
                body:not(.dark-mode) .bsp-value.bsp-blue,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-blue {
                    color: #2070CC !important;
                }
                body:not(.dark-mode) .bsp-value.bsp-green,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-green {
                    color: #2E8B20 !important;
                }
                body:not(.dark-mode) .bsp-value.bsp-white,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-white {
                    color: #6B21A8 !important;
                }
                body:not(.dark-mode) .bsp-value.bsp-gray,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-gray {
                    color: #777 !important;
                }
                body:not(.dark-mode) .bsp-value.bsp-default,
                body:not(.dark-mode) #faction_war_list_id .bsp-value.bsp-default {
                    color: #333 !important;
                }
    `;
    }

    function getAnimationStyles() {
        return `
                /* Animation keyframes */
                @keyframes pulse {
                    0%, 100% { opacity: 1; }
                    50% { opacity: 0.7; }
                }

                @keyframes slideIn {
                    from {
                        opacity: 0;
                        transform: translateY(20px);
                    }
                    to {
                        opacity: 1;
                        transform: translateY(0);
                    }
                }

                @keyframes slideInRight {
                    from {
                        opacity: 0;
                        transform: translateX(400px);
                    }
                    to {
                        opacity: 1;
                        transform: translateX(0);
                    }
                }

                @keyframes slideOutRight {
                    from {
                        opacity: 1;
                        transform: translateX(0);
                    }
                    to {
                        opacity: 0;
                        transform: translateX(400px);
                    }
                }

                /* Apply entrance animation to members - ONLY in desc-wrap */
                .desc-wrap li.member___fZiTx, .desc-wrap li[class*="member___"] {
                    animation: slideIn 0.5s ${CONFIG.animations.easing} !important;
                }

                /* Preserve original table/list structure - ONLY in desc-wrap */
                .desc-wrap ul, .desc-wrap ol, .desc-wrap li {
                    list-style: none !important;
                }
    `;
    }

    function getChainBoxStyles() {
        return `
                /* CAT Info Panel in chain-box (no war state) */
                .cat-info-panel {
                    padding: 12px 16px;
                    font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, sans-serif;
                }

                .cat-info-header {
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    margin-bottom: 10px;
                }

                .cat-info-title {
                    font-size: 14px;
                    font-weight: 700;
                    color: #e2e8f0;
                    letter-spacing: 0.5px;
                }

                .cat-info-toggle {
                    cursor: pointer;
                    font-size: 18px;
                    line-height: 1;
                    color: #718096;
                    transition: color 0.2s;
                    user-select: none;
                }

                .cat-info-toggle:hover {
                    color: #e2e8f0;
                }

                .cat-info-grid {
                    display: grid;
                    grid-template-columns: 1fr 1fr;
                    gap: 8px 16px;
                }

                .cat-info-row {
                    display: flex;
                    flex-direction: column;
                    gap: 2px;
                }

                .cat-info-label {
                    font-size: 10px;
                    color: #718096;
                    text-transform: uppercase;
                    letter-spacing: 0.5px;
                    font-weight: 600;
                }

                .cat-info-value {
                    font-size: 12px;
                    color: #e2e8f0;
                    font-weight: 500;
                    display: flex;
                    align-items: center;
                    gap: 5px;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                }

                .cat-info-status-dot {
                    width: 7px;
                    height: 7px;
                    border-radius: 50%;
                    display: inline-block;
                    flex-shrink: 0;
                }

                .cat-info-status-dot.connected {
                    background: #48bb78;
                    box-shadow: 0 0 4px rgba(72, 187, 120, 0.6);
                }

                .cat-info-status-dot.disconnected {
                    background: #fc8181;
                    box-shadow: 0 0 4px rgba(252, 129, 129, 0.6);
                }

                .cat-info-toggle-back {
                    cursor: pointer;
                    font-size: 11px;
                    font-weight: 600;
                    color: #ddd;
                    background: linear-gradient(to bottom, #646464, #343434);
                    border: 1px solid rgba(255, 255, 255, 0.15);
                    border-radius: 4px;
                    padding: 3px 8px;
                    margin-left: 8px;
                    user-select: none;
                    letter-spacing: 0.5px;
                    text-shadow: 0 1px 0 rgba(0, 0, 0, 0.75);
                    transition: all 0.2s ease;
                    display: inline-block;
                    vertical-align: middle;
                    line-height: 1.3;
                }

                .cat-info-toggle-back:hover {
                    background: linear-gradient(to bottom, #707070, #3a3a3a);
                    color: #fff;
                    border-color: rgba(255, 255, 255, 0.25);
                }

                body:not(.dark-mode) .cat-info-toggle-back {
                    background: linear-gradient(to bottom, #FFFFFF, #CECECE);
                    color: #333;
                    border-color: #bbb;
                    text-shadow: none;
                }

                body:not(.dark-mode) .cat-info-toggle-back:hover {
                    background: linear-gradient(to bottom, #F0F0F0, #C0C0C0);
                    color: #222;
                }

                .cat-original-content .chain-box-title-block {
                    display: flex;
                    align-items: center;
                }

                .cat-info-badge {
                    display: inline-block;
                    padding: 2px 8px;
                    border-radius: 3px;
                    font-size: 10px;
                    font-weight: 700;
                    letter-spacing: 0.5px;
                    text-transform: uppercase;
                    line-height: 1.4;
                    vertical-align: middle;
                }

                .cat-info-badge.enlisted {
                    background: rgba(72, 187, 120, 0.2);
                    color: #48bb78;
                    border: 1px solid rgba(72, 187, 120, 0.4);
                }

                .cat-info-badge.not-enlisted {
                    background: rgba(160, 174, 192, 0.15);
                    color: #718096;
                    border: 1px solid rgba(160, 174, 192, 0.3);
                }

                .cat-info-badge.checking {
                    background: rgba(237, 137, 54, 0.15);
                    color: #ed8936;
                    border: 1px solid rgba(237, 137, 54, 0.3);
                    animation: pulse 1.5s ease-in-out infinite;
                }

                body:not(.dark-mode) .cat-info-badge.enlisted {
                    background: rgba(56, 161, 105, 0.15);
                    color: #2f855a;
                    border-color: rgba(56, 161, 105, 0.4);
                }

                body:not(.dark-mode) .cat-info-badge.not-enlisted {
                    background: rgba(113, 128, 150, 0.1);
                    color: #4a5568;
                    border-color: rgba(113, 128, 150, 0.3);
                }

                .chain-box .chain-box-general-info {
                    padding-top: 0 !important;
                }

                .f-war-list > li[class*="warListItem___"] {
                    height: auto !important;
                    min-height: unset !important;
                    overflow: visible !important;
                }

                .f-war-list > li[class*="warListItem___"] .chain-box {
                    overflow: visible !important;
                    min-height: unset !important;
                }
    `;
    }

    function getReadOnlyStyles() {
        return `
                /* Read-only mode - subscription not activated */
                body.cat-read-only .desc-wrap .call-button,
                body.cat-read-only .desc-wrap .rally-button {
                    pointer-events: none !important;
                    opacity: 0.3 !important;
                    cursor: not-allowed !important;
                    filter: grayscale(100%) !important;
                }

                body.cat-read-only .desc-wrap .call-button:hover,
                body.cat-read-only .desc-wrap .rally-button:hover {
                    transform: none !important;
                    box-shadow: none !important;
                }

                body.cat-read-only .desc-wrap .call-button::after,
                body.cat-read-only .desc-wrap .call-button::before,
                body.cat-read-only .desc-wrap .rally-button::after,
                body.cat-read-only .desc-wrap .rally-button::before {
                    content: none !important;
                }
    `;
    }

    function getWarHelperBsStyles() {
        return `
        /* War Helper BS column — hidden by default via body class */
        body.cat-hide-warhelper-bs .__warhelper.bs,
        body.cat-hide-warhelper-bs .bsp-header.__warhelper {
            display: none !important;
            width: 0 !important;
            min-width: 0 !important;
            max-width: 0 !important;
            overflow: hidden !important;
            padding: 0 !important;
            margin: 0 !important;
        }

        /* War Helper favorite star — always hidden */
        .__warhelper_favorite {
            display: none !important;
        }

        /* War Helper BS column — match BSP column sizing (32px) */
        .desc-wrap .__warhelper.bs {
            display: inline-block !important;
            min-width: 32px !important;
            max-width: 32px !important;
            width: 32px !important;
            flex: 0 0 32px !important;
            text-align: center !important;
            padding: 2px 4px !important;
            font-size: 1em !important;
            font-weight: 700 !important;
            margin-top: 10px !important;
            flex-shrink: 0 !important;
            overflow: hidden !important;
        }

        /* War Helper BS header — match BSP header sizing */
        .white-grad > .__warhelper.bs {
            display: inline-block !important;
            min-width: 32px !important;
            width: 32px !important;
            flex: 0 0 32px !important;
            margin-right: 2px !important;
            margin-top: 2px !important;
            font-size: 11px !important;
            font-weight: 600 !important;
            text-align: center !important;
        }
    `;
    }

    function getAndroidStyles() {
        return `
                /* Android TornPDA overrides — smaller text, adjusted columns */
                body.cat-android .desc-wrap,
                body.cat-android .f-war-list,
                body.cat-android .members-list {
                    font-size: 11px !important;
                }

                /* Members column — 90px on Android. 8x body repeat beats all member-styles specificity */
                body.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android [class*="member___"] {
                    width: 90px !important;
                    max-width: 90px !important;
                    min-width: 90px !important;
                    flex: 0 0 90px !important;
                    flex-basis: 90px !important;
                }

                /* Override .desc-wrap ul li div { flex-basis: auto } from member-styles */
                body.cat-android.cat-android .desc-wrap ul li div[class*="member___"] {
                    flex-basis: 90px !important;
                    flex: 0 0 90px !important;
                    width: 90px !important;
                    max-width: 90px !important;
                    min-width: 90px !important;
                }

                /* Inner containers on Android */
                body.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android li[class*="member___"] > *:first-child,
                body.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android li[class*="member___"] [class*="userInfoBox___"] {
                    flex: 0 1 90px !important;
                    max-width: 90px !important;
                }
                body.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android li[class*="member___"] [class*="honorWrap___"],
                body.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android.cat-android li[class*="member___"] [class*="honorContainer___"] {
                    max-width: 70px !important;
                }

                /* Level column — hide on Android to save space */
                body.cat-android .level___g3CWR,
                body.cat-android .level.left,
                body.cat-android [class*="level___"] {
                    display: none !important;
                }

                /* BSP/FF columns — narrower */
                body.cat-android .bsp-column {
                    min-width: 32px !important;
                    max-width: 38px !important;
                }
                body.cat-android .ff-column {
                    min-width: 28px !important;
                    max-width: 34px !important;
                    width: 30px !important;
                    flex: 0 0 30px !important;
                    margin-top: 12px !important;
                    font-size: 10px !important;
                }

                /* Score column — narrower */
                body.cat-android .points___TQbnu,
                body.cat-android .points.left,
                body.cat-android [class*="points___"] {
                    font-size: 10px !important;
                    min-width: 30px !important;
                    max-width: 40px !important;
                    width: 35px !important;
                }

                /* Status column data rows — narrower on Android */
                body.cat-android .status___i8NBb,
                body.cat-android .status.left,
                body.cat-android [class*="status___"]:not([class*="tab___"]) {
                    font-size: 10px !important;
                    min-width: 40px !important;
                    max-width: 50px !important;
                    width: 45px !important;
                }
                /* Status header — smaller on Android */
                body.cat-android [class*="status___"][class*="tab___"] {
                    font-size: 9px !important;
                    min-width: 30px !important;
                    max-width: 40px !important;
                    width: 35px !important;
                }

                /* Attack column — hide "Attack" text, show "Atk" via CSS (gray + blue) */
                body.cat-android [class*="attack___"] .t-gray-9,
                body.cat-android [class*="attack___"] [class*="t-blue"] {
                    font-size: 0 !important;
                    line-height: 0 !important;
                    display: inline-flex !important;
                    align-items: center !important;
                    height: 100% !important;
                }
                body.cat-android [class*="attack___"] .t-gray-9::after {
                    content: 'Atk' !important;
                    font-size: 11px !important;
                    line-height: normal !important;
                }
                body.cat-android [class*="attack___"] [class*="t-blue"]::after {
                    content: 'Atk' !important;
                    font-size: 11px !important;
                    line-height: normal !important;
                }

                /* Honor text (player names) — smaller */
                body.cat-android .honor-text {
                    font-size: 9px !important;
                }

                /* Cat level indicator — keep visible even though level column is hidden */
                body.cat-android .cat-level-indicator {
                    font-size: 6px !important;
                }

                /* Last Action column — hide on PDA (not enough screen space) */
                body.cat-android .cat-la-header,
                body.cat-android .cat-la-col {
                    display: none !important;
                }

                /* Tabs (Faction / Plan / Help / Settings) — smaller on Android */
                body.cat-android .custom-tab-btn {
                    font-size: 9px !important;
                    padding: 4px 6px !important;
                }
    `;
    }

    function getLoadoutTooltipStyles() {
        return `
/* ── Loadout button ──────────────────────────────────────────── */
.cat-loadout-btn {
    cursor: pointer !important;
    opacity: 0.45;
    user-select: none !important;
    line-height: 1 !important;
    display: flex;
    align-items: center !important;
    color: #888 !important;
    transition: opacity 0.15s, color 0.15s;
    flex-shrink: 0 !important;
    margin-left: 2px !important;
}
.cat-loadout-btn:hover {
    opacity: 1 !important;
    color: #ACEA01 !important;
}

/* ── Loadout tooltip ─────────────────────────────────────────── */
@keyframes catLoFadeIn {
    from { opacity: 0; transform: translateY(3px); }
    to   { opacity: 1; transform: translateY(0); }
}
.cat-loadout-tooltip {
    position: fixed;
    z-index: 10001;
    background: #1a1a1a;
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 4px;
    padding: 8px 10px;
    min-width: 240px;
    max-width: 420px;
    font-family: 'Helvetica Neue', Arial, sans-serif;
    font-size: 11px;
    line-height: 1.4;
    color: #ccc;
    box-shadow: 0 4px 16px rgba(0,0,0,0.7);
    pointer-events: auto;
    animation: catLoFadeIn 0.12s ease-out;
}
.cat-loadout-loading {
    color: #666;
    font-style: italic;
    min-width: 0;
    padding: 5px 8px;
}

/* ── Section header ──────────────────────────────────────────── */
.cat-lo-section {
    font-size: 9px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: #ACEA01;
    margin: 6px 0 2px;
}
.cat-lo-section:first-child {
    margin-top: 0;
}

/* ── Item row ────────────────────────────────────────────────── */
.cat-lo-row {
    display: flex;
    align-items: baseline;
    gap: 5px;
    padding: 2px 0;
    white-space: nowrap;
}
.cat-lo-slot {
    flex-shrink: 0;
    width: 50px;
    font-size: 9px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #888;
}
.cat-lo-name {
    font-weight: 600;
    font-size: 11px;
    flex: 1;
}
.cat-lo-stat {
    color: #888;
    font-size: 10px;
    font-family: monospace;
    white-space: nowrap;
}
.cat-lo-bonus {
    color: #ACEA01;
    font-size: 10px;
    font-style: italic;
    opacity: 0.85;
}

/* ── Footer ──────────────────────────────────────────────────── */
.cat-lo-footer {
    margin-top: 6px;
    padding-top: 5px;
    border-top: 1px solid rgba(255,255,255,0.06);
    color: #888;
    font-size: 10px;
    display: flex;
    gap: 3px;
    flex-wrap: wrap;
    align-items: center;
}
.cat-lo-date {
    opacity: 0.6;
    font-size: 9px;
}

/* ── Bonus hover items ───────────────────────────────────────── */
.cat-lo-bonus-item {
    color: #ACEA01;
    white-space: nowrap;
}
.cat-lo-bonus-pct {
    color: rgba(172,234,1,0.65);
    font-size: 9px;
    margin-left: 2px;
}
@keyframes catLoBonusFadeIn {
    from { opacity: 0; transform: translateY(2px); }
    to   { opacity: 1; transform: translateY(0); }
}
.cat-lo-bonus-desc {
    position: fixed;
    z-index: 10002;
    background: #1a1a1a;
    border: 1px solid rgba(255,255,255,0.08);
    border-radius: 4px;
    padding: 5px 8px;
    font-size: 10px;
    color: #ccc;
    max-width: 220px;
    pointer-events: none;
    animation: catLoBonusFadeIn 0.1s ease-out;
}

/* ── Score cell icon hover ───────────────────────────────────── */
.cat-lo-score-btn:hover img {
    filter: invert(75%) sepia(50%) saturate(500%) hue-rotate(185deg) brightness(105%);
}

/* ── Score column loadout toggle ─────────────────────────────── */
.cat-lo-score-label {
    transition: color 0.15s;
}
.cat-lo-score-sep {
    margin: 0 2px;
    font-size: 0.85em;
    transition: color 0.15s;
}
.cat-lo-score-toggle {
    display: inline-flex;
    align-items: center;
    margin-left: 0;
    opacity: 1;
    vertical-align: middle;
    transition: filter 0.15s;
    pointer-events: none;
    filter: none;
}
/* Active: icon #74C0FC */
.cat-lo-score-toggle.cat-lo-score-toggle-active {
    filter: invert(75%) sepia(50%) saturate(500%) hue-rotate(185deg) brightness(105%) !important;
}
/* Shirt icon inside score cell (loadout view) */
.cat-lo-score-btn {
    position: static !important;
    margin: 0 !important;
    transform: none !important;
}
`;
    }

    // Google Fonts that need to be loaded dynamically
    const GOOGLE_FONTS = {
        'Inter': 'Inter:wght@400;700',
        'Oswald': 'Oswald:wght@400;700',
        'Source Code Pro': 'Source+Code+Pro:wght@400;700',
        'Comic Neue': 'Comic+Neue:wght@400;700',
    };
    const _loadedGoogleFonts = new Set();
    function loadGoogleFont(fontFamily) {
        // Extract ALL font names from the CSS font-family string and load any Google Fonts found
        const parts = fontFamily.split(',');
        for (const part of parts) {
            const trimmed = part.trim().replace(/^['"]|['"]$/g, '');
            if (!trimmed || _loadedGoogleFonts.has(trimmed))
                continue;
            const googleParam = GOOGLE_FONTS[trimmed];
            if (!googleParam)
                continue; // System font or generic, skip
            _loadedGoogleFonts.add(trimmed);
            const link = document.createElement('link');
            link.rel = 'stylesheet';
            link.href = `https://fonts.googleapis.com/css2?family=${googleParam}&display=swap`;
            link.id = `cat-gfont-${trimmed.replace(/\s/g, '-').toLowerCase()}`;
            (document.head || document.documentElement).appendChild(link);
        }
    }
    class CSSManager {
        constructor() {
            this.styleElement = null;
            this.init();
        }
        init() {
            this.createStyleElement();
            this.injectCSS();
            const rowStyle = String(StorageUtil.get('cat_row_style', 'basic') || 'basic');
            if (rowStyle !== 'basic') {
                const applyRowStyle = () => document.body?.classList.add(`cat-row-${rowStyle}`);
                if (document.body) {
                    applyRowStyle();
                }
                else {
                    document.addEventListener('DOMContentLoaded', applyRowStyle);
                }
            }
            const btnStyle = String(StorageUtil.get('cat_btn_style', 'gradient') || 'gradient');
            if (btnStyle === 'flat') {
                const applyBtnStyle = () => document.body?.classList.add('cat-btn-flat');
                if (document.body) {
                    applyBtnStyle();
                }
                else {
                    document.addEventListener('DOMContentLoaded', applyBtnStyle);
                }
            }
            if (String(StorageUtil.get('cat_name_colors', 'true')) === 'false') {
                const applyNoNameColors = () => document.body?.classList.add('cat-no-name-colors');
                if (document.body) {
                    applyNoNameColors();
                }
                else {
                    document.addEventListener('DOMContentLoaded', applyNoNameColors);
                }
            }
            // War Helper BS column: use cached access + user preference, hide if unknown
            const cachedBsAccess = String(StorageUtil.get('cat_bs_access', 'false')) === 'true';
            const userWantsBs = String(StorageUtil.get('cat_show_warhelper_bs', 'true')) === 'true';
            const shouldHideBs = !(cachedBsAccess && userWantsBs);
            const applyBsVisibility = () => {
                if (shouldHideBs) {
                    document.body?.classList.add('cat-hide-warhelper-bs');
                }
                else {
                    document.body?.classList.remove('cat-hide-warhelper-bs');
                }
            };
            if (document.body) {
                applyBsVisibility();
            }
            else {
                document.addEventListener('DOMContentLoaded', applyBsVisibility);
            }
            // Detect Android TornPDA and add body class for CSS overrides
            const isPDA = typeof window.flutter_inappwebview !== 'undefined';
            if (isPDA && /Android/i.test(navigator.userAgent)) {
                const applyAndroid = () => document.body?.classList.add('cat-android');
                if (document.body) {
                    applyAndroid();
                }
                else {
                    document.addEventListener('DOMContentLoaded', applyAndroid);
                }
                // Force member column width via inline styles (beats all CSS specificity)
                const forceMemberWidth = () => {
                    const members = document.querySelectorAll('.desc-wrap [class*="member___"]:not([class*="tab___"])');
                    for (const el of members) {
                        el.style.setProperty('width', '90px', 'important');
                        el.style.setProperty('max-width', '90px', 'important');
                        el.style.setProperty('min-width', '90px', 'important');
                        el.style.setProperty('flex', '0 0 90px', 'important');
                        el.style.setProperty('flex-basis', '90px', 'important');
                    }
                };
                // Run on DOM changes in desc-wrap
                const obs = new MutationObserver(forceMemberWidth);
                const startObs = () => {
                    const wrap = document.querySelector('.desc-wrap');
                    if (wrap) {
                        obs.observe(wrap, { childList: true, subtree: true });
                        forceMemberWidth();
                    }
                    else {
                        setTimeout(startObs, 1000);
                    }
                };
                if (document.body)
                    startObs();
                else
                    document.addEventListener('DOMContentLoaded', startObs);
            }
            const nameFont = StorageUtil.get('cat_name_font', '');
            if (nameFont && document.documentElement) {
                document.documentElement.style.setProperty('--cat-name-font', nameFont);
                loadGoogleFont(nameFont);
            }
            const bspFont = StorageUtil.get('cat_bsp_font', '');
            if (bspFont && document.documentElement) {
                document.documentElement.style.setProperty('--cat-bsp-font', bspFont);
                loadGoogleFont(bspFont);
            }
        }
        createStyleElement() {
            this.styleElement = document.createElement('style');
            this.styleElement.id = 'faction-war-enhancer-styles';
            (document.head || document.documentElement).appendChild(this.styleElement);
        }
        injectCSS() {
            const css = this.generateCSS();
            if (this.styleElement)
                this.styleElement.textContent = css;
        }
        generateCSS() {
            return [
                getModalStyles(),
                getFactionLayoutStyles(),
                getBspStyles(),
                getCallButtonStyles(),
                getMemberStyles(),
                getResponsiveStyles(),
                getAnimationStyles(),
                getChainBoxStyles(),
                getReadOnlyStyles(),
                getWarHelperBsStyles(),
                getAndroidStyles(),
                getLoadoutTooltipStyles(),
            ].join('\n');
        }
        updateColors(newColors) {
            Object.assign(CONFIG.colors, newColors);
            this.injectCSS();
        }
        addLogoHidingClass(element) {
            if (element && element.classList) {
                element.classList.add('hide-faction-logo');
            }
        }
        hideLogoBySelector(selector) {
            const elements = document.querySelectorAll(selector);
            elements.forEach(element => this.addLogoHidingClass(element));
        }
        toggleLogoVisibility(show = false) {
            const logoSelectors = [
                '.factionWrap___GhZMa.flexCenter___bV1QP.customBlockWrap___AtrOa',
                '[class*="factionWrap___"][class*="flexCenter___"][class*="customBlockWrap___"]',
                '.faction-logo-wrap'
            ];
            logoSelectors.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                elements.forEach((element) => {
                    if (show) {
                        element.classList.remove('hide-faction-logo');
                        element.style.display = '';
                    }
                    else {
                        element.classList.add('hide-faction-logo');
                    }
                });
            });
        }
        toggleHonorVisibility(show = false) {
            const honorImageSelectors = [
                '.honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa img',
                '.honorWrap___BHau4.flexCenter___bV1QP.honorWrapSmall___oFibH.customBlockWrap___AtrOa .honor-text-wrap img',
                '[class*="honorWrap___"][class*="flexCenter___"][class*="customBlockWrap___"] img',
                '.honor-wrap img',
                '.honor-image-wrap img'
            ];
            honorImageSelectors.forEach(selector => {
                const elements = document.querySelectorAll(selector);
                elements.forEach((element) => {
                    if (element.classList.contains('ff-scouter-arrow') || element.classList.contains('tt-ff-scouter-arrow'))
                        return;
                    if (show) {
                        element.style.opacity = '1';
                        element.style.pointerEvents = 'auto';
                    }
                    else {
                        element.style.opacity = '0';
                        element.style.pointerEvents = 'none';
                    }
                });
            });
        }
    }

    class ThrottleManager {
        constructor() {
            this.throttleTimeout = null;
        }
        throttle(func, delay) {
            return (...args) => {
                if (this.throttleTimeout)
                    return;
                this.throttleTimeout = setTimeout(() => {
                    func.apply(this, args);
                    this.throttleTimeout = null;
                }, delay);
            };
        }
        debounce(func, delay) {
            let timeout = null;
            return (...args) => {
                if (timeout)
                    clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), delay);
            };
        }
    }

    async function detectBestUrl() {
        const cached = StorageUtil.get('cat_server_url', null);
        if (cached && this.serverUrls.includes(cached)) {
            this.serverUrl = cached;
            return;
        }
        const hasFetchMethod = () => typeof PDA_httpGet === 'function' ||
            typeof customFetch === 'function' ||
            (typeof window !== 'undefined' && typeof window.customFetch === 'function') ||
            typeof GM_xmlhttpRequest !== 'undefined';
        if (!hasFetchMethod()) {
            await new Promise(resolve => {
                let checks = 0;
                const interval = setInterval(() => {
                    checks++;
                    if (hasFetchMethod() || checks >= 15) {
                        clearInterval(interval);
                        resolve();
                    }
                }, 200);
            });
        }
        for (const url of this.serverUrls) {
            try {
                const response = await this.httpRequest(`${url}/health`, {
                    method: 'GET'
                });
                if (response.ok) {
                    this.serverUrl = url;
                    StorageUtil.set('cat_server_url', url);
                    console.log(`%c[CAT] Using server: ${url}`, 'color:#ACEA01;');
                    return;
                }
            }
            catch (e) {
                console.log(`[CAT] ${url} unreachable, trying next...`);
            }
        }
        this.serverUrl = this.serverUrls[0];
        console.log('[CAT] All URLs failed, defaulting to', this.serverUrl);
        if (this._detectRetryCount < 3) {
            this._detectRetryCount++;
            const delay = 3000 * Math.pow(2, this._detectRetryCount - 1);
            setTimeout(() => {
                const nowCached = StorageUtil.get('cat_server_url', null);
                if (!nowCached) {
                    console.log(`[CAT] Retrying detectBestUrl (attempt ${this._detectRetryCount})...`);
                    this.detectBestUrl();
                }
            }, delay);
        }
    }

    function loadUserInfoFromStorage() {
        try {
            const storedInfo = StorageUtil.get('cat_user_info', null);
            if (storedInfo) {
                if (typeof storedInfo === 'object' && storedInfo !== null) {
                    this.playerName = storedInfo.name || 'Unknown';
                    this.playerId = storedInfo.id ? String(storedInfo.id) : null;
                }
                else {
                    const userInfo = JSON.parse(storedInfo);
                    this.playerName = userInfo.name || 'Unknown';
                    this.playerId = userInfo.id ? String(userInfo.id) : null;
                }
            }
        }
        catch (e) {
            console.log('Error parsing stored user info');
            this.reportError('parseStoredUserInfo', e);
        }
        if (this.playerName === 'Unknown' || !this.playerId) {
            this.playerName = this.getPlayerName();
            if (!this.playerId) {
                this.playerId = this.extractPlayerIdFromPage();
            }
        }
        setTimeout(() => {
            if (this.torn_apikey && this.playerId) {
                this.fetchUserInfoFromTornAPI().catch(e => { console.log('Could not fetch user info from API:', e); this.reportError('fetchUserInfoAPI', e); });
            }
        }, 500);
    }
    function setServerUrl(url) {
        this.serverUrl = url;
    }
    function setAuthToken(token) {
        this.authToken = token;
    }
    function setFactionId(id) {
        this.factionId = id;
    }
    function getPlayerName() {
        const titleMatch = document.title.match(/(.+?)\s*-\s*Torn/);
        if (titleMatch && titleMatch[1]) {
            const name = titleMatch[1].trim();
            if (name && name.length > 0 && name !== 'Torn') {
                return name;
            }
        }
        const playerElement = document.querySelector('[class*="player-name"], [class*="username"], .user-name');
        if (playerElement) {
            return (playerElement.textContent || '').trim();
        }
        const userMeta = document.querySelector('meta[name="user"]') ||
            document.querySelector('meta[property="user"]');
        if (userMeta) {
            const content = userMeta.getAttribute('content');
            if (content)
                return content.trim();
        }
        return 'Unknown';
    }
    function getTornApiKeyFromStorage() {
        try {
            const apiKey = StorageUtil.get('cat_api_key_script', null);
            if (apiKey) {
                return apiKey;
            }
        }
        catch (e) {
            console.log('Error accessing localStorage');
        }
        console.log('\u26A0\uFE0F  API Key non trouv\u00E9e. Veuillez la configurer.');
        return null;
    }
    function saveTornApiKey(apiKey) {
        try {
            StorageUtil.set('cat_api_key_script', apiKey);
            this.torn_apikey = apiKey;
            return true;
        }
        catch (e) {
            console.log('Erreur lors de la sauvegarde:', e);
        }
        return false;
    }

    async function autoRegisterIfNeeded() {
        const storedToken = StorageUtil.get('cat_auth_token', null);
        if (!storedToken) {
            if (!this.torn_apikey)
                return; // No API key to register with
            console.log('[CAT] Auto-registering to get per-user auth token...');
            await this.registerAndGetToken(this.torn_apikey);
        }
        // Always ensure faction ID is set
        await this.ensureUserFactionId();
    }
    async function ensureUserFactionId() {
        const existingFactionId = StorageUtil.get('cat_user_faction_id', null);
        if (existingFactionId) {
            console.log(`[CAT] User faction ID already set: ${existingFactionId}`);
            return;
        }
        if (!this.torn_apikey) {
            console.log('[CAT] ensureUserFactionId: No API key available');
            return;
        }
        console.log('[CAT] Fetching user faction ID from Torn API...');
        try {
            const userUrl = `https://api.torn.com/v2/user?key=${this.torn_apikey}`;
            const response = await this.httpRequest(userUrl, { method: 'GET' });
            if (response.ok) {
                const data = await response.json();
                const factionId = data.profile?.faction_id;
                if (factionId) {
                    StorageUtil.set('cat_user_faction_id', factionId);
                    console.log(`%c[CAT] User faction ID set: ${factionId}`, 'color:#ACEA01;');
                }
                else {
                    console.log('[CAT] User has no faction ID in profile');
                }
            }
            else {
                console.log('[CAT] Failed to fetch user profile:', response.status);
            }
        }
        catch (e) {
            console.log('[CAT] Could not fetch user faction ID:', e);
        }
    }
    async function registerAndGetToken(tornApiKey) {
        try {
            const response = await this.httpRequest(`${this.serverUrl}/api/register`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ tornApiKey })
            });
            if (response.ok) {
                const data = await response.json();
                if (data.success && data.token) {
                    StorageUtil.set('cat_auth_token', data.token);
                    this.authToken = data.token;
                    // Store player info from registration response so it persists across page navigation
                    if (data.playerName && data.playerId) {
                        this.playerName = data.playerName;
                        this.playerId = String(data.playerId);
                        const existing = StorageUtil.get('cat_user_info', null) || {};
                        StorageUtil.set('cat_user_info', { ...existing, name: data.playerName, id: data.playerId, ...(data.factionId ? { faction_id: data.factionId } : {}) });
                    }
                    if (data.factionId) {
                        StorageUtil.set('cat_user_faction_id', data.factionId);
                        console.log(`%c[CAT] Registered with per-user token for ${data.playerName} [${data.playerId}] faction ${data.factionId}`, 'color:#ACEA01;');
                    }
                    else {
                        console.log(`%c[CAT] Registered with per-user token for ${data.playerName} [${data.playerId}]`, 'color:#ACEA01;');
                    }
                    return data.token;
                }
            }
            else {
                console.log('[CAT] Registration failed:', response.status);
            }
        }
        catch (e) {
            console.log('[CAT] Registration error:', e);
            this.reportError('registerToken', e);
        }
        return null;
    }
    async function apiRequest(url, options = {}) {
        const response = await this.httpRequest(url, options);
        // If 401 and we have an API key and not already retrying, re-register and retry
        if (response.status === 401 && this.torn_apikey && !this._retrying401) {
            // Check cooldown: don't retry if we already refreshed in the last 10 seconds
            const now = Date.now();
            const timeSinceLastRefresh = now - this._last401RefreshTime;
            const COOLDOWN_MS = 10000; // 10 seconds
            if (timeSinceLastRefresh < COOLDOWN_MS) {
                const remainingSeconds = Math.ceil((COOLDOWN_MS - timeSinceLastRefresh) / 1000);
                console.log(`[CAT] 401 cooldown active, skipping refresh (retry in ${remainingSeconds}s)`);
                return response; // Return 401 without retrying
            }
            console.log('[CAT] Got 401, auto-refreshing token...');
            this._retrying401 = true;
            this._last401RefreshTime = now;
            try {
                const newToken = await this.registerAndGetToken(this.torn_apikey);
                if (newToken) {
                    // Update Authorization header with new token
                    const headers = (options.headers || {});
                    headers['Authorization'] = `Bearer ${newToken}`;
                    options.headers = headers;
                    console.log('[CAT] Token refreshed, retrying request...');
                    const retryResponse = await this.httpRequest(url, options);
                    this._retrying401 = false;
                    return retryResponse;
                }
                else {
                    console.log('[CAT] Failed to refresh token, returning 401');
                }
            }
            catch (e) {
                console.log('[CAT] Error during token refresh:', e);
                this.reportError('apiRequest401Retry', e);
            }
            finally {
                this._retrying401 = false;
            }
        }
        return response;
    }
    function promptForApiKey() {
        const apiKey = prompt('Entrez votre API Key Torn:\n\n' +
            '(Vous la trouverez sur https://www.torn.com/preferences.php#tab=api)\n\n' +
            'Votre cl\u00E9 sera stock\u00E9e localement dans le navigateur.', '');
        if (apiKey && apiKey.trim().length > 0) {
            if (this.saveTornApiKey(apiKey.trim())) {
                alert('\u2705 API Key sauvegard\u00E9e avec succ\u00E8s!');
                this.loadUserInfoFromStorage();
                this.detectFactionAutomatically();
                return true;
            }
        }
        return false;
    }
    function showApiKeyModal() {
        return new Promise((resolve) => {
            const existingModal = document.getElementById('torn-api-key-modal');
            if (existingModal) {
                existingModal.remove();
            }
            const modal = document.createElement('div');
            modal.id = 'torn-api-key-modal';
            modal.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.7);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 10001;
        `;
            const content = document.createElement('div');
            content.style.cssText = `
            background: white;
            padding: 30px;
            border-radius: 8px;
            max-width: 450px;
            width: 90%;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3);
        `;
            content.innerHTML = `
            <h2 style="margin-top: 0; color: #333;">\u2699\uFE0F Settings</h2>

            <div style="margin-bottom: 15px; padding: 12px; background: #f0f8ff; border-left: 4px solid #667eea; border-radius: 4px;">
                <p style="margin: 0 0 8px 0; font-size: 13px; color: #555;">Enter your Torn.com API key:</p>
                <p style="margin: 0; font-size: 12px; color: #888;">You'll find it on your Torn profile settings</p>
            </div>

            <div style="margin-bottom: 20px;">
                <label style="display: block; margin-bottom: 8px; font-weight: bold; color: #333;">\uD83D\uDD11 Torn API Key:</label>
                <input type="password" id="torn-api-key-input" placeholder="Paste your API key here"
                    style="width: 100%; padding: 10px; border: 2px solid #e0e0e0; border-radius: 4px; box-sizing: border-box; font-size: 14px;">
                <p style="margin: 8px 0 0 0; font-size: 12px; color: #888;">Key stored locally in your browser only &mdash; <a href="https://cat-script.com/terms" target="_blank" style="color:#667eea">Terms of Service</a></p>
                <div id="torn-api-error" style="margin-top: 8px; font-size: 12px; display: none;"></div>
            </div>

            <div style="display: flex; gap: 10px; justify-content: flex-end;">
                <button id="torn-api-cancel" style="padding: 10px 20px; background: #f0f0f0; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-weight: 500;">Skip</button>
                <button id="torn-api-confirm" style="padding: 10px 20px; background: #4ecdc4; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold;">Save</button>
            </div>
        `;
            modal.appendChild(content);
            document.body.appendChild(modal);
            const input = document.getElementById('torn-api-key-input');
            if (input) {
                setTimeout(() => input.focus(), 100);
            }
            const confirmBtn = document.getElementById('torn-api-confirm');
            const cancelBtn = document.getElementById('torn-api-cancel');
            const errorDiv = document.getElementById('torn-api-error');
            if (!confirmBtn || !cancelBtn || !errorDiv || !input) {
                modal.remove();
                resolve(false);
                return;
            }
            confirmBtn.addEventListener('click', async () => {
                const apiKey = input.value.trim();
                if (!apiKey) {
                    errorDiv.style.color = '#d32f2f';
                    errorDiv.textContent = '\u274C Please enter an API key!';
                    errorDiv.style.display = 'block';
                    return;
                }
                confirmBtn.disabled = true;
                confirmBtn.textContent = 'Validating...';
                errorDiv.style.color = '#1976d2';
                errorDiv.textContent = '\u23F3 Checking API key...';
                errorDiv.style.display = 'block';
                try {
                    const response = await this.httpRequest(`https://api.torn.com/v2/user/self/basic?key=${apiKey}`, { method: 'GET' });
                    if (response.ok) {
                        const data = await response.json();
                        if (data.profile && data.profile.name) {
                            if (this.saveTornApiKey(apiKey)) {
                                errorDiv.style.color = '#2e7d32';
                                errorDiv.textContent = `\u2705 Valid API key! Welcome ${data.profile.name}`;
                                errorDiv.style.display = 'block';
                                this.playerName = data.profile.name;
                                this.playerId = data.profile.id ? String(data.profile.id) : null;
                                // Register for per-user auth token
                                this.registerAndGetToken(apiKey).catch(e => { console.log('[CAT] Register after modal failed:', e); this.reportError('registerAfterModal', e); });
                                const enhancer = window.FactionWarEnhancer;
                                if (enhancer) {
                                    enhancer.enableAllCallButtons();
                                    if (enhancer.pollingManager) {
                                        if (!enhancer.pollingManager._isActive) {
                                            enhancer.pollingManager.start();
                                        }
                                        else {
                                            enhancer.pollingManager.requestCalls();
                                        }
                                    }
                                }
                                setTimeout(() => {
                                    document.removeEventListener('keydown', escHandler);
                                    modal.remove();
                                    resolve(true);
                                }, 1500);
                            }
                            else {
                                errorDiv.style.color = '#d32f2f';
                                errorDiv.textContent = '\u274C Error saving API key';
                                errorDiv.style.display = 'block';
                            }
                        }
                        else {
                            errorDiv.style.color = '#d32f2f';
                            errorDiv.textContent = '\u274C Invalid API response';
                            errorDiv.style.display = 'block';
                        }
                    }
                    else {
                        errorDiv.style.color = '#d32f2f';
                        errorDiv.textContent = '\u274C Invalid API key or API is unreachable';
                        errorDiv.style.display = 'block';
                    }
                }
                catch (error) {
                    console.log('API validation error:', error);
                    this.reportError('apiValidationModal', error);
                    errorDiv.style.color = '#d32f2f';
                    errorDiv.textContent = '\u274C Error validating API key';
                    errorDiv.style.display = 'block';
                }
                finally {
                    confirmBtn.disabled = false;
                    confirmBtn.textContent = 'Save';
                }
            });
            const escHandler = (e) => {
                if (e.key === 'Escape' && document.getElementById('torn-api-key-modal')) {
                    document.removeEventListener('keydown', escHandler);
                    const modalEl = document.getElementById('torn-api-key-modal');
                    if (modalEl)
                        modalEl.remove();
                    resolve(false);
                }
            };
            document.addEventListener('keydown', escHandler);
            cancelBtn.addEventListener('click', () => {
                document.removeEventListener('keydown', escHandler);
                modal.remove();
                resolve(false);
            });
            input.addEventListener('keypress', (e) => {
                if (e.key === 'Enter') {
                    confirmBtn.click();
                }
            });
        });
    }

    async function fetchUserInfoFromTornAPI() {
        if (!this.torn_apikey) {
            return null;
        }
        try {
            const apiUrl = `https://api.torn.com/v2/user/self/basic?key=${this.torn_apikey}`;
            const response = await this.httpRequest(apiUrl, { method: 'GET' });
            if (response.ok) {
                const data = await response.json();
                if (data.profile && data.profile.name) {
                    const userInfo = {
                        id: data.profile.id,
                        name: data.profile.name,
                    };
                    try {
                        StorageUtil.set('cat_user_info', userInfo);
                    }
                    catch (e) {
                        this.reportError('saveUserInfo', e);
                    }
                    this.playerName = userInfo.name;
                    this.playerId = userInfo.id ? String(userInfo.id) : null;
                    // Update faction ID if changed
                    const factionId = data.profile.faction_id;
                    if (factionId) {
                        const currentFaction = StorageUtil.get('cat_user_faction_id', null);
                        console.log(`[CAT] Your faction: ${data.profile.name} is in faction (${factionId})`);
                        if (String(factionId) !== String(currentFaction)) {
                            console.log(`[CAT] Faction changed: ${currentFaction} → ${factionId}, clearing enemy cache`);
                            StorageUtil.set('cat_user_faction_id', String(factionId));
                            // Clear enemy faction cache and re-detect
                            StorageUtil.set('cat_enemy_faction_id', null);
                            this.detectFactionAutomatically().catch(e => this.reportError('detectFactionAfterChange', e));
                        }
                    }
                    return userInfo;
                }
            }
        }
        catch (error) {
            this.reportError('fetchUserInfo', error);
        }
        return null;
    }
    async function fetchEnemyFactionIdFromAPI() {
        if (!this.torn_apikey) {
            console.log('[CAT] [fetchEnemyFactionIdFromAPI] Missing API key');
            return null;
        }
        try {
            let userFactionId = StorageUtil.get('cat_user_faction_id', null);
            if (userFactionId) {
            }
            else {
                const userUrl = `https://api.torn.com/v2/user?key=${this.torn_apikey}`;
                const userResponse = await this.httpRequest(userUrl, { method: 'GET' });
                if (!userResponse.ok) {
                    console.log('[CAT] [Step 2] Failed to fetch user. Status:', userResponse.status);
                    return null;
                }
                const userData = await userResponse.json();
                userFactionId = userData.profile?.faction_id ?? null;
                if (!userFactionId) {
                    console.log('[CAT] [Step 2] No faction ID found in user response');
                    return null;
                }
                StorageUtil.set('cat_user_faction_id', userFactionId);
            }
            // Parse faction ID directly from URL if viewing another faction's profile
            let targetFactionId = userFactionId;
            const currentSearch = window.location.search;
            if (currentSearch.includes('step=profile')) {
                const idMatch = currentSearch.match(/ID=(\d+)/);
                if (idMatch) {
                    const pageFactionId = idMatch[1];
                    if (pageFactionId !== userFactionId) {
                        targetFactionId = pageFactionId;
                    }
                }
            }
            const warsUrl = `https://api.torn.com/v2/faction/${targetFactionId}/wars?key=${this.torn_apikey}`;
            const warsResponse = await this.httpRequest(warsUrl, { method: 'GET' });
            if (!warsResponse.ok) {
                console.log('[CAT] [Step 3] Failed to fetch faction wars. Status:', warsResponse.status);
                return null;
            }
            const warsData = await warsResponse.json();
            if (warsData.wars && warsData.wars.ranked && warsData.wars.ranked.factions) {
                const factions = warsData.wars.ranked.factions;
                const enemyFaction = factions.find(f => String(f.id) !== String(targetFactionId));
                if (enemyFaction) {
                    const enemyId = `faction-${enemyFaction.id}`;
                    const expiresAt = Date.now() + (60 * 60 * 1000);
                    StorageUtil.set('cat_enemy_faction_id', {
                        id: enemyId,
                        name: enemyFaction.name,
                        expiresAt: expiresAt
                    });
                    this.factionId = enemyId;
                    return enemyId;
                }
            }
            console.log('[CAT] No active war found');
            return null;
        }
        catch (error) {
            console.log('[CAT] Error fetching enemy faction ID:', error);
            this.reportError('fetchEnemyFactionId', error);
            return null;
        }
    }
    async function getFactionInfo(factionId) {
        try {
            if (!this.torn_apikey) {
                return null;
            }
            let numericId;
            if (typeof factionId === 'string') {
                numericId = factionId.replace(/^(faction-|player-)/, '');
            }
            else {
                numericId = String(factionId);
            }
            numericId = String(parseInt(numericId));
            if (numericId === 'NaN')
                return null;
            const url = `https://api.torn.com/v2/faction/${numericId}?key=${this.torn_apikey}&selections=basic,members`;
            const response = await this.httpRequest(url, { method: 'GET' });
            if (!response.ok) {
                console.log('Failed to fetch faction info:', response.status);
                return null;
            }
            const data = await response.json();
            return data;
        }
        catch (error) {
            console.log('Error fetching faction info:', error);
            this.reportError('getFactionInfo', error);
            return null;
        }
    }
    async function getUserInfo(userId) {
        try {
            if (!this.torn_apikey) {
                return null;
            }
            const url = `https://api.torn.com/v2/user/${userId}?key=${this.torn_apikey}&selections=profile`;
            const response = await this.httpRequest(url, { method: 'GET' });
            if (!response.ok) {
                console.log('Failed to fetch user info:', response.status);
                return null;
            }
            const data = await response.json();
            return data;
        }
        catch (error) {
            console.log('Error fetching user info:', error);
            this.reportError('getUserInfo', error);
            return null;
        }
    }

    // ── Ranked wars in-memory cache (refreshes on page reload) ──
    let _rankedWarsCache = null;
    const RANKEDWARS_CACHE_TTL = 60000; // 60 seconds
    async function fetchRankedWarsData() {
        if (!this.torn_apikey)
            return null;
        const userFactionId = StorageUtil.get('cat_user_faction_id', null);
        if (!userFactionId)
            return null;
        let factionId = String(userFactionId);
        const currentSearch = window.location.search;
        if (currentSearch.includes('step=profile')) {
            const idMatch = currentSearch.match(/ID=(\d+)/);
            if (idMatch && idMatch[1] !== factionId) {
                factionId = idMatch[1];
            }
        }
        // Return cached data if still valid for this faction
        if (_rankedWarsCache && _rankedWarsCache.factionId === factionId && (Date.now() - _rankedWarsCache.timestamp) < RANKEDWARS_CACHE_TTL) {
            return _rankedWarsCache.data;
        }
        const url = `https://api.torn.com/v2/faction/${factionId}/rankedwars?key=${this.torn_apikey}`;
        const resp = await this.httpRequest(url, { method: 'GET' });
        const json = await resp.json();
        let rankedwars = null;
        if (json?.rankedwars) {
            if (Array.isArray(json.rankedwars)) {
                rankedwars = json.rankedwars;
            }
            else if (typeof json.rankedwars === 'object') {
                rankedwars = Object.values(json.rankedwars);
            }
        }
        _rankedWarsCache = { data: rankedwars, timestamp: Date.now(), factionId };
        return rankedwars;
    }
    async function sendWarData(warJson) {
        try {
            const wd = warJson;
            if (!wd || !wd.factionID)
                return;
            const factionId = String(wd.factionID);
            const hasRankedMembers = wd.rankedWarMembers && Object.keys(wd.rankedWarMembers).length > 0;
            let rankEntry = undefined;
            let chainEntry = undefined;
            if (wd.wars && Array.isArray(wd.wars)) {
                rankEntry = wd.wars.find(w => w.type === 'rank' || w.type === 'ranked' || w.key === 'rank' || w.key === 'ranked');
                chainEntry = wd.wars.find(w => w.key === 'chain' || w.type === 'chain');
            }
            const chainStart = chainEntry?.data?.chain?.start || null;
            const warId = rankEntry?.data?.warID || chainEntry?.data?.chain?.chainID || null;
            const payload = {
                factionId: factionId,
                warId: hasRankedMembers ? String(warId || 'ranked-' + factionId) : 'none',
                enemyFactionId: rankEntry?.data?.enemyFactionID ? String(rankEntry.data.enemyFactionID) : null,
                enemyFactionName: rankEntry?.data?.enemyFactionName || null,
                warStart: chainStart || null,
                warEnd: chainEntry?.data?.chain?.end || null,
                status: hasRankedMembers ? 'active' : 'no_war',
                rawData: warJson
            };
            await this.apiRequest(`${this.serverUrl}/api/faction-war`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.authToken}`
                },
                body: JSON.stringify(payload)
            });
            console.log(`[WAR] Sent war data for faction ${factionId}: status=${payload.status}`);
        }
        catch (error) {
            console.log('[WAR] Error sending war data:', error);
            this.reportError('sendWarData', error);
        }
    }
    async function scanWarFromDOM() {
        try {
            const rankBox = document.querySelector('[data-warid]');
            if (!rankBox)
                return;
            const warId = rankBox.getAttribute('data-warid');
            if (!warId)
                return;
            const factionId = StorageUtil.get('cat_user_faction_id', null);
            if (!factionId)
                return;
            const opponentLink = rankBox.querySelector('.opponentFactionName___vhESM, a[href*="factions.php"][class*="opponent"]');
            let enemyFactionId = null;
            let enemyFactionName = null;
            if (opponentLink) {
                enemyFactionName = (opponentLink.textContent || '').trim();
                const href = opponentLink.getAttribute('href') || '';
                const idMatch = href.match(/ID=(\d+)/);
                if (idMatch)
                    enemyFactionId = idMatch[1];
            }
            const timerEl = rankBox.querySelector('[class*="timer___"]');
            let warStartMs = null;
            if (timerEl) {
                const spans = timerEl.querySelectorAll('span');
                const digits = [];
                spans.forEach(s => {
                    const t = (s.textContent || '').trim();
                    if (t !== ':')
                        digits.push(t);
                });
                if (digits.length >= 8) {
                    const days = parseInt(digits[0] + digits[1], 10);
                    const hours = parseInt(digits[2] + digits[3], 10);
                    const minutes = parseInt(digits[4] + digits[5], 10);
                    const seconds = parseInt(digits[6] + digits[7], 10);
                    const totalMs = ((days * 24 + hours) * 3600 + minutes * 60 + seconds) * 1000;
                    warStartMs = Date.now() + totalMs;
                }
            }
            const isWaiting = !!rankBox.querySelector('[class*="waiting___"]');
            const payload = {
                factionId: String(factionId),
                warId: String(warId),
                enemyFactionId: enemyFactionId,
                enemyFactionName: enemyFactionName,
                warStart: warStartMs,
                warEnd: null,
                status: 'active',
                rawData: { source: 'dom_scan', warId, enemyFactionId, enemyFactionName, warStartMs, isWaiting }
            };
            await this.apiRequest(`${this.serverUrl}/api/faction-war`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.authToken}`
                },
                body: JSON.stringify(payload)
            });
            console.log(`[WAR DOM] Sent war data from DOM: warId=${warId}, enemy=${enemyFactionName}, startIn=${warStartMs ? Math.round((warStartMs - Date.now()) / 60000) + 'min' : 'unknown'}`);
        }
        catch (error) {
            console.log('[WAR DOM] Error scanning war from DOM:', error);
            this.reportError('scanWarFromDOM', error);
        }
    }
    async function fetchRankedWarsFromAPI() {
        try {
            if (!this.torn_apikey) {
                console.log('[WAR API] No API key available, skipping ranked wars fetch');
                return;
            }
            const userFactionId = StorageUtil.get('cat_user_faction_id', null);
            if (!userFactionId)
                return;
            // Only send war data for the user's own faction — never for viewed factions
            const currentSearch = window.location.search;
            if (currentSearch.includes('step=profile')) {
                const idMatch = currentSearch.match(/ID=(\d+)/);
                if (idMatch && idMatch[1] !== String(userFactionId)) {
                    console.log('[WAR API] Viewing another faction, skipping war data send');
                    return;
                }
            }
            const factionId = String(userFactionId);
            const rankedwars = await this.fetchRankedWarsData();
            if (!rankedwars || rankedwars.length === 0) {
                await this.httpRequest(`${this.serverUrl}/api/faction-war`, {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${this.authToken}`
                    },
                    body: JSON.stringify({
                        factionId: String(factionId),
                        warId: 'none',
                        status: 'no_war',
                        rawData: { rankedwars: [] }
                    })
                });
                console.log('[WAR API] No ranked wars found for faction', factionId);
                return;
            }
            const currentWar = rankedwars.find(w => w.end === 0 && w.winner === null);
            if (!currentWar) {
                // Only send the latest ended war if it ended within the last 7 days
                const sevenDaysAgo = (Date.now() / 1000) - (7 * 24 * 3600);
                const latestWar = rankedwars.find(w => w.end && w.end > sevenDaysAgo) || null;
                if (latestWar) {
                    const enemyFaction = latestWar.factions ? latestWar.factions.find(f => String(f.id) !== String(factionId)) : null;
                    await this.httpRequest(`${this.serverUrl}/api/faction-war`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': `Bearer ${this.authToken}`
                        },
                        body: JSON.stringify({
                            factionId: String(factionId),
                            warId: String(latestWar.id),
                            enemyFactionId: enemyFaction ? String(enemyFaction.id) : null,
                            enemyFactionName: enemyFaction ? enemyFaction.name : null,
                            warStart: latestWar.start ? latestWar.start * 1000 : null,
                            warEnd: latestWar.end ? latestWar.end * 1000 : null,
                            status: 'ended',
                            rawData: latestWar
                        })
                    });
                    console.log(`[WAR API] Latest war ${latestWar.id} is ended, winner=${latestWar.winner}`);
                }
                else {
                    await this.httpRequest(`${this.serverUrl}/api/faction-war`, {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'Authorization': `Bearer ${this.authToken}`
                        },
                        body: JSON.stringify({
                            factionId: String(factionId),
                            warId: 'none',
                            status: 'no_war',
                            rawData: { rankedwars: [] }
                        })
                    });
                }
                return;
            }
            const enemyFaction = currentWar.factions ? currentWar.factions.find(f => String(f.id) !== String(factionId)) : null;
            const warStartMs = currentWar.start ? currentWar.start * 1000 : null;
            const payload = {
                factionId: String(factionId),
                warId: String(currentWar.id),
                enemyFactionId: enemyFaction ? String(enemyFaction.id) : null,
                enemyFactionName: enemyFaction ? enemyFaction.name : null,
                warStart: warStartMs,
                warEnd: null,
                status: 'active',
                rawData: currentWar
            };
            await this.apiRequest(`${this.serverUrl}/api/faction-war`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.authToken}`
                },
                body: JSON.stringify(payload)
            });
        }
        catch (error) {
            console.log('[WAR API] Error fetching ranked wars:', error);
            this.reportError('fetchRankedWars', error);
        }
    }

    async function detectFactionAutomatically() {
        try {
            this.playerId = this.extractPlayerIdFromPage();
            if (!this.torn_apikey) {
                const storedFaction = StorageUtil.get('cat_user_faction_id', null);
                this.factionId = storedFaction || (this.playerId ? `player-${this.playerId}` : 'unknown-faction');
                return;
            }
            const cachedEnemyFaction = StorageUtil.get('cat_enemy_faction_id', null);
            if (cachedEnemyFaction && cachedEnemyFaction.expiresAt && cachedEnemyFaction.expiresAt > Date.now()) {
                this.factionId = cachedEnemyFaction.id;
                return;
            }
            const enemyFactionId = await this.fetchEnemyFactionIdFromAPI();
            if (enemyFactionId) {
                return;
            }
            const pageEnemyFactionId = this.extractEnemyFactionIdFromPage();
            if (pageEnemyFactionId) {
                this.factionId = pageEnemyFactionId;
                return;
            }
            const storedFaction = StorageUtil.get('cat_user_faction_id', null);
            this.factionId = storedFaction || (this.playerId ? `player-${this.playerId}` : 'unknown-faction');
        }
        catch (error) {
            console.log('Error in detectFactionAutomatically:', error);
            this.reportError('detectFaction', error);
            const enemyFactionId = this.extractEnemyFactionIdFromPage();
            if (enemyFactionId) {
                this.factionId = enemyFactionId;
            }
            else {
                const storedFaction = StorageUtil.get('cat_user_faction_id', null);
                this.factionId = storedFaction || `player-${this.playerId || 'unknown'}`;
            }
        }
    }
    function extractPageFactionIdFromUrl() {
        const currentSearch = window.location.search;
        if (currentSearch.includes('step=profile') || currentSearch.includes('step=your')) {
            const idMatch = currentSearch.match(/ID=(\d+)/);
            if (idMatch && idMatch[1]) {
                return idMatch[1];
            }
        }
        return null;
    }
    function extractEnemyFactionIdFromPage() {
        const factionLinks = document.querySelectorAll('a[href*="faction.php?step=profile"]');
        for (const link of factionLinks) {
            const url = link.getAttribute('href');
            if (!url)
                continue;
            const match = url.match(/ID=(\d+)/);
            if (match && match[1]) {
                return match[1];
            }
        }
        const factionElements = document.querySelectorAll('[class*="faction"], [data-faction-id]');
        for (const el of factionElements) {
            const htmlEl = el;
            if (htmlEl.dataset.factionId) {
                return htmlEl.dataset.factionId;
            }
            const link = el.querySelector('a');
            if (link) {
                const href = link.getAttribute('href');
                if (href && href.includes('faction.php')) {
                    const match = href.match(/ID=(\d+)/);
                    if (match && match[1]) {
                        return match[1];
                    }
                }
            }
        }
        return null;
    }
    function extractPlayerIdFromPage() {
        if (this.playerName && this.playerName !== 'Unknown') {
            const ariaSelector = `[aria-label^="User ${this.playerName}"]`;
            const userStatusElement = document.querySelector(ariaSelector);
            if (userStatusElement) {
                const container = userStatusElement.closest('li') || userStatusElement.closest('.member___fZiTx')?.parentElement;
                if (container) {
                    const profileLink = container.querySelector('a[href*="profiles.php?XID="]');
                    if (profileLink) {
                        const match = profileLink.href.match(/XID=(\d+)/);
                        if (match) {
                            return match[1];
                        }
                    }
                }
            }
        }
        const playerElement = document.querySelector('[data-player-id], [data-userid], [data-user-id]');
        if (playerElement) {
            const id = playerElement.dataset.playerId ||
                playerElement.dataset.userid ||
                playerElement.dataset.userId;
            if (id)
                return id;
        }
        const urlMatch = window.location.href.match(/ID=(\d+)/);
        if (urlMatch) {
            return urlMatch[1];
        }
        const profileLinks = document.querySelectorAll('a[href*="step=profile"]');
        for (const link of profileLinks) {
            const match = link.href.match(/ID=(\d+)/);
            if (match) {
                return match[1];
            }
        }
        const navUserElements = document.querySelectorAll('[class*="player"], [class*="user"], [class*="username"]');
        for (const elem of navUserElements) {
            const text = elem.textContent || '';
            const match = text.match(/\[(\d+)\]/);
            if (match) {
                return match[1];
            }
        }
        return null;
    }

    /**
     * Extension Transport — HTTP bridge + WebSocket for real-time updates.
     *
     * HTTP: content.js → postMessage → bridge.js → chrome.runtime → background.js → fetch
     * WS:   content.js → postMessage → bridge.js → WebSocket → cat-script.com server
     *
     * The WS replaces polling for the extension. The server pushes calls/statuses/etc in real-time.
     */
    // Synchronous detection: bridge sets this attribute before content.js runs
    const _bridgeReady = typeof document !== 'undefined' && document.documentElement.getAttribute('data-cat-bridge') === 'ready';
    let _requestId = 0;
    const _pendingRequests = new Map();
    // ── WS state ──
    let _wsConnected = false;
    let _wsOnMessage = null;
    // Listen for bridge messages (HTTP responses + WS messages)
    if (typeof window !== 'undefined') {
        window.addEventListener('message', (event) => {
            if (event.source !== window)
                return;
            // HTTP fetch response
            if (event.data?.type === 'CAT_FETCH_RESPONSE') {
                const { id, response } = event.data;
                const pending = _pendingRequests.get(id);
                if (!pending)
                    return;
                _pendingRequests.delete(id);
                clearTimeout(pending.timeout);
                const text = response.text || '';
                pending.resolve({
                    ok: response.ok,
                    status: response.status || 0,
                    text: () => Promise.resolve(text),
                    json: () => {
                        try {
                            return Promise.resolve(JSON.parse(text || '{}'));
                        }
                        catch (e) {
                            return Promise.reject(e);
                        }
                    }
                });
            }
            // WS status change
            if (event.data?.type === 'CAT_WS_STATUS') {
                _wsConnected = event.data.status === 'connected';
            }
            // WS message from server
            if (event.data?.type === 'CAT_WS_MESSAGE') {
                if (_wsOnMessage) {
                    _wsOnMessage(event.data.data);
                }
            }
        });
    }
    /**
     * Check if we're running inside the Chrome extension (bridge available)
     */
    function isExtensionMode() {
        return _bridgeReady || (typeof document !== 'undefined' && document.documentElement.getAttribute('data-cat-bridge') === 'ready');
    }
    /**
     * Send an HTTP request via the extension bridge
     */
    function extensionFetch(url, options = {}) {
        return new Promise((resolve) => {
            const id = ++_requestId;
            const timeout = setTimeout(() => {
                _pendingRequests.delete(id);
                resolve({ ok: false, status: 0, text: () => Promise.resolve(''), json: () => Promise.resolve({}) });
            }, 15000);
            _pendingRequests.set(id, { resolve, timeout });
            window.postMessage({
                type: 'CAT_FETCH_REQUEST',
                id,
                url,
                options: {
                    method: options.method || 'GET',
                    headers: options.headers || {},
                    body: options.body || undefined,
                }
            }, '*');
        });
    }
    /**
     * Connect to cat-script.com via WebSocket (through bridge.js)
     */
    function extensionWSConnect(serverUrl, authToken, factionId) {
        const wsUrl = serverUrl.replace('https://', 'wss://').replace('http://', 'ws://') + '/ws';
        window.postMessage({
            type: 'CAT_WS_CONNECT',
            url: wsUrl,
            authToken,
            factionId
        }, '*');
    }
    /**
     * Set callback for WS messages from server
     */
    function extensionWSOnMessage(callback) {
        _wsOnMessage = callback;
    }
    /**
     * Check if WS is connected
     */
    function extensionWSConnected() {
        return _wsConnected;
    }
    /**
     * Ask background to re-send current WS status (call when tab regains focus)
     */
    function extensionWSRequestStatus() {
        window.postMessage({ type: 'CAT_WS_STATUS_REQUEST' }, '*');
    }
    /**
     * Send a desktop notification via the extension bridge
     */
    function extensionNotify(title, body, url) {
        if (!_bridgeReady)
            return;
        window.postMessage({
            type: 'CAT_NOTIFICATION',
            title,
            body,
            url: url || 'https://www.torn.com/factions.php?step=your&type=1#/war/rank'
        }, '*');
    }
    /**
     * Schedule a notification at a specific time (for hospital timer alerts)
     */
    const _scheduledNotifs = new Map();
    function extensionScheduleNotify(id, title, body, timestampMs, url) {
        if (!_bridgeReady)
            return;
        // Cancel existing scheduled notif for this id
        if (_scheduledNotifs.has(id)) {
            clearTimeout(_scheduledNotifs.get(id));
            _scheduledNotifs.delete(id);
        }
        const delay = timestampMs - Date.now();
        if (delay <= 0) {
            extensionNotify(title, body, url);
            return;
        }
        const timer = setTimeout(() => {
            _scheduledNotifs.delete(id);
            extensionNotify(title, body, url);
        }, delay);
        _scheduledNotifs.set(id, timer);
    }
    function extensionCancelNotify(id) {
        if (_scheduledNotifs.has(id)) {
            clearTimeout(_scheduledNotifs.get(id));
            _scheduledNotifs.delete(id);
        }
    }

    function _detectDevice() {
        const ua = typeof navigator !== 'undefined' ? navigator.userAgent || '' : '';
        // Parse TornPDA injected device tags: ##deviceBrand=X##deviceModel=Y##
        let brand = '';
        let model = '';
        const brandMatch = ua.match(/##deviceBrand=([^#]+)##/);
        const modelMatch = ua.match(/##deviceModel=([^#]+)##/);
        if (brandMatch)
            brand = brandMatch[1];
        if (modelMatch)
            model = modelMatch[1];
        // Fallback: parse UA for common patterns
        if (!model) {
            if (/iPhone/.test(ua)) {
                brand = 'Apple';
                model = 'iPhone';
            }
            else if (/iPad/.test(ua)) {
                brand = 'Apple';
                model = 'iPad';
            }
            else {
                model = 'Android';
            }
        }
        const cores = typeof navigator !== 'undefined' && navigator.hardwareConcurrency ? navigator.hardwareConcurrency : 0;
        const memoryGB = typeof navigator !== 'undefined' && navigator.deviceMemory ? navigator.deviceMemory : 0;
        // Determine tier
        let tier = 'mid';
        if (cores > 0 && memoryGB > 0) {
            // Hardware APIs available (Android)
            if (cores >= 8 && memoryGB >= 6)
                tier = 'high';
            else if (cores <= 4 && memoryGB <= 3)
                tier = 'low';
        }
        else if (brand === 'Apple') {
            // iOS: no deviceMemory, but hardwareConcurrency works
            if (cores >= 6)
                tier = 'high';
            else if (cores <= 2)
                tier = 'low';
        }
        return { tier, model, brand, cores, memoryGB };
    }
    const pdaDevice = _detectDevice();
    // ── PDA Performance Metrics ──
    // Score adapts to device tier: low-end devices get more lenient thresholds.
    const pdaMetrics = {
        responseTimes: [], // last 20 response times (ms)
        requestTimestamps: [], // timestamps of last 20 requests (for req/s calc)
        failureTimestamps: [], // timestamps of recent failures
        _baseline: 0, // device baseline latency (ms), from first 3 requests
        _firstRecordTime: 0, // timestamp of very first record() call
        // DOM work tracking: sliding window of last 20 DOM callback durations
        _domWorkTimes: [], // ms spent in each DOM callback (timer tick, mutation batch)
        _domWorkTimestamps: [], // when each sample was recorded
        record(durationMs, success) {
            const now = Date.now();
            if (this._firstRecordTime === 0)
                this._firstRecordTime = now;
            this.responseTimes.push(durationMs);
            this.requestTimestamps.push(now);
            if (this.responseTimes.length > 20)
                this.responseTimes.shift();
            if (this.requestTimestamps.length > 20)
                this.requestTimestamps.shift();
            if (!success)
                this.failureTimestamps.push(now);
            // Keep only failures from last 30s
            const cutoff = now - 30000;
            while (this.failureTimestamps.length > 0 && this.failureTimestamps[0] < cutoff) {
                this.failureTimestamps.shift();
            }
            // Establish baseline from first 3 successful requests
            if (this._baseline === 0 && success && this.responseTimes.length >= 3) {
                const first3 = this.responseTimes.slice(0, 3);
                this._baseline = first3.reduce((a, b) => a + b, 0) / first3.length;
            }
        },
        /** Record time spent in a DOM callback (timer tick, mutation observer batch, etc.) */
        recordDomWork(durationMs) {
            this._domWorkTimes.push(durationMs);
            this._domWorkTimestamps.push(Date.now());
            if (this._domWorkTimes.length > 30) {
                this._domWorkTimes.shift();
                this._domWorkTimestamps.shift();
            }
        },
        /** DOM load: fraction of time spent in DOM work per second (0..1+) */
        getDomLoad() {
            const ts = this._domWorkTimestamps;
            if (ts.length < 2)
                return 0;
            const span = Math.max(1000, ts[ts.length - 1] - ts[0]);
            const totalMs = this._domWorkTimes.reduce((a, b) => a + b, 0);
            return totalMs / span; // e.g. 0.15 = 15% of time in DOM work
        },
        get totalRequests() {
            return this.requestTimestamps.length;
        },
        get _windowStart() {
            return this.requestTimestamps.length > 0 ? this.requestTimestamps[0] : Date.now();
        },
        getScore() {
            if (this.responseTimes.length === 0)
                return 100;
            const avgMs = this.responseTimes.reduce((a, b) => a + b, 0) / this.responseTimes.length;
            // Failure rate based on recent failures vs recent requests
            const now = Date.now();
            const recentCutoff = now - 30000;
            const recentRequests = this.requestTimestamps.filter(t => t > recentCutoff).length;
            const recentFailures = this.failureTimestamps.filter(t => t > recentCutoff).length;
            const failRate = recentRequests > 0 ? recentFailures / recentRequests : 0;
            // Bridge load = time spent on bridge per second (based on sliding window of last 20 requests)
            const timestamps = this.requestTimestamps;
            let reqPerSec;
            if (timestamps.length < 2) {
                reqPerSec = 0;
            }
            else {
                const span = Math.max(1000, timestamps[timestamps.length - 1] - timestamps[0]);
                reqPerSec = (timestamps.length - 1) / (span / 1000);
            }
            const bridgeLoad = (reqPerSec * avgMs) / 1000;
            // Latency score: 0-400ms = 100, 400-1500ms = gradual drop, 1500ms+ = 0
            const latencyScore = avgMs <= 400 ? 100 : Math.max(0, 100 - ((avgMs - 400) / 11));
            // Bridge saturation penalty: only penalize when bridge is truly overloaded (>1.5)
            // Normal PDA operation is ~1.0 load — that's fine
            const saturationPenalty = bridgeLoad > 1.5 ? Math.min(30, (bridgeLoad - 1.5) * 40) : 0;
            // failurePenalty
            const failurePenalty = failRate * 100;
            // DOM load penalty: fraction of time spent in DOM work
            // 0-5% = no penalty, 5-30% = mild, 30%+ = heavy
            const domLoad = this.getDomLoad();
            const domPenalty = Math.max(0, (domLoad - 0.05) * 60);
            const score = latencyScore - saturationPenalty - failurePenalty - domPenalty;
            return Math.max(0, Math.min(100, Math.round(score)));
        }
    };
    // ── PDA request serializer ──
    // PDA drops requests when multiple same-type requests are in-flight.
    // Two separate queues: one for reads (GET), one for writes (POST/PUT/DELETE).
    // This allows polling to continue even when a mutation is in-flight,
    // while preventing concurrent GETs or concurrent mutations from colliding.
    let _pdaReadChain = Promise.resolve();
    let _pdaMutationChain = Promise.resolve();
    function _serializePDA(fn, isRead) {
        if (isRead) {
            const queued = _pdaReadChain.then(fn, fn);
            _pdaReadChain = queued.then(() => { }, () => { });
            return queued;
        }
        else {
            const queued = _pdaMutationChain.then(fn, fn);
            _pdaMutationChain = queued.then(() => { }, () => { });
            return queued;
        }
    }
    // Sanitize headers: remove Authorization if token is null/undefined/empty
    function _sanitizeHeaders(headers) {
        const auth = headers['Authorization'];
        if (auth && (auth === 'Bearer null' || auth === 'Bearer undefined' || auth === 'Bearer ')) {
            const cleaned = { ...headers };
            delete cleaned['Authorization'];
            return cleaned;
        }
        return headers;
    }
    function isTornPDA() {
        if (typeof GM_info !== 'undefined' && GM_info.scriptHandler && GM_info.scriptHandler.includes('PDA')) {
            return true;
        }
        if (typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined') {
            return true;
        }
        if (typeof customFetch !== 'undefined') {
            return true;
        }
        return false;
    }
    function httpRequest(url, options = {}) {
        if (isExtensionMode()) {
            return extensionFetch(url, options);
        }
        return new Promise((resolve, reject) => {
            const method = (options.method || 'GET').toUpperCase();
            const headers = _sanitizeHeaders((options.headers || {}));
            const _pdaGet = typeof PDA_httpGet === 'function' ? PDA_httpGet : typeof window.PDA_httpGet === 'function' ? window.PDA_httpGet : undefined;
            const _pdaPost = typeof PDA_httpPost === 'function' ? PDA_httpPost : typeof window.PDA_httpPost === 'function' ? window.PDA_httpPost : undefined;
            const hasPDAHttp = !!_pdaGet && !!_pdaPost;
            if (hasPDAHttp) {
                const maxRetries = 3;
                const body = typeof options.body === 'string' ? options.body : '';
                // Entire request (including retries) runs inside the serialized chain
                // so the next queued request only starts after this one fully resolves.
                // GETs use the read queue, mutations use the write queue — they run independently.
                const isRead = method === 'GET';
                _serializePDA(async () => {
                    for (let attempt = 1; attempt <= maxRetries; attempt++) {
                        const _reqStart = performance.now();
                        try {
                            const responseText = method === 'POST'
                                ? await _pdaPost(url, headers, body)
                                : await _pdaGet(url, headers);
                            if (responseText === undefined || responseText === null || responseText === '') {
                                if (attempt < maxRetries) {
                                    await new Promise(r => setTimeout(r, 300 * attempt));
                                    continue;
                                }
                                pdaMetrics.record(performance.now() - _reqStart, false);
                                console.log('[PDA] Empty response after retries');
                                resolve({
                                    ok: false,
                                    status: 0,
                                    text: () => Promise.resolve(''),
                                    json: () => Promise.resolve({})
                                });
                                return;
                            }
                            let text;
                            if (typeof responseText === 'string') {
                                text = responseText;
                            }
                            else if (typeof responseText === 'object') {
                                if (typeof responseText.text === 'function') {
                                    try {
                                        text = await responseText.text();
                                    }
                                    catch (e) {
                                        text = responseText.responseText || JSON.stringify(responseText);
                                    }
                                }
                                else {
                                    text = responseText.responseText || (typeof responseText.text === 'string' ? responseText.text : '') || JSON.stringify(responseText);
                                }
                            }
                            else {
                                text = String(responseText);
                            }
                            // Extract HTTP status from Response-like objects
                            let status = 200;
                            let ok = true;
                            if (typeof responseText === 'object' && responseText !== null) {
                                const respObj = responseText;
                                if (typeof respObj.status === 'number') {
                                    status = respObj.status;
                                    ok = status >= 200 && status < 300;
                                }
                                if (typeof respObj.ok === 'boolean') {
                                    ok = respObj.ok;
                                }
                            }
                            pdaMetrics.record(performance.now() - _reqStart, ok);
                            resolve({
                                ok: ok,
                                status: status,
                                text: () => Promise.resolve(text),
                                json: () => {
                                    try {
                                        return Promise.resolve(JSON.parse(text));
                                    }
                                    catch (e) {
                                        console.log('[PDA] JSON parse error:', (e instanceof Error ? e.message : String(e)));
                                        this.reportError('httpRequestPdaJsonParse', e);
                                        return Promise.reject(e);
                                    }
                                }
                            });
                            return; // Success — exit the retry loop
                        }
                        catch (error) {
                            if (attempt < maxRetries) {
                                await new Promise(r => setTimeout(r, 300 * attempt));
                                continue;
                            }
                            pdaMetrics.record(performance.now() - _reqStart, false);
                            console.log('[PDA] Request failed after retries:', error);
                            resolve({
                                ok: false,
                                status: 0,
                                text: () => Promise.resolve(''),
                                json: () => Promise.resolve({})
                            });
                        }
                    }
                }, isRead).catch((e) => {
                    console.log('[PDA] Setup error:', (e instanceof Error ? e.message : String(e)));
                    resolve({
                        ok: false,
                        status: 0,
                        text: () => Promise.resolve(''),
                        json: () => Promise.resolve({})
                    });
                });
                return;
            }
            let actualCustomFetch = null;
            if (typeof customFetch !== 'undefined' && typeof customFetch === 'function') {
                actualCustomFetch = customFetch;
            }
            else if (typeof window !== 'undefined' && typeof window.customFetch === 'function') {
                actualCustomFetch = window.customFetch;
            }
            else if (typeof globalThis !== 'undefined' && typeof globalThis.customFetch === 'function') {
                actualCustomFetch = globalThis.customFetch;
            }
            if (actualCustomFetch && typeof actualCustomFetch === 'function') {
                try {
                    actualCustomFetch(url, options)
                        .then(async (response) => {
                        let bodyText;
                        try {
                            if (typeof response?.text === 'function') {
                                bodyText = await response.text();
                            }
                            else {
                                bodyText = '';
                            }
                        }
                        catch (textErr) {
                            console.log('[customFetch] Error extracting body:', (textErr instanceof Error ? textErr.message : String(textErr)));
                            this.reportError('customFetchBodyExtract', textErr);
                            bodyText = '';
                        }
                        const status = response?.status ?? (bodyText ? 200 : 400);
                        const normalizedResponse = {
                            ok: response?.ok ?? (status >= 200 && status < 300),
                            status: status,
                            statusText: response?.statusText ?? 'OK',
                            text: () => Promise.resolve(bodyText),
                            json: () => {
                                try {
                                    return Promise.resolve(JSON.parse(bodyText || '{}'));
                                }
                                catch (e) {
                                    return Promise.reject(e);
                                }
                            }
                        };
                        resolve(normalizedResponse);
                    })
                        .catch((error) => {
                        console.log('customFetch failed:', (error instanceof Error ? error.message : String(error)));
                        this.reportError('customFetchFailed', error);
                        resolve({
                            ok: false,
                            status: 0,
                            text: () => Promise.resolve(''),
                            json: () => Promise.resolve({})
                        });
                    });
                }
                catch (e) {
                    console.log('customFetch setup error:', (e instanceof Error ? e.message : String(e)));
                    this.reportError('customFetchSetup', e);
                    resolve({
                        ok: false,
                        status: 0,
                        text: () => Promise.resolve(''),
                        json: () => Promise.resolve({})
                    });
                }
                return;
            }
            _useGMXmlHttpRequest.call(this, url, options, resolve, reject);
        });
    }
    function _useGMXmlHttpRequest(url, options, resolve, reject) {
        if (typeof GM_xmlhttpRequest === 'undefined') {
            fetch(url, options)
                .then(r => resolve(r))
                .catch((e) => {
                console.log('[CAT] Native fetch failed (likely CORS):', (e instanceof Error ? e.message : String(e)));
                this.reportError('nativeFetchFailed', e);
                resolve({
                    ok: false,
                    status: 0,
                    text: () => Promise.resolve(''),
                    json: () => Promise.resolve({})
                });
            });
            return;
        }
        const details = {
            method: (options.method || 'GET'),
            url: url,
            headers: (options.headers || {}),
            timeout: 10000,
            onload: (response) => {
                try {
                    const status = response?.status ?? 200;
                    const ok = status >= 200 && status < 300;
                    resolve({
                        ok: ok,
                        status: status,
                        text: () => Promise.resolve(response?.responseText || ''),
                        json: () => {
                            try {
                                const text = response?.responseText || '{}';
                                return Promise.resolve(JSON.parse(text));
                            }
                            catch (e) {
                                return Promise.reject(e);
                            }
                        }
                    });
                }
                catch (e) {
                    this.reportError('gmXhrOnloadParse', e);
                    reject(e instanceof Error ? e : new Error(String(e)));
                }
            },
            onerror: () => {
                this.reportError('gmXhrError', new Error(`GM_xmlhttpRequest failed: ${url}`));
                reject(new Error(`GM_xmlhttpRequest failed`));
            },
            ontimeout: () => {
                reject(new Error('GM_xmlhttpRequest timeout'));
            }
        };
        if (options.body) {
            details.data = options.body;
        }
        try {
            GM_xmlhttpRequest(details);
        }
        catch (e) {
            reject(new Error(`GM_xmlhttpRequest setup failed: ${(e instanceof Error ? e.message : String(e))}`));
        }
    }

    /**
     * Snake_case → camelCase field mapping for CallData.
     * camelCase takes priority; snake_case is used as fallback when camelCase is falsy.
     */
    const SNAKE_TO_CAMEL = [
        ['member_id', 'memberId'],
        ['caller_name', 'callerName'],
        ['member_name', 'memberName'],
        ['caller_id', 'callerId'],
        ['faction_id', 'factionId'],
    ];
    /**
     * Normalize a raw call object from the server into a clean CallData
     * with only camelCase field names. Snake_case duplicates are stripped.
     */
    function normalizeCallData(raw) {
        // Shallow copy to avoid mutating the original
        const out = { ...raw };
        for (const [snake, camel] of SNAKE_TO_CAMEL) {
            // Use camelCase if truthy, else fall back to snake_case
            if (!out[camel] && out[snake]) {
                out[camel] = out[snake];
            }
            delete out[snake];
        }
        return out;
    }
    /**
     * Normalize an array of calls. Handles null/undefined gracefully.
     */
    function normalizeCallsArray(calls) {
        if (!calls)
            return [];
        return calls.map(c => normalizeCallData(c));
    }

    async function callMember(memberName, memberId = null, callButton = null) {
        try {
            let targetStatus = null;
            if (callButton) {
                const memberRow = callButton.closest('li');
                if (memberRow) {
                    const statusDivs = memberRow.querySelectorAll('[class*="status"]');
                    let statusElement = null;
                    for (const div of statusDivs) {
                        if (div.className.includes('status') && div.className.includes('left') &&
                            !div.className.includes('StatusWrap')) {
                            statusElement = div;
                            break;
                        }
                    }
                    if (statusElement) {
                        targetStatus = (statusElement.textContent || '').trim();
                    }
                }
            }
            const userFactionId = StorageUtil.get('cat_user_faction_id', null);
            const requestBody = {
                factionId: userFactionId || undefined,
                memberName: memberName,
                memberId: memberId || undefined,
                callerId: this.playerId,
                callerName: this.playerName,
                targetStatus: targetStatus,
                userFactionId: userFactionId
            };
            const response = await this.apiRequest(`${this.serverUrl}/api/call`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.authToken}`
                },
                body: JSON.stringify(requestBody)
            });
            const data = await response.json();
            if (!response.ok || data.error) {
                if (data.error === 'already_called' && data.targetName) {
                    this.showNotification(`You already called ${data.targetName.replace(/^\d*\.?\d+[kmbt]/i, '')}`, 'warning');
                    return { success: false, error: data.error, message: data.message };
                }
                throw new Error(data.message || `HTTP error! status: ${response.status}`);
            }
            return data;
        }
        catch (error) {
            console.log('Erreur lors du call:', error);
            this.reportError('callMember', error);
            return null;
        }
    }
    async function getCalls() {
        if (this.isCallsFetching) {
            return [];
        }
        try {
            this.isCallsFetching = true;
            const userFaction = StorageUtil.get('cat_user_faction_id', null) || '';
            const factionParam = userFaction ? `?factionId=${encodeURIComponent(userFaction)}` : '';
            const response = await this.apiRequest(`${this.serverUrl}/api/calls${factionParam}`, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${this.authToken}`
                }
            });
            if (!response) {
                throw new Error('Response is null or undefined');
            }
            if (response.ok === false || (response.status && response.status >= 400)) {
                console.log('[getCalls] HTTP error detected - status:', response.status, 'ok:', response.ok);
                throw new Error(`HTTP error! status: ${response.status ?? 'unknown'}`);
            }
            const text = await response.text();
            if (!text || !text.trim().startsWith('{')) {
                console.log('[getCalls] Invalid response (not JSON) - status:', response.status, 'text:', text.substring(0, 100));
                if (this.lastValidCalls && this.lastValidCalls.length > 0) {
                    return this.lastValidCalls;
                }
                return [];
            }
            let data;
            try {
                data = JSON.parse(text);
            }
            catch (parseErr) {
                console.log('Failed to parse JSON from getCalls. Raw text:', text.substring(0, 500));
                this.reportError('getCallsParse', parseErr);
                if (this.lastValidCalls && this.lastValidCalls.length > 0) {
                    return this.lastValidCalls;
                }
                return [];
            }
            const calls = normalizeCallsArray(data.data || []);
            this.lastValidCalls = calls;
            return calls;
        }
        catch (error) {
            console.log('Erreur lors de la r\u00E9cup\u00E9ration des calls:', error);
            this.reportError('getCalls', error);
            if (this.lastValidCalls && this.lastValidCalls.length > 0) {
                return this.lastValidCalls;
            }
            return [];
        }
        finally {
            this.isCallsFetching = false;
        }
    }
    async function cancelCall(callId) {
        try {
            const response = await this.apiRequest(`${this.serverUrl}/api/call/${callId}/cancel`, {
                method: 'POST',
                headers: {
                    'Authorization': `Bearer ${this.authToken}`
                }
            });
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            const result = await response.json();
            if (result && result.success) {
                this.lastValidCalls = [];
                try {
                    localStorage.removeItem('cat_calls_cache');
                }
                catch (_e) { /* ignore */ }
            }
            return result;
        }
        catch (error) {
            console.log('Erreur lors de l\'annulation du call:', error);
            this.reportError('cancelCall', error);
            return null;
        }
    }

    function showNotification(message, type = 'info') {
        let container = document.getElementById('faction-war-toast-container');
        if (!container) {
            container = document.createElement('div');
            container.id = 'faction-war-toast-container';
            container.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 999999;
            pointer-events: none;
        `;
            document.body.appendChild(container);
        }
        const toast = document.createElement('div');
        toast.style.cssText = `
        background: #FF794C;
        color: #4a5568;
        border-radius: 8px;
        padding: 16px 24px;
        margin-bottom: 12px;
        box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        font-size: 14px;
        font-weight: 500;
        display: flex;
        align-items: center;
        gap: 12px;
        min-width: 300px;
        pointer-events: auto;
        animation: slideInRight 0.3s ease-out;
    `;
        let emoji = '\u2139\uFE0F';
        if (type === 'warning') {
            emoji = '\u26A0\uFE0F';
        }
        else if (type === 'error') {
            emoji = '\u274C';
        }
        else if (type === 'success') {
            emoji = '\u2705';
        }
        toast.style.borderLeft = `4px solid #E55100`;
        const emojiSpan = document.createElement('span');
        emojiSpan.style.fontSize = '18px';
        emojiSpan.textContent = emoji;
        const msgSpan = document.createElement('span');
        msgSpan.textContent = message;
        toast.appendChild(emojiSpan);
        toast.appendChild(msgSpan);
        container.appendChild(toast);
        setTimeout(() => {
            toast.style.animation = 'slideOutRight 0.3s ease-in forwards';
            setTimeout(() => {
                toast.remove();
            }, 300);
        }, 4000);
    }
    function reportError(context, error) {
        if (this._reportedErrorCount >= 50)
            return;
        const message = error instanceof Error ? error.message : String(error);
        if (message.includes('status: 0') || message.includes('HTTP 0') || message.includes('GM_xmlhttpRequest failed') || message.includes('GM_xmlhttpRequest timeout') || message.includes('Failed to fetch'))
            return;
        const key = `${context}:${message}`;
        if (this._reportedErrors.has(key))
            return;
        this._reportedErrors.add(key);
        this._reportedErrorCount++;
        // Save to localStorage for debug info
        try {
            const LOG_KEY = 'cat_error_log';
            const MAX_ERRORS = 20;
            const log = JSON.parse(localStorage.getItem(LOG_KEY) || '[]');
            log.push({ t: new Date().toISOString(), c: context, m: message });
            if (log.length > MAX_ERRORS)
                log.splice(0, log.length - MAX_ERRORS);
            localStorage.setItem(LOG_KEY, JSON.stringify(log));
        }
        catch (_) { /* ignore */ }
        const stack = error instanceof Error ? error.stack || '' : '';
        this.apiRequest(`${this.serverUrl}/api/errors`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.authToken}`
            },
            body: JSON.stringify({
                context,
                message,
                stack,
                scriptVersion: VERSION
            })
        }).catch(() => { });
    }

    /**
     * Fetch fresh faction data from Torn API v2 whenever user visits faction page.
     * Detects if faction changed and updates localStorage accordingly.
     *
     * Called by: url-checker.ts when step=profile detected
     */
    async function ensureFactionFreshData() {
        if (!this.torn_apikey) {
            // No API key, skip fresh fetch
            return;
        }
        try {
            // Fetch user's current faction from Torn API v2
            const response = await this.httpRequest(`https://api.torn.com/v2/user/self/basic?key=${this.torn_apikey}`, { method: 'GET' });
            if (!response.ok) {
                console.log('[Faction Refresh] API fetch failed, keeping cached data');
                return;
            }
            const text = await response.text();
            if (!text || !text.trim().startsWith('{')) {
                console.log('[Faction Refresh] Invalid response (not JSON)');
                return;
            }
            let data;
            try {
                data = JSON.parse(text);
            }
            catch {
                console.log('[Faction Refresh] Failed to parse API response');
                return;
            }
            const currentFactionId = data?.data?.profile?.faction_id;
            const playerName = data?.data?.profile?.name;
            const playerId = data?.data?.profile?.id;
            if (!currentFactionId) {
                console.log('[Faction Refresh] No faction_id in response');
                return;
            }
            // Get stored faction ID (stored as plain numeric string)
            const storedFactionId = StorageUtil.get('cat_user_faction_id', null);
            const factionIdChanged = storedFactionId && String(currentFactionId) !== storedFactionId;
            if (factionIdChanged) {
                console.log(`[Faction Refresh] Faction changed: ${storedFactionId} → ${currentFactionId}`);
                // Update faction ID in localStorage
                StorageUtil.set('cat_user_faction_id', String(currentFactionId));
                // Clear enemy faction cache to force re-detection on next check
                StorageUtil.set('cat_enemy_faction_id', null);
                // Trigger fresh faction detection
                await this.detectFactionAutomatically();
            }
            else {
                // Even if faction didn't change, update player info
                if (playerName) {
                    StorageUtil.set('cat_user_info', {
                        id: String(playerId || ''),
                        name: playerName
                    });
                    console.log(`[Faction Refresh] Updated player info: ${playerName}`);
                }
            }
        }
        catch (error) {
            // Silently fail — don't disrupt user experience
            console.log('[Faction Refresh] Error:', error);
            this.reportError('ensureFactionFreshData', error);
        }
    }

    class APIManager {
        constructor() {
            this.serverUrls = ['https://cat-script.com'];
            this.serverUrl = this.serverUrls[0];
            this.authToken = StorageUtil.get('cat_auth_token', null);
            this.factionId = null;
            this.playerName = 'Unknown';
            this.playerId = null;
            this.torn_apikey = this.getTornApiKeyFromStorage();
            this.isCallsFetching = false;
            this.lastValidCalls = [];
            this._detectRetryCount = 0;
            this._reportedErrors = new Set();
            this._reportedErrorCount = 0;
            this._retrying401 = false;
            this._last401RefreshTime = 0;
            this.detectBestUrl();
            this.loadUserInfoFromStorage();
            setTimeout(() => this.detectFactionAutomatically(), 100);
            // Auto-register to get per-user token if we have an API key but no stored token
            setTimeout(() => this.autoRegisterIfNeeded(), 2000);
        }
    }
    // Prototype assignments
    APIManager.prototype.detectBestUrl = detectBestUrl;
    APIManager.prototype.loadUserInfoFromStorage = loadUserInfoFromStorage;
    APIManager.prototype.setServerUrl = setServerUrl;
    APIManager.prototype.setAuthToken = setAuthToken;
    APIManager.prototype.setFactionId = setFactionId;
    APIManager.prototype.getPlayerName = getPlayerName;
    APIManager.prototype.getTornApiKeyFromStorage = getTornApiKeyFromStorage;
    APIManager.prototype.saveTornApiKey = saveTornApiKey;
    APIManager.prototype.autoRegisterIfNeeded = autoRegisterIfNeeded;
    APIManager.prototype.ensureUserFactionId = ensureUserFactionId;
    APIManager.prototype.registerAndGetToken = registerAndGetToken;
    APIManager.prototype.apiRequest = apiRequest;
    APIManager.prototype.promptForApiKey = promptForApiKey;
    APIManager.prototype.showApiKeyModal = showApiKeyModal;
    APIManager.prototype.fetchUserInfoFromTornAPI = fetchUserInfoFromTornAPI;
    APIManager.prototype.fetchEnemyFactionIdFromAPI = fetchEnemyFactionIdFromAPI;
    APIManager.prototype.getFactionInfo = getFactionInfo;
    APIManager.prototype.getUserInfo = getUserInfo;
    APIManager.prototype.sendWarData = sendWarData;
    APIManager.prototype.scanWarFromDOM = scanWarFromDOM;
    APIManager.prototype.fetchRankedWarsData = fetchRankedWarsData;
    APIManager.prototype.fetchRankedWarsFromAPI = fetchRankedWarsFromAPI;
    APIManager.prototype.detectFactionAutomatically = detectFactionAutomatically;
    APIManager.prototype.extractPageFactionIdFromUrl = extractPageFactionIdFromUrl;
    APIManager.prototype.extractEnemyFactionIdFromPage = extractEnemyFactionIdFromPage;
    APIManager.prototype.extractPlayerIdFromPage = extractPlayerIdFromPage;
    APIManager.prototype.isTornPDA = isTornPDA;
    APIManager.prototype.httpRequest = httpRequest;
    APIManager.prototype._useGMXmlHttpRequest = _useGMXmlHttpRequest;
    APIManager.prototype.callMember = callMember;
    APIManager.prototype.getCalls = getCalls;
    APIManager.prototype.cancelCall = cancelCall;
    APIManager.prototype.ensureFactionFreshData = ensureFactionFreshData;
    APIManager.prototype.showNotification = showNotification;
    APIManager.prototype.reportError = reportError;

    const TTL_MS$1 = 6 * 3600 * 1000;
    const FACTION_KEY_PREFIX = 'cat_ffs_faction_';
    const MEMBER_KEY_PREFIX$1 = 'cat_ffs_member_';
    function getFFSCached(playerId) {
        const entry = StorageUtil.get(MEMBER_KEY_PREFIX$1 + playerId, null);
        if (!entry)
            return null;
        if (Date.now() - entry.cachedAt > TTL_MS$1)
            return null;
        return entry.data;
    }
    /** Fetch FF Scouter stats for a whole faction using user's own API key.
     *  Stored in localStorage — never sent to CAT server. */
    async function fetchFFScouterFaction(factionId, memberIds, httpRequestFn) {
        const apiKey = (StorageUtil.get('cat_ffscouter_api_key', '') || '').trim();
        if (!apiKey)
            return;
        // Skip if already fetched within TTL
        const factionCache = StorageUtil.get(FACTION_KEY_PREFIX + factionId, null);
        if (factionCache && Date.now() - factionCache.fetchedAt < TTL_MS$1)
            return;
        if (memberIds.length === 0)
            return;
        try {
            // Batch by 200 (FFS max)
            for (let i = 0; i < memberIds.length; i += 200) {
                const batch = memberIds.slice(i, i + 200);
                const targets = batch.join(',');
                const url = `https://ffscouter.com/api/v1/get-stats?key=${apiKey}&targets=${targets}`;
                const resp = await httpRequestFn(url, { method: 'GET' });
                if (!resp.ok)
                    return;
                const data = await resp.json();
                const entries = Array.isArray(data) ? data : (data && typeof data === 'object' && !data.error && !data.code ? Object.values(data) : []);
                if (data?.error || data?.code)
                    return; // invalid key
                for (const e of entries) {
                    if (!e?.player_id)
                        continue;
                    const ff = e.fair_fight ?? e.fairFight ?? null;
                    const bsRaw = e.bs_estimate ?? e.bsEstimate ?? null;
                    const bs = e.bs_estimate_human ?? e.bsEstimateHuman ?? (bsRaw != null ? formatBsRaw(bsRaw) : null);
                    if (ff == null && bsRaw == null)
                        continue;
                    const entry = {
                        userId: String(e.player_id),
                        ff: ff != null ? Number(ff) : null,
                        bs,
                        bsRaw: bsRaw != null ? Number(bsRaw) : null,
                        fetchedAt: Date.now(),
                    };
                    StorageUtil.set(MEMBER_KEY_PREFIX$1 + entry.userId, { data: entry, cachedAt: Date.now() });
                }
            }
            StorageUtil.set(FACTION_KEY_PREFIX + factionId, { fetchedAt: Date.now() });
        }
        catch (_) {
            // Silent fail
        }
    }
    function formatBsRaw(n) {
        if (n >= 1000000000)
            return `${(n / 1000000000).toFixed(1)}B`;
        if (n >= 100000000)
            return `${(n / 1000000).toFixed(0)}M`;
        if (n >= 1000000)
            return `${(n / 1000000).toFixed(1)}M`;
        if (n >= 1000)
            return `${(n / 1000).toFixed(0)}K`;
        return String(n);
    }
    /** Invalidate local FFS cache for given player IDs (called when server signals fresh data) */
    function invalidateFFSCache(playerIds) {
        for (const id of playerIds) {
            StorageUtil.set(MEMBER_KEY_PREFIX$1 + id, null);
            StorageUtil.set(FACTION_KEY_PREFIX + id, null);
        }
    }
    /** Build a merged ffStats map: local FFS cache takes priority over server data */
    function mergeFFSLocal(serverStats) {
        const hasKey = !!(StorageUtil.get('cat_ffscouter_api_key', '') || '').trim();
        if (!hasKey)
            return serverStats;
        const merged = { ...serverStats };
        for (const playerId of Object.keys(merged)) {
            const local = getFFSCached(playerId);
            if (local) {
                merged[playerId] = { ff: local.ff, bs: local.bs, bsRaw: local.bsRaw };
            }
        }
        // Also add players that are in local cache but not in server data
        // (e.g. faction not tracked by server yet)
        const allKeys = Object.keys(localStorage).filter(k => k.startsWith(MEMBER_KEY_PREFIX$1));
        for (const key of allKeys) {
            const playerId = key.slice(MEMBER_KEY_PREFIX$1.length);
            if (merged[playerId])
                continue; // already handled above
            const local = getFFSCached(playerId);
            if (local) {
                merged[playerId] = { ff: local.ff, bs: local.bs, bsRaw: local.bsRaw };
            }
        }
        return merged;
    }

    class PollingManager {
        constructor(apiManager) {
            this.apiManager = apiManager;
            this._enhancer = null;
            this._isActive = false;
            this.pollingInterval = null;
            this.heartbeatInterval = null;
            const isPDA = typeof window.flutter_inappwebview !== 'undefined';
            const pdaPerfMode = isPDA && String(StorageUtil.get('cat_pda_perf_mode', 'false')) === 'true';
            this.pollRate = pdaPerfMode ? 2000 : 500;
            this.onCallsUpdate = null;
            this.onConnectionChange = null;
            this.onStatusesUpdate = null;
            this.onRalliesUpdate = null;
            this.onRevivableDataUpdate = null;
            this.onTacticalMarkersUpdate = null;
            this.onSoftUncallsUpdate = null;
            this.onEnemyChainUpdate = null;
            this.onChainBonusAssignmentUpdate = null;
            this.onFFStatsUpdate = null;
            this.onMemberBarsUpdate = null;
            this.lastCallsHash = null;
            this._pendingStatuses = [];
            this._statusFlushTimer = null;
            this._statusFlushDeadline = 0;
            this._fetchingCalls = false;
            this._pollingActive = false;
            this._pollingTimeout = null;
            this._pdaWaitCount = 0;
            this._rallyInFlight = false;
            this._callInFlight = 0;
            this._callCooldownUntil = 0;
            this._wsFallbackActive = false;
            this._wsWatchdogInterval = null;
            this._wsFallbackTimeout = null;
            this._myCurrentCall = null;
            this._heartbeatFailedNotifShown = false;
        }
        isWsFallbackActive() {
            return this._wsFallbackActive;
        }
        start() {
            if (this._isActive) {
                return;
            }
            const extensionReady = typeof document !== 'undefined' && document.documentElement.getAttribute('data-cat-bridge') === 'ready';
            const pdaReady = extensionReady || typeof PDA_httpGet === 'function' || typeof customFetch === 'function' ||
                (typeof window !== 'undefined' && typeof window.customFetch === 'function') ||
                typeof GM_xmlhttpRequest !== 'undefined';
            if (!pdaReady) {
                this._pdaWaitCount++;
                if (this._pdaWaitCount <= 50) {
                    setTimeout(() => this.start(), 200);
                    return;
                }
                console.log('[HTTP] PDA functions not available after 10s, trying anyway');
            }
            // Start polling immediately — don't wait for first fetch to succeed
            // (avoids exponential backoff if first fetch fails due to VERSION-BLOCK race)
            this._isActive = true;
            this.startPolling();
            // These must run even in WS mode
            this.startHeartbeat();
            this.listenCrossTabSignals();
            if (this.onConnectionChange)
                this.onConnectionChange(true);
        }
        isOnWarPage() {
            const hash = window.location.hash;
            return hash.startsWith('#/war') || hash === '#/';
        }
        async callMember(factionId, memberId, memberName, targetStatus = null) {
            if (document.body.classList.contains('cat-read-only')) {
                console.log('[CAT] Call blocked: subscription not activated');
                return { success: false, error: 'not_activated' };
            }
            this._callInFlight++;
            try {
                let callerId = this.apiManager.playerId;
                const callerName = this.apiManager.playerName;
                if (!callerId) {
                    callerId = this.apiManager.extractPlayerIdFromPage();
                    if (callerId) {
                        this.apiManager.playerId = callerId;
                    }
                    else {
                        if (callerName && callerName !== 'Unknown') {
                            const yourFactionSide = document.querySelector('.your-faction, [class*="your-faction"]');
                            if (yourFactionSide) {
                                const profileLinks = yourFactionSide.querySelectorAll('a[href*="profiles.php?XID="]');
                                for (const link of profileLinks) {
                                    const container = link.closest('li');
                                    if (container && container.textContent?.includes(callerName)) {
                                        const match = link.href.match(/XID=(\d+)/);
                                        if (match) {
                                            callerId = match[1];
                                            this.apiManager.playerId = callerId;
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/call`, {
                    method: 'POST',
                    headers: this.getHeaders(),
                    body: JSON.stringify({
                        factionId: factionId || StorageUtil.get('cat_user_faction_id', null),
                        memberId, memberName, targetStatus,
                        callerId: callerId,
                        callerName: this.apiManager.playerName,
                        userFactionId: StorageUtil.get('cat_user_faction_id', null)
                    })
                });
                const data = await response.json();
                if (data.success) {
                    // Store callId in global mapping for attack icon detection
                    if (data.data && data.data.id) {
                        const callMemberId = data.data.memberId;
                        if (callMemberId && this._enhancer) {
                            this._enhancer.setMemberCallId(callMemberId, data.data.id);
                        }
                    }
                    this.lastCallsHash = null;
                }
                return data;
            }
            catch (error) {
                this.apiManager.reportError('pollingCreateCall', error);
                return { success: false, error: 'network_error' };
            }
            finally {
                this._callInFlight = Math.max(0, this._callInFlight - 1);
                this._callCooldownUntil = Date.now() + 2000;
            }
        }
        async cancelCall(callId, callerName = null, memberName = null) {
            this._callInFlight++;
            try {
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/call/${callId}/cancel`, { method: 'POST', headers: this.getHeaders() });
                const data = await response.json();
                if (data.success) {
                    // Clear callId from mapping when call is removed
                    if (data.data && data.data.memberId) {
                        const callMemberId = data.data.memberId;
                        if (callMemberId && this._enhancer) {
                            this._enhancer.clearMemberCallId(callMemberId);
                        }
                    }
                    this.lastCallsHash = null;
                    // Clear cached calls so stale call doesn't reappear on page refresh
                    try {
                        localStorage.removeItem('cat_calls_cache');
                    }
                    catch (_e) { /* ignore */ }
                }
                return data;
            }
            catch (error) {
                this.apiManager.reportError('pollingCancelCall', error);
                return { success: false, error: 'network_error' };
            }
            finally {
                this._callInFlight = Math.max(0, this._callInFlight - 1);
            }
        }
        async joinRally(factionId, memberId, memberName) {
            this._rallyInFlight = true;
            try {
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/rally`, {
                    method: 'POST',
                    headers: this.getHeaders(),
                    body: JSON.stringify({
                        factionId, memberId, memberName,
                        playerId: this.apiManager.playerId,
                        playerName: this.apiManager.playerName
                    })
                });
                const data = await response.json();
                // Note: don't call fetchRallies() here - fetchCalls() already returns rallies
                // and will pick up the change on next poll (within 1s)
                return data;
            }
            catch (error) {
                this.apiManager.reportError('pollingJoinRally', error);
                return { success: false, error: 'network_error' };
            }
            finally {
                this._rallyInFlight = false;
            }
        }
        async leaveRally(memberId) {
            this._rallyInFlight = true;
            try {
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/rally/${memberId}/leave`, {
                    method: 'POST',
                    headers: this.getHeaders(),
                    body: JSON.stringify({
                        playerId: this.apiManager.playerId
                    })
                });
                const data = await response.json();
                // Note: don't call fetchRallies() here - fetchCalls() already returns rallies
                // and will pick up the change on next poll (within 1s)
                return data;
            }
            catch (error) {
                this.apiManager.reportError('pollingLeaveRally', error);
                return { success: false, error: 'network_error' };
            }
            finally {
                this._rallyInFlight = false;
            }
        }
        async fetchRallies() {
            try {
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/rallies`, { method: 'GET', headers: this.getHeaders() });
                if (response.ok) {
                    const data = await response.json();
                    if (data.success && data.rallies && this.onRalliesUpdate) {
                        this.onRalliesUpdate(data.rallies);
                    }
                    return true;
                }
            }
            catch (error) {
                this.apiManager.reportError('fetchRallies', error);
            }
            return false;
        }
        async fetchMemberBars() {
            try {
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/member-bars`, { method: 'GET', headers: this.getHeaders() });
                if (response.ok) {
                    const raw = await response.text();
                    let data;
                    try {
                        data = JSON.parse(raw);
                    }
                    catch (e) {
                        return false;
                    }
                    if (data.success && data.bars && this.onMemberBarsUpdate) {
                        this.onMemberBarsUpdate(data.bars);
                    }
                    return true;
                }
            }
            catch (error) {
                this.apiManager.reportError('fetchMemberBars', error);
            }
            return false;
        }
        async sendWarUpdate(factionId, targets) {
            try {
                await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/war-update`, {
                    method: 'POST',
                    headers: this.getHeaders(),
                    body: JSON.stringify({
                        factionId, targets,
                        playerId: this.apiManager.playerId,
                        playerName: this.apiManager.playerName,
                        timestamp: Date.now(),
                        userFactionId: StorageUtil.get('cat_user_faction_id', null)
                    })
                });
            }
            catch (error) {
                console.log('[HTTP] Error sending war update:', error);
                this.apiManager.reportError('sendWarUpdate', error);
            }
        }
        async markAttacking(callId) {
            try {
                await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/call/${callId}/attacking`, {
                    method: 'POST',
                    headers: this.getHeaders(),
                    body: '{}'
                });
            }
            catch (error) {
                console.log('[HTTP] Error marking attacking:', error);
                this.apiManager.reportError('markAttacking', error);
            }
        }
        requestCalls() {
            this.lastCallsHash = null;
            this.fetchCalls();
        }
        getHeaders() {
            return {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.apiManager.authToken}`
            };
        }
        async fetchCalls() {
            if (this._fetchingCalls)
                return false;
            this._fetchingCalls = true;
            try {
                // Admin viewing another faction → use viewed faction; otherwise use user's own
                // Check both window.FactionWarEnhancer (normal) and this._enhancer (startup timing — global not set yet)
                // Also check cat_is_admin_cached (covers first polls before checkActivationStatus() resolves)
                const isAdmin = !!window.FactionWarEnhancer?.subscriptionData?.isAdmin
                    || !!this._enhancer?.subscriptionData?.isAdmin
                    || StorageUtil.get('cat_is_admin_cached', '') === 'true';
                let viewedFaction = null;
                if (isAdmin && state.catOtherFaction) {
                    viewedFaction = state.viewingFactionId;
                    // Fallback: parse URL directly if viewingFactionId not set yet (PDA timing)
                    if (!viewedFaction && window.location.search.includes('step=profile')) {
                        const idMatch = window.location.search.match(/ID=(\d+)/);
                        if (idMatch)
                            viewedFaction = idMatch[1];
                    }
                }
                const factionId = viewedFaction || StorageUtil.get('cat_user_faction_id', null) || '';
                let factionParam = factionId ? `?factionId=${encodeURIComponent(String(factionId))}` : '';
                // Non-admin viewing another faction: pass viewFactionId so server can include FF stats
                if (!viewedFaction && state.catOtherFaction && state.viewingFactionId) {
                    const sep = factionParam ? '&' : '?';
                    factionParam += `${sep}viewFactionId=${encodeURIComponent(state.viewingFactionId)}`;
                }
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/calls${factionParam}`, { method: 'GET', headers: this.getHeaders() });
                if (response.ok) {
                    const data = await response.json();
                    // Skip processing if a call/rally operation is in flight (data would be stale)
                    if (this._rallyInFlight || this._callInFlight > 0) {
                        return false;
                    }
                    if (data.success && data.calls) {
                        // Cache for instant restore on next page load
                        try {
                            localStorage.setItem('cat_calls_cache', JSON.stringify(data));
                        }
                        catch (_e) { /* quota */ }
                        // Track my own active call for heartbeat failure notification
                        const myId = String(this.apiManager.playerId || '');
                        this._myCurrentCall = myId ? (data.calls.find((c) => String(c.callerId) === myId) ?? null) : null;
                        if (this._myCurrentCall) {
                            // Reset notif flag when a fresh call is detected
                            this._heartbeatFailedNotifShown = false;
                        }
                        if (this.onCallsUpdate) {
                            this.onCallsUpdate(data.calls);
                        }
                        if (data.rallies && this.onRalliesUpdate) {
                            this.onRalliesUpdate(data.rallies);
                        }
                        if (data.statuses && this.onStatusesUpdate) {
                            this.onStatusesUpdate(data.statuses);
                        }
                        const revivableData = data.revivableData;
                        if (revivableData && this.onRevivableDataUpdate) {
                            this.onRevivableDataUpdate(revivableData);
                        }
                        const tacticalMarkers = data.tacticalMarkers;
                        if (tacticalMarkers && this.onTacticalMarkersUpdate) {
                            this.onTacticalMarkersUpdate(tacticalMarkers);
                        }
                        const softUncalls = data.softUncalls;
                        if (softUncalls && this.onSoftUncallsUpdate) {
                            this.onSoftUncallsUpdate(softUncalls);
                        }
                        const enemyChainData = data.enemyChainData;
                        if (enemyChainData && this.onEnemyChainUpdate) {
                            this.onEnemyChainUpdate(enemyChainData);
                        }
                        const chainBonusAssignment = data.chainBonusAssignment;
                        if (this.onChainBonusAssignmentUpdate) {
                            this.onChainBonusAssignmentUpdate(chainBonusAssignment || null);
                        }
                        const ffStats = data.ffStats;
                        if (ffStats && this.onFFStatsUpdate) {
                            this.onFFStatsUpdate(ffStats);
                        }
                        const ffsUpdatedIds = data.ffsUpdatedIds;
                        if (ffsUpdatedIds && ffsUpdatedIds.length > 0) {
                            invalidateFFSCache(ffsUpdatedIds);
                        }
                        return true;
                    }
                }
            }
            catch (error) {
                console.log('[HTTP] Error fetching calls:', error);
                this.apiManager.reportError('fetchCalls', error);
            }
            finally {
                this._fetchingCalls = false;
            }
            return false;
        }
        startPolling() {
            this.stopPolling();
            this._pollingActive = true;
            // Extension mode: use WebSocket instead of HTTP polling
            if (isExtensionMode()) {
                // Register WS message handler (always, even before connection is ready)
                extensionWSOnMessage((data) => {
                    if ((data.type === 'init' || data.type === 'update') && data.calls) {
                        const blocked = this._rallyInFlight || this._callInFlight > 0 || Date.now() < this._callCooldownUntil;
                        if (blocked)
                            return;
                        if (this.onCallsUpdate)
                            this.onCallsUpdate(data.calls);
                        if (data.rallies && this.onRalliesUpdate)
                            this.onRalliesUpdate(data.rallies);
                        if (data.statuses && this.onStatusesUpdate)
                            this.onStatusesUpdate(data.statuses);
                        if (data.tacticalMarkers && this.onTacticalMarkersUpdate)
                            this.onTacticalMarkersUpdate(data.tacticalMarkers);
                        if (data.softUncalls && this.onSoftUncallsUpdate)
                            this.onSoftUncallsUpdate(data.softUncalls);
                        if (this.onChainBonusAssignmentUpdate)
                            this.onChainBonusAssignmentUpdate(data.chainBonusAssignment || null);
                        try {
                            localStorage.setItem('cat_calls_cache', JSON.stringify(data));
                        }
                        catch (_e) { }
                    }
                });
                // Connect WS — retry until authToken is available
                // Initial connect uses own faction; reconnectWS() is called after checkActivationStatus() to fix admin cross-faction case
                const tryConnectWS = () => {
                    const ownFactionId = StorageUtil.get('cat_user_faction_id', null);
                    if (ownFactionId && this.apiManager.authToken) {
                        const isAdmin = !!window.FactionWarEnhancer?.subscriptionData?.isAdmin
                            || !!this._enhancer?.subscriptionData?.isAdmin
                            || StorageUtil.get('cat_is_admin_cached', '') === 'true';
                        // If viewing another faction and admin not yet confirmed, defer WS connect
                        // to reconnectWS() which runs after checkActivationStatus() resolves.
                        // Connecting to own faction would push calls=0 and overwrite enemy faction calls.
                        if (state.catOtherFaction && !isAdmin) {
                            setTimeout(tryConnectWS, 500);
                            return;
                        }
                        let factionId = ownFactionId;
                        if (isAdmin && state.catOtherFaction && state.viewingFactionId) {
                            factionId = state.viewingFactionId;
                        }
                        extensionWSConnect(this.apiManager.serverUrl, this.apiManager.authToken, factionId);
                        this.fetchCalls();
                        console.log('[CAT] WebSocket mode — initial fetch + real-time push');
                    }
                    else {
                        setTimeout(tryConnectWS, 1000);
                    }
                };
                tryConnectWS();
                // Watchdog: fallback to HTTP polling when WS is down
                this._wsWatchdogInterval = setInterval(() => {
                    if (!this._pollingActive)
                        return;
                    const wsUp = extensionWSConnected();
                    if (!wsUp && !this._wsFallbackActive) {
                        // WS just went down — start fallback polling
                        this._wsFallbackActive = true;
                        const fallbackPoll = async () => {
                            if (!this._pollingActive || !this._wsFallbackActive)
                                return;
                            await this.fetchCalls();
                            if (this._pollingActive && this._wsFallbackActive) {
                                this._wsFallbackTimeout = setTimeout(fallbackPoll, 500);
                            }
                        };
                        this._wsFallbackTimeout = setTimeout(fallbackPoll, 0);
                    }
                    else if (wsUp && this._wsFallbackActive) {
                        // WS came back up — stop fallback polling
                        this._wsFallbackActive = false;
                        if (this._wsFallbackTimeout) {
                            clearTimeout(this._wsFallbackTimeout);
                            this._wsFallbackTimeout = null;
                        }
                    }
                }, 2000);
                return; // Don't start HTTP polling loop
            }
            const poll = async () => {
                if (!this._pollingActive)
                    return;
                await this.fetchCalls();
                if (this._pollingActive) {
                    this._pollingTimeout = setTimeout(poll, this.pollRate);
                }
            };
            this._pollingTimeout = setTimeout(poll, 0);
        }
        /**
         * Reconnect WS with the correct faction (call after checkActivationStatus resolves).
         * No-op in HTTP polling mode.
         */
        reconnectWS() {
            if (!isExtensionMode())
                return;
            const ownFactionId = StorageUtil.get('cat_user_faction_id', null);
            if (!ownFactionId || !this.apiManager.authToken)
                return;
            const isAdmin = !!window.FactionWarEnhancer?.subscriptionData?.isAdmin
                || !!this._enhancer?.subscriptionData?.isAdmin
                || StorageUtil.get('cat_is_admin_cached', '') === 'true';
            let factionId = ownFactionId;
            if (isAdmin && state.catOtherFaction && state.viewingFactionId) {
                factionId = state.viewingFactionId;
            }
            extensionWSConnect(this.apiManager.serverUrl, this.apiManager.authToken, factionId);
            this.fetchCalls();
        }
        stopPolling() {
            this._pollingActive = false;
            if (this._pollingTimeout) {
                clearTimeout(this._pollingTimeout);
                this._pollingTimeout = null;
            }
            if (this.pollingInterval) {
                clearInterval(this.pollingInterval);
                this.pollingInterval = null;
            }
            if (this._wsWatchdogInterval) {
                clearInterval(this._wsWatchdogInterval);
                this._wsWatchdogInterval = null;
            }
            if (this._wsFallbackTimeout) {
                clearTimeout(this._wsFallbackTimeout);
                this._wsFallbackTimeout = null;
            }
            this._wsFallbackActive = false;
        }
        startHeartbeat() {
            this.stopHeartbeat();
            this.heartbeatInterval = setInterval(async () => {
                try {
                    await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/heartbeat`, {
                        method: 'POST',
                        headers: this.getHeaders(),
                        body: JSON.stringify({
                            playerId: this.apiManager.playerId,
                            page: this.isOnWarPage() ? 'war' : 'other'
                        })
                    });
                    // Heartbeat succeeded — reset notif flag so it can fire again next outage
                    this._heartbeatFailedNotifShown = false;
                }
                catch (e) {
                    this.apiManager.reportError('heartbeat', e);
                    // Show warning if caller has an active call with Okay target
                    if (!this._heartbeatFailedNotifShown && this._myCurrentCall && (this._myCurrentCall.targetStatus || '').toLowerCase() === 'okay') {
                        this._heartbeatFailedNotifShown = true;
                        const memberName = this._myCurrentCall.memberName || 'your target';
                        this.apiManager.showNotification(`Connection lost — your call on ${memberName} may be auto-cancelled`, 'warning');
                    }
                }
            }, 30000);
        }
        stopHeartbeat() {
            if (this.heartbeatInterval) {
                clearInterval(this.heartbeatInterval);
                this.heartbeatInterval = null;
            }
        }
        listenCrossTabSignals() {
            window.addEventListener('storage', (e) => {
                if ((e.key === 'cat_tactical_marker_signal' || e.key === 'cat_attacking_signal') && e.newValue) {
                    this.fetchCalls();
                }
            });
        }
        stop() {
            this.stopPolling();
            this.stopHeartbeat();
            this._isActive = false;
            if (this.onConnectionChange) {
                this.onConnectionChange(false);
            }
        }
        queueStatusUpdate(memberId, statusData) {
            const userFaction = StorageUtil.get('cat_user_faction_id', null);
            if (!userFaction || !memberId)
                return;
            const idx = this._pendingStatuses.findIndex(s => s.memberId === String(memberId));
            const entry = {
                memberId: String(memberId),
                factionId: statusData.factionId || null,
                status: statusData.status,
                details: statusData.details || null,
                until: statusData.until || null,
                previousStatus: statusData.previousStatus || null,
                previousArea: statusData.previousArea != null ? statusData.previousArea : null,
                departedAt: statusData.departedAt || null
            };
            if (idx !== -1) {
                this._pendingStatuses[idx] = entry;
            }
            else {
                this._pendingStatuses.push(entry);
            }
            // Absolute deadline: flush within 500ms of the *first* queued update.
            // Never reset timer on each call — that creates a sliding window that never fires under rapid WS traffic.
            const now = Date.now();
            if (!this._statusFlushTimer) {
                this._statusFlushDeadline = now + 500;
                this._statusFlushTimer = setTimeout(() => this.flushStatusUpdates(), 500);
            }
            else {
                const remaining = this._statusFlushDeadline - now;
                if (remaining <= 0) {
                    // Deadline already past (shouldn't happen, but guard)
                    clearTimeout(this._statusFlushTimer);
                    this._statusFlushTimer = setTimeout(() => this.flushStatusUpdates(), 0);
                }
                // else: timer already running, let it fire at the absolute deadline
            }
        }
        async flushStatusUpdates() {
            this._statusFlushTimer = null;
            this._statusFlushDeadline = 0;
            if (this._pendingStatuses.length === 0)
                return;
            const batch = this._pendingStatuses.splice(0);
            const userFaction = StorageUtil.get('cat_user_faction_id', null);
            // Group by factionId — entries without factionId go under userFaction
            const groups = new Map();
            for (let i = 0; i < batch.length; i++) {
                const entry = batch[i];
                const fid = entry.factionId || userFaction || '';
                if (!fid)
                    continue;
                let arr = groups.get(fid);
                if (!arr) {
                    arr = [];
                    groups.set(fid, arr);
                }
                arr.push(entry);
            }
            for (const [fid, entries] of groups) {
                try {
                    await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/status-update`, {
                        method: 'POST',
                        headers: this.getHeaders(),
                        body: JSON.stringify({
                            factionId: fid,
                            statuses: entries,
                            updatedBy: this.apiManager.playerId || null
                        })
                    });
                }
                catch (error) {
                    this.apiManager.reportError('flushStatuses', error);
                    this._pendingStatuses.push(...entries);
                }
            }
        }
        isActive() {
            return this._isActive;
        }
    }

    /**
     * Loadout Tooltip — shows defender loadout on hover of enemy member name (war page)
     * Data is fetched lazily from /api/loadout/:targetId and cached in memory.
     */
    const BONUS_DESC = {
        // Armor bonuses
        'impregnable': 'Reduced opponent melee damage (%)',
        'impenetrable': 'Reduces opponent bullet damage (%) — MG, Pistol, Rifle, Shotgun, SMG',
        'insurmountable': 'Reduced opponent damage (%) when under 1/4 life',
        'invulnerable': 'Negative Status Effects reduced (%), except distraction and DOT effects',
        'imperviable': 'Increase (%) to maximum life',
        'immutable': 'Increase (%) to Defense Passives',
        'irrepressible': 'Increase (%) to Dexterity Passives',
        'impassable': 'Chance (%) to fully block incoming damage',
        'achilles': 'Increased Foot damage (%)',
        'assassinate': 'Increased damage (%) on the first turn',
        'backstab': 'Chance (%) of double damage when opponent is distracted',
        'berserk': 'Increased Damage (%), Reduced Hit Chance (half%)',
        'bleed': 'DOT: damage ticks starting at 45% over 9 turns',
        'blindfire': 'Empties remaining clip in one turn, accuracy -5 per action',
        'blindside': 'Increased damage (%) if target has full life',
        'bloodlust': 'Life regenerated by a (%) of damage dealt',
        'burn': 'DOT: ticks starting at 45% over 3 turns',
        'comeback': 'Increased damage (%) while under 1/4 life',
        'conserve': 'Increases (%) ammo conservation',
        'cripple': 'Chance (%) to reduce opponent Dexterity by 25% (x3)',
        'crusher': 'Increased Head damage (%)',
        'cupid': 'Increased Heart damage (%)',
        'deadeye': 'Increased Critical hit damage (%)',
        'deadly': 'Chance of a hit dealing +500% damage',
        'demoralized': 'Additive 10% debuff to all opponent stats (up to x5)',
        'disarm': 'Disables opponent weapon for (X) turns on arm/hand hit',
        'double-edged': 'Chance (%) of double damage at the cost of self injury',
        'double tap': 'Chance of hitting twice in a single turn',
        'emasculate': '% of max happy gained if used for finishing hit',
        'empower': 'Increased Strength (%) while using this weapon',
        'eviscerate': 'Opponent receives extra damage (%) under this effect',
        'execute': 'Instant defeat when opponent is below (%) life',
        'expose': 'Increased critical hit rate (%)',
        'finale': 'Increased damage (%) for every turn weapon not used',
        'focus': 'Hit Chance increase (%) for every successive miss',
        'freeze': 'Additive 50% debuff to opponent Speed and Dexterity',
        'frenzy': '(%) Increase to Damage and Accuracy on each successive hit',
        'fury': 'Chance (%) of hitting twice in a single turn',
        'grace': 'Increased Hit Chance (%), Reduced Damage (half%)',
        'hazardous': 'You receive a % of the damage you deal in the same hit',
        'home run': 'Chance (%) to deflect incoming temporary items',
        'irradiate': '1-3 hours radiation poisoning on finishing hit (100%)',
        'laceration': 'DOT: ticks starting at 90% over 9 turns',
        'motivation': 'Chance (%) to increase all stats by 10% (up to x5)',
        'paralyze': '50% chance to miss turns for 300 seconds',
        'parry': '(%) Chance to block incoming melee attacks',
        'penetrate': 'Ignores (%) of enemy armor mitigation',
        'plunder': 'Increase (%) money mugged on finishing hit',
        'poisoned': 'DOT: ticks starting at 95% over 19 turns',
        'powerful': 'Increased Damage (%)',
        'proficience': 'Increase XP (%) gained on finishing hit',
        'puncture': 'Chance (%) of ignoring armor and defensive bonuses',
        'quicken': 'Increased Speed (%) while using this weapon',
        'rage': 'Chance (%) of hitting 2-8 times in a single turn',
        'revitalize': 'Chance (%) to restore energy spent on finishing hit',
        'roshambo': 'Increased Groin damage (%)',
        'severe burning': 'DOT: ticks starting at 45% over 3 turns',
        'shock': 'Chance (%) to cause opponent to miss next turn',
        'sleep': 'Causes enemy to miss turns until they receive damage',
        'slow': 'Chance (%) to reduce opponent Speed by 25% (x3)',
        'smurf': '(%) damage increase for each level under opponent',
        'specialist': 'Increased Damage (%), weapon limited to a single clip',
        'spray': 'Empties clip in one turn, deals double damage',
        'storage': 'Allows use of two temporary items in a fight',
        'stricken': '(%) Increased hospital time on finishing hit',
        'stun': 'Chance (%) to cause opponent to miss next turn',
        'suppress': '(%) chance to give opponent 25% chance to miss future turns',
        'sure shot': 'Chance (%) at a guaranteed hit',
        'throttle': 'Increased Throat damage (%)',
        'toxin': '25% debuff to a random stat (Strength/Speed/Dex/Defense, up to x3)',
        'warlord': 'Increases respect gained (%)',
        'weaken': 'Chance (%) to reduce opponent Defense by 25% (x3)',
        'wind-up': 'Increased damage (%) after spending a turn to wind up',
        'wither': 'Chance (%) to reduce opponent Strength by 25% (x3)',
    };
    function _catGet(url, headers, onload, onerror) {
        // Prefer apiManager.httpRequest — it handles PDA, extension, GM, plain fetch natively
        const apiMgr = window.FactionWarEnhancer?.apiManager;
        if (apiMgr && typeof apiMgr.httpRequest === 'function') {
            apiMgr.httpRequest(url, { method: 'GET', headers })
                .then((resp) => resp.json().then(onload).catch(onerror))
                .catch(onerror);
            return;
        }
        // Fallback: extension bridge or plain GM/fetch
        const bridgeNow = typeof document !== 'undefined' && document.documentElement.getAttribute('data-cat-bridge') === 'ready';
        if (bridgeNow || isExtensionMode()) {
            extensionFetch(url, { method: 'GET', headers }).then(resp => {
                resp.json().then(onload).catch(onerror);
            }).catch(onerror);
        }
        else if (typeof GM_xmlhttpRequest !== 'undefined') {
            GM_xmlhttpRequest({
                method: 'GET', url, headers,
                onload: (r) => { try {
                    onload(JSON.parse(r.responseText));
                }
                catch {
                    onerror();
                } },
                onerror
            });
        }
        else {
            fetch(url, { method: 'GET', headers })
                .then(r => r.json()).then(onload).catch(onerror);
        }
    }
    // Slot keys that are weapons/armor (exclude cosmetics slot 10+)
    const WEAPON_SLOTS = { 1: 'Primary', 2: 'Secondary', 3: 'Melee', 5: 'Temp' };
    const ARMOR_SLOTS = { 4: 'Body', 6: 'Helmet', 7: 'Pants', 8: 'Boots', 9: 'Gloves' };
    const RARITY_COLOR = {
        'glow-red': '#ef5350',
        'glow-orange': '#ff9800',
        'glow-yellow': '#fdd835',
    };
    function relativeTime(isoStr) {
        const ms = Date.now() - new Date(isoStr).getTime();
        const mins = Math.floor(ms / 60000);
        if (mins < 1)
            return 'just now';
        const hrs = Math.floor(ms / 3600000);
        if (hrs < 1)
            return `${mins}m ago`;
        const days = Math.floor(ms / 86400000);
        if (days < 1)
            return `${hrs}h ago`;
        return `${days}d ago`;
    }
    function formatStat(v) {
        const n = Number(v);
        return Number.isFinite(n) ? n.toFixed(1) : '-';
    }
    function buildTooltipHtml(row) {
        const slots = row.items;
        const lines = [];
        // Weapons
        const weaponItems = [];
        for (const [slotNum, label] of Object.entries(WEAPON_SLOTS)) {
            const slotData = slots[slotNum];
            const item = slotData?.item?.[0];
            if (!item || item.ID === 999)
                continue;
            const color = RARITY_COLOR[item.glowClass || ''] || '#e2e8f0';
            const bonusSpans = item.currentBonuses
                ? Object.values(item.currentBonuses).map(b => {
                    const pctMatch = (b.desc || '').match(/(\d+(?:\.\d+)?)\s*%/);
                    const pct = pctMatch ? ` <span class="cat-lo-bonus-pct">${pctMatch[1]}%</span>` : '';
                    return `<span class="cat-lo-bonus-item" data-bonus="${b.title.toLowerCase().trim()}" data-bonus-desc="${b.desc || ''}">${b.title}${pct}</span>`;
                }).join('<span class="cat-lo-bonus-sep">, </span>')
                : '';
            const stats = (Number(item.dmg) > 0 || Number(item.acc) > 0)
                ? ` <span class="cat-lo-stat">${formatStat(item.dmg)}dmg ${formatStat(item.acc)}acc</span>`
                : '';
            const bonusHtml = bonusSpans ? ` <span class="cat-lo-bonus">${bonusSpans}</span>` : '';
            weaponItems.push(`<div class="cat-lo-row"><span class="cat-lo-slot">${label}</span>` +
                `<span class="cat-lo-name" style="color:${color}">${item.name}</span>` +
                `${stats}${bonusHtml}</div>`);
        }
        if (weaponItems.length) {
            lines.push('<div class="cat-lo-section">Weapons</div>');
            lines.push(...weaponItems);
        }
        // Armor
        const armorItems = [];
        for (const [slotNum, label] of Object.entries(ARMOR_SLOTS)) {
            const slotData = slots[slotNum];
            const item = slotData?.item?.[0];
            if (!item)
                continue;
            const color = RARITY_COLOR[item.glowClass || ''] || '#a0aec0';
            const bonusSpans = item.currentBonuses
                ? Object.values(item.currentBonuses).map(b => {
                    const pctMatch = (b.desc || '').match(/(\d+(?:\.\d+)?)\s*%/);
                    const pct = pctMatch ? ` <span class="cat-lo-bonus-pct">${pctMatch[1]}%</span>` : '';
                    return `<span class="cat-lo-bonus-item" data-bonus="${b.title.toLowerCase().trim()}" data-bonus-desc="${b.desc || ''}">${b.title}${pct}</span>`;
                }).join('<span class="cat-lo-bonus-sep">, </span>')
                : '';
            const bonusHtml = bonusSpans ? ` <span class="cat-lo-bonus">${bonusSpans}</span>` : '';
            armorItems.push(`<div class="cat-lo-row"><span class="cat-lo-slot">${label}</span>` +
                `<span class="cat-lo-name" style="color:${color}">${item.name}</span>` +
                `${bonusHtml}</div>`);
        }
        if (armorItems.length) {
            lines.push('<div class="cat-lo-section">Armor</div>');
            lines.push(...armorItems);
        }
        const capturedDate = new Date(row.captured_at);
        const dateStr = capturedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' }) +
            ' ' + capturedDate.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone: 'UTC' }) + ' TCT';
        lines.push(`<div class="cat-lo-footer">` +
            `<span class="cat-lo-footer-label">Snapshot</span>` +
            `<span title="${dateStr}">${relativeTime(row.captured_at)}</span>` +
            (row.attacker_name ? `<span class="cat-lo-footer-label">by</span><span>${row.attacker_name}</span>` : '') +
            `<span class="cat-lo-date">${dateStr}</span>` +
            `</div>`);
        return lines.join('');
    }
    // localStorage-backed cache with stale-while-revalidate
    // Cache is keyed per war (userFactionId_enemyFactionId) — auto-invalidated when war changes
    const LS_KEY = 'cat_loadout_cache';
    const LS_WAR_KEY = 'cat_loadout_cache_war';
    const STALE_MS = 5 * 60 * 1000; // revalidate after 5 min
    function _getWarKey() {
        try {
            const userFaction = StorageUtil.get('cat_user_faction_id', null) || '';
            const enemyRaw = StorageUtil.get('cat_enemy_faction_id', null);
            const enemyFaction = enemyRaw?.id || '';
            return `${userFaction}_${enemyFaction}`;
        }
        catch {
            return '';
        }
    }
    function _lsLoad() {
        try {
            const warKey = _getWarKey();
            const storedWarKey = localStorage.getItem(LS_WAR_KEY);
            // Different war — discard cache
            if (warKey && storedWarKey !== warKey) {
                localStorage.removeItem(LS_KEY);
                localStorage.setItem(LS_WAR_KEY, warKey);
                return new Map();
            }
            const raw = localStorage.getItem(LS_KEY);
            if (!raw)
                return new Map();
            const obj = JSON.parse(raw);
            return new Map(Object.entries(obj));
        }
        catch {
            return new Map();
        }
    }
    function _lsSave(map) {
        try {
            localStorage.setItem(LS_WAR_KEY, _getWarKey());
            const obj = {};
            for (const [k, v] of map)
                obj[k] = v;
            localStorage.setItem(LS_KEY, JSON.stringify(obj));
        }
        catch { }
    }
    const _lsCache = _lsLoad();
    // In-memory cache: targetId → loadout row (pre-populated from localStorage)
    const _cache = new Map(Array.from(_lsCache.entries()).map(([k, v]) => [k, v.row]));
    const _pending = new Set();
    /** IDs fetched but with no loadout on server — hide icon */
    const _noLoadout = new Set();
    /** Returns true if a loadout is already cached for this targetId (used to show shirt icon immediately on page load) */
    function hasLoadoutCached(targetId) {
        return _cache.has(targetId);
    }
    /**
     * Silently prefetch a loadout for targetId.
     * onReady(true) called when loadout is available, onReady(false) if server has none.
     * No-op if already cached or pending.
     */
    function prefetchLoadout(targetId, onReady) {
        if (_cache.has(targetId)) {
            onReady(true);
            return;
        }
        if (_noLoadout.has(targetId)) {
            onReady(false);
            return;
        }
        if (_pending.has(targetId))
            return; // already in flight
        const factionId = StorageUtil.get('cat_user_faction_id', null);
        const authToken = StorageUtil.get('cat_auth_token', null) || localStorage.getItem('cat_auth_token');
        const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat.dgh.sh';
        if (!factionId || !authToken)
            return;
        _pending.add(targetId);
        _catGet(`${serverUrl}/api/loadout/${targetId}?factionId=${factionId}`, { 'Authorization': `Bearer ${authToken}` }, (data) => {
            _pending.delete(targetId);
            if (data?.success && data?.loadout) {
                const fresh = data.loadout;
                _cache.set(targetId, fresh);
                _lsCache.set(targetId, { row: fresh, ts: Date.now() });
                _lsSave(_lsCache);
                onReady(true);
            }
            else {
                _noLoadout.add(targetId);
                document.querySelectorAll(`.cat-loadout-btn[data-cat-loadout-id="${targetId}"]`).forEach(el => { el.style.display = 'none'; });
                onReady(false);
            }
        }, () => { _pending.delete(targetId); onReady(false); });
    }
    function setupLoadoutTooltips() {
        let currentTip = null;
        let currentTargetId = null;
        let _hideTimer = null;
        const cancelHide = () => { if (_hideTimer) {
            clearTimeout(_hideTimer);
            _hideTimer = null;
        } };
        const hideNow = () => {
            cancelHide();
            if (currentTip) {
                currentTip.remove();
                currentTip = null;
            }
            currentTargetId = null;
        };
        const hide = (delay = 0) => {
            cancelHide();
            if (delay > 0) {
                _hideTimer = setTimeout(hideNow, delay);
            }
            else {
                hideNow();
            }
        };
        const show = (anchor, targetId, row) => {
            hideNow();
            currentTargetId = targetId;
            const tip = document.createElement('div');
            tip.className = 'cat-loadout-tooltip';
            tip.innerHTML = buildTooltipHtml(row);
            document.body.appendChild(tip);
            currentTip = tip;
            // Position: above anchor, centered
            const rect = anchor.getBoundingClientRect();
            const tipRect = tip.getBoundingClientRect();
            let left = rect.left + (rect.width - tipRect.width) / 2;
            left = Math.max(6, Math.min(left, window.innerWidth - tipRect.width - 6));
            const top = rect.top - tipRect.height - 1;
            tip.style.left = left + 'px';
            tip.style.top = (top > 0 ? top : rect.bottom + 1) + 'px';
        };
        const doFetch = (anchor, targetId, silent) => {
            const factionId = StorageUtil.get('cat_user_faction_id', null);
            const authToken = StorageUtil.get('cat_auth_token', null) || localStorage.getItem('cat_auth_token');
            const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat.dgh.sh';
            if (!factionId || !authToken)
                return;
            _pending.add(targetId);
            if (!silent) {
                // Show loading indicator — clear any existing tip first
                hideNow();
                const loadingTip = document.createElement('div');
                loadingTip.className = 'cat-loadout-tooltip cat-loadout-loading';
                loadingTip.textContent = 'Loading loadout…';
                document.body.appendChild(loadingTip);
                currentTip = loadingTip;
                currentTargetId = targetId;
                const rect = anchor.getBoundingClientRect();
                loadingTip.style.left = rect.left + 'px';
                loadingTip.style.top = (rect.top - 30) + 'px';
            }
            _catGet(`${serverUrl}/api/loadout/${targetId}?factionId=${factionId}`, { 'Authorization': `Bearer ${authToken}` }, (data) => {
                _pending.delete(targetId);
                if (data?.success && data?.loadout) {
                    const fresh = data.loadout;
                    _cache.set(targetId, fresh);
                    _lsCache.set(targetId, { row: fresh, ts: Date.now() });
                    _lsSave(_lsCache);
                    // Reveal all buttons for this target (score cell + hospital btn)
                    document.querySelectorAll(`.cat-loadout-btn[data-cat-loadout-id="${targetId}"]`).forEach(el => { el.style.display = 'flex'; });
                    if (currentTargetId === targetId) {
                        show(anchor, targetId, fresh); // update tooltip if still open
                    }
                }
                else {
                    _noLoadout.add(targetId);
                    // Hide the icon in all score cells for this target
                    document.querySelectorAll(`.cat-loadout-btn[data-cat-loadout-id="${targetId}"]`).forEach(el => { el.style.display = 'none'; });
                    if (!silent)
                        hide();
                }
            }, () => { _pending.delete(targetId); if (!silent)
                hide(); });
        };
        const fetchAndShow = (anchor, targetId) => {
            // Already showing this target — just cancel any pending hide
            if (currentTargetId === targetId && currentTip) {
                cancelHide();
                return;
            }
            const cached = _cache.get(targetId);
            const lsEntry = _lsCache.get(targetId);
            const isStale = !lsEntry || (Date.now() - lsEntry.ts > STALE_MS);
            if (cached) {
                // Show immediately from cache
                anchor.style.display = 'flex';
                show(anchor, targetId, cached);
                // Revalidate in background if stale
                if (isStale && !_pending.has(targetId))
                    doFetch(anchor, targetId, true);
                return;
            }
            // No cache — fetch with loading indicator
            if (_pending.has(targetId))
                return;
            doFetch(anchor, targetId, false);
        };
        const _isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        // Single mousemove handler — open on loadout btn, close when leaving both btn and tip
        // Disabled on PDA: touch events don't map to mousemove reliably, use click handler instead
        document.addEventListener('mousemove', (e) => {
            if (_isPDA)
                return;
            const over = document.elementFromPoint(e.clientX, e.clientY);
            if (!over)
                return;
            // Over the loadout button?
            const btn = over.closest('.cat-loadout-btn[data-cat-loadout-id]');
            if (btn && !over.closest('[data-cat-tooltip]')) {
                cancelHide();
                const targetId = btn.dataset.catLoadoutId;
                if (currentTargetId !== targetId || !currentTip) {
                    fetchAndShow(btn, targetId);
                }
                return;
            }
            // Over the tooltip itself?
            if (currentTip && currentTip.contains(over)) {
                cancelHide();
                return;
            }
            // Not over btn or tip — schedule hide
            hide(200);
        });
        // Bonus sub-tooltip
        let bonusTip = null;
        document.addEventListener('mouseout', (e) => {
            const target = e.target.closest('.cat-lo-bonus-item');
            if (!target)
                return;
            const to = e.relatedTarget;
            if (to && target.contains(to))
                return;
            if (bonusTip) {
                bonusTip.remove();
                bonusTip = null;
            }
        });
        const showBonusTip = (target) => {
            const key = (target.dataset.bonus || '').toLowerCase().trim();
            const desc = target.dataset.bonusDesc || BONUS_DESC[key];
            if (!desc)
                return;
            if (bonusTip) {
                bonusTip.remove();
                bonusTip = null;
            }
            bonusTip = document.createElement('div');
            bonusTip.className = 'cat-lo-bonus-desc';
            bonusTip.textContent = desc;
            bonusTip.style.visibility = 'hidden';
            document.body.appendChild(bonusTip);
            const rect = target.getBoundingClientRect();
            const tipRect = bonusTip.getBoundingClientRect();
            let left = rect.left + (rect.width - tipRect.width) / 2;
            left = Math.max(6, Math.min(left, window.innerWidth - tipRect.width - 6));
            const topAbove = rect.top - tipRect.height - 4;
            bonusTip.style.left = left + 'px';
            bonusTip.style.top = (topAbove > 0 ? topAbove : rect.bottom + 4) + 'px';
            bonusTip.style.visibility = '';
        };
        // Refactor mouseover to use shared function
        document.addEventListener('mouseover', (e) => {
            const target = e.target.closest('.cat-lo-bonus-item');
            if (!target || !target.closest('.cat-loadout-tooltip'))
                return;
            showBonusTip(target);
        });
        // Mobile/PDA: tap the button (only reachable once visible)
        document.addEventListener('click', (e) => {
            // Bonus sub-tooltip tap (mobile)
            const bonusTarget = e.target.closest('.cat-lo-bonus-item');
            if (bonusTarget && bonusTarget.closest('.cat-loadout-tooltip')) {
                e.stopPropagation();
                if (bonusTip) {
                    bonusTip.remove();
                    bonusTip = null;
                    return;
                }
                showBonusTip(bonusTarget);
                return;
            }
            const target = e.target.closest('.cat-loadout-btn[data-cat-loadout-id]');
            if (!target) {
                if (bonusTip) {
                    bonusTip.remove();
                    bonusTip = null;
                }
                if (currentTip)
                    hide();
                return;
            }
            e.stopPropagation();
            e.preventDefault();
            const targetId = target.dataset.catLoadoutId;
            if (currentTip && currentTargetId === targetId) {
                hide();
                return;
            }
            fetchAndShow(target, targetId);
        }, true);
    }

    const LOADOUT_ICON_B64 = 'data:image/png;base64,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';

    function sortByBSP(factionList, headerElement) {
        // Detect faction — prefer DOM position over member classes (more reliable)
        const inYourFaction = !!(factionList.closest('.your-faction') || factionList.closest('[class*="your-faction"]'));
        const inEnemyFaction = !!(factionList.closest('.enemy-faction') || factionList.closest('[class*="enemy-faction"]'));
        let isYourFaction;
        if (inYourFaction && !inEnemyFaction) {
            isYourFaction = true;
        }
        else if (inEnemyFaction && !inYourFaction) {
            isYourFaction = false;
        }
        else {
            // Fallback to member classes
            const hasYourMembers = factionList.querySelector('li.your, li[class*="your___"]') !== null;
            const hasEnemyMembers = factionList.querySelector('li.enemy, li[class*="enemy"]') !== null;
            if (hasYourMembers && !hasEnemyMembers)
                isYourFaction = true;
            else if (hasEnemyMembers && !hasYourMembers)
                isYourFaction = false;
            else
                isYourFaction = !!(headerElement.closest('.your-faction') || headerElement.closest('[class*="your-faction"]'));
        }
        const factionKey = isYourFaction ? 'your' : 'enemy';
        const currentSort = headerElement.getAttribute('data-sort') || 'none';
        let newSort = 'asc';
        if (currentSort === 'asc') {
            newSort = 'desc';
        }
        else if (currentSort === 'desc') {
            newSort = 'asc';
        }
        // Clear sort from other headers
        const bspHeaderRow = headerElement.closest('.white-grad');
        if (bspHeaderRow) {
            bspHeaderRow.querySelectorAll('[data-sort]').forEach(el => {
                if (el !== headerElement)
                    el.removeAttribute('data-sort');
            });
        }
        headerElement.setAttribute('data-sort', newSort);
        headerElement.setAttribute('data-col', 'bsp');
        // Update header text with column + sort arrow
        const sortArrow = newSort === 'asc' ? '▲' : '▼';
        const firstText = headerElement.childNodes[0];
        if (firstText && firstText.nodeType === Node.TEXT_NODE) {
            firstText.textContent = `BSP ${sortArrow}`;
        }
        StorageUtil.set(`cat_sort_preference_${factionKey}`, {
            column: 'bsp',
            direction: newSort
        });
        let members = Array.from(factionList.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]'));
        if (members.length === 0) {
            members = Array.from(factionList.querySelectorAll('li'));
        }
        if (members.length === 0) {
            return;
        }
        // Pre-build value map — avoids querySelectorAll inside comparator
        const bspMap = new Map();
        for (let i = 0; i < members.length; i++)
            bspMap.set(members[i], this.getBSPValue(members[i]));
        members.sort((a, b) => {
            const bspA = bspMap.get(a);
            const bspB = bspMap.get(b);
            return newSort === 'asc' ? bspA - bspB : bspB - bspA;
        });
        const parentSet = new Set(members.map(m => m.parentNode).filter((p) => p instanceof HTMLElement));
        parentSet.forEach((parent) => {
            if (!parent.dataset.catFlex) {
                parent.style.display = 'flex';
                parent.style.flexDirection = 'column';
                parent.dataset.catFlex = '1';
            }
        });
        members.forEach((member, i) => {
            member.style.order = String(i);
        });
    }
    function getBSPValue(memberElement) {
        const bspColumn = memberElement.querySelector('.bsp-column, .bsp-value');
        if (bspColumn) {
            const text = (bspColumn.textContent || '').trim();
            if (text && text !== 'N/A' && text !== '') {
                return this.parseBSPText(text);
            }
        }
        const bspElement = memberElement.querySelector('.iconStats');
        if (bspElement) {
            const text = (bspElement.textContent || '').trim();
            if (text && text !== 'N/A' && text !== '') {
                return this.parseBSPText(text);
            }
        }
        return 999999;
    }
    function parseBSPText(text) {
        text = text.toLowerCase().trim();
        const match = text.match(/^([0-9.,-]+)([kmbt]?)$/);
        if (!match) {
            return 999999;
        }
        const numberPart = parseFloat(match[1].replace(/,/g, ''));
        const unit = match[2];
        let multiplier = 1;
        switch (unit) {
            case 'k':
                multiplier = 1000;
                break;
            case 'm':
                multiplier = 1000000;
                break;
            case 'b':
                multiplier = 1000000000;
                break;
            case 't':
                multiplier = 1000000000000;
                break;
            default:
                multiplier = 1;
                break;
        }
        const value = numberPart * multiplier;
        return value;
    }
    function addStatusHeaderSorting(factionList) {
        const isYourFaction = !!(factionList.closest('.your-faction') || factionList.closest('[class*="your-faction"]'));
        let statusHeader = factionList.querySelector('.white-grad [class*="status___"]');
        if (!statusHeader) {
            // white-grad is typically a sibling of factionList, not a child — check parent containers
            const parentContainer = factionList.closest('.your-faction') ||
                factionList.closest('.enemy-faction') ||
                factionList.closest('[class*="enemy-faction"]') ||
                factionList.closest('[class*="tabMenuCont"]') ||
                factionList.closest('.f-war-list') ||
                factionList.parentElement;
            if (parentContainer) {
                statusHeader = parentContainer.querySelector('.white-grad [class*="status___"]');
            }
        }
        if (!statusHeader && !isYourFaction) {
            // Fallback for enemy faction only
            const allStatusHeaders = document.querySelectorAll('.white-grad [class*="status___"]');
            for (const header of allStatusHeaders) {
                const headerInYourFaction = header.closest('.your-faction') || header.closest('[class*="your-faction"]');
                if (!headerInYourFaction) {
                    statusHeader = header;
                    break;
                }
            }
        }
        if (statusHeader && !statusHeader.hasAttribute('data-sort-enabled')) {
            statusHeader.setAttribute('data-sort-enabled', 'true');
            // Invalidate containers cache — it may have been built before this header existed
            this._sortContainersCache = null;
            statusHeader.style.cursor = 'pointer';
            statusHeader.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                // Find member container from header context, not captured factionList
                const clickedHeaderRow = statusHeader.closest('.white-grad');
                const factionContainer = clickedHeaderRow?.closest('.your-faction') ||
                    clickedHeaderRow?.closest('[class*="your-faction"]') ||
                    clickedHeaderRow?.closest('.enemy-faction') ||
                    clickedHeaderRow?.closest('[class*="enemy-faction"]') ||
                    clickedHeaderRow?.closest('[class*="factionWrap"]') || null;
                let memberContainer = factionContainer?.querySelector('.f-war-list') ||
                    factionContainer?.querySelector('ul') ||
                    clickedHeaderRow?.nextElementSibling || null;
                if (!memberContainer || !memberContainer.querySelector('li')) {
                    memberContainer = factionList;
                }
                this.sortByStatus(memberContainer, statusHeader);
            });
        }
        // Add sorting for name, level, score columns
        let headerRow = statusHeader?.closest('.white-grad') || factionList.querySelector('.white-grad');
        // white-grad is typically a sibling of factionList — check parent containers
        if (!headerRow) {
            const parentContainer = factionList.closest('.your-faction') ||
                factionList.closest('.enemy-faction') ||
                factionList.closest('[class*="enemy-faction"]') ||
                factionList.closest('[class*="tabMenuCont"]') ||
                factionList.closest('[class*="your-faction"]') ||
                factionList.parentElement;
            if (parentContainer) {
                headerRow = parentContainer.querySelector('.white-grad');
            }
        }
        if (headerRow) {
            const nameHeader = headerRow.querySelector('[class*="member___"]');
            const levelHeader = headerRow.querySelector('[class*="level___"]');
            const scoreHeader = headerRow.querySelector('[class*="points___"]');
            const setupColumnSort = (header, column) => {
                if (!header || header.hasAttribute('data-sort-enabled'))
                    return;
                header.setAttribute('data-sort-enabled', 'true');
                header.style.cursor = 'pointer';
                header.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    // Find member container from header context, not captured factionList
                    const clickedHeaderRow = header.closest('.white-grad');
                    const factionContainer = clickedHeaderRow?.closest('.your-faction') ||
                        clickedHeaderRow?.closest('[class*="your-faction"]') ||
                        clickedHeaderRow?.closest('.enemy-faction') ||
                        clickedHeaderRow?.closest('[class*="enemy-faction"]') ||
                        clickedHeaderRow?.closest('[class*="factionWrap"]') || null;
                    let memberContainer = factionContainer?.querySelector('.f-war-list') ||
                        factionContainer?.querySelector('ul') ||
                        clickedHeaderRow?.nextElementSibling || null;
                    if (!memberContainer || !memberContainer.querySelector('li')) {
                        memberContainer = factionList;
                    }
                    this.sortByColumn(memberContainer, header, column);
                });
            };
            setupColumnSort(nameHeader, 'name');
            setupColumnSort(levelHeader, 'level');
            // Your faction: simple score sort. Enemy faction: score/loadout toggle (handled below).
            if (isYourFaction)
                setupColumnSort(scoreHeader, 'score');
            // Score header — enemy: toggle between score sort and loadout view
            if (!isYourFaction && scoreHeader && !scoreHeader.hasAttribute('data-sort-enabled')) {
                scoreHeader.setAttribute('data-sort-enabled', 'true');
                scoreHeader.style.cursor = 'pointer';
                // Wrap existing text node "Score" in a span, add separator + icon
                const sortIconEl = scoreHeader.querySelector('[class*="sortIcon"]');
                const textNode = Array.from(scoreHeader.childNodes).find(n => n.nodeType === Node.TEXT_NODE && (n.textContent || '').trim());
                const scoreSpan = document.createElement('span');
                scoreSpan.className = 'cat-lo-score-label';
                scoreSpan.textContent = textNode ? (textNode.textContent || '').trim() : 'Score';
                if (textNode)
                    scoreHeader.removeChild(textNode);
                const sep = document.createElement('span');
                sep.className = 'cat-lo-score-sep';
                sep.textContent = '/';
                const icon = document.createElement('span');
                icon.className = 'cat-lo-score-toggle';
                icon.innerHTML = `<img src="${LOADOUT_ICON_B64}" width="12" height="12" style="display:block;image-rendering:auto;">`;
                if (sortIconEl) {
                    sortIconEl.before(scoreSpan, sep, icon);
                }
                else {
                    scoreHeader.appendChild(scoreSpan);
                    scoreHeader.appendChild(sep);
                    scoreHeader.appendChild(icon);
                }
                // Restore loadout view on page load if it was active
                if (localStorage.getItem(LS_LO_VIEW) === '1') {
                    // Defer until memberContainer is available (DOM may not be ready yet)
                    setTimeout(() => {
                        const clickedHeaderRow = scoreHeader.closest('.white-grad');
                        const factionContainer = clickedHeaderRow?.closest('.enemy-faction') ||
                            clickedHeaderRow?.closest('[class*="enemy-faction"]') ||
                            clickedHeaderRow?.closest('[class*="factionWrap"]') || null;
                        let memberContainer = factionContainer?.querySelector('.f-war-list') ||
                            factionContainer?.querySelector('ul') ||
                            clickedHeaderRow?.nextElementSibling || null;
                        if (!memberContainer || !memberContainer.querySelector('li'))
                            memberContainer = factionList;
                        toggleLoadoutScoreView(memberContainer, icon, true);
                    }, 800);
                }
                scoreHeader.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const clickedHeaderRow = scoreHeader.closest('.white-grad');
                    const factionContainer = clickedHeaderRow?.closest('.enemy-faction') ||
                        clickedHeaderRow?.closest('[class*="enemy-faction"]') ||
                        clickedHeaderRow?.closest('[class*="factionWrap"]') || null;
                    let memberContainer = factionContainer?.querySelector('.f-war-list') ||
                        factionContainer?.querySelector('ul') ||
                        clickedHeaderRow?.nextElementSibling || null;
                    if (!memberContainer || !memberContainer.querySelector('li'))
                        memberContainer = factionList;
                    const isLoView = memberContainer.hasAttribute('data-cat-lo-view');
                    const currentSort = scoreHeader.getAttribute('data-sort') || 'none';
                    if (isLoView) {
                        // loadout → score desc (restart cycle)
                        toggleLoadoutScoreView(memberContainer, icon, false);
                        scoreHeader.setAttribute('data-sort', 'asc'); // sortByColumn will flip asc→desc
                        this.sortByColumn(memberContainer, scoreHeader, 'score');
                    }
                    else if (currentSort === 'desc') {
                        // score desc → score asc
                        this.sortByColumn(memberContainer, scoreHeader, 'score');
                    }
                    else if (currentSort === 'asc') {
                        // score asc → loadout view
                        toggleLoadoutScoreView(memberContainer, icon, true);
                    }
                    else {
                        // none → score desc (first click)
                        scoreHeader.setAttribute('data-sort', 'asc'); // sortByColumn will flip asc→desc
                        this.sortByColumn(memberContainer, scoreHeader, 'score');
                    }
                });
            }
        }
    }
    function _buildSortContainersCache() {
        const result = [];
        const factionContainers = document.querySelectorAll('[class*="tabMenuCont"]');
        const seen = new Set();
        for (const container of factionContainers) {
            if (seen.has(container))
                continue;
            seen.add(container);
            const memberContainer = container.querySelector('.f-war-list') || container.querySelector('ul');
            if (!memberContainer)
                continue;
            // Detect faction once and cache
            const hasYour = !!memberContainer.querySelector('li.your, li[class*="your___"]');
            const hasEnemy = !!memberContainer.querySelector('li.enemy, li[class*="enemy"]');
            let isYour = false;
            if (hasYour && !hasEnemy)
                isYour = true;
            else if (!hasYour && hasEnemy)
                isYour = false;
            else
                isYour = !!(container.classList.contains('your-faction') || container.className.includes('your-faction') || container.classList.contains('right'));
            // Find status header for this container
            const headerRow = container.querySelector('.white-grad');
            const statusHeader = headerRow?.querySelector('[class*="status___"]') || null;
            // Pre-query member rows — reused on every sort call until structural change
            const cachedMembers = Array.from(memberContainer.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]'));
            result.push({ container, memberContainer, factionKey: isYour ? 'your' : 'enemy', statusHeader, cachedMembers });
        }
        return result;
    }
    function restoreSavedSort(force = false) {
        // Skip if nothing changed since last sort (dirty flag cleared after processing)
        if (!force && !this._sortDirty)
            return;
        this._sortDirty = false;
        // Build/reuse container cache — invalidated when DOM structure changes (_sortContainersCache = null)
        if (!this._sortContainersCache) {
            this._sortContainersCache = _buildSortContainersCache();
        }
        const containers = this._sortContainersCache;
        for (const entry of containers) {
            const { container, memberContainer, factionKey, statusHeader } = entry;
            // Validate cache — if container disconnected, rebuild next call
            if (!container.isConnected) {
                this._sortContainersCache = null;
                this._catOrderApplied_your = false;
                this._catOrderApplied_enemy = false;
                return;
            }
            const savedSort = StorageUtil.get(`cat_sort_preference_${factionKey}`, null);
            if (!savedSort)
                continue;
            // Use cached member rows — re-queried only on structural DOM changes
            const members = entry.cachedMembers;
            // Status sort — statusHeader may be null if cache was built before addStatusHeaderSorting ran
            let resolvedStatusHeader = statusHeader;
            if (!resolvedStatusHeader && savedSort.column === 'status') {
                resolvedStatusHeader = container.querySelector('.white-grad [class*="status___"]') || null;
                if (resolvedStatusHeader)
                    entry.statusHeader = resolvedStatusHeader; // update cache
            }
            if (savedSort.column === 'status' && resolvedStatusHeader) {
                const statusTexts = [];
                for (let i = 0; i < members.length; i++) {
                    const row = members[i];
                    // Prefer cached statusEl (avoids querySelector on every hash build)
                    let el = row._catStatusEl || null;
                    if (el && !el.isConnected) {
                        el = null;
                        row._catStatusEl = null;
                    }
                    if (!el) {
                        el = row.querySelector('.status.left, [class*="status___"]');
                        if (el)
                            row._catStatusEl = el;
                    }
                    if (el) {
                        // Use catHospStatus to read 'Hospital' even when CAT timer text is shown
                        const rawTxt = el.dataset.catHospStatus || el.dataset.originalStatus || (el.textContent || '').trim();
                        const txt = rawTxt.substring(0, 3);
                        let svg = row._catSvgStatus || null;
                        if (svg && !svg.isConnected)
                            svg = null;
                        if (!svg) {
                            svg = row.querySelector('svg[fill*="svg_status_"]');
                            if (svg)
                                row._catSvgStatus = svg;
                        }
                        const ol = svg ? ((svg.getAttribute('fill') || '').includes('status_online') ? 'n' : (svg.getAttribute('fill') || '').includes('status_idle') ? 'i' : 'f') : '';
                        statusTexts.push(txt + ol);
                    }
                    else {
                        statusTexts.push('');
                    }
                }
                // Also include hospTime keys in hash — allows sorting before DOM statuses load
                const hospTime = this._enhancer?.hospTime || {};
                const hospKeys = Object.keys(hospTime);
                const statusHash = statusTexts.join('|') + '||' + hospKeys.length;
                // Skip if nothing loaded yet — neither DOM statuses nor hospTime
                const hasAnyStatus = statusTexts.some(s => s.length > 0) || hospKeys.length > 0;
                if (!hasAnyStatus) {
                    this._sortDirty = true;
                    continue;
                }
                const hashKey = `_lastSortHash_${factionKey}`;
                if (statusHash === this[hashKey])
                    continue;
                this[hashKey] = statusHash;
                // Clear data-sort from all other headers first
                const headerRowEl = container.querySelector('.white-grad');
                if (headerRowEl)
                    headerRowEl.querySelectorAll('[data-sort]').forEach(el => { if (el !== resolvedStatusHeader)
                        el.removeAttribute('data-sort'); });
                resolvedStatusHeader.setAttribute('data-sort', savedSort.direction);
                this.sortByStatus(memberContainer, resolvedStatusHeader, savedSort.direction);
                continue;
            }
            if (savedSort.column === 'status')
                continue;
            if (savedSort.column === 'last-action')
                continue;
            // BSP / FF / score / level / name sort — use pre-cached members
            if (members.length === 0)
                continue;
            // Cache whether CSS order has been applied — avoid .some() scan every dirty tick
            const orderKey = `_catOrderApplied_${factionKey}`;
            const hasOrder = this[orderKey] || (members.length > 0 && !!members[0].style.order && members[0].style.order !== '0');
            if (!hasOrder || force) {
                // Hash the sort-relevant values to skip if nothing changed
                // Use cached JS refs on rows where available — avoids querySelector per member
                const nonStatusHashKey = `_lastSortHash_${factionKey}_${savedSort.column}`;
                let nonStatusHash = '';
                if (savedSort.column === 'bsp' || savedSort.column === 'ff') {
                    for (let i = 0; i < members.length; i++) {
                        const row = members[i];
                        let el = row._catBspEl || null;
                        if (el && !el.isConnected)
                            el = null;
                        if (!el) {
                            el = row.querySelector('.bsp-value, .iconStats, .ff-column .ff-value') || null;
                            if (el)
                                row._catBspEl = el;
                        }
                        nonStatusHash += (el?.textContent || '?') + '|';
                    }
                }
                else if (savedSort.column === 'score') {
                    for (let i = 0; i < members.length; i++) {
                        const row = members[i];
                        let el = row._catScoreEl || null;
                        if (el && !el.isConnected)
                            el = null;
                        if (!el) {
                            el = row.querySelector('[class*="points___"]') || null;
                            if (el)
                                row._catScoreEl = el;
                        }
                        nonStatusHash += (el?.textContent || '?') + '|';
                    }
                }
                else if (savedSort.column === 'level') {
                    for (let i = 0; i < members.length; i++) {
                        const row = members[i];
                        // Reuse _catLevelEl already cached by injectLevelIndicators
                        let el = row._catLevelEl || null;
                        if (el && !el.isConnected)
                            el = null;
                        if (!el) {
                            el = row.querySelector('[class*="level___"]') || null;
                            if (el)
                                row._catLevelEl = el;
                        }
                        nonStatusHash += (el?.textContent || '?') + '|';
                    }
                }
                else {
                    // name — rarely changes, just hash count
                    nonStatusHash = String(members.length);
                }
                if (!force && nonStatusHash && nonStatusHash === this[nonStatusHashKey])
                    continue;
                this[nonStatusHashKey] = nonStatusHash;
                const isYour = factionKey === 'your';
                let sorted = false;
                if (savedSort.column === 'bsp') {
                    const colPref = StorageUtil.get(isYour ? 'cat_stats_column_your' : 'cat_stats_column_enemy', '') || StorageUtil.get('cat_stats_column', 'bsp') || 'bsp';
                    if (colPref === 'bsp') {
                        const bspHeader = container.querySelector('.bsp-header');
                        if (bspHeader) {
                            bspHeader.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                            this.sortByBSP(memberContainer, bspHeader);
                            sorted = true;
                        }
                    }
                }
                else if (savedSort.column === 'ff') {
                    const colPref = StorageUtil.get(isYour ? 'cat_stats_column_your' : 'cat_stats_column_enemy', '') || StorageUtil.get('cat_stats_column', 'ff') || 'ff';
                    if (colPref === 'ff') {
                        const bspHeader = container.querySelector('.bsp-header');
                        if (bspHeader) {
                            bspHeader.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                            this.sortByFF(memberContainer, bspHeader);
                            sorted = true;
                        }
                    }
                }
                else if (savedSort.column === 'ts') {
                    const tsHeader = container.querySelector('.bsp-header');
                    if (tsHeader) {
                        tsHeader.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                        this.sortByTS(memberContainer, tsHeader);
                        sorted = true;
                    }
                }
                else {
                    const headerRow = container.querySelector('.white-grad');
                    let header = null;
                    if (savedSort.column === 'score')
                        header = headerRow?.querySelector('[class*="points___"]') || null;
                    else if (savedSort.column === 'level')
                        header = headerRow?.querySelector('[class*="level___"]') || null;
                    else if (savedSort.column === 'name')
                        header = headerRow?.querySelector('[class*="name___"], [class*="member___"]') || null;
                    if (header) {
                        header.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                        this.sortByColumn(memberContainer, header, savedSort.column);
                        sorted = true;
                    }
                }
                if (sorted)
                    this[orderKey] = true;
            }
        }
    }
    function sortByStatus(factionList, headerElement, forcedDirection = null) {
        // Detect faction by checking member classes
        const hasYourMembers = factionList.querySelector('li.your, li[class*="your___"]') !== null;
        const hasEnemyMembers = factionList.querySelector('li.enemy, li[class*="enemy"]') !== null;
        let isYourFaction = false;
        if (hasYourMembers && !hasEnemyMembers) {
            isYourFaction = true;
        }
        else if (hasEnemyMembers && !hasYourMembers) {
            isYourFaction = false;
        }
        else {
            // Fallback to container class check
            isYourFaction = !!(factionList.closest('.your-faction') || factionList.closest('[class*="your-faction"]'));
        }
        const factionKey = isYourFaction ? 'your' : 'enemy';
        const currentSort = headerElement.getAttribute('data-sort') || 'none';
        let newSort = forcedDirection || 'asc';
        if (!forcedDirection) {
            if (currentSort === 'none') {
                newSort = 'asc';
            }
            else if (currentSort === 'asc') {
                newSort = 'desc';
            }
            else {
                newSort = 'asc';
            }
        }
        // Clear sort from other headers
        const statusHeaderRow = headerElement.closest('.white-grad');
        if (statusHeaderRow) {
            statusHeaderRow.querySelectorAll('[data-sort]').forEach(el => {
                if (el !== headerElement)
                    el.removeAttribute('data-sort');
            });
        }
        headerElement.setAttribute('data-sort', newSort);
        StorageUtil.set(`cat_sort_preference_${factionKey}`, {
            column: 'status',
            direction: newSort
        });
        let members = Array.from(factionList.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]'));
        if (members.length === 0) {
            members = Array.from(factionList.querySelectorAll('li'));
        }
        if (members.length === 0) {
            return;
        }
        // Pre-build value map
        const statusMap = new Map();
        for (let i = 0; i < members.length; i++)
            statusMap.set(members[i], this.getStatusValue(members[i]));
        members.sort((a, b) => {
            const statusA = statusMap.get(a);
            const statusB = statusMap.get(b);
            const isOkayA = statusA >= 40000 && statusA <= 40002;
            const isOkayB = statusB >= 40000 && statusB <= 40002;
            if (isOkayA && !isOkayB)
                return -1;
            if (!isOkayA && isOkayB)
                return 1;
            return newSort === 'asc' ? statusA - statusB : statusB - statusA;
        });
        const parentSet = new Set(members.map(m => m.parentNode).filter((p) => p instanceof HTMLElement));
        parentSet.forEach((parent) => {
            if (!parent.dataset.catFlex) {
                parent.style.display = 'flex';
                parent.style.flexDirection = 'column';
                parent.dataset.catFlex = '1';
            }
        });
        members.forEach((member, i) => {
            member.style.order = String(i);
        });
    }
    function sortByColumn(factionList, headerElement, column) {
        // Detect faction by checking member classes
        const hasYourMembers = factionList.querySelector('li.your, li[class*="your___"]') !== null;
        const hasEnemyMembers = factionList.querySelector('li.enemy, li[class*="enemy"]') !== null;
        let isYourFaction = false;
        if (hasYourMembers && !hasEnemyMembers) {
            isYourFaction = true;
        }
        else if (hasEnemyMembers && !hasYourMembers) {
            isYourFaction = false;
        }
        else {
            // Fallback to container class check
            isYourFaction = !!(factionList.closest('.your-faction') || factionList.closest('[class*="your-faction"]'));
        }
        const factionKey = isYourFaction ? 'your' : 'enemy';
        const currentSort = headerElement.getAttribute('data-sort') || 'none';
        let newSort = 'asc';
        if (currentSort === 'none') {
            newSort = 'asc';
        }
        else if (currentSort === 'asc') {
            newSort = 'desc';
        }
        else {
            newSort = 'asc';
        }
        // Reset other headers sort indicators in same faction list
        const headerRow = headerElement.closest('.white-grad');
        if (headerRow) {
            headerRow.querySelectorAll('[data-sort]').forEach(el => {
                if (el !== headerElement)
                    el.removeAttribute('data-sort');
            });
        }
        headerElement.setAttribute('data-sort', newSort);
        StorageUtil.set(`cat_sort_preference_${factionKey}`, {
            column,
            direction: newSort
        });
        let members = Array.from(factionList.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]'));
        if (members.length === 0) {
            members = Array.from(factionList.querySelectorAll('li'));
        }
        if (members.length === 0)
            return;
        const valueMap = new Map();
        for (let i = 0; i < members.length; i++) {
            const m = members[i];
            let v = '';
            if (column === 'name') {
                let el = m._catNameEl || null;
                if (el && !el.isConnected)
                    el = null;
                if (!el) {
                    el = m.querySelector('[class*="honorWrap___"] a, [class*="name___"] a, a[href*="profiles.php"]');
                    if (el)
                        m._catNameEl = el;
                }
                v = (el?.textContent || '').trim().toLowerCase();
            }
            else if (column === 'level') {
                let el = m._catLevelEl || null;
                if (el && !el.isConnected)
                    el = null;
                if (!el) {
                    el = m.querySelector('[class*="level___"]');
                    if (el)
                        m._catLevelEl = el;
                }
                v = parseInt((el?.textContent || '0').replace(/\D/g, '')) || 0;
            }
            else if (column === 'score') {
                let el = m._catScoreEl || null;
                if (el && !el.isConnected)
                    el = null;
                if (!el) {
                    el = m.querySelector('[class*="points___"]');
                    if (el)
                        m._catScoreEl = el;
                }
                v = parseInt((el?.textContent || '0').replace(/[^0-9]/g, '')) || 0;
            }
            valueMap.set(m, v);
        }
        members.sort((a, b) => {
            const valA = valueMap.get(a);
            const valB = valueMap.get(b);
            if (typeof valA === 'string' && typeof valB === 'string') {
                return newSort === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
            }
            return newSort === 'asc' ? valA - valB : valB - valA;
        });
        const parentSet = new Set(members.map(m => m.parentNode).filter((p) => p instanceof HTMLElement));
        parentSet.forEach((parent) => {
            if (!parent.dataset.catFlex) {
                parent.style.display = 'flex';
                parent.style.flexDirection = 'column';
                parent.dataset.catFlex = '1';
            }
        });
        members.forEach((member, i) => {
            member.style.order = String(i);
        });
    }
    function getStatusValue(memberElement) {
        const row = memberElement;
        let statusElement = row.dataset.catStatusId ? document.getElementById(row.dataset.catStatusId) : null;
        if (!statusElement) {
            statusElement = memberElement.querySelector('.status.left, [class*="status___"]');
            if (statusElement) {
                if (!statusElement.id)
                    statusElement.id = `_cat_st_sv${Math.random().toString(36).slice(2, 6)}`;
                row.dataset.catStatusId = statusElement.id;
            }
        }
        if (!statusElement) {
            return 999999;
        }
        // If CSS class is okay/hospital, the player has landed — ignore stale originalStatus/travelData
        const isOkayClass = statusElement.classList.contains('okay') || statusElement.className.includes('okay');
        const isHospClassSV = statusElement.classList.contains('hospital') || statusElement.className.includes('hospital');
        const statusText = (isOkayClass || isHospClassSV)
            ? (statusElement.textContent || '').trim()
            : (statusElement.dataset.originalStatus || (statusElement.textContent || '').trim());
        // Timer at 00:00:00 or just expired — Torn hasn't confirmed new status yet, keep stable position
        if (statusText === '00:00:00' || (!statusText && row.hasAttribute('data-cat-hosp-ready'))) {
            return 1; // Sort at top of hosp group (0s remaining) — stays there until Torn confirms
        }
        if (statusText.includes('Hospital')) {
            const timerMatch = statusText.match(/(\d+):(\d+):(\d+)/);
            if (timerMatch) {
                const hours = parseInt(timerMatch[1]);
                const minutes = parseInt(timerMatch[2]);
                const seconds = parseInt(timerMatch[3]);
                const totalSeconds = hours * 3600 + minutes * 60 + seconds;
                return totalSeconds;
            }
            // Try hospTime directly — available from localStorage before DOM timers are injected
            let uid = row.dataset.catUid;
            if (!uid) {
                const a = row.querySelector('a[href*="profiles.php?XID="], a[href*="user2ID"]');
                if (a) {
                    const m = a.href.match(/(?:XID|user2ID)=(\d+)/);
                    if (m) {
                        uid = m[1];
                        row.dataset.catUid = uid;
                    }
                }
            }
            if (uid && this._enhancer?.hospTime?.[uid]) {
                const endMs = this._enhancer.hospTime[uid];
                const secs = Math.max(0, Math.floor(((endMs > 9999999999 ? endMs : endMs * 1000) - Date.now()) / 1000));
                return secs;
            }
            // Fallback: check dataset written by hospital-timers.ts
            const cachedSecs = statusElement.dataset.catHospSeconds;
            if (cachedSecs)
                return parseInt(cachedSecs);
            return 1000;
        }
        else if (statusText.includes('Jail')) {
            const timerMatch = statusText.match(/(\d+):(\d+):(\d+)/);
            if (timerMatch) {
                const hours = parseInt(timerMatch[1]);
                const minutes = parseInt(timerMatch[2]);
                const seconds = parseInt(timerMatch[3]);
                const totalSeconds = hours * 3600 + minutes * 60 + seconds;
                return 10000 + totalSeconds;
            }
            return 10000;
        }
        else if (statusText.includes('Traveling')) {
            const travelArea = this.getTravelArea(memberElement);
            return 20000 + (travelArea * 10);
        }
        else if (statusText.includes('Abroad')) {
            const abroadArea = this.getTravelArea(memberElement);
            return 30000 + (abroadArea * 10);
        }
        else if (!isOkayClass && !isHospClassSV && (this._enhancer?.travelData?.[row.dataset.catUid || '']?.status === 'Abroad' ||
            (statusElement && statusElement.dataset.originalStatus === 'Abroad'))) {
            // Abroad with destination name shown (e.g. "South Africa") — same bucket as Abroad
            const abroadArea = this.getTravelArea(memberElement);
            return 30000 + (abroadArea * 10);
        }
        else if (!isOkayClass && !isHospClassSV && (this._enhancer?.travelData?.[row.dataset.catUid || '']?.status === 'Traveling' ||
            (statusElement && statusElement.dataset.originalStatus === 'Traveling'))) {
            // Traveling with destination name shown
            const travelArea = this.getTravelArea(memberElement);
            return 20000 + (travelArea * 10);
        }
        else if (/okay|online|idle|offline/i.test(statusText)) {
            let svg = row._catSvgStatus || null;
            if (svg && !svg.isConnected)
                svg = null;
            if (!svg) {
                svg = memberElement.querySelector('svg[fill*="svg_status_"]');
                if (svg)
                    row._catSvgStatus = svg;
            }
            if (svg) {
                const fill = svg.getAttribute('fill') || '';
                if (fill.includes('status_online'))
                    return 40000;
                if (fill.includes('status_idle'))
                    return 40001;
                if (fill.includes('status_offline'))
                    return 40002;
            }
            const ariaEl = memberElement.querySelector('[aria-label*="is online"], [aria-label*="is idle"], [aria-label*="is offline"]');
            if (ariaEl) {
                const aria = ariaEl.getAttribute('aria-label') || '';
                if (aria.includes('is online'))
                    return 40000;
                if (aria.includes('is idle'))
                    return 40001;
                if (aria.includes('is offline'))
                    return 40002;
            }
            if (statusText.includes('Idle'))
                return 40001;
            if (statusText.includes('Offline'))
                return 40002;
            return 40000;
        }
        else {
            const rawTimerMatch = statusText.match(/^(\d+):(\d+):(\d+)$/);
            if (rawTimerMatch) {
                const hours = parseInt(rawTimerMatch[1]);
                const minutes = parseInt(rawTimerMatch[2]);
                const seconds = parseInt(rawTimerMatch[3]);
                const totalSeconds = hours * 3600 + minutes * 60 + seconds;
                return totalSeconds;
            }
            return 70000;
        }
    }
    function sortByFF(factionList, headerElement) {
        const hasYourMembers = factionList.querySelector('li.your, li[class*="your___"]') !== null;
        const hasEnemyMembers = factionList.querySelector('li.enemy, li[class*="enemy"]') !== null;
        let isYourFaction = false;
        if (hasYourMembers && !hasEnemyMembers) {
            isYourFaction = true;
        }
        else if (hasEnemyMembers && !hasYourMembers) {
            isYourFaction = false;
        }
        else {
            isYourFaction = !!(factionList.closest('.your-faction') || factionList.closest('[class*="your-faction"]'));
        }
        const factionKey = isYourFaction ? 'your' : 'enemy';
        const currentSort = headerElement.getAttribute('data-sort') || 'none';
        let newSort = 'asc';
        if (currentSort === 'asc') {
            newSort = 'desc';
        }
        else if (currentSort === 'desc') {
            newSort = 'asc';
        }
        const bspHeaderRow = headerElement.closest('.white-grad');
        if (bspHeaderRow) {
            bspHeaderRow.querySelectorAll('[data-sort]').forEach(el => {
                if (el !== headerElement)
                    el.removeAttribute('data-sort');
            });
        }
        headerElement.setAttribute('data-sort', newSort);
        headerElement.setAttribute('data-col', 'ff');
        // Update header text with column + sort arrow
        const sortArrow = newSort === 'asc' ? '▲' : '▼';
        const firstTextFF = headerElement.childNodes[0];
        if (firstTextFF && firstTextFF.nodeType === Node.TEXT_NODE) {
            firstTextFF.textContent = `FF ${sortArrow}`;
        }
        else {
            headerElement.textContent = `FF ${sortArrow}`;
        }
        StorageUtil.set(`cat_sort_preference_${factionKey}`, {
            column: 'ff',
            direction: newSort
        });
        let members = Array.from(factionList.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]'));
        if (members.length === 0) {
            members = Array.from(factionList.querySelectorAll('li'));
        }
        if (members.length === 0)
            return;
        // Pre-build value map
        const ffMap = new Map();
        for (let i = 0; i < members.length; i++)
            ffMap.set(members[i], this.getFFValue(members[i]));
        members.sort((a, b) => {
            const ffA = ffMap.get(a);
            const ffB = ffMap.get(b);
            return newSort === 'asc' ? ffA - ffB : ffB - ffA;
        });
        const parentSet = new Set(members.map(m => m.parentNode).filter((p) => p instanceof HTMLElement));
        parentSet.forEach((parent) => {
            if (!parent.dataset.catFlex) {
                parent.style.display = 'flex';
                parent.style.flexDirection = 'column';
                parent.dataset.catFlex = '1';
            }
        });
        members.forEach((member, i) => {
            member.style.order = String(i);
        });
    }
    function getFFValue(memberElement) {
        const row = memberElement;
        let ffColumn = row._catFFValue || null;
        if (ffColumn && !ffColumn.isConnected)
            ffColumn = null;
        if (!ffColumn) {
            ffColumn = memberElement.querySelector('.ff-column .ff-value');
            if (ffColumn)
                row._catFFValue = ffColumn;
        }
        if (ffColumn) {
            const text = (ffColumn.textContent || '').trim();
            if (text && text !== '-') {
                // BS estimate uses same format as BSP (e.g. "13.1b", "940m")
                return this.parseBSPText(text);
            }
        }
        return 999999;
    }
    function sortByTS(factionList, headerElement) {
        const hasYourMembers = factionList.querySelector('li.your, li[class*="your___"]') !== null;
        const hasEnemyMembers = factionList.querySelector('li.enemy, li[class*="enemy"]') !== null;
        const memberSelector = hasYourMembers
            ? 'li.your, li[class*="your___"]'
            : hasEnemyMembers
                ? 'li.enemy, li[class*="enemy"]'
                : 'li[class*="member"], li';
        const members = Array.from(factionList.querySelectorAll(memberSelector));
        if (members.length === 0)
            return;
        const currentSort = headerElement.getAttribute('data-sort') || 'none';
        const newSort = currentSort === 'desc' ? 'asc' : 'desc';
        headerElement.setAttribute('data-sort', newSort);
        // Save sort preference so restoreSavedSort can restore it after refresh
        const factionKey = hasYourMembers && !hasEnemyMembers ? 'your' : 'enemy';
        StorageUtil.set(`cat_sort_preference_${factionKey}`, { column: 'ts', direction: newSort });
        // Pre-build value map
        const tsMap = new Map();
        for (let i = 0; i < members.length; i++)
            tsMap.set(members[i], getTSValue.call(this, members[i]));
        members.sort((a, b) => {
            const va = tsMap.get(a);
            const vb = tsMap.get(b);
            return newSort === 'asc' ? va - vb : vb - va;
        });
        const parentSet = new Set(members.map(m => m.parentNode).filter((p) => p instanceof HTMLElement));
        parentSet.forEach((parent) => {
            if (!parent.dataset.catFlex) {
                parent.style.display = 'flex';
                parent.style.flexDirection = 'column';
                parent.dataset.catFlex = '1';
            }
        });
        members.forEach((member, i) => { member.style.order = String(i); });
    }
    function getTSValue(memberElement) {
        // Look in the member row itself or its children
        const col = memberElement.querySelector('.ts-value') || memberElement.querySelector('.ts-column .ts-value');
        if (col) {
            const text = (col.textContent || '').trim();
            if (text && text !== '-')
                return this.parseBSPText(text);
        }
        return -1;
    }
    function getTravelArea(memberElement) {
        try {
            const row = memberElement;
            let statusElement = row.dataset.catStatusId ? document.getElementById(row.dataset.catStatusId) : null;
            if (!statusElement) {
                statusElement = memberElement.querySelector('.status.left, [class*="status___"]');
                if (statusElement) {
                    if (!statusElement.id)
                        statusElement.id = `_cat_st_ta${Math.random().toString(36).slice(2, 6)}`;
                    row.dataset.catStatusId = statusElement.id;
                }
            }
            if (!statusElement)
                return 0;
            const displayedText = (statusElement.textContent || '').trim();
            for (const [areaNum, areaName] of Object.entries(CONFIG.areas)) {
                if (displayedText.includes(areaName)) {
                    return parseInt(areaNum);
                }
            }
        }
        catch (_e) {
            this.apiManager.reportError('parseAreaNumber', _e);
        }
        return 0;
    }
    // ─── Revive SVG / Last Action ─────────────────────────────────────────────────
    const REVIVE_SVG = {
        'Everyone': `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="8" viewBox="0 0 21 16.72"><path d="m16.2,8.36c-.27.02-.52.18-.66.41l-1.07,1.98-1.32-3.8c-.09-.29-.36-.49-.66-.5-.3,0-.59.15-.74.41l-.91,2.15L9.43.59c-.08-.33-.41-.66-.74-.58-.35,0-.65.24-.74.58l-1.24,9.84-.99-5.29c-.06-.32-.33-.57-.66-.58-.32-.06-.63.11-.74.41l-1.65,3.47H0v1.49h3.06c.28,0,.54-.16.66-.41l.91-1.82,1.49,7.44c.09.34.4.57.74.58h0c.37-.01.69-.29.74-.66l1.24-9.67,1.07,6.36c.06.32.33.57.66.58.31.02.6-.14.74-.41l1.16-2.65,1.24,3.64c.09.29.36.49.66.5.3,0,.59-.15.74-.41l1.65-3.14h3.22v-1.49h-3.8Z" fill="#4caf50"/></svg>`,
        'Friends & faction': `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="8" viewBox="0 0 21 17.14"><path d="m6.11,17.14l-1.49-7.44-.91,1.82c-.12.25-.38.41-.66.41H0v-1.49h2.65l1.65-3.47c.12-.3.43-.47.74-.41.33.01.6.25.66.58l.99,5.29,1.24-9.83c.09-.33.4-.57.74-.58.33-.08.66.25.74.58l1.41,8.43.91-2.15c.16-.26.44-.41.74-.41.3.01.57.21.66.5l1.32,3.8,1.07-1.98c.14-.24.39-.39.66-.41h3.8v1.49h-3.22l-1.65,3.14c-.16.26-.44.42-.74.41-.3-.01-.57-.21-.66-.5l-1.24-3.64-1.16,2.64c-.14.27-.43.44-.74.41-.33-.01-.6-.25-.66-.58l-1.07-6.36-1.24,9.67c-.06.37-.37.65-.74.66-.35-.01-.65-.25-.74-.58Zm6.89-9.14v-.33c0-.89.03-1.4,1.06-1.64,1.12-.26,2.23-.49,1.7-1.47-1.58-2.91-.45-4.56,1.24-4.56s2.82,1.59,1.25,4.56c-.52.98.55,1.21,1.7,1.47,1.03.24,1.06.75,1.06,1.65v.32h-8.01Z" fill="#ffe066"/></svg>`,
        'No one': `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="8" viewBox="0 0 21 16.72"><path d="m6.12,15.14l-.71-3.57-3.99,3.99-1.41-1.42,4.93-4.93-.3-1.51-.91,1.82c-.12.25-.38.41-.66.41H0v-1.49h2.64l1.65-3.47c.12-.3.43-.47.74-.41.33.01.6.25.66.58l.52,2.78.9-.9.81-6.43c.09-.34.4-.57.74-.58.33-.08.66.25.74.58l.59,3.54L14.14,0l1.42,1.41-5.14,5.14.41,2.47.91-2.15c.16-.26.44-.42.74-.41.3.01.57.21.66.5l1.32,3.8,1.07-1.98c.14-.24.39-.39.66-.41h3.8v1.49h-3.22l-1.65,3.14c-.16.26-.44.41-.74.41-.3,0-.57-.21-.66-.5l-1.24-3.64-1.16,2.64c-.14.27-.44.44-.74.41-.33-.01-.6-.25-.66-.58l-.68-4.02-.8.8-.84,6.52c-.06.37-.37.65-.74.66-.35,0-.65-.24-.74-.58Zm.58-4.71l.02-.17-.04.04.02.13Z" fill="#ff6b6b"/></svg>`,
    };
    let _reviveCache = null;
    let _reviveCacheAt = 0;
    const _REVIVE_TTL = 60000;
    let _reviveFetching = false;
    let _revivePendingCallbacks = [];
    function invalidateReviveCache() {
        _reviveCache = null;
        _reviveCacheAt = 0;
        // Re-fetch and re-render Last Action cells + revive icons
        const memberContainer = document.querySelector('.your-faction .f-war-list, .your-faction ul');
        if (!memberContainer)
            return;
        fetchReviveSettings(..._getReviveArgs(), (data) => {
            _renderLastActionCells(memberContainer, data.lastAction);
            // Re-render revive icons on all rows that already have an indicator
            memberContainer.querySelectorAll('li.your[data-cat-uid], li[class*="your___"][data-cat-uid]').forEach(row => {
                const indicator = row._catIndicator;
                if (!indicator || !indicator.isConnected)
                    return;
                const uid = row.dataset.catUid;
                const revive = data.settings[uid] ?? null;
                let existing = indicator.querySelector('.cat-rv-indicator-icon');
                if (!existing) {
                    existing = document.createElement('span');
                    existing.className = 'cat-rv-indicator-icon';
                    existing.style.cssText = 'display:inline-block;vertical-align:middle;margin-left:2px;';
                    indicator.appendChild(existing);
                }
                const svg = revive ? (REVIVE_SVG[revive] || '') : '';
                if (existing.innerHTML !== svg)
                    existing.innerHTML = svg;
            });
        });
    }
    function _getReviveArgs() {
        return [
            window.FactionWarEnhancer?.factionId || localStorage.getItem('cat_user_faction_id') || '',
            localStorage.getItem('cat_auth_token') || '',
            localStorage.getItem('cat_server_url') || 'https://cat.dgh.sh',
        ];
    }
    function fetchReviveSettings(factionId, authToken, serverUrl, cb) {
        if (_reviveCache && (Date.now() - _reviveCacheAt) < _REVIVE_TTL) {
            cb(_reviveCache);
            return;
        }
        _reviveCache = null;
        _revivePendingCallbacks.push(cb);
        if (_reviveFetching)
            return;
        _reviveFetching = true;
        const url = `${serverUrl}/api/revive-settings/${factionId}`;
        const headers = { 'Authorization': `Bearer ${authToken}` };
        const apiMgr = window.FactionWarEnhancer?.apiManager;
        const onload = (data) => {
            _reviveFetching = false;
            const result = {
                settings: (data?.success && data.settings) ? data.settings : {},
                lastAction: (data?.success && data.lastAction) ? data.lastAction : {},
            };
            _reviveCache = result;
            _reviveCacheAt = Date.now();
            for (const fn of _revivePendingCallbacks)
                fn(result);
            _revivePendingCallbacks = [];
        };
        const onerror = () => {
            _reviveFetching = false;
            const empty = { settings: {}, lastAction: {} };
            _reviveCache = empty;
            _reviveCacheAt = Date.now();
            for (const fn of _revivePendingCallbacks)
                fn(empty);
            _revivePendingCallbacks = [];
        };
        if (apiMgr && typeof apiMgr.httpRequest === 'function') {
            apiMgr.httpRequest(url, { method: 'GET', headers })
                .then((resp) => resp.json().then(onload).catch(onerror))
                .catch(onerror);
        }
        else {
            fetch(url, { method: 'GET', headers })
                .then(r => r.json()).then(onload).catch(onerror);
        }
    }
    // ─── Last Action column ───────────────────────────────────────────────────────
    function formatLastAction(ts) {
        if (!ts)
            return '?';
        const diff = Math.floor(Date.now() / 1000) - ts;
        if (diff < 60)
            return `${diff}s`;
        if (diff < 3600)
            return `${Math.floor(diff / 60)}m`;
        if (diff < 86400)
            return `${Math.floor(diff / 3600)}h`;
        return `${Math.floor(diff / 86400)}d`;
    }
    let _laInterval = null;
    function _sortByLastAction(memberContainer, headerEl, headerRow) {
        const currentSort = headerEl.getAttribute('data-sort') || 'none';
        const newSort = currentSort === 'asc' ? 'desc' : 'asc';
        headerRow.querySelectorAll('[data-sort]').forEach(el => { if (el !== headerEl)
            el.removeAttribute('data-sort'); });
        headerEl.setAttribute('data-sort', newSort);
        // Save preference so restoreSavedSort doesn't overwrite this sort
        StorageUtil.set('cat_sort_preference_your', { column: 'last-action', direction: newSort });
        const sortIcon = headerEl.querySelector('[class*="sortIcon"]');
        if (sortIcon) {
            sortIcon.className = sortIcon.className.replace(/\s*(asc|desc)___\S+/g, '');
            const dirClass = newSort === 'asc' ? 'asc___' : 'desc___';
            const existing = document.querySelector(`[class*="${dirClass}"]`);
            if (existing) {
                const dirMatch = [...existing.classList].find(c => c.startsWith(dirClass));
                if (dirMatch)
                    sortIcon.classList.add(dirMatch);
            }
        }
        const lastAction = _reviveCache?.lastAction || {};
        const members = Array.from(memberContainer.querySelectorAll('li.your[data-cat-uid], li[class*="your___"][data-cat-uid]'));
        members.sort((a, b) => {
            const ta = lastAction[a.dataset.catUid] ?? 0;
            const tb = lastAction[b.dataset.catUid] ?? 0;
            return newSort === 'asc' ? ta - tb : tb - ta;
        });
        members.forEach((m, i) => { m.style.order = String(i); });
    }
    function _injectLastActionHeader(memberContainer, attempt = 0) {
        const prev = memberContainer.previousElementSibling;
        const headerRow = (prev?.classList.contains('white-grad') ? prev : null)
            || memberContainer.closest('.members-cont, [class*="membersCont"]')?.querySelector('.white-grad');
        if (headerRow && !headerRow.querySelector('.cat-la-header')) {
            const laHeader = document.createElement('div');
            laHeader.className = 'cat-la-header left tab___UztMc';
            laHeader.style.cssText = 'position:absolute;right:0;top:0;bottom:0;display:flex;align-items:center;padding:0 6px;white-space:nowrap;cursor:pointer;';
            laHeader.innerHTML = 'Last Act.<div class="sortIcon___SmuX8" style="display:inline-block;margin-left:2px;vertical-align:middle;"></div>';
            headerRow.style.position = 'relative';
            headerRow.appendChild(laHeader);
            laHeader.addEventListener('click', () => _sortByLastAction(memberContainer, laHeader, headerRow));
        }
        else if (!headerRow && attempt < 5) {
            setTimeout(() => _injectLastActionHeader(memberContainer, attempt + 1), 300);
        }
    }
    function initLastActionColumn(memberContainer) {
        const factionId = (localStorage.getItem('cat_user_faction_id') || '').replace(/"/g, '');
        const isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        if (!factionId || !window.location.href.includes('step=your') || isPDA)
            return;
        _injectLastActionHeader(memberContainer);
        fetchReviveSettings(..._getReviveArgs(), (data) => {
            _renderLastActionCells(memberContainer, data.lastAction);
        });
        if (_laInterval)
            clearInterval(_laInterval);
        _laInterval = setInterval(() => {
            if (!document.contains(memberContainer)) {
                clearInterval(_laInterval);
                _laInterval = null;
                return;
            }
            if (!isWarRankPage()) {
                clearInterval(_laInterval);
                _laInterval = null;
                return;
            }
            _renderLastActionCells(memberContainer, _reviveCache?.lastAction || {});
        }, 60000);
    }
    function injectReviveIconInIndicator(row, indicator, isRevivable) {
        const uid = row.dataset.catUid;
        if (!uid)
            return;
        const isEnemy = row.classList.contains('enemy') || row.className.includes('enemy___');
        if (isEnemy) {
            // For enemy: show green if revivable, red if not
            let existing = indicator.querySelector('.cat-rv-indicator-icon');
            if (!existing) {
                existing = document.createElement('span');
                existing.className = 'cat-rv-indicator-icon';
                existing.style.cssText = 'display:inline-block;vertical-align:middle;margin-left:2px;';
                indicator.appendChild(existing);
            }
            const svg = isRevivable ? REVIVE_SVG['Everyone'] : REVIVE_SVG['No one'];
            if (existing.innerHTML !== svg)
                existing.innerHTML = svg;
            return;
        }
        // For your faction: show revive setting
        fetchReviveSettings(..._getReviveArgs(), (data) => {
            const revive = data.settings[uid] ?? null;
            let existing = indicator.querySelector('.cat-rv-indicator-icon');
            if (!existing) {
                existing = document.createElement('span');
                existing.className = 'cat-rv-indicator-icon';
                existing.style.cssText = 'display:inline-block;vertical-align:middle;margin-left:2px;';
                indicator.appendChild(existing);
            }
            const svg = revive ? (REVIVE_SVG[revive] || '') : '';
            if (existing.innerHTML !== svg)
                existing.innerHTML = svg;
        });
    }
    function _renderLastActionCells(memberContainer, lastAction) {
        memberContainer.querySelectorAll('li.your[data-cat-uid], li[class*="your___"][data-cat-uid]').forEach(row => {
            const uid = row.dataset.catUid;
            let cell = row.querySelector('.cat-la-col');
            if (!cell) {
                cell = document.createElement('div');
                cell.className = 'cat-la-col left';
                const laColor = document.body.classList.contains('dark-mode') ? '#aaa' : '#555';
                cell.style.cssText = `position:absolute !important;right:12px;top:50%;transform:translateY(-50%);width:36px;min-width:36px;max-width:36px;display:inline-flex !important;align-items:center;justify-content:flex-end;font-size:0.78em;font-weight:700;font-family:Monaco,Menlo,monospace;color:${laColor};padding:0 2px;text-align:right;overflow:hidden;`;
                row.appendChild(cell);
            }
            const text = formatLastAction(lastAction[uid] ?? null);
            cell.textContent = text;
            cell.style.display = text === '?' ? 'none' : 'inline-flex';
        });
    }
    const SHIRT_IMG = `<img src="${LOADOUT_ICON_B64}" width="17" height="17" style="display:block;image-rendering:auto;flex-shrink:0;">`;
    const LS_LO_VIEW = 'cat_score_lo_view';
    function toggleLoadoutScoreView(memberContainer, icon, activate) {
        const scoreLabel = icon.parentElement?.querySelector('.cat-lo-score-label');
        if (activate) {
            memberContainer.setAttribute('data-cat-lo-view', '1');
            icon.classList.add('cat-lo-score-toggle-active');
            localStorage.setItem(LS_LO_VIEW, '1');
            if (scoreLabel) {
                scoreLabel.style.color = '#fff';
            }
            const sep = icon.parentElement?.querySelector('.cat-lo-score-sep');
            if (sep)
                sep.style.color = '#fff';
            memberContainer.querySelectorAll('li.enemy [class*="points___"], li[class*="enemy___"] [class*="points___"]').forEach(cell => {
                const row = cell.closest('li[data-cat-uid]');
                const uid = row?.dataset.catUid;
                if (!uid)
                    return;
                if (cell.dataset.catScoreSaved === undefined) {
                    cell.dataset.catScoreSaved = cell.innerHTML;
                }
                // Start hidden — prefetchLoadout will reveal if server has data
                cell.innerHTML = `<span class="cat-loadout-btn cat-lo-score-btn" data-cat-loadout-id="${uid}" style="display:none;align-items:center;justify-content:center;width:100%;height:100%;">${SHIRT_IMG}</span>`;
                const btn = cell.querySelector('.cat-lo-score-btn');
                if (btn) {
                    prefetchLoadout(uid, (hasData) => {
                        if (hasData)
                            btn.style.display = 'flex';
                        // else stays hidden (hasNoLoadout also hides via querySelectorAll in prefetchLoadout)
                    });
                }
                cell.setAttribute('style', (cell.getAttribute('style') || '') + ';overflow:visible !important;display:flex !important;align-items:center !important;justify-content:center !important;padding:0 !important;box-sizing:border-box !important;font-size:0 !important;margin-top:4px !important;');
            });
        }
        else {
            memberContainer.removeAttribute('data-cat-lo-view');
            icon.classList.remove('cat-lo-score-toggle-active');
            localStorage.removeItem(LS_LO_VIEW);
            if (scoreLabel)
                scoreLabel.style.color = '';
            const sep2 = icon.parentElement?.querySelector('.cat-lo-score-sep');
            if (sep2)
                sep2.style.color = '';
            memberContainer.querySelectorAll('li.enemy [class*="points___"], li[class*="enemy___"] [class*="points___"]').forEach(cell => {
                const saved = cell.dataset.catScoreSaved;
                if (saved !== undefined) {
                    cell.innerHTML = saved;
                    delete cell.dataset.catScoreSaved;
                    cell.style.overflow = '';
                    cell.style.display = '';
                    cell.style.alignItems = '';
                    cell.style.justifyContent = '';
                    cell.style.padding = '';
                    cell.style.boxSizing = '';
                    cell.style.fontSize = '';
                    cell.style.marginTop = '';
                }
            });
        }
    }

    function _checkFactions() {
        this._checkYourFaction();
        this._checkEnemyFaction();
        this._checkNoBsp();
        // WarHelper cleanup — only scan if never cleaned or WarHelper was detected before (rare)
        if (!this._catWhClean) {
            const wh = document.querySelector('.finally-bs-col, .__warhelper.bs.level');
            if (wh) {
                document.querySelectorAll('.finally-bs-col, .finally-bs-filter, .finally-bs-swap').forEach(el => el.remove());
                document.querySelectorAll('.__warhelper.bs.level').forEach(el => {
                    el.classList.remove('level');
                    el.className = el.className.replace(/\blevel___\S*/g, '');
                });
                // Don't set _catWhClean — WarHelper may add elements again; keep scanning
            }
            else {
                this._catWhClean = true; // No WarHelper found — skip future scans
            }
        }
        // Sync .bsp-header labels — cache refs, read prefs once per call (not per header)
        let bspHeaders = this._catBspHeaders || null;
        if (!bspHeaders || (bspHeaders.length > 0 && !bspHeaders[0].isConnected)) {
            bspHeaders = Array.from(document.querySelectorAll('.bsp-header'));
            this._catBspHeaders = bspHeaders;
        }
        if (bspHeaders.length > 0) {
            // Read prefs once for the whole loop — localStorage reads are synchronous I/O
            const fallbackPref = StorageUtil.get('cat_stats_column', 'bsp') || 'bsp';
            const prefYour = StorageUtil.get('cat_stats_column_your', '') || fallbackPref;
            const prefEnemy = StorageUtil.get('cat_stats_column_enemy', '') || fallbackPref;
            for (let _i = 0; _i < bspHeaders.length; _i++) {
                const header = bspHeaders[_i];
                const isEnemy = !!(header.closest('.enemy-faction'));
                const pref = isEnemy ? prefEnemy : prefYour;
                // Skip DOM write if already correct
                if (header.dataset.col === pref)
                    continue;
                header.setAttribute('data-col', pref);
                const label = pref === 'ff' ? 'FF' : pref === 'ts' ? 'TS' : 'BSP';
                const firstText = header.childNodes[0];
                if (firstText && firstText.nodeType === Node.TEXT_NODE) {
                    const arrow = firstText.textContent?.match(/[▲▼]/) ? ` ${firstText.textContent.match(/[▲▼]/)[0]}` : '';
                    firstText.textContent = label + arrow;
                }
                else {
                    header.textContent = label;
                }
            }
        }
    }
    function _checkYourFaction() {
        // Fast exit when latched — verify first call-button still live
        if (this._yourFactionEnhanced) {
            let cachedYourContainer = this._catYourContainer || null;
            if (!cachedYourContainer || !cachedYourContainer.isConnected) {
                cachedYourContainer = document.querySelector('.your-faction[class*="tabMenuCont"]');
                this._catYourContainer = cachedYourContainer;
            }
            if (cachedYourContainer) {
                let cachedYourBtn = this._catYourBtn || null;
                if (!cachedYourBtn || !cachedYourBtn.isConnected) {
                    cachedYourBtn = cachedYourContainer.querySelector('.call-button');
                    this._catYourBtn = cachedYourBtn;
                }
                const onOwnPage = window.location.href.includes('step=your');
                const hasLaHeader = !onOwnPage || !!cachedYourContainer.querySelector('.cat-la-header');
                if (cachedYourBtn && cachedYourBtn.isConnected && hasLaHeader)
                    return;
                // DOM re-rendered or last-action header missing — reset latch
                this._yourFactionEnhanced = false;
                this._catYourBtn = null;
            }
        }
        const yourFactionContainer = this._catYourContainer?.isConnected
            ? this._catYourContainer
            : document.querySelector('.your-faction[class*="tabMenuCont"]');
        this._catYourContainer = yourFactionContainer;
        if (!yourFactionContainer)
            return;
        const yourFactionLists = yourFactionContainer.querySelectorAll('ul, ol');
        const hasFFData = this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0;
        let allEnhanced = true;
        yourFactionLists.forEach(list => {
            if (!list.hasAttribute('data-enhanced')) {
                allEnhanced = false;
                const hasMembers = list.querySelectorAll('li').length >= 3;
                const hasLevels = list.querySelector('[class*="level"]');
                if (hasMembers && hasLevels) {
                    this.addBspToYourFaction(list);
                    setTimeout(() => this.changeLevelToLvl(), 100);
                    initLastActionColumn(list);
                    // Inject TS column immediately if that's the saved preference
                    const prefYour = StorageUtil.get('cat_stats_column_your', '') || StorageUtil.get('cat_stats_column', 'bsp') || 'bsp';
                    if (prefYour === 'ts' && !!(StorageUtil.get('cat_tornstats_api_key', '') || '').trim()) {
                        this.addTSColumn(list);
                    }
                }
            }
            else {
                if (hasFFData && !list.querySelector('.ff-column')) {
                    // FF data arrived after initial enhancement — add FF column now
                    this.addFFColumn(list);
                    this.addFFSwitchArrow(list);
                }
                // TS column may be missing if API key wasn't set at first render
                const prefYour = StorageUtil.get('cat_stats_column_your', '') || StorageUtil.get('cat_stats_column', 'bsp') || 'bsp';
                if (prefYour === 'ts' && !!(StorageUtil.get('cat_tornstats_api_key', '') || '').trim() && !list.querySelector('.ts-column')) {
                    this.addTSColumn(list);
                }
            }
        });
        if (allEnhanced && yourFactionLists.length > 0) {
            // Only latch if FF is also handled (or no FF data)
            if (!hasFFData || yourFactionContainer.querySelector('.ff-column')) {
                this._yourFactionEnhanced = true;
            }
        }
    }
    function _checkEnemyFaction() {
        // Cache enemyFactionContainer — re-query only when disconnected
        let enemyFactionContainer = this._catEnemyContainer || null;
        if (!enemyFactionContainer || !enemyFactionContainer.isConnected) {
            enemyFactionContainer = document.querySelector('.enemy-faction[class*="tabMenuCont"], .enemy-faction.left');
            this._catEnemyContainer = enemyFactionContainer;
            // Container changed — reset cached button/col refs
            this._catEnemyCallBtn = null;
            this._catEnemyBspCol = null;
        }
        if (!enemyFactionContainer) {
            return;
        }
        // Re-evaluate catOtherFaction in case checkUrl() ran before cat_user_faction_id was stored
        if (!state.catOtherFaction && window.location.search.includes('step=profile')) {
            const idMatch = window.location.search.match(/ID=(\d+)/);
            if (idMatch) {
                const pageFactionId = idMatch[1];
                const userFactionId = (localStorage.getItem('cat_user_faction_id') || '').replace(/"/g, '');
                if (userFactionId && pageFactionId !== userFactionId) {
                    state.catOtherFaction = true;
                    state.viewingFactionId = pageFactionId;
                    document.documentElement.classList.add('cat-other-faction');
                    // Reset latch so call buttons are re-injected with correct faction context
                    this._enemyFactionEnhanced = false;
                    this._catEnemyCallBtn = null;
                    this._catEnemyBspCol = null;
                }
            }
        }
        // If latched, verify the DOM still has our enhancements (panel may have been re-opened)
        if (this._enemyFactionEnhanced) {
            // Use cached refs — re-query only when disconnected
            let cachedCallBtn = this._catEnemyCallBtn || null;
            if (!cachedCallBtn || !cachedCallBtn.isConnected) {
                cachedCallBtn = enemyFactionContainer.querySelector('.call-button');
                this._catEnemyCallBtn = cachedCallBtn;
            }
            let cachedBspCol = this._catEnemyBspCol || null;
            if (!cachedBspCol || !cachedBspCol.isConnected) {
                cachedBspCol = enemyFactionContainer.querySelector('.bsp-column, .ff-column, .ts-column');
                this._catEnemyBspCol = cachedBspCol;
            }
            // isConnected check: SPA re-renders can swap the container node — the old one
            // may still have .call-button children but be detached from the live document.
            const stillLive = cachedCallBtn && cachedCallBtn.isConnected;
            const hasBspCol = cachedBspCol && cachedBspCol.isConnected;
            if (!stillLive || !hasBspCol) {
                // DOM re-rendered, panel closed, or TS columns missing — reset latch so we re-inject
                this._enemyFactionEnhanced = false;
                this._noBspChecked = false;
                this._catEnemyCallBtn = null;
                this._catEnemyBspCol = null;
            }
            else {
                return;
            }
        }
        // Admin viewing another faction: fix attack column visibility & container width
        const isAdmin = this._enhancer?.subscriptionData?.isAdmin || false;
        if (isAdmin && state.catOtherFaction) {
            // Remove attack___wBWp2 from attack header so css hide rule [class*="attack___"] won't match
            const attackHeader = enemyFactionContainer.querySelector('.attack.tab___UztMc[class*="attack___"]');
            if (attackHeader) {
                attackHeader.classList.forEach(cls => {
                    if (cls.startsWith('attack___'))
                        attackHeader.classList.remove(cls);
                });
            }
            // Widen the members container so the attack column isn't cut off
            const membersContainer = enemyFactionContainer.closest('[class*="membersCont___"]')
                || document.querySelector('.members-cont[class*="membersCont___"]');
            if (membersContainer) {
                membersContainer.style.minWidth = '360px';
            }
        }
        const enemyFactionLists = enemyFactionContainer.querySelectorAll('ul, ol');
        let callButtonsEnhanced = true;
        enemyFactionLists.forEach(list => {
            const hasBspColumn = list.querySelector('.bsp-column');
            const hasCallButtons = list.querySelector('.call-button');
            const hasFFColumn = list.querySelector('.ff-column');
            const hasFFData = this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0;
            const prefEnemy = StorageUtil.get('cat_stats_column_enemy', '') || StorageUtil.get('cat_stats_column', 'bsp') || 'bsp';
            const needsTSColumn = prefEnemy === 'ts' && !!(StorageUtil.get('cat_tornstats_api_key', '') || '').trim() && !list.querySelector('.ts-column');
            if (hasBspColumn && hasCallButtons && (!hasFFData || hasFFColumn) && !needsTSColumn)
                return;
            const hasMembers = list.querySelectorAll('li').length >= 3;
            if (!hasMembers) {
                callButtonsEnhanced = false;
                return;
            }
            if (hasMembers) {
                if (!hasBspColumn) {
                    this.addBspHeader(list);
                    this.addBspColumn(list);
                }
                if (needsTSColumn) {
                    this.addTSColumn(list);
                }
                if (!hasCallButtons) {
                    callButtonsEnhanced = false;
                    this.addCallButtons(list);
                    // Apply cached call data immediately instead of waiting for the timer
                    if (this._enhancer?.currentCalls?.length) {
                        this._enhancer.updateCallButtons(this._enhancer.currentCalls);
                    }
                }
                // FF Scouter column
                if (this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0) {
                    if (!list.querySelector('.ff-column')) {
                        this.addFFColumn(list);
                    }
                    this.addFFSwitchArrow(list);
                }
                this.addStatusHeaderSorting(list);
                setTimeout(() => this.changeLevelToLvl(), 100);
                // Check if BSP data is available
                const hasIconStats = list.querySelector('.iconStats');
                const parentContainer = list.closest('[class*="tabMenuCont"]') || list.closest('.enemy-faction');
                if (parentContainer) {
                    if (hasIconStats) {
                        parentContainer.classList.remove('no-bsp');
                    }
                    else {
                        parentContainer.classList.add('no-bsp');
                    }
                }
            }
        });
        // Latch as soon as call buttons are injected — don't wait for BSP (which may never load)
        if (callButtonsEnhanced && enemyFactionLists.length > 0) {
            this._enemyFactionEnhanced = true;
        }
    }
    function _checkNoBsp() {
        if (this._noBspChecked)
            return;
        const containers = document.querySelectorAll('[class*="tabMenuCont"]');
        if (containers.length === 0)
            return;
        let allClassified = true;
        containers.forEach(container => {
            const hasBspHeader = container.querySelector('.bsp-header');
            const hasBspColumn = container.querySelector('.bsp-column');
            const hasIconStats = container.querySelector('.iconStats');
            if (hasBspHeader || hasBspColumn || hasIconStats) {
                container.classList.remove('no-bsp');
                return;
            }
            if (!container.classList.contains('no-bsp')) {
                const hasMembers = container.querySelectorAll('[class*="member___"]').length > 0 ||
                    container.querySelectorAll('li.enemy, li.your, li[class*="enemy"], li[class*="your"]').length > 0;
                if (hasMembers) {
                    container.classList.add('no-bsp');
                }
                else {
                    allClassified = false;
                }
            }
        });
        if (allClassified && containers.length > 0) {
            this._noBspChecked = true;
        }
    }

    function enhanceExistingElements() {
        const factionWarSelectors = [
            '[class*="membersWrap"]',
            '[class*="factionWar"]',
            '.faction-war',
            '[class*="faction-war"]',
            '.desc-wrap',
            '[class*="desc"]'
        ];
        factionWarSelectors.forEach(selector => {
            const elements = document.querySelectorAll(selector);
            elements.forEach(element => this.enhanceElement(element));
        });
        const descWrap = document.querySelector('.desc-wrap');
        if (descWrap) {
            const factionWarElements = descWrap.querySelectorAll('.f-war-list, ul, [class*="list"]');
            factionWarElements.forEach(list => {
                if (list.querySelectorAll('li, [class*="member"]').length > 0) {
                    this.enhanceElement(list);
                }
            });
            setTimeout(() => {
                descWrap.querySelectorAll('.f-war-list:not([data-enhanced]), ul:not([data-enhanced]), ol:not([data-enhanced])').forEach(element => {
                    this.enhanceElement(element);
                });
            }, 200);
        }
        else {
            document.querySelectorAll('[class*="war"], [class*="faction"]').forEach(element => {
                if (element.querySelectorAll('li, [class*="member"]').length > 0) {
                    this.enhanceElement(element);
                }
            });
        }
        setTimeout(() => this._checkFactions(), 100);
        setTimeout(() => this._checkFactions(), 500);
        // _factionCheckInterval removed — handled by master game loop in dom-observer.ts (every 2s)
    }
    function enhanceElement(element) {
        if (element.matches && (element.matches('svg') ||
            element.matches('script') ||
            element.matches('style') ||
            element.matches('.call-column') ||
            element.closest('.call-column'))) {
            return;
        }
        const isValidContainer = element.matches && (element.matches('.f-war-list') ||
            element.matches('ul.members-list') ||
            element.matches('ol.members-list') ||
            element.matches('.faction-war') ||
            element.matches('[class*="membersWrap"]') ||
            element.matches('[class*="factionWar"]') ||
            (element.matches('ul') && element.querySelectorAll('li').length > 5 && element.closest('.desc-wrap')) ||
            (element.matches('ol') && element.querySelectorAll('li').length > 5 && element.closest('.desc-wrap')) ||
            (element.matches('div') && element.querySelectorAll('li, [class*="member"]').length > 5 && element.closest('.desc-wrap')));
        if (isValidContainer) {
            if (!element.hasAttribute('data-enhanced')) {
                element.setAttribute('data-enhanced', 'true');
                this.addLoadingAnimation(element);
                const members = element.querySelectorAll('li');
                members.forEach((member, index) => {
                    member.style.animationDelay = `${index * 0.1}s`;
                });
            }
            this.addBspHeader(element);
            this.addBspColumn(element);
            if (!element.querySelector('.bsp-header') && !element.querySelector('.bsp-column')) {
                const parentContainer = element.closest('[class*="tabMenuCont"]') || element.closest('.enemy-faction') || element.closest('.your-faction');
                if (parentContainer) {
                    parentContainer.classList.add('no-bsp');
                }
            }
            this.addCallButtons(element);
            this.addStatusHeaderSorting(element);
            setTimeout(() => this.changeLevelToLvl(), 100);
        }
    }

    /** Cache TTL: 7 days (same as War Helper) */
    const TTL_MS = 7 * 24 * 3600 * 1000;
    const CACHE_KEY_PREFIX = 'cat_ts_faction_';
    const MEMBER_KEY_PREFIX = 'cat_ts_member_';
    /** Returns cached TS data for a player, or null if expired / not fetched */
    function getTSCached(playerId) {
        const entry = StorageUtil.get(MEMBER_KEY_PREFIX + playerId, null);
        if (!entry)
            return null;
        if (Date.now() - entry.cachedAt > TTL_MS)
            return null;
        return entry.data;
    }
    /** Fetch TornStats spy data for a whole faction. Stores results in localStorage.
     *  NEVER sends data to the CAT server — purely client-side.
     *  Uses GM_xmlhttpRequest via httpRequestFn to bypass CORS (same as War Helper). */
    async function fetchTornStatsFaction(factionId, httpRequestFn) {
        const apiKey = StorageUtil.get('cat_tornstats_api_key', '') || '';
        if (!apiKey)
            return;
        // Skip if already fetched within TTL
        const factionCache = StorageUtil.get(CACHE_KEY_PREFIX + factionId, null);
        if (factionCache && Date.now() - factionCache.fetchedAt < TTL_MS)
            return;
        try {
            const url = `https://www.tornstats.com/api/v2/${apiKey}/spy/faction/${factionId}`;
            const resp = await httpRequestFn(url, { method: 'GET' });
            if (!resp.ok)
                return;
            const json = await resp.json();
            const members = json?.faction?.members;
            if (!members)
                return;
            for (const [id, memberData] of Object.entries(members)) {
                const spy = memberData?.spy;
                if (!spy)
                    continue;
                // Skip data older than 1 year (same filter as War Helper)
                if (spy.timestamp && Date.now() / 1000 - spy.timestamp >= 365 * 86400)
                    continue;
                const str = spy.strength ? parseInt(spy.strength) : null;
                const spd = spy.speed ? parseInt(spy.speed) : null;
                const def = spy.defense ? parseInt(spy.defense) : null;
                const dex = spy.dexterity ? parseInt(spy.dexterity) : null;
                const tot = spy.total ? parseInt(spy.total) : null;
                const score = (str != null && spd != null && def != null && dex != null)
                    ? Math.sqrt(str) + Math.sqrt(def) + Math.sqrt(spd) + Math.sqrt(dex)
                    : null;
                const entry = {
                    userId: id,
                    total: tot,
                    strength: str,
                    speed: spd,
                    defense: def,
                    dexterity: dex,
                    score,
                    timestamp: spy.timestamp ?? null,
                };
                StorageUtil.set(MEMBER_KEY_PREFIX + id, { data: entry, cachedAt: Date.now() });
            }
            StorageUtil.set(CACHE_KEY_PREFIX + factionId, { fetchedAt: Date.now() });
        }
        catch (_) {
            // Silent fail — TornStats is optional
        }
    }
    /** Format a raw BS number into human-readable string (e.g. 1.2B) */
    function formatTSTotal(total) {
        if (total == null)
            return '-';
        if (total >= 1000000000)
            return `${(total / 1000000000).toFixed(1)}B`;
        if (total >= 100000000)
            return `${(total / 1000000).toFixed(0)}M`;
        if (total >= 1000000)
            return `${(total / 1000000).toFixed(1)}M`;
        if (total >= 1000)
            return `${(total / 1000).toFixed(0)}K`;
        return String(total);
    }

    // ── Per-faction column preference ───────────────────────────
    // Each faction panel has its own column preference so switching in one
    // does not affect the other.
    function getFactionColKey(factionList) {
        // Primary: ancestor class (normal war page)
        if (factionList.closest('.enemy-faction'))
            return 'cat_stats_column_enemy';
        // Fallback: profileMode container has no .enemy-faction class — detect by member li classes
        const hasEnemyMembers = !!(factionList.querySelector('li.enemy, li[class*="enemy___"]'));
        const hasYourMembers = !!(factionList.querySelector('li.your, li[class*="your___"]'));
        if (hasEnemyMembers && !hasYourMembers)
            return 'cat_stats_column_enemy';
        return 'cat_stats_column_your';
    }
    /** Read the per-faction column preference, falling back to global key for backwards compat. */
    function getColPref(factionList, fallback = 'bsp') {
        const key = getFactionColKey(factionList);
        const perFaction = StorageUtil.get(key, '');
        if (perFaction)
            return perFaction;
        // Backwards compat: migrate global preference on first read
        const global = StorageUtil.get('cat_stats_column', fallback) || fallback;
        return global;
    }
    function setColPref(factionList, col) {
        StorageUtil.set(getFactionColKey(factionList), col);
    }
    function getSortPref(factionList) {
        const key = getFactionColKey(factionList).replace('cat_stats_column', 'cat_stats_sort');
        return StorageUtil.get(key, 'desc') || 'desc';
    }
    function setSortPref(factionList, sort) {
        const key = getFactionColKey(factionList).replace('cat_stats_column', 'cat_stats_sort');
        StorageUtil.set(key, sort);
    }
    // ── Helper: extract player ID from a member row ─────────────
    function getPlayerIdFromRow(li) {
        // Check cached ID first
        const cached = li._catPlayerId;
        if (cached)
            return cached;
        const link = li.querySelector('a[href*="profiles.php?XID="], a[href*="XID="]');
        if (link) {
            const match = link.href.match(/XID=(\d+)/);
            if (match) {
                li._catPlayerId = match[1];
                return match[1];
            }
        }
        const dataId = li.getAttribute('data-id') || li.getAttribute('data-user-id');
        if (dataId) {
            li._catPlayerId = dataId;
            return dataId;
        }
        return null;
    }
    // Dynamic FF color: compare target BS to viewer BS (like War Helper does for BSP)
    // If viewer BS is known → ratio-based coloring (personalized)
    // If viewer BS is unknown → fallback to absolute BS thresholds
    function getFFColorClass(targetBsRaw, viewerBsRaw) {
        if (targetBsRaw == null)
            return 'bsp-gray';
        // Dynamic mode: ratio-based (personalized per viewer)
        if (viewerBsRaw != null && viewerBsRaw > 0) {
            const ratio = targetBsRaw / viewerBsRaw;
            if (ratio >= 5)
                return 'bsp-red'; // Much stronger than you
            if (ratio >= 2)
                return 'bsp-orange'; // Stronger
            if (ratio >= 0.75)
                return 'bsp-blue'; // Similar range
            if (ratio >= 0.25)
                return 'bsp-green'; // Weaker
            if (ratio >= 0.05)
                return 'bsp-white'; // Much weaker
            return 'bsp-gray';
        }
        // Fallback: absolute BS thresholds (when viewer stats unknown)
        if (targetBsRaw >= 10000000000)
            return 'bsp-red'; // 10B+
        if (targetBsRaw >= 2000000000)
            return 'bsp-orange'; // 2B+
        if (targetBsRaw >= 500000000)
            return 'bsp-blue'; // 500M+
        if (targetBsRaw >= 100000000)
            return 'bsp-green'; // 100M+
        if (targetBsRaw >= 10000000)
            return 'bsp-white'; // 10M+
        return 'bsp-gray';
    }
    // ── Cycling header click: BSP↓ → BSP↑ → FF↓ → FF↑ → TS↓ → TS↑ → BSP↓ ──
    // Skips FF if no FF data, skips TS if no TS key configured.
    function handleStatsHeaderClick(header, factionList) {
        // Derive member container from the header's DOM position — more reliable than captured factionList
        // when the same .f-war-list element spans both factions.
        const factionContainer = header.closest('.your-faction') ||
            header.closest('[class*="your-faction"]') ||
            header.closest('.enemy-faction') ||
            header.closest('[class*="enemy-faction"]') ||
            header.closest('[class*="tabMenuCont"]');
        let memberContainer = (factionContainer?.querySelector('.f-war-list') ||
            factionContainer?.querySelector('ul') ||
            factionList);
        if (!memberContainer.querySelector('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]')) {
            memberContainer = factionList.closest('.f-war-list') ||
                factionList.querySelector('.f-war-list') ||
                factionList.parentElement ||
                factionList;
        }
        // Use factionContainer as the scoped root instead of the captured factionList
        const scopedFactionList = factionContainer || factionList;
        const hasFF = this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0;
        const hasTS = !!(StorageUtil.get('cat_tornstats_api_key', '') || '').trim();
        const hasBsp = scopedFactionList.querySelectorAll('.iconStats').length > 0
            || scopedFactionList.closest('.enemy-faction')?.querySelectorAll('.iconStats').length
            || scopedFactionList.closest('[class*="tabMenuCont"]')?.querySelectorAll('.iconStats').length;
        const currentCol = header.getAttribute('data-col') || getColPref(scopedFactionList);
        const currentSort = header.getAttribute('data-sort') || 'none';
        // Build ordered cycle based on available data AND user preferences
        const showBsp = String(StorageUtil.get('cat_col_show_bsp', 'true')) === 'true';
        const showFF = String(StorageUtil.get('cat_col_show_ff', 'true')) === 'true';
        const showTS = String(StorageUtil.get('cat_col_show_ts', 'true')) === 'true';
        const cycle = [];
        if (hasBsp && showBsp)
            cycle.push('bsp');
        if (hasFF && showFF)
            cycle.push('ff');
        if (hasTS && showTS)
            cycle.push('ts');
        if (cycle.length === 0)
            cycle.push('bsp'); // fallback
        // Find next col/sort in cycle
        let nextCol;
        let nextSort;
        if (currentSort !== 'asc') {
            // ↓ → ↑ (stay same col)
            nextCol = currentCol;
            nextSort = 'asc';
            // If current col not in cycle, reset to first
            if (!cycle.includes(nextCol)) {
                nextCol = cycle[0];
                nextSort = 'desc';
            }
        }
        else {
            // ↑ → advance to next col ↓
            const idx = cycle.indexOf(currentCol);
            nextCol = cycle[(idx + 1) % cycle.length];
            nextSort = 'desc';
        }
        // Save column preference & toggle visibility
        if (nextCol !== currentCol) {
            setColPref(scopedFactionList, nextCol);
            header.setAttribute('data-col', nextCol);
            const root = factionContainer || document;
            root.querySelectorAll('.bsp-column').forEach(el => el.style.setProperty('display', nextCol === 'bsp' ? 'inline-block' : 'none', 'important'));
            root.querySelectorAll('.ff-column').forEach(el => el.style.setProperty('display', nextCol === 'ff' ? 'inline-flex' : 'none', 'important'));
            root.querySelectorAll('.ts-column').forEach(el => el.style.setProperty('display', nextCol === 'ts' ? 'inline-flex' : 'none', 'important'));
            // Toggle li flex layout — only active when TS column is shown
            root.querySelectorAll('li[data-cat-flex-row]').forEach(li => {
                if (nextCol === 'ts') {
                    li.style.setProperty('display', 'flex', 'important');
                    li.style.setProperty('align-items', 'center', 'important');
                }
                else {
                    li.style.removeProperty('display');
                    li.style.removeProperty('align-items');
                }
            });
            // Inject TS column on first switch to ts
            if (nextCol === 'ts')
                this.addTSColumn(scopedFactionList);
            // Refresh BSP "Wait" cells when switching back to bsp
            if (nextCol === 'bsp') {
                this.updateWaitingBspCells(factionContainer || scopedFactionList);
            }
        }
        // Set opposite direction so sort function toggles to desired direction
        header.setAttribute('data-sort', nextSort === 'asc' ? 'desc' : 'asc');
        if (nextCol === 'ff') {
            this.sortByFF(memberContainer, header);
        }
        else if (nextCol === 'ts') {
            this.sortByTS(scopedFactionList, header);
        }
        else {
            this.sortByBSP(memberContainer, header);
        }
        // Persist sort direction for this faction
        setSortPref(scopedFactionList, nextSort);
        updateStatsHeaderText(header);
    }
    function updateStatsHeaderText(header) {
        const col = header.getAttribute('data-col') || StorageUtil.get('cat_stats_column', 'bsp') || 'bsp';
        const sort = header.getAttribute('data-sort') || 'none';
        const arrow = sort === 'asc' ? '▲' : sort === 'desc' ? '▼' : '';
        const label = col === 'ff' ? 'FF' : col === 'ts' ? 'TS' : 'BSP';
        const firstText = header.childNodes[0];
        if (firstText && firstText.nodeType === Node.TEXT_NODE) {
            firstText.textContent = `${label} ${arrow}`;
        }
        else {
            header.textContent = `${label} ${arrow}`;
        }
    }
    function addBspToYourFaction(factionList) {
        factionList.setAttribute('data-enhanced', 'true');
        const hasBspData = factionList.querySelectorAll('.iconStats').length > 0;
        if (!hasBspData) {
            const parentContainer = factionList.closest('[class*="tabMenuCont"]') || factionList.closest('.your-faction');
            if (parentContainer) {
                parentContainer.classList.add('no-bsp');
            }
        }
        this.addBspHeaderToYourFaction(factionList);
        this.addBspColumnToYourFaction(factionList);
        // FF column for your faction too
        if (this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0) {
            if (!factionList.querySelector('.ff-column')) {
                this.addFFColumn(factionList);
            }
            this.addFFSwitchArrow(factionList);
        }
        this.addCDColumnToYourFaction(factionList);
        this.addStatusHeaderSorting(factionList);
    }
    function addBspHeaderToYourFaction(factionList) {
        if (factionList.querySelectorAll('.iconStats').length === 0) {
            return;
        }
        let headerContainer = factionList.querySelector('.white-grad');
        if (!headerContainer) {
            const parentContainer = factionList.closest('.your-faction') || factionList.parentElement;
            if (parentContainer) {
                headerContainer = parentContainer.querySelector('.white-grad');
            }
        }
        // Check for existing BSP header in both the list AND the header container
        if (factionList.querySelector('.bsp-header') || (headerContainer && headerContainer.querySelector('.bsp-header'))) {
            // If a BSP header exists but was added by enemy code, replace its click handler
            // Only replace if the header is NOT inside an enemy-faction block
            const existingBsp = headerContainer?.querySelector('.bsp-header');
            // Only replace if the header is genuinely inside the your-faction scope.
            // Check: the header must NOT be inside any container that has enemy members (li.enemy).
            // The .enemy-faction class check is insufficient for profileMode containers.
            const bspContainer = existingBsp?.closest('[class*="tabMenuCont"], [class*="membersCont"]');
            const bspHasEnemyMembers = !!(bspContainer?.querySelector('li.enemy, li[class*="enemy___"]'));
            if (existingBsp && !existingBsp.closest('.enemy-faction') && !bspHasEnemyMembers) {
                const newBsp = existingBsp.cloneNode(true);
                const _prefC = getColPref(factionList);
                newBsp.setAttribute('data-col', _prefC);
                const firstText = newBsp.childNodes[0];
                const label = _prefC === 'ff' ? 'FF' : _prefC === 'ts' ? 'TS' : 'BSP';
                if (firstText && firstText.nodeType === Node.TEXT_NODE) {
                    firstText.textContent = label;
                }
                else {
                    newBsp.textContent = label;
                }
                newBsp.style.cursor = 'pointer';
                newBsp.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    handleStatsHeaderClick.call(this, newBsp, factionList);
                });
                existingBsp.replaceWith(newBsp);
            }
            return;
        }
        if (!headerContainer) {
            const allWhiteGrad = document.querySelectorAll('.white-grad');
            for (const container of allWhiteGrad) {
                if (container.querySelector('.level___g3CWR')) {
                    const containerParent = container.closest('.your-faction') || container.closest('[class*="faction"]');
                    const factionParent = factionList.closest('.your-faction') || factionList.closest('[class*="faction"]');
                    if (containerParent === factionParent ||
                        (containerParent && factionParent && containerParent.contains(factionList)) ||
                        container.parentElement === factionList.parentElement) {
                        headerContainer = container;
                        break;
                    }
                }
            }
        }
        let levelHeaderElement = null;
        if (headerContainer) {
            levelHeaderElement = headerContainer.querySelector('.level.left.level___g3CWR.tab___UztMc');
            if (!levelHeaderElement) {
                levelHeaderElement = headerContainer.querySelector('.level___g3CWR');
            }
            if (!levelHeaderElement) {
                levelHeaderElement = headerContainer.querySelector('[class*="level"]');
            }
            if (!levelHeaderElement) {
                const elements = headerContainer.querySelectorAll('*');
                for (const element of elements) {
                    const txt = element.textContent?.trim();
                    if ((txt === 'Level' || txt === 'Lvl') && element.children.length === 0) {
                        levelHeaderElement = element;
                        break;
                    }
                }
            }
        }
        if (levelHeaderElement) {
            const bspHeader = document.createElement('div');
            bspHeader.className = 'bsp-header left tab___UztMc';
            const _pref0 = getColPref(factionList);
            bspHeader.textContent = _pref0 === 'ff' ? 'FF' : _pref0 === 'ts' ? 'TS' : 'BSP';
            bspHeader.setAttribute('data-col', _pref0);
            bspHeader.style.cssText = `
            min-width: 38px !important;
            width: 38px !important;
            text-align: center !important;
            margin-right: 3px !important;
            padding: 2px !important;
        `;
            bspHeader.style.cursor = 'pointer';
            bspHeader.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                handleStatsHeaderClick.call(this, bspHeader, factionList);
            });
            if (levelHeaderElement.nextSibling) {
                levelHeaderElement.parentNode.insertBefore(bspHeader, levelHeaderElement.nextSibling);
            }
            else {
                levelHeaderElement.parentNode.appendChild(bspHeader);
            }
        }
    }
    function addBspColumnToYourFaction(factionList) {
        // Skip if BSP extension is not installed (no .iconStats elements)
        if (factionList.querySelectorAll('.iconStats').length === 0) {
            return;
        }
        const members = factionList.querySelectorAll('li');
        members.forEach((member) => {
            if (member.querySelector('.bsp-column'))
                return;
            // Only inject into real member rows: must contain a profile link or .iconStats
            if (!member.querySelector('a[href*="profiles.php"], a[href*="XID="]') && !member.querySelector('.iconStats'))
                return;
            const bspElement = member.querySelector('.iconStats');
            let bspValue = 'Wait';
            let bspClass = 'bsp-wait';
            let bspCustomColor = '';
            if (bspElement) {
                bspValue = (bspElement.textContent || '').trim() || 'Wait';
                if (bspValue !== 'Wait') {
                    const bspStyle = bspElement.style.background || bspElement.style.backgroundColor;
                    const resolved = resolveBspColor(bspStyle);
                    bspClass = resolved.cls;
                    bspCustomColor = resolved.custom || '';
                }
            }
            const bspColumn = document.createElement('div');
            bspColumn.className = 'bsp-column left';
            bspColumn.innerHTML = `<span class="bsp-value ${bspClass}">${bspValue}</span>`;
            if (bspCustomColor)
                bspColumn.querySelector('.bsp-value').style.color = bspCustomColor;
            const activeColYF = getColPref(factionList);
            const hideForOther = activeColYF !== 'bsp';
            bspColumn.style.cssText = `
            color: #ffffff !important;
            padding: 2px 4px !important;
            font-size: 1em !important;
            font-weight: 700 !important;
            display: ${hideForOther ? 'none' : 'inline-block'} !important;
            margin-right: 3px !important;
            min-width: 35px !important;
            max-width: 35px !important;
            text-align: center !important;
            font-family: 'Monaco', 'Menlo', monospace !important;
        `;
            const levelDiv = member.querySelector('[class*="level___"], .level');
            if (levelDiv) {
                if (levelDiv.nextSibling) {
                    levelDiv.parentNode.insertBefore(bspColumn, levelDiv.nextSibling);
                }
                else {
                    levelDiv.parentNode.appendChild(bspColumn);
                }
            }
            else {
                member.appendChild(bspColumn);
            }
        });
    }
    function addBspColumn(factionList) {
        // Skip if BSP extension is not installed (no .iconStats elements)
        if (factionList.querySelectorAll('.iconStats').length === 0) {
            return;
        }
        const memberSelectors = [
            'li[class*="member"]',
            'li[class*="enemy"]',
            'li',
            '[class*="member"]',
            'tr[class*="member"]',
            'div[class*="member"]'
        ];
        let members = [];
        memberSelectors.forEach(selector => {
            const found = factionList.querySelectorAll(selector);
            if (found.length > 0 && members.length === 0) {
                members = found;
            }
        });
        members.forEach((member) => {
            if (member.tagName !== 'LI' && member.className.includes('memberRowWp') && member.className.includes('enemy')) {
                return;
            }
            if (member.querySelector('.bsp-column'))
                return;
            // Only inject into real member rows: must contain a profile link or .iconStats
            if (!member.querySelector('a[href*="profiles.php"], a[href*="XID="]') && !member.querySelector('.iconStats'))
                return;
            {
                const bspElement = member.querySelector('.iconStats');
                let bspValue = 'Wait';
                let bspClass = 'bsp-wait';
                let bspCustomColor2 = '';
                if (bspElement) {
                    bspValue = (bspElement.textContent || '').trim() || 'Wait';
                    if (bspValue !== 'Wait') {
                        const bspStyle = bspElement.style.background || bspElement.style.backgroundColor;
                        const resolved = resolveBspColor(bspStyle);
                        bspClass = resolved.cls;
                        bspCustomColor2 = resolved.custom || '';
                    }
                }
                const bspColumn = document.createElement('div');
                bspColumn.className = 'bsp-column left';
                bspColumn.innerHTML = `<span class="bsp-value ${bspClass}">${bspValue}</span>`;
                if (bspCustomColor2)
                    bspColumn.querySelector('.bsp-value').style.color = bspCustomColor2;
                // Hide if FF or TS column is currently active
                const activeCol = getColPref(factionList);
                if (activeCol !== 'bsp') {
                    bspColumn.style.setProperty('display', 'none', 'important');
                }
                let levelDiv = member.querySelector('.level___g3CWR, [class*="level___"], .level');
                if (!levelDiv) {
                    const allDivs = member.querySelectorAll('div');
                    for (const div of allDivs) {
                        if (div.textContent && /^\d+$/.test(div.textContent.trim()) && div.classList.contains('left')) {
                            levelDiv = div;
                            break;
                        }
                    }
                }
                if (levelDiv) {
                    if (levelDiv.nextSibling) {
                        levelDiv.parentNode.insertBefore(bspColumn, levelDiv.nextSibling);
                    }
                    else {
                        levelDiv.parentNode.appendChild(bspColumn);
                    }
                }
                else {
                    const pointsDiv = member.querySelector('.points___TQbnu, [class*="points___"], .points');
                    if (pointsDiv) {
                        pointsDiv.parentNode.insertBefore(bspColumn, pointsDiv);
                    }
                    else {
                        const leftDivs = member.querySelectorAll('div.left');
                        if (leftDivs.length >= 2) {
                            const secondDiv = leftDivs[1];
                            if (secondDiv.nextSibling) {
                                secondDiv.parentNode.insertBefore(bspColumn, secondDiv.nextSibling);
                            }
                            else {
                                secondDiv.parentNode.appendChild(bspColumn);
                            }
                        }
                        else {
                            member.appendChild(bspColumn);
                        }
                    }
                }
            }
        });
    }
    function parseBspValue(value) {
        const cleanValue = value.toLowerCase().replace(/[^0-9.kmb]/g, '');
        let multiplier = 1;
        if (cleanValue.includes('k')) {
            multiplier = 1000;
        }
        else if (cleanValue.includes('m')) {
            multiplier = 1000000;
        }
        else if (cleanValue.includes('b')) {
            multiplier = 1000000000;
        }
        const numericValue = parseFloat(cleanValue.replace(/[kmb]/g, ''));
        return numericValue * multiplier;
    }
    function updateWaitingBspCells(container) {
        const waitCells = container.querySelectorAll('.bsp-column .bsp-value.bsp-wait');
        waitCells.forEach(cell => {
            const member = cell.closest('li');
            if (!member)
                return;
            const bspElement = member.querySelector('.iconStats');
            if (!bspElement)
                return;
            const bspValue = (bspElement.textContent || '').trim();
            if (!bspValue)
                return;
            const bspStyle = bspElement.style.background || bspElement.style.backgroundColor;
            const { cls: bspClass, custom: bspCustom } = resolveBspColor(bspStyle);
            cell.textContent = bspValue;
            cell.className = `bsp-value ${bspClass}`;
            if (bspCustom)
                cell.style.color = bspCustom;
        });
    }
    function addBspHeader(factionList) {
        // Skip if BSP extension is not installed (no .iconStats elements)
        if (factionList.querySelectorAll('.iconStats').length === 0) {
            return;
        }
        let headerContainer = factionList.querySelector('.white-grad');
        if (!headerContainer) {
            // Explicitly exclude your-faction container — [class*="faction"] would match it too
            const parentContainer = factionList.closest('.enemy-faction') ||
                factionList.closest('[class*="enemy-faction"]') ||
                factionList.closest('[class*="tabMenuCont"]') ||
                factionList.parentElement;
            if (parentContainer) {
                headerContainer = parentContainer.querySelector('.white-grad');
            }
        }
        if (!headerContainer) {
            const allWhiteGrad = document.querySelectorAll('.white-grad');
            for (const container of allWhiteGrad) {
                // Skip headers belonging to "your faction" only if factionList is NOT in your-faction
                if (container.closest('.your-faction') && !factionList.closest('.your-faction'))
                    continue;
                const hasLevel = container.querySelector('[class*="level___"]') ||
                    Array.from(container.querySelectorAll('*')).some(el => el.textContent?.trim() === 'Level');
                if (hasLevel) {
                    const containerParent = container.closest('.enemy-faction') || container.closest('[class*="faction"]');
                    const factionParent = factionList.closest('.enemy-faction') || factionList.closest('[class*="faction"]');
                    if (containerParent === factionParent ||
                        (containerParent && containerParent.contains(factionList)) ||
                        container.parentElement === factionList.parentElement) {
                        headerContainer = container;
                        break;
                    }
                }
            }
        }
        let levelHeaderElement = null;
        const searchTarget = headerContainer || factionList;
        levelHeaderElement = searchTarget.querySelector('.level.left.level___g3CWR.tab___UztMc');
        if (!levelHeaderElement) {
            levelHeaderElement = searchTarget.querySelector('[class*="level___"]');
        }
        if (!levelHeaderElement) {
            const elements = searchTarget.querySelectorAll('*');
            for (const element of elements) {
                if (element.textContent?.trim() === 'Level' && element.children.length === 0) {
                    levelHeaderElement = element;
                    break;
                }
            }
        }
        const bspAlreadyExists = (headerContainer && headerContainer.querySelector('.bsp-header')) ||
            factionList.querySelector('.bsp-header');
        if (bspAlreadyExists) {
            return;
        }
        if (levelHeaderElement) {
            const bspHeader = document.createElement('div');
            bspHeader.className = 'bsp-header left';
            const _pref1 = getColPref(factionList);
            bspHeader.textContent = _pref1 === 'ff' ? 'FF' : _pref1 === 'ts' ? 'TS' : 'BSP';
            bspHeader.setAttribute('data-col', _pref1);
            if (levelHeaderElement.className) {
                const levelClasses = levelHeaderElement.className.split(' ').filter((cls) => !cls.includes('level') && cls !== 'left');
                bspHeader.className += ' ' + levelClasses.join(' ');
            }
            bspHeader.style.cssText = `
            min-width: 32px !important;
            width: 32px !important;
            flex: 0 0 32px !important;
            text-align: center !important;
            margin-right: 3px !important;
            padding: 2px !important;
            background: none !important;
            border: none !important;
        `;
            bspHeader.style.cursor = 'pointer';
            bspHeader.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                handleStatsHeaderClick.call(this, bspHeader, factionList);
            });
            if (levelHeaderElement.nextSibling) {
                levelHeaderElement.parentNode.insertBefore(bspHeader, levelHeaderElement.nextSibling);
            }
            else {
                levelHeaderElement.parentNode.appendChild(bspHeader);
            }
        }
    }
    // ── FF Scouter Column ───────────────────────────────────────
    function addFFColumn(factionList) {
        const ffStats = this._enhancer?.ffStats;
        if (!ffStats || Object.keys(ffStats).length === 0)
            return;
        const members = factionList.querySelectorAll('li');
        const preference = getColPref(factionList);
        const hasBsp = factionList.querySelectorAll('.iconStats').length > 0;
        const hasTS = !!(StorageUtil.get('cat_tornstats_api_key', '') || '').trim();
        // Show FF only when: preference is 'ff', or no BSP and no TS fallback
        const showFF = preference === 'ff' || (!hasBsp && !hasTS && preference !== 'ts');
        // Get viewer's own BS for dynamic color comparison
        const viewerId = this._enhancer?.apiManager?.playerId
            || StorageUtil.get('cat_user_info', null)?.id?.toString()
            || null;
        const viewerBsRaw = viewerId ? ffStats[viewerId]?.bsRaw ?? null : null;
        members.forEach((member) => {
            if (member.querySelector('.ff-column'))
                return;
            const playerId = getPlayerIdFromRow(member);
            const stats = playerId ? ffStats[playerId] : null;
            const ffValue = stats?.ff;
            const bsDisplay = stats?.bs || '-';
            const ffTooltip = ffValue != null ? `FF: ${ffValue.toFixed(2)}` : '';
            const colorClass = getFFColorClass(stats?.bsRaw ?? null, viewerBsRaw);
            const ffColumn = document.createElement('div');
            ffColumn.className = 'ff-column left';
            ffColumn.innerHTML = `<span class="ff-value bsp-value ${colorClass}" title="${ffTooltip}">${bsDisplay}</span>`;
            ffColumn.style.cssText = `
            display: ${showFF ? 'inline-flex' : 'none'} !important;
            align-items: center !important;
            justify-content: center !important;
            color: #ffffff !important;
            padding: 0 4px !important;
            font-size: 1em !important;
            font-weight: 700 !important;
            margin-right: 3px !important;
            min-width: 35px !important;
            max-width: 42px !important;
            text-align: center !important;
            font-family: 'Monaco', 'Menlo', monospace !important;
        `;
            // Insert after level div (same position as BSP)
            const levelDiv = member.querySelector('[class*="level___"], .level');
            const bspCol = member.querySelector('.bsp-column');
            const insertAfter = bspCol || levelDiv;
            if (insertAfter) {
                if (insertAfter.nextSibling) {
                    insertAfter.parentNode.insertBefore(ffColumn, insertAfter.nextSibling);
                }
                else {
                    insertAfter.parentNode.appendChild(ffColumn);
                }
            }
            else {
                member.appendChild(ffColumn);
            }
        });
        // FF column present → add has-ff so .no-bsp widening rules are neutralised
        const parentForFF = factionList.closest('[class*="tabMenuCont"]') || factionList.closest('.enemy-faction') || factionList.closest('.your-faction');
        if (parentForFF) {
            parentForFF.classList.add('has-ff');
        }
        // Toggle BSP visibility based on preference
        if (showFF) {
            factionList.querySelectorAll('.bsp-column').forEach(el => el.style.setProperty('display', 'none', 'important'));
        }
        // If no BSP header exists, create an FF-only header
        if (!hasBsp) {
            const parentContainer = factionList.closest('.enemy-faction') ||
                factionList.closest('[class*="tabMenuCont"]') ||
                factionList.parentElement;
            const existingHeader = parentContainer?.querySelector('.bsp-header') || factionList.querySelector('.bsp-header');
            if (!existingHeader) {
                let headerContainer = parentContainer?.querySelector('.white-grad') || null;
                if (!headerContainer) {
                    const allWhiteGrad = document.querySelectorAll('.white-grad');
                    for (const container of allWhiteGrad) {
                        if (container.closest('.your-faction') && !factionList.closest('.your-faction'))
                            continue;
                        const hasLevel = container.querySelector('[class*="level___"]');
                        if (hasLevel) {
                            const containerParent = container.closest('.enemy-faction') || container.closest('[class*="faction"]');
                            const factionParent = factionList.closest('.enemy-faction') || factionList.closest('[class*="faction"]');
                            if (containerParent === factionParent ||
                                (containerParent && containerParent.contains(factionList)) ||
                                container.parentElement === factionList.parentElement) {
                                headerContainer = container;
                                break;
                            }
                        }
                    }
                }
                if (headerContainer) {
                    const levelHeaderElement = headerContainer.querySelector('[class*="level___"]') ||
                        Array.from(headerContainer.querySelectorAll('*')).find(el => el.textContent?.trim() === 'Level' && el.children.length === 0) || null;
                    if (levelHeaderElement) {
                        const ffHeader = document.createElement('div');
                        ffHeader.className = 'bsp-header left';
                        const pref = getColPref(factionList);
                        const label = pref === 'ff' ? 'FF' : pref === 'ts' ? 'TS' : 'BSP';
                        ffHeader.textContent = label;
                        if (levelHeaderElement.className) {
                            const levelClasses = levelHeaderElement.className.split(' ').filter((cls) => !cls.includes('level') && cls !== 'left');
                            ffHeader.className += ' ' + levelClasses.join(' ');
                        }
                        ffHeader.style.cssText = `
                        min-width: 32px !important; width: 32px !important; flex: 0 0 32px !important;
                        text-align: center !important; margin-right: 3px !important; padding: 2px !important;
                        background: none !important; border: none !important; cursor: pointer !important;
                    `;
                        ffHeader.setAttribute('data-col', pref);
                        ffHeader.addEventListener('click', (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            handleStatsHeaderClick.call(this, ffHeader, factionList);
                        });
                        if (levelHeaderElement.nextSibling) {
                            levelHeaderElement.parentNode.insertBefore(ffHeader, levelHeaderElement.nextSibling);
                        }
                        else {
                            levelHeaderElement.parentNode.appendChild(ffHeader);
                        }
                    }
                }
            }
        }
    }
    function updateFFColumns() {
        const ffStats = this._enhancer?.ffStats;
        if (!ffStats)
            return;
        // Get viewer's own BS for dynamic color comparison
        const viewerId = this._enhancer?.apiManager?.playerId
            || StorageUtil.get('cat_user_info', null)?.id?.toString()
            || null;
        const viewerBsRaw = viewerId ? ffStats[viewerId]?.bsRaw ?? null : null;
        document.querySelectorAll('.ff-column').forEach(col => {
            const member = col.closest('li');
            if (!member)
                return;
            const playerId = getPlayerIdFromRow(member);
            const stats = playerId ? ffStats[playerId] : null;
            const ffValue = stats?.ff;
            const bsDisplay = stats?.bs || '-';
            const ffTooltip = ffValue != null ? `FF: ${ffValue.toFixed(2)}` : '';
            const colorClass = getFFColorClass(stats?.bsRaw ?? null, viewerBsRaw);
            // Cache .ff-value ref on the column element
            let span = col._catFFSpan || null;
            if (span && !span.isConnected)
                span = null;
            if (!span) {
                span = col.querySelector('.ff-value');
                if (span)
                    col._catFFSpan = span;
            }
            if (span) {
                span.textContent = bsDisplay;
                span.className = `ff-value bsp-value ${colorClass}`;
                span.setAttribute('title', ffTooltip);
            }
        });
    }
    function addFFSwitchArrow(factionList) {
        // Just update the header text to reflect current column + sort state
        // Search in the faction-scoped container to avoid picking up the wrong header when both factions are visible
        const parentContainer = factionList.closest('.enemy-faction') ||
            factionList.closest('.your-faction') ||
            factionList.closest('[class*="tabMenuCont"]') ||
            factionList.parentElement;
        // Try to find the header inside the faction container's header row (.white-grad), not anywhere in the DOM
        const headerRow = parentContainer?.querySelector('.white-grad') || parentContainer;
        const bspHeader = (headerRow?.querySelector('.bsp-header') || parentContainer?.querySelector('.bsp-header'));
        if (!bspHeader)
            return;
        // Set data-col attribute for cycling logic — always sync with stored preference
        // If stored preference is 'bsp' but BSP is not available, fallback to first available col
        let preference = getColPref(factionList);
        const hasBspNow = !!(factionList.closest('.enemy-faction') || factionList.closest('[class*="tabMenuCont"]') || factionList.closest('.your-faction') || factionList)
            ?.querySelector('.iconStats');
        if (preference === 'bsp' && !hasBspNow) {
            const hasFFNow = !!(factionList.closest('.enemy-faction, [class*="tabMenuCont"], .your-faction, body')?.querySelector('.ff-column'));
            const hasTSNow = !!(StorageUtil.get('cat_tornstats_api_key', '') || '').trim();
            preference = hasFFNow ? 'ff' : hasTSNow ? 'ts' : 'bsp';
            setColPref(factionList, preference);
        }
        // Only sync data-col and data-sort from storage if the header hasn't been set by a click
        // (i.e. data-col is absent or matches stored pref, and no active sort arrow is present)
        const existingCol = bspHeader.getAttribute('data-col');
        if (!existingCol || existingCol === preference) {
            bspHeader.setAttribute('data-col', preference);
            // Restore persisted sort direction
            const savedSort = getSortPref(factionList);
            bspHeader.setAttribute('data-sort', savedSort === 'asc' ? 'desc' : 'asc'); // updateStatsHeaderText reads opposite
            updateStatsHeaderText(bspHeader);
        }
    }
    /** Resolve best available stat for a player in TS column mode.
     *  Priority: TS total → FFScouter bsRaw → BSP (iconStats text)
     *  Returns { value, raw, source } where source is 'ts'|'ff'|'bsp'|null */
    function resolveBspColor(bspStyle) {
        if (!bspStyle)
            return { cls: 'bsp-default', custom: null };
        if (bspStyle.includes('#FF0000') || bspStyle.includes('rgb(255, 0, 0)'))
            return { cls: 'bsp-red', custom: null };
        if (bspStyle.includes('#FFB30F') || bspStyle.includes('rgb(255, 179, 15)'))
            return { cls: 'bsp-orange', custom: null };
        if (bspStyle.includes('#47A6FF') || bspStyle.includes('rgb(71, 166, 255)'))
            return { cls: 'bsp-blue', custom: null };
        if (bspStyle.includes('#73DF5D') || bspStyle.includes('rgb(115, 223, 93)'))
            return { cls: 'bsp-green', custom: null };
        if (bspStyle.includes('#FFFFFF') || bspStyle.includes('rgb(255, 255, 255)'))
            return { cls: 'bsp-white', custom: null };
        if (bspStyle.includes('#949494') || bspStyle.includes('rgb(148, 148, 148)'))
            return { cls: 'bsp-gray', custom: null };
        // Custom color set by user in BSP settings — pass it through as inline style
        return { cls: 'bsp-custom', custom: bspStyle };
    }
    function getBspColorClass(iconStats) {
        const s = iconStats.style.background || iconStats.style.backgroundColor;
        return resolveBspColor(s).cls;
    }
    function resolveTSFallback(playerId, member, ffStats) {
        // 1. TornStats
        if (playerId) {
            const tsEntry = getTSCached(playerId);
            if (tsEntry?.total != null) {
                return { display: formatTSTotal(tsEntry.total), raw: tsEntry.total, source: 'ts', tsEntry };
            }
        }
        // 2. BSP (iconStats element in the row) — use its own color
        const iconStats = member.querySelector('.iconStats');
        if (iconStats?.textContent) {
            const bspText = iconStats.textContent.trim();
            if (bspText && bspText !== '-') {
                return { display: bspText, raw: null, source: 'bsp', colorClass: getBspColorClass(iconStats) };
            }
        }
        // 3. FFScouter (bsRaw from server ffStats)
        if (playerId && ffStats) {
            const ff = ffStats[playerId];
            if (ff?.bsRaw != null) {
                return { display: ff.bs || formatTSTotal(ff.bsRaw), raw: ff.bsRaw, source: 'ff' };
            }
        }
        return { display: '-', raw: null, source: null };
    }
    const SOURCE_COLORS = {
        ts: '#86B202',
        ff: '#63b3ed',
        bsp: '#FFB30F',
    };
    function addTSColumn(factionList) {
        const preference = getColPref(factionList);
        const showTS = preference === 'ts';
        const ffStats = this._enhancer?.ffStats;
        const viewerId = this._enhancer?.apiManager?.playerId
            || StorageUtil.get('cat_user_info', null)?.id?.toString()
            || null;
        const viewerEntry = viewerId ? getTSCached(viewerId) : null;
        const viewerTotal = viewerEntry?.total ?? null;
        factionList.querySelectorAll('li').forEach((member) => {
            if (member.querySelector('.ts-column'))
                return;
            const playerId = getPlayerIdFromRow(member);
            const { display, raw, source, colorClass: bspColorClass, tsEntry } = resolveTSFallback(playerId, member, ffStats);
            const colorClass = bspColorClass ?? getFFColorClass(raw, viewerTotal);
            const badgeColor = source ? SOURCE_COLORS[source] : '#555';
            let tooltip = '';
            if (source === 'ts' && tsEntry) {
                const age = tsEntry.timestamp ? `${Math.floor((Date.now() / 1000 - tsEntry.timestamp) / 86400)}d ago` : '';
                tooltip = [
                    `TornStats${age ? ` (${age})` : ''}`,
                    `STR: ${formatTSTotal(tsEntry.strength)}`,
                    `DEF: ${formatTSTotal(tsEntry.defense)}`,
                    `SPD: ${formatTSTotal(tsEntry.speed)}`,
                    `DEX: ${formatTSTotal(tsEntry.dexterity)}`,
                    `Total: ${formatTSTotal(tsEntry.total)}`,
                ].join('\n');
            }
            else if (source) {
                tooltip = `Source: ${source.toUpperCase()}${raw != null ? ` — ${formatTSTotal(raw)}` : ''}`;
            }
            const col = document.createElement('div');
            col.className = 'ts-column left';
            col.setAttribute('data-cat-tooltip', tooltip);
            col.style.cssText = `
            display: ${showTS ? 'inline-flex' : 'none'} !important;
            flex-direction: column !important;
            align-items: center !important;
            justify-content: center !important;
            align-self: center !important;
            padding: 0 4px !important;
            margin-right: 3px !important;
            min-width: 35px !important;
            max-width: 42px !important;
        `;
            col.innerHTML = `
            <span class="ts-value bsp-value ${colorClass}" style="font-size:1em;font-weight:700;font-family:'Monaco','Menlo',monospace;color:#fff;">${display}</span>
            <span class="ts-badge" style="font-size:8px;font-weight:600;color:${badgeColor};line-height:1;letter-spacing:0.3px;">${source ? source.toUpperCase() : ''}</span>
        `;
            // Click → open attack page (respects "Attack in New Tab" setting)
            if (playerId) {
                col.style.setProperty('cursor', 'pointer', 'important');
                col.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const url = `https://www.torn.com/page.php?sid=attack&user2ID=${playerId}`;
                    const newTab = String(StorageUtil.get('cat_attack_new_tab', 'true')) === 'true';
                    if (newTab) {
                        window.open(url, '_blank');
                    }
                    else {
                        window.location.href = url;
                    }
                });
            }
            const bspCol = member.querySelector('.bsp-column');
            const ffCol = member.querySelector('.ff-column');
            const levelDiv = member.querySelector('[class*="level___"], .level');
            const insertAfter = ffCol || bspCol || levelDiv;
            if (insertAfter) {
                if (insertAfter.nextSibling) {
                    insertAfter.parentNode.insertBefore(col, insertAfter.nextSibling);
                }
                else {
                    insertAfter.parentNode.appendChild(col);
                }
            }
            else {
                member.appendChild(col);
            }
            // Force li to be a flex container so align-self / margin:auto work
            const li = member;
            if (li.tagName === 'LI' && !li.dataset.catFlexRow) {
                li.style.display = 'flex';
                li.style.alignItems = 'center';
                li.dataset.catFlexRow = '1';
            }
        });
        // Re-apply saved sort if the user's active sort preference is TS
        if (showTS) {
            const factionKey = factionList.querySelector('li.your, li[class*="your___"]') ? 'your' : 'enemy';
            const savedSortPref = StorageUtil.get(`cat_sort_preference_${factionKey}`, null);
            if (!savedSortPref || savedSortPref.column === 'ts') {
                // Only auto-sort by TS if user explicitly chose TS sort (or no preference saved yet)
                const savedSort = getSortPref(factionList);
                {
                    const fakeHeader = document.createElement('div');
                    fakeHeader.setAttribute('data-sort', savedSort === 'asc' ? 'desc' : 'asc');
                    this.sortByTS(factionList, fakeHeader);
                }
            }
        }
    }
    // ── CD column (energy + drug/medical cooldowns) — your-faction only ──
    const CAT_ASSETS = 'https://cat-script.com/assets';
    function formatCDSeconds(totalSecs, fetchedAt) {
        const fetchedTs = Math.floor(new Date(fetchedAt).getTime() / 1000);
        const elapsed = Math.floor(Date.now() / 1000) - fetchedTs;
        const remaining = Math.max(0, totalSecs - elapsed);
        if (remaining === 0)
            return '✓';
        const h = Math.floor(remaining / 3600);
        const m = Math.floor((remaining % 3600) / 60);
        if (h > 0)
            return `${h}h${m > 0 ? String(m).padStart(2, '0') + 'm' : ''}`;
        return `${m}m`;
    }
    function buildCDInner(bars) {
        const energyText = bars ? `${bars.energy_cur}` : '-';
        const drugText = bars ? formatCDSeconds(bars.drug_cd, bars.fetched_at) : '-';
        const medText = bars ? formatCDSeconds(bars.medical_cd, bars.fetched_at) : '-';
        const drugColor = !bars ? '#888' : bars.drug_cd === 0 ? '#73DF5D' : '#FFB30F';
        const medColor = !bars ? '#888' : bars.medical_cd === 0 ? '#73DF5D' : '#FFB30F';
        const energyPct = bars ? Math.round((bars.energy_cur / bars.energy_max) * 100) : 0;
        const energyColor = !bars ? '#888' : energyPct >= 90 ? '#73DF5D' : energyPct >= 50 ? '#FFB30F' : '#FF4444';
        return `
        <div style="display:flex;align-items:center;white-space:nowrap;margin-left:6px;">
            <svg class="cd-icon-energy" xmlns="http://www.w3.org/2000/svg" fill="#588e22" stroke-width="0" width="11" height="11" viewBox="4.5 5 11 11" style="flex-shrink:0;"><path d="M123,330l1-5h-4l7-6-1,5h4Z" transform="translate(-115 -314)"></path></svg><span class="cd-energy" style="color:${energyColor};font-size:0.78em;font-weight:700;font-family:monospace;">${energyText}</span>
            <img class="cd-img-drug" src="${CAT_ASSETS}/drugcd.png" width="10" height="10" style="flex-shrink:0;margin-left:4px;">
            <span class="cd-drug" style="color:${drugColor};font-size:0.78em;font-weight:700;font-family:monospace;">${drugText}</span>
            <img class="cd-img-med" src="${CAT_ASSETS}/medcd.png" width="10" height="10" style="flex-shrink:0;margin-left:4px;">
            <span class="cd-med" style="color:${medColor};font-size:0.78em;font-weight:700;font-family:monospace;">${medText}</span>
        </div>
    `;
    }
    function addCDColumnToYourFaction(factionList) {
        if (state.catOtherFaction)
            return;
        if (factionList.closest('.enemy-faction'))
            return;
        const barsCache = this._enhancer?.memberBarsCache;
        const members = factionList.querySelectorAll('li');
        members.forEach((member) => {
            if (member.querySelector('.cd-column'))
                return;
            const playerId = getPlayerIdFromRow(member);
            const bars = playerId && barsCache ? barsCache.get(Number(playerId)) : null;
            const col = document.createElement('div');
            col.className = 'cd-column left';
            col.innerHTML = buildCDInner(bars ?? null);
            col.style.cssText = `
            display: inline-flex !important;
            flex-direction: column !important;
            justify-content: center !important;
            align-self: center !important;
            margin-right: 4px !important;
            min-width: 60px !important;
            line-height: 1.3 !important;
        `;
            // Apply stored prefs
            if (String(StorageUtil.get('cat_cd_show_energy', 'true')) !== 'true') {
                col.querySelector('.cd-icon-energy')?.style.setProperty('display', 'none');
                col.querySelector('.cd-energy')?.style.setProperty('display', 'none');
            }
            if (String(StorageUtil.get('cat_cd_show_drug', 'true')) !== 'true') {
                col.querySelector('.cd-img-drug')?.style.setProperty('display', 'none');
                col.querySelector('.cd-drug')?.style.setProperty('display', 'none');
            }
            if (String(StorageUtil.get('cat_cd_show_med', 'true')) !== 'true') {
                col.querySelector('.cd-img-med')?.style.setProperty('display', 'none');
                col.querySelector('.cd-med')?.style.setProperty('display', 'none');
            }
            const statusEl = member.querySelector('[class*="status___"], .status.left');
            if (statusEl) {
                if (statusEl.nextSibling) {
                    statusEl.parentNode.insertBefore(col, statusEl.nextSibling);
                }
                else {
                    statusEl.parentNode.appendChild(col);
                }
            }
            else {
                member.appendChild(col);
            }
        });
    }
    function updateCDColumns() {
        const barsCache = this._enhancer?.memberBarsCache;
        if (!barsCache)
            return;
        const yourFaction = document.querySelector('.your-faction');
        if (!yourFaction)
            return;
        yourFaction.querySelectorAll('.cd-column').forEach(col => {
            const member = col.closest('li');
            if (!member)
                return;
            const playerId = getPlayerIdFromRow(member);
            const bars = playerId ? barsCache.get(Number(playerId)) : null;
            const energyPct = bars ? Math.round((bars.energy_cur / bars.energy_max) * 100) : 0;
            const energyColor = !bars ? '#888' : energyPct >= 90 ? '#73DF5D' : energyPct >= 50 ? '#FFB30F' : '#FF4444';
            const drugColor = !bars ? '#888' : bars.drug_cd === 0 ? '#73DF5D' : '#FFB30F';
            const medColor = !bars ? '#888' : bars.medical_cd === 0 ? '#73DF5D' : '#FFB30F';
            const energySpan = col.querySelector('.cd-energy');
            const drugSpan = col.querySelector('.cd-drug');
            const medSpan = col.querySelector('.cd-med');
            if (energySpan && drugSpan && medSpan) {
                // Update text/color only — images stay, no flicker
                energySpan.textContent = bars ? `${bars.energy_cur}` : '-';
                energySpan.style.color = energyColor;
                drugSpan.textContent = bars ? formatCDSeconds(bars.drug_cd, bars.fetched_at) : '-';
                drugSpan.style.color = drugColor;
                medSpan.textContent = bars ? formatCDSeconds(bars.medical_cd, bars.fetched_at) : '-';
                medSpan.style.color = medColor;
            }
            else {
                // Fallback: first render (spans not yet present)
                col.innerHTML = buildCDInner(bars ?? null);
            }
        });
    }
    function updateTSColumns() {
        const ffStats = this._enhancer?.ffStats;
        const viewerId = this._enhancer?.apiManager?.playerId
            || StorageUtil.get('cat_user_info', null)?.id?.toString()
            || null;
        const viewerEntry = viewerId ? getTSCached(viewerId) : null;
        const viewerTotal = viewerEntry?.total ?? null;
        document.querySelectorAll('.ts-column').forEach(col => {
            const member = col.closest('li');
            if (!member)
                return;
            const playerId = getPlayerIdFromRow(member);
            const { display, raw, source, colorClass: bspColorClass, tsEntry } = resolveTSFallback(playerId, member, ffStats);
            const colorClass = bspColorClass ?? getFFColorClass(raw, viewerTotal);
            const badgeColor = source ? SOURCE_COLORS[source] : '#555';
            let tooltip = '';
            if (source === 'ts' && tsEntry) {
                const age = tsEntry.timestamp ? `${Math.floor((Date.now() / 1000 - tsEntry.timestamp) / 86400)}d ago` : '';
                tooltip = [
                    `TornStats${age ? ` (${age})` : ''}`,
                    `STR: ${formatTSTotal(tsEntry.strength)}`,
                    `DEF: ${formatTSTotal(tsEntry.defense)}`,
                    `SPD: ${formatTSTotal(tsEntry.speed)}`,
                    `DEX: ${formatTSTotal(tsEntry.dexterity)}`,
                    `Total: ${formatTSTotal(tsEntry.total)}`,
                ].join('\n');
            }
            else if (source) {
                tooltip = `Source: ${source.toUpperCase()}${raw != null ? ` — ${formatTSTotal(raw)}` : ''}`;
            }
            col.setAttribute('data-cat-tooltip', tooltip);
            const span = col.querySelector('.ts-value');
            const badge = col.querySelector('.ts-badge');
            if (span) {
                span.textContent = display;
                span.className = `ts-value bsp-value ${colorClass}`;
            }
            if (badge) {
                badge.textContent = source ? source.toUpperCase() : '';
                badge.style.color = badgeColor;
            }
        });
    }

    // Serial queue for call/uncall operations — prevents race conditions on rapid clicks
    let _callQueue = Promise.resolve();
    function enqueueCallOp(fn) {
        _callQueue = _callQueue.then(fn, fn);
    }
    function addCallButtons(factionList) {
        // Allow admins to add call buttons when viewing other factions
        const isAdmin = this._enhancer?.subscriptionData?.isAdmin || false;
        if (state.catOtherFaction && !isAdmin) {
            return;
        }
        const attackSelectors = [
            'a[href*="getInAttack"]',
            'a[href*="attack"]',
            'button[onclick*="attack"]',
            '.attack a',
            'a[href*="/loader.php?sid=attack"]',
            'a[href*="/page.php?sid=attack"]',
            'a[href*="loader2.php?sid=getInAttack"]'
        ];
        let activeAttackButtons = [];
        attackSelectors.forEach(selector => {
            const found = factionList.querySelectorAll(selector);
            if (found.length > 0) {
                activeAttackButtons.push(...found);
            }
        });
        activeAttackButtons = [...new Set(activeAttackButtons)];
        const hasEnemyElements = factionList.querySelectorAll('[class*="enemy"]').length > 0;
        const hasEnemyMembers = factionList.querySelectorAll('li[class*="enemy"]').length > 0;
        const hasAttackDivs = factionList.querySelectorAll('.attack').length > 0;
        const hasAttackButtons = activeAttackButtons.length > 0;
        const isOwnFactionContext = factionList.closest('[class*="own"]') ||
            factionList.closest('[class*="friendly"]') ||
            factionList.querySelector('[class*="own"]') ||
            factionList.querySelector('[class*="friendly"]');
        const isEnemyFaction = (hasAttackDivs || hasAttackButtons) &&
            (hasEnemyElements || hasEnemyMembers) &&
            !isOwnFactionContext;
        const memberSelectors = [
            'li[class*="member"]',
            'li[class*="enemy"]',
            'li',
            '[class*="member"]',
            'tr[class*="member"]',
            'div[class*="member"]'
        ];
        let members = [];
        memberSelectors.forEach(selector => {
            const found = factionList.querySelectorAll(selector);
            if (found.length > 0) {
                if (members.length === 0) {
                    members = found;
                }
            }
        });
        if (members.length === 0) {
            return;
        }
        if (isEnemyFaction) {
            members.forEach((member, index) => {
                if (member.tagName !== 'LI' && member.className.includes('memberRowWp') && member.className.includes('enemy')) {
                    return;
                }
                const hasCallButton = !!member.querySelector('.call-button');
                member.querySelector('.attack');
                if (!hasCallButton) {
                    const attackDiv = member.querySelector('.attack');
                    let attackElement = null;
                    if (attackDiv) {
                        attackElement = attackDiv.querySelector('a') || attackDiv.querySelector('span');
                    }
                    if (!attackElement) {
                        attackSelectors.forEach(selector => {
                            if (!attackElement) {
                                attackElement = member.querySelector(selector);
                            }
                        });
                    }
                    if (attackDiv) {
                        const attackContainer = attackDiv;
                        // Extract member info at block scope for both call and rally buttons
                        const enhancerRef = window.FactionWarEnhancer;
                        const memberRow = member.closest('li') || member.closest('tr') || member;
                        let extractedMemberName = 'Unknown';
                        let extractedMemberId = null;
                        const memberElement = memberRow.querySelector('[class*="member___"], .member');
                        if (memberElement) {
                            const clone = memberElement.cloneNode(true);
                            clone.querySelectorAll('.iconStats, .bsp-value, .bsp-column, [class*="iconStats"]').forEach((el) => el.remove());
                            const cleanText = (clone.textContent || '').trim().split('\n')[0].trim();
                            if (cleanText)
                                extractedMemberName = enhancerRef ? enhancerRef.cleanMemberName(cleanText) : cleanText;
                        }
                        // Tag member name element for loadout tooltip (filled in after extractedMemberId is resolved below)
                        const attackLink = memberRow.querySelector('a[href*="getInAttack"], a[href*="user2ID"]');
                        if (attackLink) {
                            const match = attackLink.href.match(/user2ID=(\d+)/);
                            if (match)
                                extractedMemberId = match[1];
                        }
                        if (!extractedMemberId) {
                            const profileLink = memberRow.querySelector('a[href*="profiles.php?XID="]');
                            if (profileLink) {
                                const m = profileLink.href.match(/XID=(\d+)/);
                                if (m)
                                    extractedMemberId = m[1];
                            }
                        }
                        if (extractedMemberId && enhancerRef && enhancerRef._memberNames && enhancerRef._memberNames[extractedMemberId]) {
                            extractedMemberName = enhancerRef._memberNames[extractedMemberId];
                        }
                        // Use viewed faction ID if admin viewing another faction, otherwise use player's own faction
                        const isAdmin = enhancerRef?.subscriptionData?.isAdmin || false;
                        const viewingOtherFaction = state.catOtherFaction && state.viewingFactionId;
                        let extractedFactionId = StorageUtil.get('cat_user_faction_id', null) || null;
                        if (isAdmin && viewingOtherFaction) {
                            extractedFactionId = state.viewingFactionId;
                        }
                        const callButton = document.createElement('button');
                        callButton.className = 'call-button';
                        callButton.dataset.cat = '1';
                        callButton.textContent = 'Call';
                        callButton.dataset.factionId = extractedFactionId || '';
                        // Styles handled by .call-button CSS class
                        callButton.onclick = function (e) {
                            e.preventDefault();
                            e.stopPropagation();
                            const enhancer = window.FactionWarEnhancer;
                            if (!enhancer || !enhancer.pollingManager) {
                                alert('Polling Manager non initialisé');
                                return;
                            }
                            // Check if admin viewing another faction
                            const isAdmin = enhancer.subscriptionData?.isAdmin || false;
                            const viewingOtherFaction = state.catOtherFaction && state.viewingFactionId;
                            if (document.body.classList.contains('cat-read-only') && !(isAdmin && viewingOtherFaction)) {
                                return;
                            }
                            if (!enhancer.pollingManager.isActive()) {
                                enhancer.pollingManager.start();
                            }
                            const existingCallId = callButton.dataset.callId;
                            if (existingCallId === 'pending') {
                                // Button is disabled during pending — this branch should not be reachable
                                return;
                            }
                            if (existingCallId) {
                                const uncallMemberRow = callButton.closest('li') || callButton.closest('tr');
                                let uncallMemberName = callButton.dataset.cachedMemberName || 'Unknown';
                                const uncallMemberId = callButton.dataset.memberId || null;
                                if (uncallMemberName === 'Unknown' && uncallMemberId && enhancer && enhancer._memberNames && enhancer._memberNames[uncallMemberId]) {
                                    uncallMemberName = enhancer._memberNames[uncallMemberId];
                                }
                                else if (uncallMemberName === 'Unknown' && uncallMemberRow) {
                                    const uncallMemberEl = uncallMemberRow.querySelector('[class*="member___"], .member');
                                    if (uncallMemberEl) {
                                        const clone = uncallMemberEl.cloneNode(true);
                                        clone.querySelectorAll('.iconStats, .bsp-value, .bsp-column, [class*="iconStats"]').forEach((el) => el.remove());
                                        const rawName = (clone.textContent || '').trim().split('\n')[0].trim();
                                        uncallMemberName = enhancer ? enhancer.cleanMemberName(rawName) : rawName;
                                    }
                                }
                                if (callButton.firstChild && callButton.firstChild.nodeType === 3) {
                                    callButton.firstChild.nodeValue = 'Call';
                                }
                                else {
                                    callButton.textContent = 'Call';
                                }
                                callButton.className = 'call-button';
                                if (callButton.style.length > 0)
                                    callButton.removeAttribute('style');
                                delete callButton.dataset.callId;
                                callButton.dataset.callState = '';
                                document.querySelectorAll('.call-button.call-locked').forEach(btn => {
                                    btn.className = 'call-button';
                                    btn.disabled = false;
                                });
                                // Block polling from overwriting optimistic UI while cancel is queued/in-flight
                                if (enhancer.pollingManager)
                                    enhancer.pollingManager._callInFlight++;
                                // Remove call from local state instantly on uncall
                                const uncallMemberId2 = uncallMemberId || callButton.dataset.cachedMemberId || '';
                                if (uncallMemberId2) {
                                    enhancer.currentCalls = enhancer.currentCalls.filter(c => c.memberId !== uncallMemberId2);
                                    enhancer.updateCallButtons(enhancer.currentCalls);
                                }
                                // Cancel scheduled notification on uncall
                                if (typeof window.flutter_inappwebview !== 'undefined' && uncallMemberId) {
                                    try {
                                        window.flutter_inappwebview.callHandler('cancelNotification', {
                                            id: Math.abs(parseInt(uncallMemberId, 10)) % 10000
                                        });
                                    }
                                    catch (_) { }
                                }
                                else if (isExtensionMode() && uncallMemberId) {
                                    extensionCancelNotify(`hosp-${uncallMemberId}`);
                                }
                                enqueueCallOp(async () => {
                                    try {
                                        const result = await enhancer.pollingManager.cancelCall(existingCallId, enhancer.apiManager.playerName, uncallMemberName);
                                        if (!result || !result.success) {
                                            console.log('❌ [UNCALL] Failed:', result?.error);
                                            callButton.textContent = 'Error';
                                            setTimeout(() => { callButton.textContent = 'Call'; }, 2000);
                                        }
                                    }
                                    catch (err) {
                                        enhancer.apiManager.reportError('uncallUI', err);
                                        callButton.textContent = 'Error';
                                        setTimeout(() => { callButton.textContent = 'Call'; }, 2000);
                                    }
                                    finally {
                                        // Decrement the early increment from the click handler
                                        if (enhancer.pollingManager)
                                            enhancer.pollingManager._callInFlight = Math.max(0, enhancer.pollingManager._callInFlight - 1);
                                    }
                                });
                                return;
                            }
                            const memberRow = callButton.closest('li') || callButton.closest('tr');
                            let memberName = callButton.dataset.cachedMemberName || 'Unknown';
                            let memberId = null;
                            let targetStatus = null;
                            if (memberRow) {
                                if (memberName === 'Unknown') {
                                    const memberElement = memberRow.querySelector('[class*="member___"], .member');
                                    if (memberElement) {
                                        const clone = memberElement.cloneNode(true);
                                        clone.querySelectorAll('.iconStats, .bsp-value, .bsp-column, [class*="iconStats"]').forEach((el) => el.remove());
                                        const cleanText = (clone.textContent || '').trim().split('\n')[0].trim();
                                        if (cleanText)
                                            memberName = enhancer ? enhancer.cleanMemberName(cleanText) : cleanText;
                                    }
                                }
                                const attackLink = memberRow.querySelector('a[href*="getInAttack"], a[href*="user2ID"]');
                                if (attackLink) {
                                    const match = attackLink.href.match(/user2ID=(\d+)/);
                                    if (match) {
                                        memberId = match[1];
                                    }
                                }
                                if (!memberId) {
                                    const profileLink = memberRow.querySelector('a[href*="profiles.php?XID="]');
                                    if (profileLink) {
                                        const m = profileLink.href.match(/XID=(\d+)/);
                                        if (m)
                                            memberId = m[1];
                                    }
                                }
                                const statusElement = memberRow.querySelector('.status.left, [class*="status___"]');
                                if (statusElement) {
                                    targetStatus = statusElement.textContent.trim();
                                }
                            }
                            if (memberId && enhancer && enhancer._memberNames && enhancer._memberNames[memberId]) {
                                memberName = enhancer._memberNames[memberId];
                            }
                            const callerName = enhancer.apiManager.playerName || '...';
                            if (callButton.firstChild && callButton.firstChild.nodeType === 3) {
                                callButton.firstChild.nodeValue = callerName;
                            }
                            else {
                                callButton.textContent = callerName;
                            }
                            callButton.className = 'call-button my-call';
                            if (callButton.style.length > 0)
                                callButton.removeAttribute('style');
                            callButton.dataset.callState = '';
                            callButton.dataset.callId = 'pending';
                            callButton.dataset.memberId = memberId || '';
                            callButton.disabled = true;
                            document.querySelectorAll('.call-button').forEach(btn => {
                                if (btn !== callButton && !btn.classList.contains('my-call') && !btn.classList.contains('other-call')) {
                                    btn.className = 'call-button call-locked';
                                    btn.disabled = true;
                                    btn.dataset.callState = '';
                                }
                            });
                            enqueueCallOp(async () => {
                                // Re-evaluate faction ID at click time (PDA timing: dataset may have been set before admin state was ready)
                                let factionIdToUse = StorageUtil.get('cat_user_faction_id', null) || '';
                                const clickIsAdmin = enhancer.subscriptionData?.isAdmin || false;
                                if (clickIsAdmin && state.catOtherFaction && state.viewingFactionId) {
                                    factionIdToUse = state.viewingFactionId;
                                }
                                try {
                                    const result = await enhancer.pollingManager.callMember(factionIdToUse, memberId, memberName, targetStatus);
                                    if (result && result.success && result.data) {
                                        callButton.disabled = false;
                                        callButton.dataset.callId = result.data.id;
                                        callButton.dataset.memberId = result.data.memberId || memberId || '';
                                        // Inject call locally for instant HIT BONUS marker display
                                        const localCall = {
                                            id: result.data.id,
                                            factionId: StorageUtil.get('cat_user_faction_id', null) || '',
                                            memberId: result.data.memberId || memberId || '',
                                            memberName: memberName,
                                            callerId: enhancer.apiManager.playerId || '',
                                            callerName: enhancer.apiManager.playerName || '',
                                            targetStatus: targetStatus || null,
                                            createdAt: Date.now(),
                                        };
                                        // Reset callState so updateCallButtons fully processes this row
                                        callButton.dataset.callState = '';
                                        enhancer.currentCalls = [...enhancer.currentCalls.filter(c => c.memberId !== localCall.memberId), localCall];
                                        enhancer.updateCallButtons(enhancer.currentCalls);
                                        // Schedule notification when target is hosp with known timer
                                        const isPDA = typeof window.flutter_inappwebview !== 'undefined';
                                        const notifMasterOn = String(StorageUtil.get('cat_pda_notifications', 'true')) === 'true';
                                        const notifHospEnabled = String(StorageUtil.get('cat_pda_notif_hosp', 'true')) === 'true';
                                        const notifLeadTime = parseInt(String(StorageUtil.get('cat_pda_notif_lead', '20')), 10) * 1000;
                                        if (notifMasterOn && notifHospEnabled && memberId && enhancer.hospTime[memberId]) {
                                            const endTime = enhancer.hospTime[memberId];
                                            const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                                            const notifTime = notifLeadTime > 0 ? endMs - notifLeadTime : endMs;
                                            if (notifTime > Date.now()) {
                                                const cleanName = result.data.memberName || memberName;
                                                const leadLabel = notifLeadTime > 0 ? `${notifLeadTime / 1000}s` : 'now';
                                                if (isPDA) {
                                                    // PDA notification
                                                    const notifId = Math.abs(parseInt(memberId, 10)) % 10000;
                                                    try {
                                                        window.flutter_inappwebview.callHandler('scheduleNotification', {
                                                            title: `${cleanName} is Okay in ${leadLabel}!`,
                                                            subtitle: 'Your called target is about to leave hospital',
                                                            id: notifId,
                                                            timestamp: notifTime,
                                                            urlCallback: `https://www.torn.com/page.php?sid=attack&user2ID=${memberId}`,
                                                            launchNativeToast: false,
                                                        });
                                                    }
                                                    catch (_) { }
                                                }
                                                else if (isExtensionMode()) {
                                                    // Desktop extension notification
                                                    extensionScheduleNotify(`hosp-${memberId}`, `${cleanName} is Okay in ${leadLabel}!`, 'Your called target is about to leave hospital', notifTime, `https://www.torn.com/page.php?sid=attack&user2ID=${memberId}`);
                                                }
                                            }
                                        }
                                    }
                                    else {
                                        if (result?.error === 'already_called') {
                                            enhancer.apiManager.showNotification(`Target already called by ${result.existingCaller}`, 'warning');
                                            callButton.textContent = result.existingCaller || 'Called';
                                            callButton.className = 'call-button other-call';
                                            if (callButton.style.length > 0)
                                                callButton.removeAttribute('style');
                                            callButton.disabled = true;
                                        }
                                        else if (result?.error === 'caller_already_has_call') {
                                            enhancer.apiManager.showNotification(`You already called ${result.targetName}`, 'warning');
                                            callButton.textContent = 'Call';
                                            callButton.className = 'call-button';
                                            if (callButton.style.length > 0)
                                                callButton.removeAttribute('style');
                                        }
                                        else if (result?.error === 'not_activated' || result?.error === 'no_active_war') {
                                            callButton.textContent = 'Call';
                                            callButton.className = 'call-button';
                                            if (callButton.style.length > 0)
                                                callButton.removeAttribute('style');
                                            document.body.classList.add('cat-read-only');
                                        }
                                        else {
                                            enhancer.apiManager.showNotification('Erreur: ' + (result?.error || 'unknown'), 'error');
                                            callButton.textContent = 'Call';
                                            callButton.className = 'call-button';
                                            if (callButton.style.length > 0)
                                                callButton.removeAttribute('style');
                                        }
                                        callButton.disabled = false;
                                        // Unlock all locked buttons when call failed (player has no active call)
                                        if (result?.error !== 'caller_already_has_call') {
                                            document.querySelectorAll('.call-button.call-locked').forEach(btn => {
                                                btn.className = 'call-button';
                                                btn.disabled = false;
                                                btn.dataset.callState = '';
                                            });
                                        }
                                    }
                                }
                                catch (err) {
                                    enhancer.apiManager.reportError('callMemberUI', err);
                                    callButton.textContent = 'Call';
                                    callButton.className = 'call-button';
                                    if (callButton.style.length > 0)
                                        callButton.removeAttribute('style');
                                    callButton.disabled = false;
                                    document.querySelectorAll('.call-button.call-locked').forEach(btn => {
                                        btn.className = 'call-button';
                                        btn.disabled = false;
                                        btn.dataset.callState = '';
                                    });
                                }
                            });
                        };
                        attackContainer.style.display = 'flex';
                        attackContainer.style.alignItems = 'center';
                        attackContainer.style.gap = '4px';
                        attackContainer.style.justifyContent = 'flex-start';
                        attackContainer.style.flexWrap = 'nowrap';
                        attackContainer.style.overflow = 'visible';
                        // Create Rally button
                        const rallyButton = document.createElement('button');
                        rallyButton.className = 'rally-button';
                        rallyButton.innerHTML = '<img src="data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M2.5%209.30803L14.89%206.07703V17.923L9.849%2016.538C9.93273%2017.782%209.01792%2018.8695%207.778%2019C6.51522%2018.8634%205.59405%2017.7412%205.706%2016.476V15.615L2.5%2014.692V9.30803Z%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M21.5%2012.157C21.9142%2012.157%2022.25%2011.8212%2022.25%2011.407C22.25%2010.9928%2021.9142%2010.657%2021.5%2010.657V12.157ZM19.636%2010.657C19.2218%2010.657%2018.886%2010.9928%2018.886%2011.407C18.886%2011.8212%2019.2218%2012.157%2019.636%2012.157V10.657ZM18.7823%2017.2649C19.0733%2017.5597%2019.5481%2017.5627%2019.8429%2017.2717C20.1377%2016.9807%2020.1407%2016.5059%2019.8497%2016.2111L18.7823%2017.2649ZM18.5337%2014.8781C18.2427%2014.5833%2017.7679%2014.5803%2017.4731%2014.8713C17.1783%2015.1623%2017.1753%2015.6371%2017.4663%2015.9319L18.5337%2014.8781ZM19.8513%206.60432C20.1426%206.30978%2020.1399%205.83491%2019.8453%205.54368C19.5508%205.25245%2019.0759%205.25513%2018.7847%205.54968L19.8513%206.60432ZM17.4667%206.88268C17.1754%207.17722%2017.1781%207.65209%2017.4727%207.94332C17.7672%208.23455%2018.2421%208.23187%2018.5333%207.93732L17.4667%206.88268ZM15.64%2017.923C15.64%2017.5088%2015.3042%2017.173%2014.89%2017.173C14.4758%2017.173%2014.14%2017.5088%2014.14%2017.923H15.64ZM14.14%2019C14.14%2019.4142%2014.4758%2019.75%2014.89%2019.75C15.3042%2019.75%2015.64%2019.4142%2015.64%2019H14.14ZM14.14%206.077C14.14%206.49121%2014.4758%206.827%2014.89%206.827C15.3042%206.827%2015.64%206.49121%2015.64%206.077H14.14ZM15.64%205C15.64%204.58579%2015.3042%204.25%2014.89%204.25C14.4758%204.25%2014.14%204.58579%2014.14%205H15.64ZM1.75%209.308C1.75%209.72221%202.08579%2010.058%202.5%2010.058C2.91421%2010.058%203.25%209.72221%203.25%209.308H1.75ZM3.25%208.231C3.25%207.81679%202.91421%207.481%202.5%207.481C2.08579%207.481%201.75%207.81679%201.75%208.231H3.25ZM1.75%2015.769C1.75%2016.1832%202.08579%2016.519%202.5%2016.519C2.91421%2016.519%203.25%2016.1832%203.25%2015.769H1.75ZM3.25%2014.692C3.25%2014.2778%202.91421%2013.942%202.5%2013.942C2.08579%2013.942%201.75%2014.2778%201.75%2014.692H3.25ZM5.86909%2014.8829C5.46479%2014.7929%205.06402%2015.0476%204.97395%2015.4519C4.88387%2015.8562%205.13861%2016.257%205.54291%2016.3471L5.86909%2014.8829ZM9.68591%2017.2701C10.0902%2017.3601%2010.491%2017.1054%2010.5811%2016.7011C10.6711%2016.2968%2010.4164%2015.896%2010.0121%2015.8059L9.68591%2017.2701ZM21.5%2010.657H19.636V12.157H21.5V10.657ZM19.8497%2016.2111L18.5337%2014.8781L17.4663%2015.9319L18.7823%2017.2649L19.8497%2016.2111ZM18.7847%205.54968L17.4667%206.88268L18.5333%207.93732L19.8513%206.60432L18.7847%205.54968ZM14.14%2017.923V19H15.64V17.923H14.14ZM15.64%206.077V5H14.14V6.077H15.64ZM3.25%209.308V8.231H1.75V9.308H3.25ZM3.25%2015.769V14.692H1.75V15.769H3.25ZM5.54291%2016.3471L9.68591%2017.2701L10.0121%2015.8059L5.86909%2014.8829L5.54291%2016.3471Z%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fsvg%3E" style="width:12px;height:12px;display:block;">';
                        // Styles handled by .rally-button CSS class
                        rallyButton.dataset.memberId = extractedMemberId || '';
                        rallyButton.dataset.memberName = extractedMemberName;
                        rallyButton.onclick = (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            // Re-evaluate faction ID at click time (PDA timing)
                            let rallyFactionId = extractedFactionId;
                            const rallyIsAdmin = window.FactionWarEnhancer?.subscriptionData?.isAdmin || false;
                            if (rallyIsAdmin && state.catOtherFaction && state.viewingFactionId) {
                                rallyFactionId = state.viewingFactionId;
                            }
                            this.handleRallyClick(rallyButton, extractedMemberId, extractedMemberName, rallyFactionId);
                        };
                        if (attackElement) {
                            attackContainer.insertBefore(callButton, attackElement);
                            attackContainer.insertBefore(rallyButton, attackElement);
                        }
                        else {
                            attackContainer.appendChild(callButton);
                            attackContainer.appendChild(rallyButton);
                        }
                        // markAttacking is now triggered from the attack page "Start fight" button
                        // (see chain-warning.ts hookStartFightButton)
                    }
                }
            });
            setTimeout(() => {
                const enhancer = window.FactionWarEnhancer;
                // Reset hash so updateCallButtons always re-applies after new buttons are injected
                if (enhancer) {
                    enhancer._lastCallsHash = undefined;
                    enhancer._lastBtnCount = undefined;
                    enhancer._memberRowsValid = false;
                }
                if (enhancer && enhancer.currentCalls && enhancer.currentCalls.length > 0) {
                    enhancer.updateCallButtons(enhancer.currentCalls);
                }
                else {
                    // Try cache first for instant paint on PDA (slow first fetch)
                    try {
                        const cached = localStorage.getItem('cat_calls_cache');
                        if (cached) {
                            const parsed = JSON.parse(cached);
                            if (parsed && parsed.calls && parsed.calls.length > 0 && enhancer) {
                                enhancer.updateCallButtons(parsed.calls);
                            }
                        }
                    }
                    catch (_) { }
                    if (enhancer && enhancer.pollingManager) {
                        enhancer.pollingManager.lastCallsHash = null;
                        enhancer.pollingManager.fetchCalls();
                    }
                }
            }, 0);
        }
    }

    function changeLevelToLvl() {
        const levelHeaders = document.querySelectorAll('.white-grad .level___g3CWR, [class*="white-grad"] .level___g3CWR');
        levelHeaders.forEach((header) => {
            if ((header.textContent || '').includes('Level')) {
                const span = header.querySelector('span');
                if (span && span.textContent === 'Level') {
                    span.textContent = 'Lvl';
                }
                else if (header.childNodes.length > 0) {
                    for (const node of header.childNodes) {
                        if (node.nodeType === Node.TEXT_NODE && (node.textContent || '').trim() === 'Level') {
                            node.textContent = 'Lvl';
                            break;
                        }
                    }
                }
            }
        });
    }
    function addLoadingAnimation(element) {
        element.style.opacity = '0';
        element.style.transform = 'translateY(20px)';
        setTimeout(() => {
            element.style.transition = `all ${CONFIG.animations.duration} ${CONFIG.animations.easing}`;
            element.style.opacity = '1';
            element.style.transform = 'translateY(0)';
        }, 100);
    }

    const BONUS_NEAR_THRESHOLD = 5;
    const PISTOL_IMG = `<img src="data:image/gif;base64,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" width="28" height="28" alt="">`;
    const TACTICAL_MARKER_IMGS = {
        smoke: 'smoke.png',
        tear: 'tear.png',
        help: 'help.png'
    };
    const TACTICAL_MARKER_LABELS = {
        smoke: 'Request Smoke',
        tear: 'Request Tear',
        help: 'Help!'
    };
    function parseTargetsFromDOM() {
        const targets = [];
        const enemyContainer = document.querySelector('.enemy-faction.left') ||
            document.querySelector('ul.membersCont');
        if (!enemyContainer)
            return targets;
        const memberElements = enemyContainer.querySelectorAll('li.enemy, li[class*="enemy"]');
        memberElements.forEach(memberEl => {
            try {
                // Use cached refs where available (set by injectLevelIndicators / scanHospitalizedMembers)
                const row = memberEl;
                let memberId = row.dataset.catUid || null;
                if (!memberId) {
                    const attackLink = memberEl.querySelector('a[href*="user2ID"], a[href*="getInAttack"]');
                    if (attackLink) {
                        const match = attackLink.href.match(/user2ID=(\d+)/);
                        if (match)
                            memberId = match[1];
                    }
                    if (memberId)
                        row.dataset.catUid = memberId;
                }
                // Prefer JS ref (set by injectLevelIndicators/scanHosp), fallback to querySelector
                let nameEl = row._catNameEl || null;
                if (nameEl && !nameEl.isConnected) {
                    nameEl = null;
                    row._catNameEl = null;
                }
                if (!nameEl) {
                    nameEl = memberEl.querySelector('.honor-text, [class*="honorText"]');
                    if (nameEl)
                        row._catNameEl = nameEl;
                }
                const name = nameEl ? (nameEl.textContent || '').trim() : null;
                let statusEl = row._catStatusEl || null;
                if (statusEl && !statusEl.isConnected) {
                    statusEl = null;
                    row._catStatusEl = null;
                }
                if (!statusEl) {
                    statusEl = memberEl.querySelector('.status.left, [class*="status___"]');
                    if (statusEl)
                        row._catStatusEl = statusEl;
                }
                const status = statusEl
                    ? (statusEl.dataset.catHospStatus || statusEl.dataset.originalStatus || (statusEl.textContent || '').trim())
                    : 'Unknown';
                let levelEl = row._catLevelEl || null;
                if (levelEl && !levelEl.isConnected) {
                    levelEl = null;
                    row._catLevelEl = null;
                }
                if (!levelEl) {
                    levelEl = memberEl.querySelector('.level.left, [class*="level___"]');
                    if (levelEl)
                        row._catLevelEl = levelEl;
                }
                const level = levelEl ? parseInt((levelEl.textContent || '').trim()) || 0 : 0;
                if (memberId && name) {
                    targets.push({
                        id: memberId,
                        name: name,
                        status: status,
                        level: level
                    });
                }
            }
            catch (e) {
                this.apiManager.reportError('parseTargetDOM', e);
            }
        });
        return targets;
    }
    const RALLY_ICON_SVG = '<img src="data:image/svg+xml,%3Csvg%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20fill-rule%3D%22evenodd%22%20clip-rule%3D%22evenodd%22%20d%3D%22M2.5%209.30803L14.89%206.07703V17.923L9.849%2016.538C9.93273%2017.782%209.01792%2018.8695%207.778%2019C6.51522%2018.8634%205.59405%2017.7412%205.706%2016.476V15.615L2.5%2014.692V9.30803Z%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3Cpath%20d%3D%22M21.5%2012.157C21.9142%2012.157%2022.25%2011.8212%2022.25%2011.407C22.25%2010.9928%2021.9142%2010.657%2021.5%2010.657V12.157ZM19.636%2010.657C19.2218%2010.657%2018.886%2010.9928%2018.886%2011.407C18.886%2011.8212%2019.2218%2012.157%2019.636%2012.157V10.657ZM18.7823%2017.2649C19.0733%2017.5597%2019.5481%2017.5627%2019.8429%2017.2717C20.1377%2016.9807%2020.1407%2016.5059%2019.8497%2016.2111L18.7823%2017.2649ZM18.5337%2014.8781C18.2427%2014.5833%2017.7679%2014.5803%2017.4731%2014.8713C17.1783%2015.1623%2017.1753%2015.6371%2017.4663%2015.9319L18.5337%2014.8781ZM19.8513%206.60432C20.1426%206.30978%2020.1399%205.83491%2019.8453%205.54368C19.5508%205.25245%2019.0759%205.25513%2018.7847%205.54968L19.8513%206.60432ZM17.4667%206.88268C17.1754%207.17722%2017.1781%207.65209%2017.4727%207.94332C17.7672%208.23455%2018.2421%208.23187%2018.5333%207.93732L17.4667%206.88268ZM15.64%2017.923C15.64%2017.5088%2015.3042%2017.173%2014.89%2017.173C14.4758%2017.173%2014.14%2017.5088%2014.14%2017.923H15.64ZM14.14%2019C14.14%2019.4142%2014.4758%2019.75%2014.89%2019.75C15.3042%2019.75%2015.64%2019.4142%2015.64%2019H14.14ZM14.14%206.077C14.14%206.49121%2014.4758%206.827%2014.89%206.827C15.3042%206.827%2015.64%206.49121%2015.64%206.077H14.14ZM15.64%205C15.64%204.58579%2015.3042%204.25%2014.89%204.25C14.4758%204.25%2014.14%204.58579%2014.14%205H15.64ZM1.75%209.308C1.75%209.72221%202.08579%2010.058%202.5%2010.058C2.91421%2010.058%203.25%209.72221%203.25%209.308H1.75ZM3.25%208.231C3.25%207.81679%202.91421%207.481%202.5%207.481C2.08579%207.481%201.75%207.81679%201.75%208.231H3.25ZM1.75%2015.769C1.75%2016.1832%202.08579%2016.519%202.5%2016.519C2.91421%2016.519%203.25%2016.1832%203.25%2015.769H1.75ZM3.25%2014.692C3.25%2014.2778%202.91421%2013.942%202.5%2013.942C2.08579%2013.942%201.75%2014.2778%201.75%2014.692H3.25ZM5.86909%2014.8829C5.46479%2014.7929%205.06402%2015.0476%204.97395%2015.4519C4.88387%2015.8562%205.13861%2016.257%205.54291%2016.3471L5.86909%2014.8829ZM9.68591%2017.2701C10.0902%2017.3601%2010.491%2017.1054%2010.5811%2016.7011C10.6711%2016.2968%2010.4164%2015.896%2010.0121%2015.8059L9.68591%2017.2701ZM21.5%2010.657H19.636V12.157H21.5V10.657ZM19.8497%2016.2111L18.5337%2014.8781L17.4663%2015.9319L18.7823%2017.2649L19.8497%2016.2111ZM18.7847%205.54968L17.4667%206.88268L18.5333%207.93732L19.8513%206.60432L18.7847%205.54968ZM14.14%2017.923V19H15.64V17.923H14.14ZM15.64%206.077V5H14.14V6.077H15.64ZM3.25%209.308V8.231H1.75V9.308H3.25ZM3.25%2015.769V14.692H1.75V15.769H3.25ZM5.54291%2016.3471L9.68591%2017.2701L10.0121%2015.8059L5.86909%2014.8829L5.54291%2016.3471Z%22%20fill%3D%22%23ffffff%22%2F%3E%3C%2Fsvg%3E" style="width:12px;height:12px;display:block;">';
    function removeSoftUncallBadge(memberRow) {
        const badge = memberRow.querySelector('.cat-soft-uncall-badge');
        if (!badge)
            return;
        // Restore rally button with proper SVG icon
        const rallyBtn = document.createElement('button');
        rallyBtn.className = 'rally-button';
        rallyBtn.innerHTML = RALLY_ICON_SVG;
        const memberId = memberRow.dataset.catUid || '';
        if (memberId)
            rallyBtn.dataset.memberId = memberId;
        badge.replaceWith(rallyBtn);
        // Restore "Attack" from "Atk"
        const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
        if (atkText && atkText.textContent?.trim() === 'Atk') {
            atkText.textContent = ' Attack ';
        }
    }
    function softUncallTooltip(type, callerName, badgeText) {
        if (type === 'target-okay')
            return `Target is now Okay — auto-uncall in ${badgeText}`;
        if (type === 'caller-offline')
            return `${callerName || 'Caller'} is offline — auto-uncall in ${badgeText}`;
        return callerName ? `${callerName} hospitalized — auto-uncall in ${badgeText}` : `Caller hospitalized — auto-uncall in ${badgeText}`;
    }
    function insertSoftUncallBadge(memberRow, badgeText, callerName = '', shortenAttack = false, isTargetOkay = false, uncallType) {
        // Replace the rally button with the soft uncall badge
        const rallyBtn = memberRow.querySelector('.rally-button');
        if (!rallyBtn)
            return;
        const badge = document.createElement('span');
        badge.className = isTargetOkay ? 'cat-soft-uncall-badge cat-hosp-uncall' : 'cat-soft-uncall-badge';
        badge.textContent = badgeText;
        badge.dataset.catTooltip = softUncallTooltip(uncallType ?? (isTargetOkay ? 'target-okay' : 'caller-hosp'), callerName, badgeText);
        rallyBtn.replaceWith(badge);
        if (shortenAttack) {
            const attackText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
            if (attackText && attackText.textContent?.trim() === 'Attack') {
                attackText.textContent = ' Atk ';
            }
        }
    }
    function _applyCallsToButtons(factionCalls) {
        const callMapById = {};
        const callMapByName = {};
        factionCalls.forEach(call => {
            const memberId = call.memberId || '';
            const memberName = call.memberName || '';
            const callerId = call.callerId || '';
            const callerName = call.callerName || '';
            const callId = call.id || '';
            const entry = { callerId, callerName, callId, memberId, memberName };
            if (memberId)
                callMapById[memberId] = entry;
            if (memberName)
                callMapByName[memberName] = entry;
        });
        const playerNameLower = this.apiManager.playerName ? this.apiManager.playerName.trim().toLowerCase() : '';
        const playerId = this.apiManager.playerId || '';
        const playerHasActiveCall = factionCalls.some(call => {
            if (playerId && call.callerId && String(call.callerId) === String(playerId))
                return true;
            const callerName = (call.callerName || '').replace(/^\u2694\uFE0F/, '').trim().toLowerCase();
            return !!playerNameLower && callerName === playerNameLower;
        });
        const callButtons = document.querySelectorAll('.call-button');
        // Read chain count once outside loop — was queried once per button before
        let chainBoxEl = this._catChainBoxStat || null;
        if (!chainBoxEl || !chainBoxEl.isConnected) {
            chainBoxEl = document.querySelector('.chain-box-center-stat');
            this._catChainBoxStat = chainBoxEl;
        }
        const domChain = chainBoxEl ? parseInt(chainBoxEl.textContent || '0', 10) || 0 : 0;
        const pollingChain = this.enemyChainData?.ownChain ?? 0;
        const chainBoxOwnChain = Math.max(domChain, pollingChain);
        callButtons.forEach((button) => {
            const memberRow = button.closest('li') || button.closest('tr');
            if (!memberRow)
                return;
            let memberId = button.dataset.cachedMemberId;
            let memberName = button.dataset.cachedMemberName;
            if (memberId === undefined) {
                const attackLink = memberRow.querySelector('a[href*="getInAttack"], a[href*="user2ID"]');
                if (attackLink) {
                    const match = attackLink.href.match(/user2ID=(\d+)/);
                    if (match)
                        memberId = match[1];
                }
                if (!memberId) {
                    const profileLink = memberRow.querySelector('a[href*="profiles.php?XID="]');
                    if (profileLink) {
                        const m = profileLink.href.match(/XID=(\d+)/);
                        if (m)
                            memberId = m[1];
                    }
                }
                if (!memberId)
                    memberId = '';
                const memberNameElement = memberRow.querySelector('[class*="member___"], .member');
                if (memberNameElement) {
                    const clone = memberNameElement.cloneNode(true);
                    clone.querySelectorAll('.iconStats, .bsp-value, .bsp-column, [class*="iconStats"]').forEach((el) => el.remove());
                    memberName = this.cleanMemberName((clone.textContent || '').trim().split('\n')[0].trim());
                }
                else {
                    memberName = '';
                }
                button.dataset.cachedMemberId = memberId;
                button.dataset.cachedMemberName = memberName;
            }
            let callData = null;
            if (memberId && callMapById[memberId]) {
                callData = callMapById[memberId];
            }
            else if (memberName && callMapByName[memberName]) {
                callData = callMapByName[memberName];
            }
            let desiredState;
            // Include bonus assignment in state so badge updates when assignment changes
            const bonusOwnChain = this.enemyChainData?.ownChain ?? 0;
            const bonusKey = this.chainBonusAssignment
                ? ':bonus:' + this.chainBonusAssignment.playerId + ':' + this.chainBonusAssignment.nextBonus + ':' + bonusOwnChain
                : '';
            if (callData) {
                const isMyCall = (playerId && callData.callerId && String(callData.callerId) === String(playerId)) ||
                    (callData.callerName && playerNameLower &&
                        callData.callerName.replace(/^\u2694\uFE0F/, '').trim().toLowerCase() === playerNameLower);
                const onlineStatus = !isMyCall ? (this.onlineStatuses?.[callData.callerId] || 'offline') : '';
                desiredState = (isMyCall ? 'my:' : 'other:') + callData.callerName + ':' + (callData.callId || '') + ':' + onlineStatus + bonusKey;
            }
            else {
                desiredState = (playerHasActiveCall ? 'none:locked' : 'none') + bonusKey;
            }
            // Cache statusEl + atkText per row — avoids repeated querySelector per button
            let statusEl = memberRow._catStatusEl || null;
            if (statusEl && !statusEl.isConnected)
                statusEl = null;
            if (!statusEl) {
                statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                if (statusEl)
                    memberRow._catStatusEl = statusEl;
            }
            let atkText = memberRow._catAtkText || null;
            if (atkText && !atkText.isConnected)
                atkText = null;
            if (!atkText) {
                atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                if (atkText)
                    memberRow._catAtkText = atkText;
            }
            if (button.dataset.callState === desiredState) {
                // Soft uncall badge may need insert/update even when call state unchanged
                if (callData) {
                    const existingBadge = memberRow.querySelector('.cat-soft-uncall-badge');
                    const softUncall = this.softUncalls?.find(su => su.memberId === (callData.memberId || ''));
                    if (softUncall) {
                        const isGreen = softUncall.type === 'target-okay';
                        const remainingSec = Math.max(0, Math.ceil((softUncall.expiresAt - Date.now()) / 1000));
                        const badgeText = `${remainingSec}s`;
                        if (existingBadge) {
                            if (existingBadge.textContent !== badgeText)
                                existingBadge.textContent = badgeText;
                            existingBadge.classList.toggle('cat-hosp-uncall', isGreen);
                            existingBadge.dataset.catTooltip = softUncallTooltip(softUncall.type, softUncall.callerName || '', badgeText);
                            if (atkText && atkText.textContent?.trim() === 'Attack')
                                atkText.textContent = ' Atk ';
                        }
                        else {
                            insertSoftUncallBadge(memberRow, badgeText, softUncall.callerName || '', true, isGreen, softUncall.type);
                        }
                    }
                    else if (existingBadge) {
                        removeSoftUncallBadge(memberRow);
                    }
                    // Re-insert bonus badge if DOM was mutated (e.g. status changed hosp→okay)
                    const isBonusEarly = this.chainBonusAssignment &&
                        String(callData.callerId) === String(this.chainBonusAssignment.playerId);
                    const hasBadgeInDom = memberRow.querySelector('.cat-bonus-badge');
                    if (isBonusEarly && !hasBadgeInDom) {
                        if (statusEl) {
                            const badge = document.createElement('span');
                            badge.className = 'cat-bonus-badge';
                            badge.dataset.catTooltip = `Bonus ${this.chainBonusAssignment.nextBonus} hit assigned to ${this.chainBonusAssignment.playerName}`;
                            badge.innerHTML = '<span class="bonus-hit">HIT</span><span class="bonus-label">BONUS</span>';
                            statusEl.appendChild(badge);
                            memberRow._catBonusBadge = badge;
                        }
                    }
                    else if (!isBonusEarly) {
                        if (hasBadgeInDom) {
                            hasBadgeInDom.remove();
                            memberRow._catBonusBadge = null;
                        }
                        if (statusEl)
                            statusEl.style.removeProperty('margin-left');
                        button.style.removeProperty('margin-left');
                        if (atkText && atkText.textContent?.trim() === 'Atk')
                            atkText.textContent = ' Attack ';
                    }
                    if (isBonusEarly && !button.classList.contains('chain-bonus-assigned')) {
                        button.classList.add('chain-bonus-assigned');
                    }
                    else if (!isBonusEarly && button.classList.contains('chain-bonus-assigned')) {
                        button.classList.remove('chain-bonus-assigned');
                    }
                }
                return;
            }
            button.dataset.callState = desiredState;
            if (callData) {
                const isMyCall = desiredState.startsWith('my:');
                const newClass = isMyCall ? 'call-button my-call' : 'call-button other-call';
                const isAttacking = callData.callerName.startsWith('\u2694\uFE0F');
                const displayName = isAttacking ? callData.callerName.slice(2) : callData.callerName;
                if (button.firstChild && button.firstChild.nodeType === 3) {
                    button.firstChild.nodeValue = displayName;
                }
                else {
                    button.textContent = displayName;
                }
                // Pistol SVG: insert inside status element when attacking (cached on row)
                let existingPistol = memberRow._catPistol || null;
                if (existingPistol && !existingPistol.isConnected)
                    existingPistol = null;
                if (isAttacking && !existingPistol) {
                    if (statusEl) {
                        const pistolSpan = document.createElement('span');
                        pistolSpan.className = 'cat-pistol-icon';
                        pistolSpan.dataset.cat = '1';
                        pistolSpan.innerHTML = PISTOL_IMG;
                        statusEl.appendChild(pistolSpan);
                        memberRow._catPistol = pistolSpan;
                    }
                }
                else if (!isAttacking && existingPistol) {
                    existingPistol.remove();
                    memberRow._catPistol = null;
                }
                // Soft uncall badge: show countdown when caller is hospitalized or target is now Okay
                const existingBadge = memberRow.querySelector('.cat-soft-uncall-badge');
                const softUncall = this.softUncalls?.find(su => su.memberId === (callData.memberId || ''));
                if (softUncall) {
                    const isGreen = softUncall.type === 'target-okay';
                    const remainingSec = Math.max(0, Math.ceil((softUncall.expiresAt - Date.now()) / 1000));
                    const badgeText = `${remainingSec}s`;
                    if (existingBadge) {
                        if (existingBadge.textContent !== badgeText)
                            existingBadge.textContent = badgeText;
                        existingBadge.classList.toggle('cat-hosp-uncall', isGreen);
                        existingBadge.dataset.catTooltip = softUncallTooltip(softUncall.type, softUncall.callerName || '', badgeText);
                        if (atkText && atkText.textContent?.trim() === 'Attack')
                            atkText.textContent = ' Atk ';
                    }
                    else {
                        insertSoftUncallBadge(memberRow, badgeText, softUncall.callerName || '', true, isGreen, softUncall.type);
                    }
                }
                else if (existingBadge) {
                    removeSoftUncallBadge(memberRow);
                }
                // Chain bonus: read chain count once outside the per-button loop (cached above loop)
                const ownChain = chainBoxOwnChain;
                const bonusTarget = this.chainBonusAssignment?.nextBonus ?? 0;
                const hitsAway = bonusTarget - ownChain;
                const isBonusAssigned = this.chainBonusAssignment &&
                    String(callData.callerId) === String(this.chainBonusAssignment.playerId) &&
                    hitsAway >= 1 && hitsAway <= BONUS_NEAR_THRESHOLD;
                const bonusClass = isBonusAssigned ? newClass + ' chain-bonus-assigned' : newClass;
                if (button.className !== bonusClass)
                    button.className = bonusClass;
                // Chain bonus badge in tactical marker space
                let existingBonusBadge = memberRow._catBonusBadge || null;
                if (existingBonusBadge && !existingBonusBadge.isConnected)
                    existingBonusBadge = null;
                if (isBonusAssigned) {
                    if (!existingBonusBadge) {
                        const attackDiv = button.closest('[class*="attack"]') || button.parentElement;
                        if (attackDiv) {
                            attackDiv.style.position = 'relative';
                            const badge = document.createElement('span');
                            badge.className = 'cat-bonus-badge';
                            badge.dataset.catTooltip = `Bonus ${this.chainBonusAssignment.nextBonus} hit assigned to ${this.chainBonusAssignment.playerName}`;
                            badge.innerHTML = '<span class="bonus-hit">HIT</span><span class="bonus-label">BONUS</span>';
                            badge.style.cssText = 'position:absolute;top:50%;transform:translateY(-50%);z-index:1;margin-left:2px;';
                            attackDiv.appendChild(badge);
                            memberRow._catBonusBadge = badge;
                            if (statusEl)
                                statusEl.style.setProperty('margin-left', '-6px', 'important');
                            button.style.setProperty('margin-left', '18px', 'important');
                            if (atkText && atkText.textContent?.trim() === 'Attack')
                                atkText.textContent = ' Atk ';
                        }
                    }
                    else {
                        existingBonusBadge.dataset.catTooltip = `Bonus ${this.chainBonusAssignment.nextBonus} hit assigned to ${this.chainBonusAssignment.playerName}`;
                    }
                }
                else {
                    if (existingBonusBadge) {
                        existingBonusBadge.remove();
                        memberRow._catBonusBadge = null;
                    }
                    if (statusEl)
                        statusEl.style.removeProperty('margin-left');
                    button.style.removeProperty('margin-left');
                    if (atkText && atkText.textContent?.trim() === 'Atk')
                        atkText.textContent = ' Attack ';
                }
                // Color other-call caller names by online status (if setting enabled)
                if (!isMyCall && String(StorageUtil.get('cat_caller_status_color', 'true')) === 'true') {
                    const olStatus = this.onlineStatuses?.[callData.callerId] || 'offline';
                    const color = olStatus === 'online' ? '#ACEA01' : olStatus === 'idle' ? '#F5B800' : '#999';
                    button.style.setProperty('color', color, 'important');
                }
                else if (!isBonusAssigned) {
                    if (button.style.length > 0)
                        button.removeAttribute('style');
                }
                button.disabled = !isMyCall;
                button.dataset.callId = callData.callId;
                button.dataset.memberId = callData.memberId || '';
                button.dataset.tooltip = isBonusAssigned
                    ? `${displayName} — Bonus ${this.chainBonusAssignment.nextBonus} hit`
                    : displayName;
            }
            else {
                // Remove pistol icon, soft uncall badge, and bonus badge if present
                const leftoverPistol = memberRow._catPistol;
                if (leftoverPistol) {
                    leftoverPistol.remove();
                    memberRow._catPistol = null;
                }
                const leftoverBonus = memberRow._catBonusBadge;
                if (leftoverBonus) {
                    leftoverBonus.remove();
                    memberRow._catBonusBadge = null;
                    if (statusEl)
                        statusEl.style.removeProperty('margin-left');
                    button.style.removeProperty('margin-left');
                }
                if (atkText && atkText.textContent?.trim() === 'Atk')
                    atkText.textContent = ' Attack ';
                removeSoftUncallBadge(memberRow);
                if (button.firstChild && button.firstChild.nodeType === 3) {
                    button.firstChild.nodeValue = 'Call';
                }
                else {
                    button.textContent = 'Call';
                }
                const newClass = playerHasActiveCall ? 'call-button call-locked' : 'call-button';
                if (button.className !== newClass)
                    button.className = newClass;
                if (button.style.length > 0)
                    button.removeAttribute('style');
                button.disabled = !!playerHasActiveCall;
                delete button.dataset.callId;
                delete button.dataset.memberId;
                button.dataset.tooltip = playerHasActiveCall ? 'You already have an active call' : '';
            }
        });
    }
    function updateCallButtons(calls) {
        if (this.isUpdatingButtons)
            return;
        const suHash = this.softUncalls?.length ? this.softUncalls.map(su => `${su.callId}:${Math.ceil((su.expiresAt - Date.now()) / 1000)}`).join(',') : '';
        const olHash = calls.length && this.onlineStatuses ? calls.map(c => `${c.callerId}:${this.onlineStatuses[c.callerId] || ''}`).join(',') : '';
        const bonusHash = this.chainBonusAssignment ? `|bonus:${this.chainBonusAssignment.playerId}:${this.chainBonusAssignment.nextBonus}` : '';
        const callsHash = JSON.stringify(calls.map(c => `${c.id || c.memberId}:${c.callerName}`).sort()) + suHash + olHash + bonusHash;
        // Only recount buttons when structural change happened (memberRowsValid reset) — querySelectorAll is O(DOM)
        const btnCount = this._memberRowsValid && this._lastBtnCount !== undefined
            ? this._lastBtnCount
            : document.querySelectorAll('.call-button').length;
        if (callsHash === this._lastCallsHash && btnCount === this._lastBtnCount) {
            return;
        }
        this._lastCallsHash = callsHash;
        this._lastBtnCount = this._memberRowsValid ? btnCount : document.querySelectorAll('.call-button').length;
        // Pause EnhancementManager mutation processing while we update call buttons
        // (avoids reacting to our own DOM changes)
        if (this.enhancementManager) {
            this.enhancementManager._mutationsPaused = true;
        }
        try {
            this.isUpdatingButtons = true;
            // Use player's own faction ID for call filtering (server stores calls under player's faction)
            const currentFactionId = StorageUtil.get('cat_user_faction_id', null) || '';
            // Normalize faction IDs for comparison (support both 'faction-123' and '123' formats)
            const normalizedCurrentFactionId = normalizeFactionId(currentFactionId);
            const isFactionKnown = normalizedCurrentFactionId && normalizedCurrentFactionId !== 'unknown-faction' && !normalizedCurrentFactionId.startsWith('player-');
            // Check if user has calls on multiple factions (likely admin)
            const uniqueFactions = new Set(calls.map(call => {
                return normalizeFactionId(call.factionId);
            }).filter(f => f && f !== 'unknown-faction'));
            const hasMultipleFactions = uniqueFactions.size > 1;
            // Don't filter calls if:
            // - user has calls on multiple factions (admin cross-faction testing)
            // - admin is viewing another faction (their calls are stored under enemy factionId, not own)
            const isViewingOtherFaction = state.catOtherFaction;
            const factionCalls = (isFactionKnown && !hasMultipleFactions && !isViewingOtherFaction) ? calls.filter(call => {
                const normalizedCallFactionId = normalizeFactionId(call.factionId);
                return normalizedCallFactionId === normalizedCurrentFactionId;
            }) : calls;
            this._applyCallsToButtons(factionCalls);
        }
        catch (error) {
            console.log('[WS] Error updating call buttons:', error);
            this.apiManager.reportError('updateCallButtonsWS', error);
        }
        finally {
            this.isUpdatingButtons = false;
            // Resume EnhancementManager mutation processing
            if (this.enhancementManager) {
                this.enhancementManager._mutationsPaused = false;
            }
        }
    }
    async function updateCallButtonsFromServer() {
        if (this.isUpdatingButtons)
            return;
        try {
            this.isUpdatingButtons = true;
            let calls = await this.apiManager.getCalls();
            if (!calls || calls.length === 0) {
                if (this.apiManager.lastValidCalls && this.apiManager.lastValidCalls.length > 0) {
                    calls = this.apiManager.lastValidCalls;
                }
                else {
                    calls = [];
                }
            }
            else {
                const currentFactionId = StorageUtil.get('cat_user_faction_id', null) || '';
                const normalizedCurrent = normalizeFactionId(currentFactionId);
                calls = calls.filter((call) => {
                    const callFid = normalizeFactionId(String(call.factionId || ''));
                    return callFid === normalizedCurrent;
                });
            }
            this._applyCallsToButtons(calls);
        }
        catch (error) {
            console.log('Error updating call buttons:', error);
            this.apiManager.reportError('updateCallButtonsServer', error);
        }
        finally {
            this.isUpdatingButtons = false;
        }
    }
    function updateTacticalMarkers() {
        if (!this.tacticalMarkers)
            return;
        // Use cached member rows if valid, otherwise re-query
        if (!this._memberRowsValid || !this._cachedMemberRows) {
            this._cachedMemberRows = Array.from(document.querySelectorAll('.desc-wrap li[class*="member___"], .desc-wrap li.enemy, .desc-wrap li.your'));
            this._memberRowsValid = true;
        }
        const memberRows = this._cachedMemberRows;
        for (let i = 0; i < memberRows.length; i++) {
            const row = memberRows[i];
            if (!row.isConnected) {
                this._memberRowsValid = false;
                continue;
            }
            // Extract member ID — use cached data-cat-uid first
            let memberId = row.dataset.catUid || null;
            if (!memberId) {
                const attackLink = row.querySelector('a[href*="getInAttack"], a[href*="user2ID"]');
                if (attackLink) {
                    const match = attackLink.href.match(/user2ID=(\d+)/);
                    if (match)
                        memberId = match[1];
                }
                if (!memberId) {
                    const profileLink = row.querySelector('a[href*="profiles.php?XID="]');
                    if (profileLink) {
                        const match = profileLink.href.match(/XID=(\d+)/);
                        if (match)
                            memberId = match[1];
                    }
                }
                if (memberId)
                    row.dataset.catUid = memberId;
            }
            if (!memberId)
                continue;
            const marker = this.tacticalMarkers[memberId];
            // Cache tactical marker & pistol refs on the row to avoid querySelector every second
            let existingMarker = row._catTactical || null;
            if (existingMarker && !existingMarker.isConnected)
                existingMarker = null;
            let pistolEl = row._catPistol || null;
            if (pistolEl && !pistolEl.isConnected) {
                pistolEl = null;
                row._catPistol = null;
            }
            const hasPistol = !!pistolEl;
            // If pistol is active (someone is attacking), hide tactical marker
            if (hasPistol) {
                if (existingMarker) {
                    existingMarker.remove();
                    row._catTactical = null;
                }
                continue;
            }
            if (marker) {
                const type = marker.markerType;
                const imgFile = TACTICAL_MARKER_IMGS[type];
                if (!imgFile)
                    continue;
                const serverUrl = StorageUtil.get('cat_server_url', null) || 'https://cat-script.com';
                const imgSrc = `${serverUrl}/assets/${imgFile}`;
                if (existingMarker) {
                    if (existingMarker.getAttribute('data-marker') !== type) {
                        existingMarker.setAttribute('data-marker', type);
                        existingMarker.innerHTML = `<img src="${imgSrc}" alt="${type}">`;
                        existingMarker.setAttribute('title', `${TACTICAL_MARKER_LABELS[type] || type} (${marker.setByName})`);
                    }
                }
                else {
                    const statusEl = row.querySelector('[class*="status___"], .status.left');
                    if (!statusEl)
                        continue;
                    const span = document.createElement('span');
                    span.className = 'cat-tactical-marker';
                    span.dataset.cat = '1';
                    span.setAttribute('data-marker', type);
                    span.title = `${TACTICAL_MARKER_LABELS[type] || type} (${marker.setByName})`;
                    span.innerHTML = `<img src="${imgSrc}" alt="${type}">`;
                    statusEl.appendChild(span);
                    row._catTactical = span;
                }
            }
            else if (existingMarker) {
                existingMarker.remove();
                row._catTactical = null;
            }
        }
    }

    function injectTabsMenu() {
        if (document.getElementById('custom-tabs-menu')) {
            return;
        }
        // Block all functionality if version is too old
        if (state.updateRequired) {
            if (document.getElementById('cat-update-required-banner'))
                return;
            const warList = document.getElementById('faction_war_list_id')
                || document.querySelector('.f-war-list');
            if (!warList) {
                setTimeout(() => this.injectTabsMenu(), 200);
                return;
            }
            const factionWarInfo = document.querySelector('.faction-war-info, [class*="factionWarInfo"]');
            const descWrap = factionWarInfo ? (factionWarInfo.closest('.desc-wrap, [class*="warDesc"]') || factionWarInfo.parentNode) : null;
            const banner = document.createElement('div');
            banner.id = 'cat-update-required-banner';
            banner.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:8px;padding:6px 12px;background:linear-gradient(to right,#3d0000,#4a0000);border:1px solid #cc3333;border-radius:5px;margin:8px 0 0 0;font-family:"Helvetica Neue",Arial,sans-serif;font-size:11px;box-sizing:border-box;width:100%;';
            banner.innerHTML = `<span style="color:#ff6666;font-weight:600;">CAT Script — Update required: v${state.updateAvailable || 'latest'}+</span><span style="color:#888;">Script blocked</span><a href="https://greasyfork.org/en/scripts/555846-cat-script-v3" target="_blank" style="color:#ff6666;text-decoration:underline;font-weight:600;margin-left:4px;">Update</a>`;
            if (descWrap) {
                descWrap.prepend(banner);
                banner.style.marginTop = '0';
                banner.style.borderRadius = '5px 5px 0 0';
            }
            else {
                warList.parentNode.insertBefore(banner, warList.nextSibling);
            }
            // Move banner into desc-wrap when war panel opens
            if (!descWrap && warList) {
                const obs = new MutationObserver(() => {
                    const fwi = document.querySelector('.faction-war-info, [class*="factionWarInfo"]');
                    if (!fwi)
                        return;
                    const dw = fwi.closest('.desc-wrap, [class*="warDesc"]') || fwi.parentNode;
                    if (!dw)
                        return;
                    obs.disconnect();
                    const b = document.getElementById('cat-update-required-banner');
                    if (!b)
                        return;
                    dw.prepend(b);
                    b.style.marginTop = '0';
                    b.style.borderRadius = '5px 5px 0 0';
                });
                obs.observe(warList, { childList: true, subtree: true });
            }
            document.body.classList.add('hide-call-buttons');
            return;
        }
        const warList = document.getElementById('faction_war_list_id')
            || document.querySelector('.f-war-list');
        const factionWarInfo = document.querySelector('.faction-war-info, [class*="factionWarInfo"]');
        const hasWar = !!factionWarInfo || !!document.querySelector('[data-warid], [class*="rankBox"]');
        if (!warList) {
            setTimeout(() => {
                this.injectTabsMenu();
            }, 200);
            return;
        }
        // Show info banner when viewing OUR enemy faction page (not own war page)
        const isAdmin = this._enhancer?.subscriptionData?.isAdmin || false;
        const cachedEnemy = StorageUtil.get('cat_enemy_faction_id', null);
        const knownEnemyId = normalizeFactionId(cachedEnemy?.id);
        const isOurEnemy = !!(knownEnemyId && state.viewingFactionId && normalizeFactionId(state.viewingFactionId) === knownEnemyId);
        if (state.catOtherFaction && hasWar && isOurEnemy && !isAdmin && !document.getElementById('cat-other-faction-banner')) {
            const banner = document.createElement('div');
            banner.id = 'cat-other-faction-banner';
            banner.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:8px;padding:6px 12px;background:linear-gradient(to right,#1a2a3d,#1a3040);border:1px solid #3388cc;border-radius:5px;margin:8px 0 0 0;font-family:"Helvetica Neue",Arial,sans-serif;font-size:11px;box-sizing:border-box;width:100%;';
            banner.innerHTML = `<span style="color:#66aaff;font-weight:600;">CAT Script</span><span style="color:#aaa;">You're viewing the enemy faction page. Go to <a href="/factions.php#/war" style="color:#66aaff;text-decoration:underline;font-weight:600;">your war page</a> to use call buttons and full features.</span>`;
            warList.parentNode.insertBefore(banner, warList);
        }
        // Determine anchor: if war is open, prepend inside .desc-wrap
        // Otherwise insert after the <ul> war list
        const descWrap = factionWarInfo ? (factionWarInfo.closest('.desc-wrap, [class*="warDesc"]') || factionWarInfo.parentNode) : null;
        const tabsMenu = document.createElement('div');
        tabsMenu.id = 'custom-tabs-menu';
        tabsMenu.style.cssText = `
            display: flex;
            width: 100%;
            margin: 8px 0 0 0;
            background: linear-gradient(180deg, #666 0%, #333 100%);
            border-top: 1px solid #555;
            border-bottom: 1px solid #222;
            border-radius: 5px;
            position: relative;
            z-index: 100;
            gap: 0;
            box-sizing: border-box;
            overflow: hidden;
            box-shadow: 0 0 2px rgba(0,0,0,0.25);
        `;
        const tabStyles = document.createElement('style');
        tabStyles.innerHTML = `
            @keyframes blinking {
                0% { background: #333; }
                50% { background: #5c1a1a; }
                100% { background: #333; }
            }

            .custom-tab-btn {
                padding: 8px 8px !important;
                background: linear-gradient(180deg, #666 0%, #333 100%);
                font-family: "Helvetica Neue", Arial, sans-serif !important;
                color: #fff !important;
                border: none !important;
                cursor: pointer !important;
                font-size: 11px !important;
                font-weight: 600 !important;
                flex: 1 !important;
                min-width: 0 !important;
                transition: all 0.15s ease !important;
                position: relative !important;
                user-select: none !important;
                letter-spacing: 0.3px !important;
                white-space: nowrap !important;
                text-shadow: 0 0 2px #000 !important;
            }

            .custom-tab-btn:not(:last-child)::after {
                content: '' !important;
                position: absolute !important;
                right: 0 !important;
                top: 0 !important;
                bottom: 0 !important;
                width: 1px !important;
                border-left: 1px solid #000 !important;
                background: linear-gradient(180deg, #666 0%, #555 50%, #333 100%) !important;
                pointer-events: none !important;
            }

.custom-tab-btn:not(.blinking):not(.active) {
                background: linear-gradient(180deg, #666 0%, #333 100%) !important;
            }

            .custom-tab-btn.blinking {
                animation: blinking 2s ease-in-out infinite !important;
                transition: none !important;
            }

            .custom-tab-btn:hover:not(.active):not(.blinking) {
                background: linear-gradient(180deg, #555 0%, #444 100%) !important;
            }

            .custom-tab-btn.active {
                color: #ddd !important;
                background: linear-gradient(180deg, #333 0%, #666 100%) !important;
                animation: none !important;
                text-shadow: 0 0 2px rgba(0,0,0,0.65) !important;
            }

            .custom-tab-content {
                display: none !important;
                background: #333 !important;
                color: #ddd;
                border: 1px solid #222;
                border-top: none;
                border-radius: 0 0 5px 5px;
                visibility: hidden !important;
                height: 0 !important;
                overflow: hidden !important;
                padding: 0 !important;
                margin: 0 !important;
                font-size: 12px;
                font-family: "Helvetica Neue", Arial, sans-serif !important;
                font-weight: 400 !important;
                -webkit-font-smoothing: antialiased !important;
            }

            .custom-tab-content * {
                font-family: inherit !important;
            }

            .custom-tab-content.active {
                display: block !important;
                visibility: visible !important;
                height: auto !important;
                overflow: visible !important;
                padding: 12px !important;
                margin: 0 0 10px 0 !important;
            }

            .custom-tab-content a {
                color: #6e9ecf !important;
                text-decoration: none !important;
            }
            .custom-tab-content a:hover {
                text-decoration: underline !important;
            }
            .cat-chain-alert-row {
                display: grid !important;
                grid-template-columns: 1fr 1fr !important;
                gap: 8px !important;
                width: 100% !important;
                box-sizing: border-box !important;
            }
            @media (max-width: 500px) {
                .cat-chain-alert-row {
                    grid-template-columns: 1fr !important;
                }
                .cat-chain-alert-col:not(:first-child) > div:first-child {
                    margin-top: 8px !important;
                }
                .cat-chain-alert-col input[type="range"] {
                    min-width: 70px !important;
                }
            }

            .api-key-input::placeholder {
                color: #ffffff !important;
                opacity: 1 !important;
            }

            .cat-icon-tab {
                flex: 0 0 36px !important;
                font-size: 14px !important;
                padding: 0 !important;
                text-align: center !important;
                letter-spacing: 0 !important;
                text-shadow: none !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
            }

            .cat-bell-tab {
                flex: 0 0 36px !important;
                font-size: 14px !important;
                padding: 0 !important;
                text-align: center !important;
                letter-spacing: 0 !important;
                text-shadow: none !important;
                display: flex !important;
                align-items: center !important;
                justify-content: center !important;
            }

            .cat-bell-tab.has-new::before {
                content: '' !important;
                position: absolute !important;
                top: 5px !important;
                right: 5px !important;
                width: 7px !important;
                height: 7px !important;
                background: #e53e3e !important;
                border-radius: 50% !important;
                animation: cat-bell-pulse 2s ease-in-out infinite !important;
            }

            @keyframes cat-bell-pulse {
                0%, 100% { opacity: 1; transform: scale(1); }
                50% { opacity: 0.6; transform: scale(1.3); }
            }

            .cat-contact-card {
                padding: 8px 10px !important;
                background: rgba(255,255,255,0.03) !important;
                border: 1px solid #444 !important;
                border-radius: 3px !important;
                display: flex !important;
                align-items: center !important;
                gap: 8px !important;
                text-decoration: none !important;
                transition: background 0.2s ease, border-color 0.2s ease !important;
                cursor: pointer !important;
            }
            .cat-contact-card:hover {
                background: rgba(255,255,255,0.07) !important;
                border-color: #666 !important;
                text-decoration: none !important;
            }
            a.cat-contact-card,
            a.cat-contact-card:hover,
            a.cat-contact-card:visited,
            a.cat-contact-card:active {
                text-decoration: none !important;
            }

        `;
        document.head.appendChild(tabStyles);
        const apiKey = StorageUtil.get('cat_api_key_script', '');
        const hasApiKey = apiKey && String(apiKey).trim() !== '';
        const userFactionId = StorageUtil.get('cat_user_faction_id', null);
        const showPlan = hasApiKey && userFactionId;
        const isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
        const isTMDesktop = !isPDA && !isExtensionMode() && !isSafari && typeof GM_xmlhttpRequest !== 'undefined';
        let tabs;
        if (isTMDesktop && !isAdmin) {
            document.body.classList.add('cat-no-api-key');
            tabs = [];
            const mb = document.createElement('div');
            mb.id = 'custom-tabs-menu';
            mb.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:10px;padding:8px 14px;background:rgba(0,0,0,0.4);border:1px solid #444;border-radius:5px;margin:8px 0;font-size:12px;';
            mb.innerHTML = '<span style="color:#ccc;">CAT Script is now available as a <b style="color:#86B202;">Chrome Extension</b></span><a href="https://cat-script.com/extension" target="_blank" style="padding:4px 10px;background:#86B202;color:#000;border-radius:3px;font-size:11px;font-weight:600;text-decoration:none;">Learn more</a>';
            const wl = document.getElementById('faction_war_list_id') || document.querySelector('.f-war-list');
            if (wl)
                wl.parentNode.insertBefore(mb, wl.nextSibling);
            return;
        }
        else if (!hasApiKey) {
            // No API key: only show Settings
            document.body.classList.add('cat-no-api-key');
            tabs = ['Settings'];
        }
        else if (state.catOtherFaction) {
            tabs = showPlan ? ['Plan', 'Chain', 'Settings'] : ['Settings'];
        }
        else if (hasWar) {
            tabs = showPlan ? ['Faction', 'Plan', 'Chain', 'Settings'] : ['Faction', 'Settings'];
        }
        else {
            // No war: hide Faction tab (no data to show)
            tabs = showPlan ? ['Plan', 'Chain', 'Settings'] : ['Settings'];
        }
        const enhancer = this;
        const canActivate = enhancer._enhancer?.canActivateWar || false;
        if (hasWar && canActivate && !state.catOtherFaction && !isTMDesktop) {
            tabs.push('Stats');
        }
        const tabContents = {};
        // ── Chain tab logic ──────────────────────────────────────────
        const CHAIN_STORAGE_KEY = 'cat_chain_alert';
        const defaultChainSettings = {
            flashEnabled: false,
            thresholdSecs: 150,
            flashColor: '#ff0000',
            flashOpacity: 20,
            beepEnabled: false,
            beepVolume: 50,
            beepType: 0,
            minChainHits: 0,
        };
        const chainAlertSettings = { ...defaultChainSettings, ...(StorageUtil.get(CHAIN_STORAGE_KEY, null) || {}) };
        let chainTickInterval = null;
        let chainTestMode = false;
        let chainTestInterval = null;
        let chainAudioCtx = null;
        // ── Single-tab audio leader election ──────────────────────────
        const AUDIO_LEADER_KEY = 'cat_audio_leader_v1';
        const AUDIO_LEADER_TTL = 4000; // ms — steal if heartbeat older than this
        // Unique ID per tab session (survives re-renders within same tab)
        let _tabAudioId = (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('cat_tab_audio_id')) || '';
        if (!_tabAudioId) {
            _tabAudioId = `${Date.now()}_${Math.random().toString(36).slice(2)}`;
            if (typeof sessionStorage !== 'undefined')
                sessionStorage.setItem('cat_tab_audio_id', _tabAudioId);
        }
        let _audioHeartbeatInterval = null;
        function _claimAudioLeader() {
            try {
                localStorage.setItem(AUDIO_LEADER_KEY, JSON.stringify({ id: _tabAudioId, ts: Date.now() }));
            }
            catch (_) { /* ignore */ }
        }
        function _isAudioLeader() {
            try {
                const raw = localStorage.getItem(AUDIO_LEADER_KEY);
                if (!raw) {
                    _claimAudioLeader();
                    return true;
                }
                const data = JSON.parse(raw);
                if (data.id === _tabAudioId)
                    return true;
                if (Date.now() - data.ts > AUDIO_LEADER_TTL) {
                    _claimAudioLeader();
                    return true;
                }
                return false;
            }
            catch (_) {
                return true;
            }
        }
        function _startAudioHeartbeat() {
            if (_audioHeartbeatInterval)
                return;
            _claimAudioLeader();
            _audioHeartbeatInterval = setInterval(() => {
                // Only keep heartbeat alive if beep is enabled
                if (chainAlertSettings.beepEnabled)
                    _claimAudioLeader();
            }, 2000);
            window.addEventListener('beforeunload', _releaseAudioLeader, { once: true });
        }
        function _releaseAudioLeader() {
            if (_audioHeartbeatInterval) {
                clearInterval(_audioHeartbeatInterval);
                _audioHeartbeatInterval = null;
            }
            try {
                const raw = localStorage.getItem(AUDIO_LEADER_KEY);
                if (raw) {
                    const data = JSON.parse(raw);
                    if (data.id === _tabAudioId)
                        localStorage.removeItem(AUDIO_LEADER_KEY);
                }
            }
            catch (_) { /* ignore */ }
        }
        // ──────────────────────────────────────────────────────────────
        function saveChainSettings() {
            StorageUtil.set(CHAIN_STORAGE_KEY, chainAlertSettings);
        }
        function parseThresholdInput(val) {
            const m = val.match(/^([0-4]):([0-5]\d)$/);
            if (!m)
                return null;
            const secs = parseInt(m[1]) * 60 + parseInt(m[2]);
            if (secs < 20 || secs > 300)
                return null;
            return secs;
        }
        function formatThreshold(secs) {
            const m = Math.floor(secs / 60);
            const s = secs % 60;
            return `${m}:${s.toString().padStart(2, '0')}`;
        }
        function getChainTimerSecs() {
            const el = document.querySelector('.chain-box-timeleft');
            if (!el)
                return 0;
            const text = (el.textContent || '').trim();
            const parts = text.split(':');
            if (parts.length !== 2)
                return 0;
            const mins = parseInt(parts[0]);
            const secs = parseInt(parts[1]);
            if (isNaN(mins) || isNaN(secs))
                return 0;
            return mins * 60 + secs;
        }
        function getChainHits() {
            const el = document.querySelector('.chain-box-center-stat');
            if (!el)
                return 0;
            return parseInt((el.textContent || '0').replace(/,/g, ''), 10) || 0;
        }
        function isChainActive() {
            const title = document.querySelector('.chain-box-title');
            if (!title)
                return false;
            return (title.textContent || '').toLowerCase().includes('active');
        }
        function playBeep(force = false) {
            if (!force && !_isAudioLeader())
                return;
            try {
                if (!chainAudioCtx || chainAudioCtx.state === 'closed') {
                    chainAudioCtx = new AudioContext();
                }
                if (chainAudioCtx.state === 'suspended')
                    chainAudioCtx.resume();
                const types = ['sine', 'square', 'sawtooth', 'triangle'];
                const osc = chainAudioCtx.createOscillator();
                const gain = chainAudioCtx.createGain();
                osc.type = types[chainAlertSettings.beepType] || 'sine';
                gain.gain.value = chainAlertSettings.beepVolume / 100;
                osc.connect(gain);
                gain.connect(chainAudioCtx.destination);
                osc.start();
                setTimeout(() => osc.stop(), 400);
            }
            catch (_) { /* AudioContext may not be available */ }
        }
        function showFlash(color, opacity) {
            let overlay = document.getElementById('cat-chain-flash-overlay');
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.id = 'cat-chain-flash-overlay';
                overlay.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:-1;display:none;';
                document.body.appendChild(overlay);
            }
            const hex = color.replace('#', '');
            const r = parseInt(hex.slice(0, 2), 16);
            const g = parseInt(hex.slice(2, 4), 16);
            const b = parseInt(hex.slice(4, 6), 16);
            const alpha = opacity / 100;
            overlay.style.backgroundColor = `rgba(${r},${g},${b},${alpha})`;
            overlay.style.display = 'block';
        }
        function hideFlash() {
            const overlay = document.getElementById('cat-chain-flash-overlay');
            if (overlay)
                overlay.style.display = 'none';
        }
        function triggerAlert(test = false) {
            if (chainAlertSettings.flashEnabled || test) {
                showFlash(chainAlertSettings.flashColor, chainAlertSettings.flashOpacity);
                setTimeout(hideFlash, 600);
            }
            if (chainAlertSettings.beepEnabled || test) {
                playBeep();
            }
        }
        const BONUS_HITS_CHAIN = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
        function getNextBonus(hits) {
            return BONUS_HITS_CHAIN.find(b => b > hits) ?? null;
        }
        function updateChainDisplay() {
            const countdownEl = document.getElementById('cat-chain-countdown');
            const hitsEl = document.getElementById('cat-chain-hits');
            const progressEl = document.getElementById('cat-chain-progress');
            if (!countdownEl || !hitsEl || !progressEl)
                return;
            const active = isChainActive();
            const secs = active ? getChainTimerSecs() : 0;
            const hits = active ? getChainHits() : 0;
            if (!active || secs === 0) {
                countdownEl.textContent = '--:--';
                countdownEl.style.color = '#555';
                countdownEl.style.textShadow = 'none';
                hitsEl.textContent = 'No active chain';
                progressEl.style.width = '0%';
                progressEl.style.background = '#555';
                return;
            }
            const m = Math.floor(secs / 60);
            const s = secs % 60;
            countdownEl.textContent = `${m}:${s.toString().padStart(2, '0')}`;
            // Color by urgency
            if (secs <= 30) {
                countdownEl.style.color = '#E54C19';
                countdownEl.style.textShadow = '0 0 8px rgba(229,76,25,0.5)';
            }
            else if (secs <= 60) {
                countdownEl.style.color = '#F08C00';
                countdownEl.style.textShadow = '0 0 8px rgba(240,140,0,0.4)';
            }
            else if (secs <= chainAlertSettings.thresholdSecs) {
                countdownEl.style.color = '#ffd700';
                countdownEl.style.textShadow = '0 0 8px rgba(255,215,0,0.4)';
            }
            else {
                countdownEl.style.color = '#82c91e';
                countdownEl.style.textShadow = '0 0 8px rgba(130,201,30,0.4)';
            }
            // Hits + next bonus
            const next = getNextBonus(hits);
            if (next) {
                const prev = BONUS_HITS_CHAIN[BONUS_HITS_CHAIN.indexOf(next) - 1] ?? 0;
                const pct = Math.min(100, ((hits - prev) / (next - prev)) * 100);
                hitsEl.textContent = `${hits.toLocaleString()} hits — next bonus: ${next.toLocaleString()}`;
                progressEl.style.width = `${pct}%`;
                progressEl.style.background = '#86B202';
            }
            else {
                hitsEl.textContent = `${hits.toLocaleString()} hits`;
                progressEl.style.width = '100%';
                progressEl.style.background = '#82c91e';
            }
            // Trigger alert on threshold (test mode handled by chainTestInterval)
            if (!chainTestMode && (chainAlertSettings.flashEnabled || chainAlertSettings.beepEnabled)) {
                if (secs > 0 && secs <= chainAlertSettings.thresholdSecs && hits >= chainAlertSettings.minChainHits) {
                    triggerAlert();
                }
            }
        }
        function initChainTab() {
            // Stop any running test when tab is rebuilt
            if (chainTestInterval) {
                clearInterval(chainTestInterval);
                chainTestInterval = null;
            }
            chainTestMode = false;
            hideFlash();
            // Restore settings into UI
            const flashCb = document.getElementById('cat-chain-flash-enabled');
            const thresholdInput = document.getElementById('cat-chain-threshold');
            const colorPicker = document.getElementById('cat-chain-flash-color');
            const opacitySlider = document.getElementById('cat-chain-flash-opacity');
            const opacityVal = document.getElementById('cat-chain-flash-opacity-val');
            const beepCb = document.getElementById('cat-chain-beep-enabled');
            const volumeSlider = document.getElementById('cat-chain-beep-volume');
            const volumeVal = document.getElementById('cat-chain-beep-volume-val');
            const beepType = document.getElementById('cat-chain-beep-type');
            const minHitsInput = document.getElementById('cat-chain-min-hits');
            const testCb = document.getElementById('cat-chain-test-cb');
            if (flashCb)
                flashCb.checked = chainAlertSettings.flashEnabled;
            if (thresholdInput)
                thresholdInput.value = formatThreshold(chainAlertSettings.thresholdSecs);
            if (colorPicker)
                colorPicker.value = chainAlertSettings.flashColor;
            if (opacitySlider)
                opacitySlider.value = String(chainAlertSettings.flashOpacity);
            if (opacityVal)
                opacityVal.textContent = `${chainAlertSettings.flashOpacity}%`;
            if (beepCb)
                beepCb.checked = chainAlertSettings.beepEnabled;
            if (volumeSlider)
                volumeSlider.value = String(chainAlertSettings.beepVolume);
            if (volumeVal)
                volumeVal.textContent = `${chainAlertSettings.beepVolume}%`;
            if (beepType)
                beepType.value = String(chainAlertSettings.beepType);
            if (minHitsInput)
                minHitsInput.value = chainAlertSettings.minChainHits > 0 ? String(chainAlertSettings.minChainHits) : '';
            flashCb?.addEventListener('change', () => { chainAlertSettings.flashEnabled = flashCb.checked; saveChainSettings(); });
            thresholdInput?.addEventListener('change', () => {
                const parsed = parseThresholdInput(thresholdInput.value);
                if (parsed !== null) {
                    chainAlertSettings.thresholdSecs = parsed;
                    saveChainSettings();
                    thresholdInput.style.borderColor = '#555';
                }
                else {
                    thresholdInput.style.borderColor = '#E54C19';
                    thresholdInput.value = formatThreshold(chainAlertSettings.thresholdSecs);
                }
            });
            colorPicker?.addEventListener('input', () => { chainAlertSettings.flashColor = colorPicker.value; saveChainSettings(); });
            opacitySlider?.addEventListener('input', () => {
                chainAlertSettings.flashOpacity = parseInt(opacitySlider.value);
                if (opacityVal)
                    opacityVal.textContent = `${chainAlertSettings.flashOpacity}%`;
                saveChainSettings();
            });
            beepCb?.addEventListener('change', () => {
                chainAlertSettings.beepEnabled = beepCb.checked;
                saveChainSettings();
                if (beepCb.checked)
                    _startAudioHeartbeat();
                else
                    _releaseAudioLeader();
            });
            volumeSlider?.addEventListener('input', () => {
                chainAlertSettings.beepVolume = parseInt(volumeSlider.value);
                if (volumeVal)
                    volumeVal.textContent = `${chainAlertSettings.beepVolume}%`;
                saveChainSettings();
            });
            beepType?.addEventListener('change', () => { chainAlertSettings.beepType = parseInt(beepType.value); saveChainSettings(); });
            minHitsInput?.addEventListener('change', () => {
                const v = parseInt(minHitsInput.value);
                chainAlertSettings.minChainHits = isNaN(v) || v < 0 ? 0 : v;
                minHitsInput.value = chainAlertSettings.minChainHits > 0 ? String(chainAlertSettings.minChainHits) : '';
                saveChainSettings();
            });
            testCb?.addEventListener('change', () => {
                chainTestMode = testCb.checked;
                if (chainTestMode) {
                    // First blink immediately
                    showFlash(chainAlertSettings.flashColor, chainAlertSettings.flashOpacity);
                    playBeep();
                    setTimeout(hideFlash, 600);
                    // Then repeat every 1500ms
                    chainTestInterval = setInterval(() => {
                        showFlash(chainAlertSettings.flashColor, chainAlertSettings.flashOpacity);
                        playBeep();
                        setTimeout(hideFlash, 600);
                    }, 1500);
                }
                else {
                    if (chainTestInterval) {
                        clearInterval(chainTestInterval);
                        chainTestInterval = null;
                    }
                    hideFlash();
                }
            });
            updateChainDisplay();
        }
        // Start tick immediately at boot — runs even when Chain tab is closed
        if (!chainTickInterval) {
            chainTickInterval = setInterval(updateChainDisplay, 1000);
        }
        // Start audio heartbeat if beep already enabled at load time
        if (chainAlertSettings.beepEnabled)
            _startAudioHeartbeat();
        tabs.forEach((tabName) => {
            const btn = document.createElement('button');
            btn.className = 'custom-tab-btn';
            btn.textContent = tabName;
            btn.dataset.tab = tabName.toLowerCase();
            btn.style.flex = '1';
            btn.style.minWidth = '0';
            if (tabName.toLowerCase() === 'settings') {
                btn.id = 'settings-tab-btn';
                if (!hasApiKey) {
                    btn.innerHTML = 'Settings \u2014 Please enter a public API Key <span class="cat-click-here">\u2014 Click here</span>';
                    btn.classList.add('blinking');
                }
            }
            const content = document.createElement('div');
            content.className = 'custom-tab-content';
            content.dataset.tab = tabName.toLowerCase();
            content.style.display = 'none';
            tabContents[tabName.toLowerCase()] = content;
            if (tabName.toLowerCase() === 'settings') {
                const currentKey = StorageUtil.get('cat_api_key_script', '');
                content.innerHTML = `
                    <div style="padding: 0;">
                        <div style="border: 1px solid rgba(134,178,2,0.25); border-radius: 8px; margin-bottom: 14px; overflow: visible;">

                            <!-- Card header -->
                            <div style="display: flex; align-items: center; gap: 6px; padding: 10px 12px 8px; border-bottom: 1px solid rgba(134,178,2,0.12);">
                                <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 700;">API Keys</div>
                                <span id="cat-tos-trigger" data-cat-tooltip="Privacy &amp; Terms of Service&#10;&#10;Key Storage&#10;All 3 keys stored locally. Torn key sent once at registration to verify identity — never stored. TornStats &amp; FF Scouter keys never leave your browser.&#10;&#10;Server Data&#10;Player ID, name, faction (15d). Auth token (90d). Statuses (2h), logs (48h), errors (7d). Calls &amp; wars: permanent.&#10;&#10;Data Sharing&#10;Faction members see calls. Leaders/co-leaders see statuses, leaderboard &amp; dashboard. No third-party sharing.&#10;&#10;Torn Key Access Level: Public Access." style="display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; border-radius: 50%; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.15); color: #888; font-size: 10px; font-weight: 700; line-height: 1; cursor: help;">?</span>
                                <div id="cat-tos-tooltip" style="display:none;"></div>
                                <a href="https://cat-script.com/terms" target="_blank" style="color: #ACEA01; font-size: 10px; text-decoration: none; border-bottom: 1px solid rgba(172,234,1,0.3); margin-left: auto;">ToS & Privacy</a>
                            </div>

                            <!-- 3 inputs -->
                            <div style="padding: 10px 12px; display: flex; flex-direction: column; gap: 8px;">

                                <div>
                                    <div style="font-size: 10px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; margin-bottom: 4px;">Torn API Key</div>
                                    <input type="password" id="tab-setting-torn-apikey" placeholder="Public API key" value="${this._esc(currentKey)}"
                                        style="width: 100%; padding: 6px 8px; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; box-sizing: border-box; font-size: 11px; background: rgba(0,0,0,0.3); color: #ddd;">
                                    <p style="margin: 3px 0 0 0; font-size: 10px; color: #555;">Stored locally.</p>
                                    <div id="tab-setting-api-validation" style="margin-top: 4px; font-size: 10px; display: none; padding: 4px 6px; border-radius: 3px; background: rgba(0,0,0,0.25);"></div>
                                </div>

                                <div>
                                    <div style="font-size: 10px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; margin-bottom: 4px;">TornStats <span style="color:#555;font-weight:400;text-transform:none;letter-spacing:0;">— optional</span></div>
                                    <input type="password" id="tab-setting-tornstats-apikey" placeholder="TS_..." value="${this._esc(String(StorageUtil.get('cat_tornstats_api_key', '') || ''))}"
                                        style="width: 100%; padding: 6px 8px; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; box-sizing: border-box; font-size: 11px; background: rgba(0,0,0,0.3); color: #ddd;">
                                    <p style="margin: 3px 0 0 0; font-size: 10px; color: #555;">Battle stats spy data — stored locally.</p>
                                    <div id="tab-setting-tornstats-validation" style="margin-top: 4px; font-size: 10px; display: none; padding: 4px 6px; border-radius: 3px; background: rgba(0,0,0,0.25);"></div>
                                </div>

                                <div>
                                    <div style="font-size: 10px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; margin-bottom: 4px;">FF Scouter <span style="color:#555;font-weight:400;text-transform:none;letter-spacing:0;">— optional</span></div>
                                    <input type="password" id="tab-setting-ffscouter-apikey" placeholder="FF Scouter key..." value="${this._esc(String(StorageUtil.get('cat_ffscouter_api_key', '') || ''))}"
                                        style="width: 100%; padding: 6px 8px; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; box-sizing: border-box; font-size: 11px; background: rgba(0,0,0,0.3); color: #ddd;">
                                    <p style="margin: 3px 0 0 0; font-size: 10px; color: #555;">FF &amp; battle stats — stored locally.</p>
                                    <div id="tab-setting-ffscouter-validation" style="margin-top: 4px; font-size: 10px; display: none; padding: 4px 6px; border-radius: 3px; background: rgba(0,0,0,0.25);"></div>
                                </div>

                            </div>

                            <!-- Footer with button -->
                            <div style="border-top: 1px solid rgba(134,178,2,0.15); padding: 10px 12px; background: rgba(134,178,2,0.04); border-radius: 0 0 8px 8px;">
                                <button id="tab-setting-save" style="width: 100%; padding: 9px 12px; background: rgba(134,178,2,0.18); color: #86B202; border: 1px solid rgba(134,178,2,0.35); border-radius: 5px; cursor: pointer; font-weight: 700; font-size: 12px; letter-spacing: 0.3px;">Test &amp; Save all keys</button>
                            </div>

                        </div>

                        ${hasApiKey ? `<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 12px;">
                            <div id="auto-sort-setting" style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-auto-sort" ${String(StorageUtil.get('cat_auto_sort', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    Auto Sort
                                </label>
                                <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Disable if the script causes lag</p>
                            </div>
                            <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-level-bar" ${String(StorageUtil.get('cat_show_level_bar', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    Level XP Bar
                                </label>
                                <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Show XP progress below the life bar</p>
                            </div>
                            <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-name-colors" ${String(StorageUtil.get('cat_name_colors', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    Status Colors
                                </label>
                                <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Color names by online status</p>
                            </div>
                            <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-caller-status" ${String(StorageUtil.get('cat_caller_status_color', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    Caller Status
                                </label>
                                <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Color caller names by online status</p>
                            </div>
                            ${typeof window.flutter_inappwebview === 'undefined' && typeof window.PDA_httpGet === 'undefined' ? `<div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-attack-newtab" ${String(StorageUtil.get('cat_attack_new_tab', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    Attack in New Tab
                                </label>
                                <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Open attack pages in a new tab</p>
                            </div>` : ''}
                            ${typeof window.flutter_inappwebview !== 'undefined' ? `<div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-pda-notif" ${String(StorageUtil.get('cat_pda_notifications', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    PDA Notifications
                                </label>
                                <div id="cat-pda-notif-options" style="display: flex; flex-direction: column; gap: 6px; margin: 8px 0 0 22px;${String(StorageUtil.get('cat_pda_notifications', 'true')) !== 'true' ? ' opacity: 0.4; pointer-events: none;' : ''}">
                                    <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #aaa; font-size: 11px;">
                                        <input type="checkbox" id="tab-setting-pda-notif-hosp" ${String(StorageUtil.get('cat_pda_notif_hosp', 'true')) === 'true' ? 'checked' : ''} style="width: 13px; height: 13px; cursor: pointer;">
                                        Target leaving hospital
                                    </label>
                                    <div style="display: flex; align-items: center; gap: 8px; margin-left: 21px;">
                                        <span style="font-size: 10px; color: #666;">Alert:</span>
                                        <select id="tab-setting-pda-lead" style="padding: 2px 4px; background: rgba(0,0,0,0.3); border: 1px solid #555; border-radius: 3px; color: #ddd; font-size: 10px; cursor: pointer;">
                                            <option value="0" ${String(StorageUtil.get('cat_pda_notif_lead', '20')) === '0' ? 'selected' : ''}>At Okay</option>
                                            <option value="10" ${String(StorageUtil.get('cat_pda_notif_lead', '20')) === '10' ? 'selected' : ''}>10s before</option>
                                            <option value="20" ${String(StorageUtil.get('cat_pda_notif_lead', '20')) === '20' ? 'selected' : ''}>20s before</option>
                                            <option value="30" ${String(StorageUtil.get('cat_pda_notif_lead', '20')) === '30' ? 'selected' : ''}>30s before</option>
                                            <option value="60" ${String(StorageUtil.get('cat_pda_notif_lead', '20')) === '60' ? 'selected' : ''}>1 min before</option>
                                        </select>
                                    </div>
                                    <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #aaa; font-size: 11px;">
                                        <input type="checkbox" id="tab-setting-pda-notif-caller-hosp" ${String(StorageUtil.get('cat_pda_notif_caller_hosp', 'true')) === 'true' ? 'checked' : ''} style="width: 13px; height: 13px; cursor: pointer;">
                                        You got hospitalized (med out alert)
                                    </label>
                                </div>
                                <p style="margin: 4px 0 0 22px; font-size: 9px; color: #555;">If alert is set before Okay, you'll also get a confirmation when the target is out</p>
                            </div>
                            <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                                    <input type="checkbox" id="tab-setting-pda-perf" ${String(StorageUtil.get('cat_pda_perf_mode', 'false')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    Lite Mode
                                </label>
                                <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Some lags in Torn PDA? Try lite mode</p>
                            </div>
                            <div id="cat-pda-perf-card" style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                                <div style="display: flex; align-items: center; justify-content: space-between;">
                                    <div style="display: flex; align-items: center; gap: 5px;">
                                        <span style="color: #ccc; font-size: 12px; font-weight: 500;">Performance</span>
                                        <span id="cat-pda-perf-help" style="cursor: pointer; width: 14px; height: 14px; border-radius: 50%; border: 1px solid #555; display: inline-flex; align-items: center; justify-content: center; font-size: 9px; color: #888; font-weight: 600;">?</span>
                                    </div>
                                    <span id="cat-pda-perf-score" style="font-size: 18px; font-weight: 700; font-family: Monaco, Menlo, monospace;">--</span>
                                </div>
                                <div id="cat-pda-perf-tooltip" style="display: none; margin-top: 6px; padding: 6px 8px; background: rgba(0,0,0,0.5); border: 1px solid #555; border-radius: 3px; font-size: 10px; color: #aaa; line-height: 1.5;">
                                    This score estimates how smoothly CAT runs on your device. It takes into account your phone's speed, request latency, and how busy the PDA bridge is.<br>
                                    <span style="color: #68d391;">80+</span> Smooth &nbsp;
                                    <span style="color: #f6e05e;">60+</span> OK &nbsp;
                                    <span style="color: #ed8936;">40+</span> Slow &nbsp;
                                    <span style="color: #fc8181;">&lt;40</span> Laggy<br>
                                    <span style="color: #888;">This is indicative only. If you experience lag, try enabling Lite Mode above.</span>
                                </div>
                                <div style="margin-top: 6px; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden;">
                                    <div id="cat-pda-perf-bar" style="height: 100%; width: 0%; border-radius: 2px; transition: width 0.5s, background 0.5s;"></div>
                                </div>
                                <div style="display: flex; justify-content: space-between; margin-top: 4px;">
                                    <span id="cat-pda-perf-avg" style="font-size: 9px; color: #666;">--</span>
                                    <span id="cat-pda-perf-mode" style="font-size: 9px; color: #666;">${String(StorageUtil.get('cat_pda_perf_mode', 'false')) === 'true' ? 'Lite' : 'Standard'}</span>
                                </div>
                                <div id="cat-pda-perf-device" style="margin-top: 3px; font-size: 9px; color: #555; text-align: center;"></div>
                            </div>` : ''}
                            <div id="cat-bs-toggle-container"></div>
                        </div>

                        ${hasApiKey ? `<div style="margin-bottom: 12px; padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="font-size: 11px; color: #86B202; font-weight: 600; margin-bottom: 6px;">Stat Columns</div>
                            <div style="display: flex; gap: 12px; flex-wrap: wrap;">
                                <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #ccc; font-size: 12px;">
                                    <input type="checkbox" id="tab-setting-col-bsp" ${String(StorageUtil.get('cat_col_show_bsp', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    BSP
                                </label>
                                <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #ccc; font-size: 12px;">
                                    <input type="checkbox" id="tab-setting-col-ff" ${String(StorageUtil.get('cat_col_show_ff', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    FF Scouter
                                </label>
                                <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #ccc; font-size: 12px;">
                                    <input type="checkbox" id="tab-setting-col-ts" ${String(StorageUtil.get('cat_col_show_ts', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    TornStats
                                </label>
                            </div>
                            <p style="margin: 4px 0 0 0; font-size: 10px; color: #666;">Choose which stat columns appear in the cycle (min. 1)</p>
                        </div>` : ''}

                        <div style="margin-bottom: 12px; padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="font-size: 11px; color: #86B202; font-weight: 600; margin-bottom: 6px;">CD Column</div>
                            <div style="display: flex; gap: 12px; flex-wrap: wrap;">
                                <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #ccc; font-size: 12px;">
                                    <input type="checkbox" id="tab-setting-cd-energy" ${String(StorageUtil.get('cat_cd_show_energy', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    ⚡ Energy
                                </label>
                                <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #ccc; font-size: 12px;">
                                    <input type="checkbox" id="tab-setting-cd-drug" ${String(StorageUtil.get('cat_cd_show_drug', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    <img src="https://cat-script.com/assets/drugcd.png" width="12" height="12" style="vertical-align:middle;"> Drug CD
                                </label>
                                <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #ccc; font-size: 12px;">
                                    <input type="checkbox" id="tab-setting-cd-med" ${String(StorageUtil.get('cat_cd_show_med', 'true')) === 'true' ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                                    <img src="https://cat-script.com/assets/medcd.png" width="12" height="12" style="vertical-align:middle;">
                                    Med CD
                                </label>
                            </div>
                            <p style="margin: 4px 0 0 0; font-size: 10px; color: #666;">Choose which items appear in the CD column (your faction only)</p>
                        </div>

                        <div style="margin-bottom: 12px; padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="display: flex; align-items: center; gap: 8px; color: #ccc; font-size: 12px; font-weight: 500;">
                                <span>Travel ETA</span>
                                <select id="tab-setting-travel-mode" style="padding: 3px 6px; background: rgba(0,0,0,0.3); border: 1px solid #555; border-radius: 3px; color: #ddd; font-size: 11px; cursor: pointer;">
                                    <option value="airstrip" ${(String(StorageUtil.get('cat_travel_eta_mode', 'airstrip') || 'airstrip')) === 'airstrip' ? 'selected' : ''}>PI (Airstrip)</option>
                                    <option value="bct" ${String(StorageUtil.get('cat_travel_eta_mode', 'airstrip') || '') === 'bct' ? 'selected' : ''}>Business Class</option>
                                    <option value="wlt" ${String(StorageUtil.get('cat_travel_eta_mode', 'airstrip') || '') === 'wlt' ? 'selected' : ''}>WLT</option>
                                    <option value="standard" ${String(StorageUtil.get('cat_travel_eta_mode', 'airstrip') || '') === 'standard' ? 'selected' : ''}>Standard</option>
                                </select>
                            </div>
                            <div style="display: flex; align-items: center; gap: 8px; margin-top: 6px;">
                                <span style="font-size: 11px; color: #aaa;">ETA Color</span>
                                <input type="color" id="tab-setting-eta-color" value="${StorageUtil.get('cat_eta_color', null) || '#FFB74D'}" style="width: 24px; height: 20px; border: 1px solid #555; border-radius: 3px; cursor: pointer; background: transparent; padding: 0;">
                                <button id="tab-setting-eta-color-reset" title="Reset to default" style="padding: 2px 4px; background: rgba(255,255,255,0.06); border: 1px solid #555; border-radius: 3px; cursor: pointer; display:flex;align-items:center;"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg></button>
                            </div>
                            <label style="display: flex; align-items: center; gap: 6px; cursor: pointer; color: #aaa; font-size: 11px; margin-top: 6px;">
                                <input type="checkbox" id="tab-setting-eta-tooltip" ${String(StorageUtil.get('cat_eta_tooltip', 'false')) !== 'false' ? 'checked' : ''} style="width: 13px; height: 13px; cursor: pointer;">
                                Show tooltip on hover
                            </label>
                        </div>

                        <div style="margin-bottom: 12px;">
                            <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; font-weight: 600;">Appearance</div>
                            <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px; display: flex; flex-direction: column; gap: 10px;">
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <span style="font-size: 11px; color: #aaa; width: 65px; flex-shrink: 0;">Row Style</span>
                                    <div id="cat-rs-picker" style="display:flex;flex-wrap:wrap;background:rgba(0,0,0,0.3);border-radius:6px;padding:3px;gap:4px;">
                                        <div class="cat-rs-opt" data-rs="basic" style="text-align:center;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:#888;transition:all .15s;">Basic</div>
                                        <div class="cat-rs-opt" data-rs="colors" style="text-align:center;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:#888;transition:all .15s;">Colors</div>
                                        <div class="cat-rs-opt" data-rs="bar" style="text-align:center;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:#888;transition:all .15s;">Bar</div>
                                        <div class="cat-rs-opt" data-rs="contrast" style="text-align:center;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:#888;transition:all .15s;">Contrast</div>
                                    </div>
                                </div>
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <span style="font-size: 11px; color: #aaa; width: 65px; flex-shrink: 0;">Button</span>
                                    <div id="cat-bs-picker" style="display:flex;background:rgba(0,0,0,0.3);border-radius:6px;padding:3px;gap:4px;">
                                        <div class="cat-bs-opt" data-bs="gradient" style="text-align:center;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:#888;transition:all .15s;">Gradient</div>
                                        <div class="cat-bs-opt" data-bs="flat" style="text-align:center;padding:5px 8px;border-radius:4px;cursor:pointer;font-size:11px;color:#888;transition:all .15s;">Flat</div>
                                    </div>
                                </div>
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <span style="font-size: 11px; color: #aaa; width: 65px; flex-shrink: 0;">Name Font</span>
                                    <select id="tab-setting-name-font" style="padding: 4px 6px; background: rgba(0,0,0,0.3); border: 1px solid #555; border-radius: 3px; color: #ddd; font-size: 11px; cursor: pointer;">
                                        <option value="" ${!StorageUtil.get('cat_name_font', '') ? 'selected' : ''}>Default</option>
                                        <option value="Arial, Helvetica, sans-serif" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Arial') ? 'selected' : ''}>Arial</option>
                                        <option value="Verdana, Geneva, sans-serif" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Verdana') ? 'selected' : ''}>Verdana</option>
                                        <option value="Tahoma, Geneva, sans-serif" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Tahoma') ? 'selected' : ''}>Tahoma</option>
                                        <option value="'Segoe UI', 'Inter', sans-serif" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Segoe') ? 'selected' : ''}>Segoe UI</option>
                                        <option value="Impact, 'Oswald', sans-serif" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Impact') ? 'selected' : ''}>Impact</option>
                                        <option value="'Courier New', Courier, monospace" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Courier') ? 'selected' : ''}>Courier</option>
                                        <option value="Consolas, 'Source Code Pro', monospace" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Consolas') ? 'selected' : ''}>Consolas</option>
                                        <option value="'Comic Sans MS', 'Comic Neue', cursive" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Comic') ? 'selected' : ''}>Comic Sans MS</option>
                                        <option value="Papyrus, fantasy" ${String(StorageUtil.get('cat_name_font', '') || '').includes('Papyrus') ? 'selected' : ''}>Papyrus</option>
                                    </select>
                                </div>
                                ${document.querySelector('.iconStats, .bsp-column') ? `<div style="display: flex; align-items: center; gap: 8px;">
                                    <span style="font-size: 11px; color: #aaa; width: 65px; flex-shrink: 0;">BSP Font</span>
                                    <select id="tab-setting-bsp-font" style="padding: 4px 6px; background: rgba(0,0,0,0.3); border: 1px solid #555; border-radius: 3px; color: #ddd; font-size: 11px; cursor: pointer;">
                                        <option value="" ${!StorageUtil.get('cat_bsp_font', '') ? 'selected' : ''}>Default</option>
                                        <option value="Arial, Helvetica, sans-serif" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Arial') ? 'selected' : ''}>Arial</option>
                                        <option value="Verdana, Geneva, sans-serif" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Verdana') ? 'selected' : ''}>Verdana</option>
                                        <option value="Tahoma, Geneva, sans-serif" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Tahoma') ? 'selected' : ''}>Tahoma</option>
                                        <option value="'Segoe UI', 'Inter', sans-serif" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Segoe') ? 'selected' : ''}>Segoe UI</option>
                                        <option value="Impact, 'Oswald', sans-serif" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Impact') ? 'selected' : ''}>Impact</option>
                                        <option value="'Courier New', Courier, monospace" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Courier') ? 'selected' : ''}>Courier</option>
                                        <option value="Consolas, 'Source Code Pro', monospace" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Consolas') ? 'selected' : ''}>Consolas</option>
                                        <option value="'Comic Sans MS', 'Comic Neue', cursive" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Comic') ? 'selected' : ''}>Comic Sans MS</option>
                                        <option value="Papyrus, fantasy" ${String(StorageUtil.get('cat_bsp_font', '') || '').includes('Papyrus') ? 'selected' : ''}>Papyrus</option>
                                    </select>
                                </div>` : ''}
                                <div style="height: 1px; background: rgba(255,255,255,0.06);"></div>
                                <div style="display: flex; align-items: center; gap: 8px;">
                                    <span style="font-size: 11px; color: #aaa; width: 65px; flex-shrink: 0;">Score</span>
                                    <div style="display: flex; align-items: center; gap: 8px; flex: 1;">
                                        <div style="display: flex; align-items: center; gap: 4px;">
                                            <input type="color" id="tab-setting-score-color" value="${StorageUtil.get('cat_score_color', null) || '#888888'}" style="width: 24px; height: 20px; border: 1px solid #555; border-radius: 3px; cursor: pointer; background: transparent; padding: 0;">
                                        </div>
                                        <div style="display: flex; align-items: center; gap: 4px;">
                                            <input type="range" id="tab-setting-score-size" min="10" max="18" value="${StorageUtil.get('cat_score_font_size', null) || 12}" style="width: 50px; height: 4px; cursor: pointer; accent-color: #667eea;">
                                            <span id="tab-setting-score-size-value" style="font-size: 10px; color: #666;">${StorageUtil.get('cat_score_font_size', null) || 12}px</span>
                                        </div>
                                        <div style="display: flex; align-items: center; gap: 3px;">
                                            <input type="checkbox" id="tab-setting-score-shadow" ${StorageUtil.get('cat_score_shadow', false) ? 'checked' : ''} style="width: 13px; height: 13px; cursor: pointer; accent-color: #667eea;">
                                            <span style="font-size: 10px; color: #666;">Shadow</span>
                                            <input type="color" id="tab-setting-score-shadow-color" value="${StorageUtil.get('cat_score_shadow_color', null) || '#000000'}" style="width: 20px; height: 16px; border: 1px solid #555; border-radius: 3px; cursor: pointer; background: transparent; padding: 0;">
                                        </div>
                                        <button id="tab-setting-score-reset" style="padding: 3px 6px; background: rgba(255,255,255,0.06); border: 1px solid #555; border-radius: 3px; cursor: pointer; font-size: 9px; color: #666;">Reset</button>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div style="display: flex; gap: 8px; justify-content: flex-start;">
                            <button id="tab-setting-clear-cache" style="padding: 7px 12px; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.06); border-radius: 4px; cursor: pointer; font-weight: 500; color: #e66; font-size: 11px;">Clear Cache</button>
                        </div>` : ''}
                    </div>
                `;
            }
            else if (tabName.toLowerCase() === 'stats') {
                content.innerHTML = `
                    <div style="padding: 0; font-size: 13px;">
                        <div id="users-tab-loader" style="text-align: center; padding: 20px 0;">
                            <p style="margin: 0; color: #cbd5e0;">Loading stats...</p>
                        </div>
                        <div id="users-tab-container" style="display: none;"></div>
                    </div>
                `;
            }
            else if (tabName.toLowerCase() === 'faction') {
                content.innerHTML = `
                    <div style="padding: 0; font-size: 13px;">
                        <div id="faction-stats-loader" style="text-align: center; padding: 20px 0;">
                            <p style="margin: 0; color: #cbd5e0;">\u{1F4CA} Loading faction stats...</p>
                        </div>
                        <div id="faction-stats-container" style="display: none;"></div>
                    </div>
                `;
            }
            else if (tabName.toLowerCase() === 'plan') {
                content.innerHTML = `
                    <div style="padding: 0; font-size: 13px;">
                        <div id="plan-tab-loader" style="text-align: center; padding: 20px 0;">
                            <p style="margin: 0; color: #cbd5e0;">Loading subscription...</p>
                        </div>
                        <div id="plan-tab-container" style="display: none;"></div>
                    </div>
                `;
            }
            else if (tabName.toLowerCase() === 'chain') {
                content.innerHTML = `
                    <div style="padding: 0;">
                        <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; margin-bottom: 8px;">Chain Timer</div>
                        <div id="cat-chain-timer-display" style="padding:10px 12px;background:rgba(0,0,0,0.25);border:1px solid #444;border-radius:3px;margin-bottom:12px;">
                            <div style="display:flex;align-items:baseline;gap:10px;margin-bottom:6px;">
                                <div id="cat-chain-countdown" style="font-size:28px;font-weight:700;font-family:monospace;color:#82c91e;text-shadow:0 0 8px rgba(130,201,30,0.4);letter-spacing:2px;flex-shrink:0;">--:--</div>
                                <div id="cat-chain-hits" style="font-size:12px;color:#aaa;">No active chain</div>
                            </div>
                            <div style="background:rgba(255,255,255,0.06);border-radius:2px;height:4px;overflow:hidden;">
                                <div id="cat-chain-progress" style="height:100%;background:#82c91e;width:0%;transition:width 0.5s;border-radius:2px;"></div>
                            </div>
                        </div>

                        <div class="cat-chain-alert-row" style="margin-bottom:12px;">
                            <div class="cat-chain-alert-col">
                                <div style="font-size:11px;color:#86B202;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;margin-bottom:6px;">Flash Alert</div>
                                <div style="padding:8px 10px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;width:100%;">
                                    <label style="display:flex;align-items:center;gap:8px;cursor:pointer;color:#ccc;font-size:12px;">
                                        <input type="checkbox" id="cat-chain-flash-enabled" style="width:14px;height:14px;cursor:pointer;">
                                        Enable
                                    </label>
                                    <div style="display:flex;align-items:center;gap:6px;flex-wrap:wrap;">
                                        <span style="font-size:11px;color:#888;white-space:nowrap;">Below</span>
                                        <input type="text" id="cat-chain-threshold" placeholder="2:30" maxlength="4"
                                            style="width:40px;padding:3px 5px;background:rgba(0,0,0,0.3);border:1px solid #555;color:#ddd;font-size:11px;text-align:center;border-radius:2px;outline:none;">
                                        <span style="font-size:10px;color:#888;">(0:20–5:00)</span>
                                    </div>
                                    <div style="display:flex;align-items:center;gap:6px;width:100%;">
                                        <span style="font-size:11px;color:#888;">Color</span>
                                        <input type="color" id="cat-chain-flash-color" value="#ff0000"
                                            style="width:28px;height:22px;border:1px solid #555;background:transparent;padding:0;cursor:pointer;border-radius:2px;">
                                        <span style="font-size:11px;color:#888;white-space:nowrap;margin-left:6px;">Opacity</span>
                                        <input type="range" id="cat-chain-flash-opacity" min="0" max="100" value="20"
                                            style="cursor:pointer;accent-color:#86B202;flex:1;">
                                        <span id="cat-chain-flash-opacity-val" style="font-size:10px;color:#888;min-width:26px;">20%</span>
                                    </div>
                                </div>
                            </div>
                            <div class="cat-chain-alert-col">
                                <div style="font-size:11px;color:#86B202;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;margin-bottom:6px;">Beep Alert</div>
                                <div style="padding:8px 10px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;display:flex;flex-direction:column;gap:8px;height:100%;box-sizing:border-box;width:100%;">
                                    <label style="display:flex;align-items:center;gap:8px;cursor:pointer;color:#ccc;font-size:12px;">
                                        <input type="checkbox" id="cat-chain-beep-enabled" style="width:14px;height:14px;cursor:pointer;">
                                        Enable
                                    </label>
                                    <div style="display:flex;align-items:center;gap:6px;width:100%;">
                                        <span style="font-size:11px;color:#888;white-space:nowrap;">Volume</span>
                                        <input type="range" id="cat-chain-beep-volume" min="1" max="100" value="50"
                                            style="cursor:pointer;accent-color:#86B202;flex:1;">
                                        <span id="cat-chain-beep-volume-val" style="font-size:10px;color:#888;min-width:26px;">50%</span>
                                    </div>
                                    <div style="display:flex;align-items:center;gap:6px;">
                                        <span style="font-size:11px;color:#888;white-space:nowrap;">Wave</span>
                                        <select id="cat-chain-beep-type" style="padding:3px 6px;background:rgba(0,0,0,0.3);border:1px solid #555;color:#ddd;font-size:11px;border-radius:2px;outline:none;">
                                            <option value="0">Sine</option>
                                            <option value="1">Square</option>
                                            <option value="2">Sawtooth</option>
                                            <option value="3">Triangle</option>
                                        </select>
                                    </div>
                                </div>
                            </div>
                        </div>

                        <div style="display:flex;align-items:center;gap:8px;padding:6px 10px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;margin-bottom:6px;">
                            <span style="font-size:11px;color:#888;white-space:nowrap;">Min chain hits</span>
                            <input type="number" id="cat-chain-min-hits" min="0" max="100000" step="1" placeholder="0"
                                style="width:70px;padding:3px 5px;background:rgba(0,0,0,0.3);border:1px solid #555;color:#ddd;font-size:11px;text-align:center;border-radius:2px;outline:none;">
                            <span style="font-size:10px;color:#555;">(alert only when chain ≥ this)</span>
                        </div>

                        <label style="display:inline-flex;align-items:center;gap:8px;cursor:pointer;color:#ccc;font-size:12px;padding:6px 10px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;margin-top:6px;">
                            <input type="checkbox" id="cat-chain-test-cb" style="width:14px;height:14px;cursor:pointer;">
                            Test Alert
                        </label>
                    </div>
                `;
            }
            btn.onclick = (e) => {
                e.preventDefault();
                if (btn.classList.contains('active')) {
                    btn.classList.remove('active');
                    content.classList.remove('active');
                }
                else {
                    document.querySelectorAll('.custom-tab-btn').forEach(b => b.classList.remove('active'));
                    document.querySelectorAll('.custom-tab-content').forEach(c => c.classList.remove('active'));
                    btn.classList.add('active');
                    content.classList.add('active');
                    if (tabName.toLowerCase() === 'faction') {
                        (async () => {
                            const loader = document.getElementById('faction-stats-loader');
                            const container = document.getElementById('faction-stats-container');
                            const enh = (window.FactionWarEnhancer || enhancer);
                            let enemyFactionId = enh.apiManager.factionId;
                            const userFactionId = StorageUtil.get('cat_user_faction_id', null);
                            // Use server's enemy_faction_id as source of truth if available
                            const serverEnemyId = enh.subscriptionData?.currentWar?.enemy_faction_id;
                            if (serverEnemyId) {
                                enemyFactionId = String(serverEnemyId);
                            }
                            // Check 12h cache — always try cache first (instant render after DOM recreation)
                            const CACHE_KEY = 'cat_faction_stats_cache';
                            const CACHE_TTL = 12 * 60 * 60 * 1000;
                            const cached = StorageUtil.get(CACHE_KEY, null);
                            if (cached && cached.cachedAt && (Date.now() - cached.cachedAt < CACHE_TTL)) {
                                const cacheMatchesFaction = String(cached.enemyFactionId) === String(enemyFactionId) && String(cached.userFactionId) === String(userFactionId);
                                // Render from cache only if faction matches (don't serve stale data from a previous war)
                                if (cacheMatchesFaction) {
                                    if (cached.enemyData && cached.userData) {
                                        await enh.renderDualFactionStats(cached.enemyData, cached.userData, container, loader, cached.leaders || null);
                                    }
                                    else if (cached.enemyData) {
                                        enh.renderFactionStats(cached.enemyData, container, loader);
                                    }
                                    window._factionStatsLoaded = true;
                                    return;
                                }
                            }
                            // No valid cache — fetch from API (only once per session)
                            if (window._factionStatsLoaded)
                                return;
                            window._factionStatsLoaded = true;
                            if (enemyFactionId && userFactionId) {
                                const enemyFactionInfo = await enh.apiManager.getFactionInfo(enemyFactionId);
                                const userFactionInfo = await enh.apiManager.getFactionInfo(userFactionId);
                                if (enemyFactionInfo && userFactionInfo) {
                                    await enh.renderDualFactionStats(enemyFactionInfo, userFactionInfo, container, loader);
                                    StorageUtil.set(CACHE_KEY, { enemyFactionId, userFactionId, enemyData: enemyFactionInfo, userData: userFactionInfo, leaders: enh._lastLeaderHtml || null, cachedAt: Date.now() });
                                    enh.sendFactionMembersToServer(userFactionId, userFactionInfo, 'your faction');
                                    enh.sendFactionMembersToServer(userFactionId, enemyFactionInfo, 'opposite faction');
                                }
                                else {
                                    if (loader)
                                        loader.innerHTML = '<p style="color: #ef5350;">Failed to load faction stats. Make sure API key is configured.</p>';
                                }
                            }
                            else if (enemyFactionId) {
                                const factionInfo = await enh.apiManager.getFactionInfo(enemyFactionId);
                                if (factionInfo) {
                                    StorageUtil.set(CACHE_KEY, { enemyFactionId, userFactionId, enemyData: factionInfo, userData: null, cachedAt: Date.now() });
                                    enh.renderFactionStats(factionInfo, container, loader);
                                }
                                else {
                                    if (loader)
                                        loader.innerHTML = '<p style="color: #ef5350;">Failed to load faction stats. Make sure API key is configured.</p>';
                                }
                            }
                            else {
                                if (loader)
                                    loader.innerHTML = '<p style="color: #ffa726;">Faction not detected. Go to faction war page first.</p>';
                            }
                        })();
                    }
                    if (tabName.toLowerCase() === 'stats') {
                        enhancer._renderUsersTab();
                    }
                    if (tabName.toLowerCase() === 'settings') {
                        setTimeout(() => {
                            enhancer.setupSettingsTabHandlers();
                        }, 100);
                    }
                    if (tabName.toLowerCase() === 'plan') {
                        enhancer._loadPlanTab();
                    }
                    if (tabName.toLowerCase() === 'chain') {
                        initChainTab();
                    }
                }
            };
            tabsMenu.appendChild(btn);
        });
        // ── Support & Bell tabs (hidden when no API key) ──
        if (hasApiKey) {
            // ── Support tab ──
            const DISCORD_ICON = '<svg width="16" height="16" viewBox="0 0 127.14 96.36" fill="#5865F2" style="flex-shrink:0;"><path d="M107.7 8.07A105.15 105.15 0 0081.47 0a72.06 72.06 0 00-3.36 6.83 97.68 97.68 0 00-29.11 0A72.37 72.37 0 0045.64 0a105.89 105.89 0 00-26.25 8.09C2.79 32.65-1.71 56.6.54 80.21a105.73 105.73 0 0032.17 16.15 77.7 77.7 0 006.89-11.11 68.42 68.42 0 01-10.85-5.18c.91-.66 1.8-1.34 2.66-2.04a75.57 75.57 0 0064.32 0c.87.71 1.76 1.39 2.66 2.04a68.68 68.68 0 01-10.87 5.19 77 77 0 006.89 11.1 105.25 105.25 0 0032.19-16.14c2.64-27.38-4.51-51.11-18.9-72.15zM42.45 65.69C36.18 65.69 31 60 31 53.05s5-12.68 11.45-12.68S54 46.06 53.89 53.05 48.84 65.69 42.45 65.69zm42.24 0C78.41 65.69 73.25 60 73.25 53.05s5-12.68 11.44-12.68S96.23 46.06 96.12 53.05 91.08 65.69 84.69 65.69z"/></svg>';
            const supportBtn = document.createElement('button');
            supportBtn.className = 'custom-tab-btn cat-icon-tab';
            supportBtn.dataset.tab = 'support';
            supportBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><defs><linearGradient id="supportGrad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#ddd"/><stop offset="100%" stop-color="#888"/></linearGradient></defs><path d="M11.5 2C6.81 2 3 5.81 3 10.5V17c0 1.1.9 2 2 2h2v-6H5v-2.5C5 6.92 7.92 4 11.5 4S18 6.92 18 10.5V13h-2v6h2.5c.83 0 1.5-.67 1.5-1.5v-7C20 5.81 16.19 2 11.5 2z" fill="url(#supportGrad)"/></svg>';
            supportBtn.title = 'Support';
            const supportContent = document.createElement('div');
            supportContent.className = 'custom-tab-content';
            supportContent.dataset.tab = 'support';
            supportContent.style.display = 'none';
            supportContent.innerHTML = `
            <div style="padding: 0; font-size: 12px; line-height: 1.5;">
                <div style="margin-bottom: 12px;">
                    <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; font-weight: 600;">Support</div>
                    <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                        <div style="color: #ccc; font-size: 12px;">Need help with the script, found a bug, or have a suggestion? Reach out through any of the channels below.</div>
                    </div>
                </div>
                <div id="cat-support-columns"></div>
            </div>
        `;
            // Build the 3-column layout via DOM to avoid Torn CSS overrides
            const colContainer = supportContent.querySelector('#cat-support-columns');
            if (colContainer) {
                colContainer.style.cssText = 'display:grid !important;grid-template-columns:1fr 1fr 1fr !important;gap:6px !important;width:100% !important;box-sizing:border-box !important;align-items:stretch !important;';
                // ── Contact column ──
                const colContact = document.createElement('div');
                colContact.style.cssText = 'min-width:0 !important;overflow:hidden !important;display:flex !important;flex-direction:column !important;';
                colContact.innerHTML = `
                <div style="font-size: 10px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; font-weight: 600;">Contact</div>
                <div style="display:flex;flex-direction:column;gap:4px;">
                    <a href="https://discord.gg/WMJRbFsSgQ" target="_blank" class="cat-contact-card" style="padding:5px 7px;">
                        ${DISCORD_ICON}
                        <div><div style="color:#ccc;font-weight:500;font-size:11px;">Fluffy Kittens Discord</div><div style="color:#888;font-size:9px;">Help & updates</div></div>
                    </a>
                    <a href="https://www.torn.com/messages.php#/p=compose&XID=2353554" target="_blank" class="cat-contact-card" style="padding:5px 7px;">
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="#aaa" style="flex-shrink:0;"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
                        <div><div style="color:#ccc;font-weight:500;font-size:11px;">Torn PM</div><div style="color:#888;font-size:9px;">Jesuus [2353554]</div></div>
                    </a>
                    <a href="https://discordapp.com/users/250306102334324736" target="_blank" class="cat-contact-card" style="padding:5px 7px;">
                        ${DISCORD_ICON}
                        <div><div style="color:#ccc;font-weight:500;font-size:11px;">Discord DM</div><div style="color:#888;font-size:9px;">Jesus.3999</div></div>
                    </a>
                </div>`;
                // Force flex:1 on inner content via JS
                const contactInner = colContact.querySelector('div:last-child');
                if (contactInner)
                    contactInner.style.cssText += ';flex:1 !important;justify-content:space-between !important;';
                // ── Debug column ──
                const colDebug = document.createElement('div');
                colDebug.style.cssText = 'min-width:0 !important;overflow:hidden !important;display:flex !important;flex-direction:column !important;';
                colDebug.innerHTML = `
                <div style="font-size: 10px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; font-weight: 600;">Debug</div>
                <div id="cat-debug-card" style="padding:6px 8px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;display:flex;flex-direction:column;gap:4px;">
                    <div style="display:flex;flex-direction:column;gap:3px;font-size:10px;">
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">Version :</span> <span id="cat-dbg-version" style="color:#ccc;">${VERSION}</span></div>
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">User :</span> <span id="cat-dbg-player" style="color:#ccc;">...</span></div>
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">Status :</span> <span id="cat-dbg-server" style="color:#ccc;">...</span></div>
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">WS :</span> <span id="cat-dbg-ws" style="color:#ccc;">...</span></div>
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">Faction :</span> <span id="cat-dbg-faction" style="color:#ccc;">...</span></div>
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">API Key :</span> <span id="cat-dbg-apikey" style="color:#ccc;">...</span></div>
                        <div style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;"><span style="color:#666;">Update needed :</span> <span id="cat-dbg-update" style="color:#ccc;">...</span></div>
                    </div>
                    <button id="cat-copy-debug" style="width:100%;padding:4px;background:rgba(255,255,255,0.05);border:1px solid #555;border-radius:3px;color:#aaa;font-size:9px;cursor:pointer;transition:background 0.15s;margin-top:auto;">Copy debug info</button>
                    <button id="cat-clear-statuses" style="width:100%;padding:4px;background:rgba(220,50,50,0.08);border:1px solid #7a3030;border-radius:3px;color:#e88;font-size:9px;cursor:pointer;transition:background 0.15s;margin-top:4px;">Wrong status / timer</button>
                </div>`;
                const debugInner = colDebug.querySelector('div:last-child');
                if (debugInner)
                    debugInner.style.cssText += ';flex:1 !important;display:flex !important;flex-direction:column !important;';
                // Make the rows container stretch to fill available space
                const debugRows = debugInner?.querySelector('div');
                if (debugRows)
                    debugRows.style.cssText += ';flex:1 !important;display:flex !important;flex-direction:column !important;justify-content:space-evenly !important;';
                // ── Links column ──
                const colLinks = document.createElement('div');
                colLinks.style.cssText = 'min-width:0 !important;overflow:hidden !important;display:flex !important;flex-direction:column !important;';
                colLinks.innerHTML = `
                <div style="font-size: 10px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; font-weight: 600;">Links</div>
                <div style="display:flex;flex-direction:column;gap:4px;">
                    <a href="https://cat-script.com" target="_blank" style="padding:5px 4px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;text-decoration:none;text-align:center;color:#ccc;font-size:10px;transition:background 0.15s;font-weight:500;display:block;">Website</a>
                    <a href="https://cat-script.com/terms" target="_blank" style="padding:5px 4px;background:rgba(255,255,255,0.03);border:1px solid #444;border-radius:3px;text-decoration:none;text-align:center;color:#ccc;font-size:10px;transition:background 0.15s;font-weight:500;display:block;">Terms</a>
                    <div style="flex:1;display:flex;align-items:center;justify-content:center;margin-top:4px;">
                        <img src="https://cat-script.com/catlogo.png" alt="CAT Script" style="max-width:100%;max-height:60px;object-fit:contain;opacity:0.7;">
                    </div>
                </div>`;
                const linksInner = colLinks.querySelector('div:last-child');
                if (linksInner)
                    linksInner.style.cssText += ';flex:1 !important;justify-content:space-between !important;';
                colContainer.appendChild(colContact);
                colContainer.appendChild(colDebug);
                colContainer.appendChild(colLinks);
            }
            tabContents['support'] = supportContent;
            // Helper: gather debug info (called live, not at render time)
            const getDebugInfo = () => {
                const e2 = window.FactionWarEnhancer || this._enhancer;
                const name = e2?.apiManager?.playerName || 'Unknown';
                const id = e2?.apiManager?.playerId || StorageUtil.get('cat_user_info', null)?.id || '?';
                const connected = e2?.pollingManager?._isActive || false;
                const userFaction = StorageUtil.get('cat_user_faction_id', null) || '?';
                const hasApiKey = !!(StorageUtil.get('cat_api_key_script', ''));
                return { name, id, connected, userFaction, hasApiKey };
            };
            // Update debug values when support tab is shown
            const refreshDebugDisplay = () => {
                const { name, id, connected, userFaction, hasApiKey } = getDebugInfo();
                const playerEl = supportContent.querySelector('#cat-dbg-player');
                const serverEl = supportContent.querySelector('#cat-dbg-server');
                const factionEl = supportContent.querySelector('#cat-dbg-faction');
                const apikeyEl = supportContent.querySelector('#cat-dbg-apikey');
                if (playerEl)
                    playerEl.textContent = `${name} [${id}]`;
                if (serverEl) {
                    serverEl.textContent = connected ? 'Connected' : 'Disconnected';
                    serverEl.style.color = connected ? '#86B202' : '#e66';
                }
                const wsEl = supportContent.querySelector('#cat-dbg-ws');
                if (wsEl) {
                    if (!isExtensionMode()) {
                        wsEl.textContent = 'N/A (TM mode)';
                        wsEl.style.color = '#666';
                    }
                    else {
                        const wsConn = extensionWSConnected();
                        const _enh = window.FactionWarEnhancer || this._enhancer;
                        const fallback = _enh?.pollingManager?.isWsFallbackActive?.() || false;
                        if (wsConn) {
                            wsEl.textContent = 'Connected';
                            wsEl.style.color = '#86B202';
                        }
                        else if (fallback) {
                            wsEl.textContent = 'Polling (fallback)';
                            wsEl.style.color = '#ed8936';
                        }
                        else {
                            wsEl.textContent = 'Disconnected';
                            wsEl.style.color = '#e66';
                        }
                    }
                }
                if (factionEl)
                    factionEl.textContent = String(userFaction);
                if (apikeyEl) {
                    apikeyEl.textContent = hasApiKey ? 'Set' : 'Not set';
                    apikeyEl.style.color = hasApiKey ? '#86B202' : '#e66';
                }
                const updateEl = supportContent.querySelector('#cat-dbg-update');
                if (updateEl) {
                    if (state.updateAvailable) {
                        updateEl.innerHTML = `<a href="https://greasyfork.org/en/scripts/555846-cat-script-v3" target="_blank" style="color:#e66;font-weight:600;text-decoration:underline;">Yes (v${state.updateAvailable})</a>`;
                    }
                    else {
                        updateEl.textContent = 'No';
                        updateEl.style.color = '#86B202';
                    }
                }
            };
            // Copy debug info handler
            const copyDebugBtn = supportContent.querySelector('#cat-copy-debug');
            if (copyDebugBtn) {
                copyDebugBtn.addEventListener('click', () => {
                    const { name, id, connected, userFaction, hasApiKey } = getDebugInfo();
                    const lines = [
                        `CAT Script v${VERSION}`,
                        `Player: ${name} [${id}]`,
                        `Server: ${connected ? 'Connected' : 'Disconnected'}`,
                        `Faction: ${userFaction}`,
                        `API Key: ${hasApiKey ? 'Set' : 'Not set'}`,
                        `Update: ${state.updateAvailable ? 'Yes (v' + state.updateAvailable + ')' : 'No'}`,
                        `URL: ${window.location.href}`,
                        `UA: ${navigator.userAgent}`,
                    ];
                    // Append error log if any
                    try {
                        const log = JSON.parse(localStorage.getItem('cat_error_log') || '[]');
                        if (log.length > 0) {
                            lines.push('', `--- Errors (${log.length}) ---`);
                            log.forEach(e => lines.push(`[${e.t}] ${e.c}: ${e.m}`));
                        }
                    }
                    catch (_) { /* ignore */ }
                    navigator.clipboard.writeText(lines.join('\n')).then(() => {
                        copyDebugBtn.textContent = 'Copied!';
                        setTimeout(() => { copyDebugBtn.textContent = 'Copy debug info'; }, 1500);
                    }).catch(() => { });
                });
            }
            const clearStatusesBtn = supportContent.querySelector('#cat-clear-statuses');
            if (clearStatusesBtn) {
                clearStatusesBtn.addEventListener('click', async () => {
                    const enhancer = window.FactionWarEnhancer || this._enhancer;
                    const serverUrl = enhancer?.apiManager?.serverUrl || StorageUtil.get('cat_server_url', null) || 'https://cat-script.com';
                    const authToken = enhancer?.apiManager?.authToken || StorageUtil.get('cat_auth_token', null);
                    const userFactionId = StorageUtil.get('cat_user_faction_id', null);
                    const enemyFactionId = enhancer?.subscriptionData?.currentWar?.enemy_faction_id || StorageUtil.get('cat_enemy_faction_id', null);
                    if (!authToken) {
                        clearStatusesBtn.textContent = 'No auth token';
                        setTimeout(() => { clearStatusesBtn.textContent = 'Wrong status / timer'; }, 2000);
                        return;
                    }
                    const factionIds = [userFactionId, enemyFactionId].filter(Boolean);
                    if (factionIds.length === 0) {
                        clearStatusesBtn.textContent = 'No faction found';
                        setTimeout(() => { clearStatusesBtn.textContent = 'Wrong status / timer'; }, 2000);
                        return;
                    }
                    try {
                        clearStatusesBtn.textContent = 'Clearing...';
                        const resp = await enhancer.apiManager.httpRequest(`${serverUrl}/api/clear-statuses`, {
                            method: 'DELETE',
                            headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' },
                            body: JSON.stringify({ factionIds })
                        });
                        if (resp.ok) {
                            clearStatusesBtn.textContent = 'Cleared!';
                        }
                        else {
                            clearStatusesBtn.textContent = `Error ${resp.status}`;
                        }
                    }
                    catch (_) {
                        clearStatusesBtn.textContent = 'Network error';
                    }
                    setTimeout(() => { clearStatusesBtn.textContent = 'Wrong status / timer'; }, 2000);
                });
            }
            let _debugRefreshInterval = null;
            supportBtn.onclick = (e) => {
                e.preventDefault();
                if (supportBtn.classList.contains('active')) {
                    supportBtn.classList.remove('active');
                    supportContent.classList.remove('active');
                    if (_debugRefreshInterval !== null) {
                        clearInterval(_debugRefreshInterval);
                        _debugRefreshInterval = null;
                    }
                }
                else {
                    document.querySelectorAll('.custom-tab-btn').forEach(b => b.classList.remove('active'));
                    document.querySelectorAll('.custom-tab-content').forEach(c => c.classList.remove('active'));
                    supportBtn.classList.add('active');
                    supportContent.classList.add('active');
                    refreshDebugDisplay();
                    _debugRefreshInterval = setInterval(refreshDebugDisplay, 1000);
                }
            };
            tabsMenu.appendChild(supportBtn);
            // ── Help tab ──
            const helpBtn = document.createElement('button');
            helpBtn.className = 'custom-tab-btn cat-icon-tab';
            helpBtn.dataset.tab = 'help';
            helpBtn.innerHTML = '<svg width="17" height="17" viewBox="0 0 24 24" fill="none"><defs><linearGradient id="helpGrad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#ddd"/><stop offset="100%" stop-color="#888"/></linearGradient></defs><circle cx="12" cy="12" r="10" stroke="url(#helpGrad)" stroke-width="2"/><path d="M9.5 9.5C9.5 8.12 10.62 7 12 7s2.5 1.12 2.5 2.5c0 1.5-1.5 2-2 3" stroke="url(#helpGrad)" stroke-width="2" stroke-linecap="round"/><circle cx="12" cy="17" r="1" fill="url(#helpGrad)"/></svg>';
            helpBtn.title = 'Help';
            const helpContent = document.createElement('div');
            helpContent.className = 'custom-tab-content';
            helpContent.dataset.tab = 'help';
            helpContent.style.display = 'none';
            helpContent.innerHTML = `
            <div style="padding: 0; font-size: 12px; line-height: 1.5;">
                <div style="margin-bottom: 12px;">
                    <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; font-weight: 600;">Getting Started</div>
                    <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px; display: flex; flex-direction: column; gap: 6px;">
                        <div style="color: #ccc; font-size: 12px;">Coordination script for ranked wars. Call targets so multiple members don't hit the same enemy.</div>
                        <div style="color: #888; font-size: 11px;">Enter your Torn API key in Settings (PUBLIC key, get it <a href="https://www.torn.com/preferences.php#tab=api" target="_blank" style="color:#7cb4d0;text-decoration:none;">here</a>). The script then works automatically on all war pages.</div>
                    </div>
                </div>
                <div style="margin-bottom: 12px;">
                    <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; font-weight: 600;">Features</div>
                    <div style="display: flex; flex-direction: column; gap: 6px;">
                        <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="color: #ccc; font-weight: 500; margin-bottom: 3px;">Calls</div>
                            <div style="color: #888; font-size: 11px;">Click "Call" next to an enemy to reserve them. Other members see the call in real-time. A call is removed when you kill the target or when you get hospitalized. The <span style="display:inline-flex;vertical-align:middle;">${PISTOL_IMG}</span> icon shows when someone is currently attacking a target.</div>
                        </div>
                        <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="color: #ccc; font-weight: 500; margin-bottom: 3px;">Rally</div>
                            <div style="color: #888; font-size: 11px;">Click the megaphone icon next to an enemy to rally your faction on that target. Other members can join the rally to coordinate a group attack. You can only be in one rally at a time.</div>
                        </div>
                        <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="color: #ccc; font-weight: 500; margin-bottom: 3px;">Auto Sort</div>
                            <div style="color: #888; font-size: 11px;">Enemies are sorted automatically: OK and uncalled targets go to the top, hospitalized ones sink down. Can be disabled in Settings.</div>
                        </div>
                        <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="color: #ccc; font-weight: 500; margin-bottom: 3px;">Plan Tab</div>
                            <div style="color: #888; font-size: 11px;">Shows the faction xanax balance and current war info. Leaders and co-leaders can activate the script for a war and access viewer dashboard credentials.</div>
                        </div>
                        <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="color: #ccc; font-weight: 500; margin-bottom: 3px;">Travel ETA</div>
                            <div style="color: #888; font-size: 11px;">Traveling players show an approximate arrival time. Estimates may vary due to Torn's ~3% travel time variance, Private Island ownership, and the moment the departure was detected.</div>
                        </div>
                        <div style="padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                            <div style="display:flex;align-items:center;gap:6px;color: #ccc; font-weight: 500; margin-bottom: 3px;">Soft Uncall <span style="font-size:8px;font-weight:700;color:#fff;background:#86B202;padding:2px 6px;border-radius:3px;border:1px solid #9ecf1a;letter-spacing:0.5px;line-height:1;">NEW</span></div>
                            <div style="color: #888; font-size: 11px;">When a member who called a target gets hospitalized, an orange countdown badge appears on the target. After the countdown, the call is automatically cancelled so someone else can take it. If the caller med out before the timer runs out, the call stays.</div>
                        </div>
                    </div>
                </div>
                <div style="margin-bottom: 12px;">
                    <div style="font-size: 11px; color: #5865F2; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; font-weight: 600;">Discord Notifications</div>
                    <div style="padding: 8px 10px; background: rgba(88,101,242,0.05); border: 1px solid rgba(88,101,242,0.15); border-radius: 3px;">
                        <div style="color: #ccc; font-weight: 500; margin-bottom: 5px;">Send alerts to your Discord server</div>
                        <ul style="color: #888; font-size: 11px; margin: 0; padding-left: 16px; display: flex; flex-direction: column; gap: 2px;">
                            <li>Tactical markers (smoke / tear / help) — auto-deleted on hospitalization</li>
                            <li>War events (scheduled, started, ended)</li>
                            <li>Chain alerts (break warning, bonus, milestone, broke)</li>
                        </ul>
                        <div style="color: #666; font-size: 10px; margin-top: 6px; font-style: italic;">Setup: paste your Discord webhook URL in the Plan tab (leader / co-leader only).</div>
                    </div>
                </div>
                <div style="text-align: center; padding-top: 8px;">
                    <div style="color: #555; font-size: 10px;">Made with \u2764\uFE0F by <a href="https://www.torn.com/profiles.php?XID=2353554" target="_blank" style="color: #7cb4d0; text-decoration: none;">Jesuus [2353554]</a> - <a href="https://www.torn.com/factions.php?step=profile&ID=46666" target="_blank" style="color: #7cb4d0; text-decoration: none;">Fluffy Kittens</a></div>
                </div>
            </div>
        `;
            helpBtn.onclick = (e) => {
                e.preventDefault();
                if (helpBtn.classList.contains('active')) {
                    helpBtn.classList.remove('active');
                    helpContent.classList.remove('active');
                }
                else {
                    document.querySelectorAll('.custom-tab-btn').forEach(b => b.classList.remove('active'));
                    document.querySelectorAll('.custom-tab-content').forEach(c => c.classList.remove('active'));
                    helpBtn.classList.add('active');
                    helpContent.classList.add('active');
                }
            };
            tabsMenu.appendChild(helpBtn);
            tabContents['help'] = helpContent;
            // ── Bell "What's New" tab ──
            const bellBtn = document.createElement('button');
            bellBtn.className = 'custom-tab-btn cat-bell-tab';
            bellBtn.id = 'cat-bell-btn';
            bellBtn.dataset.tab = 'whatsnew';
            bellBtn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="bellGrad" x1="12" y1="2" x2="12" y2="22" gradientUnits="userSpaceOnUse"><stop offset="0%" stop-color="#ddd"/><stop offset="100%" stop-color="#888"/></linearGradient></defs><path d="M12 2C10.9 2 10 2.9 10 4V4.3C7.7 5.2 6 7.4 6 10V15L4 17V18H20V17L18 15V10C18 7.4 16.3 5.2 14 4.3V4C14 2.9 13.1 2 12 2ZM12 22C13.1 22 14 21.1 14 20H10C10 21.1 10.9 22 12 22Z" fill="url(#bellGrad)"/></svg>';
            bellBtn.style.position = 'relative';
            const lastSeen = localStorage.getItem('cat_last_seen_version');
            if (lastSeen !== VERSION) {
                bellBtn.classList.add('has-new');
            }
            const bellContent = document.createElement('div');
            bellContent.className = 'custom-tab-content';
            bellContent.dataset.tab = 'whatsnew';
            bellContent.style.display = 'none';
            bellContent.innerHTML = `
            <div id="cat-whatsnew" style="padding: 0;">
                <div class="cat-wn-title" style="font-size: 14px; font-weight: 700; color: #fff; margin-bottom: 8px;">What's New \u2014 v${VERSION}</div>
                <div style="background:rgba(151,117,250,0.08);border:1px solid rgba(151,117,250,0.25);border-radius:4px;padding:8px 12px;margin-bottom:12px;font-size:11px;color:#ccc;line-height:1.6;">
                    Want to share your <b style="color:#fff;">Energy, Drug CD &amp; Med CD</b> with your faction?<br>
                    Register on <a href="https://intel.cat-script.com" target="_blank" style="color:#9775fa;text-decoration:underline;font-weight:600;">intel.cat-script.com</a> \u2014 your bars will be visible to your whole faction in real time.
                </div>
                <div class="cat-wn-body" style="font-size: 12px; color: #ccc; line-height: 1.7;">
                    <div style="margin-bottom: 10px;">
                        <div style="color: #ACEA01; font-weight: 600; margin-bottom: 4px;">New</div>
                        <ul style="margin: 0; padding-left: 18px; color: #bbb;">
                            <li><b>Discord chain alerts</b> \u2014 set a minimum hit threshold in Plan tab \u2192 Discord Notifications to avoid alerts on small chains</li>
                        </ul>
                    </div>
                    <div style="margin-bottom: 10px;">
                        <div style="color: #4FC3F7; font-weight: 600; margin-bottom: 4px;">Fix</div>
                        <ul style="margin: 0; padding-left: 18px; color: #bbb;">
                            <li>Enemy members showing wrong status</li>
                        </ul>
                    </div>
                </div>
            </div>
        `;
            tabContents['whatsnew'] = bellContent;
            bellBtn.onclick = (e) => {
                e.preventDefault();
                if (bellBtn.classList.contains('active')) {
                    bellBtn.classList.remove('active');
                    bellContent.classList.remove('active');
                }
                else {
                    document.querySelectorAll('.custom-tab-btn').forEach(b => b.classList.remove('active'));
                    document.querySelectorAll('.custom-tab-content').forEach(c => c.classList.remove('active'));
                    bellBtn.classList.add('active');
                    bellContent.classList.add('active');
                    bellBtn.classList.remove('has-new');
                    localStorage.setItem('cat_last_seen_version', VERSION);
                }
            };
            tabsMenu.appendChild(bellBtn);
        } // end if (hasApiKey)
        // Insert tabs at the anchor point
        const contentValues = Object.values(tabContents);
        if (descWrap) {
            // War open: prepend as first children of desc-wrap
            for (let i = contentValues.length - 1; i >= 0; i--) {
                descWrap.prepend(contentValues[i]);
            }
            descWrap.prepend(tabsMenu);
            tabsMenu.style.marginTop = '0';
            tabsMenu.style.borderRadius = '5px 5px 0 0';
        }
        else {
            // No war: insert after the <ul> war list
            warList.parentNode.insertBefore(tabsMenu, warList.nextSibling);
            for (let i = contentValues.length - 1; i >= 0; i--) {
                warList.parentNode.insertBefore(contentValues[i], tabsMenu.nextSibling);
            }
        }
        // Inject desktop notification settings via DOM (after tabs are in DOM)
        setTimeout(() => this.setupSettingsTabHandlers(), 200);
        // Update notification bar (inline, non-intrusive)
        const existingUpdateBar = document.getElementById('cat-update-bar');
        if (existingUpdateBar)
            existingUpdateBar.remove();
        if (state.updateAvailable) {
            const updateBar = document.createElement('div');
            updateBar.id = 'cat-update-bar';
            updateBar.style.cssText = 'display:flex;align-items:center;justify-content:center;gap:8px;padding:6px 12px;background:linear-gradient(to right,#2d1f00,#3d2a00);border:1px solid #b8860b;border-radius:4px;margin:6px 0 0 0;font-family:"Helvetica Neue",Arial,sans-serif;font-size:11px;';
            updateBar.innerHTML = `<span style="color:#ffd666;">CAT Script - Update available: <b>v${state.updateAvailable}</b></span><span style="color:#888;">— you have v${VERSION}</span><a href="https://greasyfork.org/en/scripts/555846-cat-script-v3" target="_blank" style="color:#ffd666;text-decoration:underline;font-weight:600;margin-left:4px;">Click here</a>`;
            tabsMenu.insertAdjacentElement('afterend', updateBar);
        }
        // Watch for desc-wrap appearing (user opens war panel after page load)
        // If tabs were placed after <ul>, move them into desc-wrap when it appears
        if (!descWrap && warList) {
            const observer = new MutationObserver(() => {
                const fwi = document.querySelector('.faction-war-info, [class*="factionWarInfo"]');
                if (!fwi)
                    return;
                const dw = fwi.closest('.desc-wrap, [class*="warDesc"]') || fwi.parentNode;
                if (!dw)
                    return;
                observer.disconnect();
                const menu = document.getElementById('custom-tabs-menu');
                if (!menu)
                    return;
                // Collect tab contents
                const contents = document.querySelectorAll('.custom-tab-content');
                // Move into desc-wrap as first children (reverse order then menu)
                for (let i = contents.length - 1; i >= 0; i--) {
                    dw.prepend(contents[i]);
                }
                dw.prepend(menu);
                menu.style.marginTop = '0';
                menu.style.borderRadius = '5px 5px 0 0';
            });
            observer.observe(warList, { childList: true, subtree: true });
        }
        this.startUsersDataLoop();
    }

    async function _loadPlanTab() {
        const loader = document.getElementById('plan-tab-loader');
        const container = document.getElementById('plan-tab-container');
        if (!loader || !container)
            return;
        const userFactionId = StorageUtil.get('cat_user_faction_id', null);
        if (!userFactionId || !this.apiManager.authToken) {
            loader.style.display = '';
            loader.innerHTML = '<p style="color:#ffa726;">Faction not detected or token missing.</p>';
            container.style.display = 'none';
            return;
        }
        // Check if user is admin (from cached data or will be fetched)
        const isUserAdmin = !!this._enhancer?.subscriptionData?.isAdmin;
        // Determine which faction to show: if on another faction's page and user is admin, show that faction
        const viewingOtherFaction = state.catOtherFaction && state.viewingFactionId && state.viewingFactionId !== userFactionId;
        const targetFactionId = (viewingOtherFaction && isUserAdmin) ? state.viewingFactionId : userFactionId;
        // Use cached data only if viewing own faction
        let data = (!viewingOtherFaction || !isUserAdmin)
            ? (this._enhancer?.subscriptionData || null)
            : null;
        let canActivate = this._enhancer?.canActivateWar || false;
        if (!data) {
            // Fetch subscription data
            loader.style.display = '';
            loader.innerHTML = '<p style="margin:0;color:#cbd5e0;">Loading subscription...</p>';
            container.style.display = 'none';
            try {
                const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/${encodeURIComponent(targetFactionId)}`, { method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` } });
                if (!response.ok) {
                    loader.innerHTML = '<p style="color:#ef5350;">Loading error.</p>';
                    return;
                }
                data = await response.json();
                if (!data || !data.success) {
                    loader.innerHTML = '<p style="color:#ef5350;">Server error.</p>';
                    return;
                }
                // If viewing other faction as admin, force canActivate to show credentials
                if (viewingOtherFaction && isUserAdmin) {
                    canActivate = true;
                }
                // Update cached subscriptionData so banner shows correct balance
                if (this._enhancer && !viewingOtherFaction) {
                    this._enhancer.subscriptionData = data;
                    // Rebuild banner with correct data (more robust than patching DOM)
                    if (document.getElementById('cat-activation-banner')) {
                        this._enhancer.showActivationBanner();
                    }
                }
            }
            catch (e) {
                this.apiManager.reportError('planTabFetch', e);
                loader.innerHTML = `<p style="color:#ef5350;">Error: ${this._esc(e instanceof Error ? e.message : String(e))}</p>`;
                return;
            }
        }
        if (!data)
            return;
        // Get rank/price: from subscription response (works for admin cross-faction) or fetchDynamicPrice fallback
        let factionRankTier = data.rankTier || '';
        let factionPrice = data.price ?? 0;
        if (factionRankTier && factionPrice) {
            // Update enhancer globals for own faction only
            if (this._enhancer && !viewingOtherFaction) {
                this._enhancer.currentRankTier = factionRankTier;
                this._enhancer.currentPrice = factionPrice;
            }
        }
        else if (this._enhancer && !viewingOtherFaction) {
            await this._enhancer.fetchDynamicPrice();
            factionRankTier = this._enhancer.currentRankTier || 'gold';
            factionPrice = this._enhancer.currentPrice || 30;
        }
        try {
            const sub = Object.assign({ xanax_balance: 0, trial_used: false, faction_name: '', viewer_token: '', viewer_user: '', viewer_pass: '' }, data.subscription);
            const war = data.currentWar;
            const activated = data.isActivatedForCurrentWar;
            const activation = data.currentWarActivation;
            let html = '';
            // Balance cards
            html += `<div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;">`;
            html += `<div style="flex:1;min-width:80px;padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;">
            <div style="font-size:10px;color:#888;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px;">Balance</div>
            <div style="font-size:18px;font-weight:700;color:${sub.xanax_balance >= factionPrice ? '#ACEA01' : '#e66'};">${sub.xanax_balance} <span style="font-size:11px;color:#888;">xanax</span></div>
        </div>`;
            html += `<div style="flex:1;min-width:80px;padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;">
            <div style="font-size:10px;color:#888;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px;">Free Trial</div>
            <div style="font-size:13px;font-weight:600;color:${sub.trial_used ? '#e66' : '#ACEA01'};">${sub.trial_used ? 'Used' : 'Available'}</div>
        </div>`;
            html += `<div style="flex:1;min-width:80px;padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;">
            <div style="font-size:10px;color:#888;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px;">Current War</div>
            <div style="font-size:13px;font-weight:600;color:${war ? (activated ? '#6a4' : '#da2') : '#666'};">${war ? (activated ? 'Activated' : 'Not Activated') : 'No War'}</div>
        </div>`;
            html += `</div>`;
            // War details
            if (war) {
                html += `<div style="padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;margin-bottom:10px;">
                <div style="font-size:10px;color:#888;text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;">War Details</div>
                <div style="font-size:12px;color:#ccc;"><strong style="color:#e87;">${this._esc(war.enemy_faction_name || 'Unknown')}</strong> VS <strong style="color:#ACEA01;">${this._esc(sub.faction_name || 'Your Faction')}</strong></div>`;
                if (activation) {
                    html += `<div style="font-size:11px;color:#888;margin-top:3px;">Activated by <strong style="color:#ccc;">${this._esc(activation.activated_by_name || 'Unknown')}</strong> (${this._esc(activation.activation_type || '')}) — ${new Date(activation.activated_at || 0).toLocaleString()}</div>`;
                }
                html += `</div>`;
            }
            // Activation controls (leader/co-leader/admin only)
            if (canActivate && !activated) {
                html += `<div style="padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;margin-bottom:10px;">
                <div style="font-size:11px;color:#ccc;margin-bottom:8px;">Activate script for this war:</div>
                <div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">`;
                if (!sub.trial_used) {
                    html += `<button id="plan-activate-trial" style="background:rgba(255,255,255,0.1);color:#ddd;border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:6px 14px;font-weight:600;font-size:11px;cursor:pointer;">Free Trial</button>`;
                }
                if (sub.xanax_balance >= factionPrice) {
                    html += `<button id="plan-activate-paid" style="background:rgba(255,255,255,0.1);color:#ddd;border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:6px 14px;font-weight:600;font-size:11px;cursor:pointer;">Activate (${factionPrice} xanax)</button>`;
                }
                if (sub.trial_used && sub.xanax_balance < factionPrice) {
                    html += `<span style="font-size:11px;color:#e66;">Insufficient balance (need ${factionPrice} xanax).</span>`;
                }
                html += `</div></div>`;
            }
            else if (!canActivate && war && !activated) {
                html += `<div style="padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;margin-bottom:10px;">
                <div style="font-size:11px;color:#888;">Ask your leader or co-leader to activate the script for this war.</div>
            </div>`;
            }
            // Recharge info
            html += `<div style="padding:8px 10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;">
            <div style="font-size:11px;color:#888;"><span style="color:#ACEA01;font-weight:600;">Cost:</span> ${factionPrice} xanax per war (<span style="text-transform:uppercase;opacity:0.8;">${factionRankTier}</span> tier). Pricing varies by faction rank (5-40 xanax). To recharge: send xanax to <a href="https://www.torn.com/profiles.php?XID=2353554" target="_blank" style="color:#6e9ecf;">JESUUS [2353554]</a> with your faction ID in the message. Anyone can send.</div>
        </div>`;
            // Viewer Dashboard (leader/co-leader/admin only) — collapsible
            if (canActivate && sub.viewer_token) {
                const viewerUrl = `${this.apiManager.serverUrl}/view/${sub.viewer_token}/`;
                html += `<div style="margin-top:10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;">
                <div class="cat-dropdown-header" data-target="cat-viewer-body" style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;cursor:pointer;user-select:none;">
                    <div style="display:flex;align-items:center;gap:6px;">
                        <span class="cat-dropdown-arrow" style="font-size:8px;color:#888;transition:transform 0.2s;">&#9654;</span>
                        <span style="font-size:10px;color:#ACEA01;text-transform:uppercase;letter-spacing:0.5px;">Dashboard Viewer</span>
                    </div>
                    <div style="font-size:9px;color:#ACEA01;font-style:italic;">Only visible for leader / co-leader / delegate</div>
                </div>
                <div id="cat-viewer-body" style="display:none;padding:0 10px 8px 10px;">
                <div style="font-size:11px;color:#888;margin-bottom:10px;">Share these credentials to give read-only access to the war dashboard.</div>
                <div style="margin-bottom:8px;">
                    <div style="font-size:10px;color:#888;margin-bottom:3px;">URL</div>
                    <a href="${viewerUrl}" target="_blank" style="font-size:11px;color:#6e9ecf;word-break:break-all;">${viewerUrl}</a>
                </div>
                <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
                    <div style="flex:1;">
                        <div style="font-size:10px;color:#888;margin-bottom:3px;">Username</div>
                        <div style="font-size:12px;color:#ddd;font-family:monospace;background:rgba(0,0,0,0.3);padding:5px 8px;border:1px solid rgba(255,255,255,0.06);border-radius:4px;" id="viewer-username">${sub.viewer_user}</div>
                    </div>
                    <button id="copy-viewer-user" title="Copy username" style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:5px 8px;cursor:pointer;color:#ccc;font-size:10px;margin-top:12px;">Copy</button>
                </div>
                <div style="display:flex;align-items:center;gap:8px;">
                    <div style="flex:1;">
                        <div style="font-size:10px;color:#888;margin-bottom:3px;">Password</div>
                        <div style="font-size:12px;color:#ddd;font-family:monospace;background:rgba(0,0,0,0.3);padding:5px 8px;border:1px solid rgba(255,255,255,0.06);border-radius:4px;" id="viewer-password-display">${'\u2022'.repeat(10)}</div>
                    </div>
                    <button id="toggle-viewer-pass" title="Show password" style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:5px 8px;cursor:pointer;margin-top:12px;display:flex;align-items:center;"><svg id="viewer-eye-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg></button>
                    <button id="copy-viewer-pass" title="Copy password" style="background:rgba(255,255,255,0.06);border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:5px 8px;cursor:pointer;color:#ccc;font-size:10px;margin-top:12px;">Copy</button>
                </div>
                </div>
            </div>`;
            }
            // Hosp→Okay auto-uncall delay — visible for all, editable by leaders/admins
            const hospDelay = sub.hosp_uncall_delay ?? 30;
            const hospEnabled = hospDelay > 0;
            const hospSliderValue = hospEnabled ? hospDelay : 30;
            html += `<div style="margin-top:10px;background:rgba(0,0,0,0.2);border:1px solid rgba(130,201,30,0.15);border-radius:4px;">
            <div class="cat-dropdown-header" data-target="cat-hosp-body" style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;cursor:pointer;user-select:none;">
                <div style="display:flex;align-items:center;gap:6px;">
                    <span class="cat-dropdown-arrow" style="font-size:8px;color:#888;transition:transform 0.2s;">&#9654;</span>
                    <span style="font-size:10px;color:#82C91E;text-transform:uppercase;letter-spacing:0.5px;">Hosp\u2192Okay Auto-uncall</span>
                </div>
                <div style="font-size:9px;color:#82C91E;font-style:italic;">${canActivate ? 'Editable' : 'Read-only'}</div>
            </div>
            <div id="cat-hosp-body" style="display:none;padding:0 10px 8px 10px;">
            <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
                <div style="font-size:11px;color:#888;">When a called target exits Hospital and becomes Okay, auto-uncall after this delay if nobody attacks.</div>
                <label style="position:relative;display:inline-block;width:28px;height:16px;flex-shrink:0;cursor:${canActivate ? 'pointer' : 'default'};">
                    <input id="hosp-uncall-toggle" type="checkbox" ${hospEnabled ? 'checked' : ''} ${canActivate ? '' : 'disabled'} style="opacity:0;width:0;height:0;" />
                    <span style="position:absolute;top:0;left:0;right:0;bottom:0;background:${hospEnabled ? '#82C91E' : '#555'};border-radius:8px;transition:background 0.2s;"></span>
                    <span style="position:absolute;top:2px;left:${hospEnabled ? '14px' : '2px'};width:12px;height:12px;background:#fff;border-radius:50%;transition:left 0.2s;"></span>
                </label>
            </div>
            <div id="hosp-uncall-slider-row" style="display:flex;align-items:center;gap:10px;${hospEnabled ? '' : 'opacity:0.4;pointer-events:none;'}">
                <input id="hosp-uncall-slider" type="range" min="10" max="60" step="5" value="${hospSliderValue}" ${canActivate ? '' : 'disabled'} style="flex:1;accent-color:#82C91E;cursor:${canActivate ? 'pointer' : 'default'};" />
                <span id="hosp-uncall-value" style="font-size:13px;font-weight:600;color:#82C91E;min-width:30px;text-align:right;">${hospSliderValue}s</span>
            </div>
            <div id="hosp-uncall-status" style="font-size:11px;margin-top:5px;min-height:14px;"></div>
            </div>
        </div>`;
            // Discord Webhook — visible for all leaders/co-leaders, but only editable for isAdmin
            if (canActivate) {
                const existingWebhook = sub.discord_webhook_url || '';
                const existingRoleId = sub.discord_role_id || '';
                const webhookEnabled = sub.discord_webhook_enabled !== false;
                const warOnly = sub.discord_war_only === true;
                const minChainHits = sub.discord_min_chain_hits ?? 0;
                const pickerStyle = 'display:flex;background:rgba(0,0,0,0.3);border-radius:6px;padding:3px;gap:4px;';
                const optBase = 'text-align:center;padding:4px 10px;border-radius:4px;cursor:pointer;font-size:10px;transition:all .15s;';
                const optOn = (active) => active ? 'background:rgba(102,126,234,0.25);color:#ddd;font-weight:600;' : 'color:#888;font-weight:400;';
                html += `<div style="margin-top:10px;background:rgba(0,0,0,0.2);border:1px solid rgba(88,101,242,0.15);border-radius:4px;">
                <div class="cat-dropdown-header" data-target="cat-discord-body" style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;cursor:pointer;user-select:none;">
                    <div style="display:flex;align-items:center;gap:6px;">
                        <span class="cat-dropdown-arrow" style="font-size:8px;color:#888;transition:transform 0.2s;">&#9654;</span>
                        <span style="font-size:10px;color:#5865F2;text-transform:uppercase;letter-spacing:0.5px;">Discord Notifications</span>
                        <span id="discord-help-toggle" title="How to create a webhook" style="display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;background:rgba(88,101,242,0.15);color:#5865F2;font-size:9px;font-weight:700;cursor:pointer;line-height:1;user-select:none;">?</span>
                    </div>
                    <div style="font-size:9px;color:#5865F2;font-style:italic;">Only visible for leader / co-leader / delegate</div>
                </div>
                <div id="cat-discord-body" style="display:none;padding:0 10px 8px 10px;">
                <div id="discord-help-content" style="display:none;font-size:10px;color:#aaa;margin-bottom:8px;padding:8px 10px;background:rgba(88,101,242,0.06);border:1px solid rgba(88,101,242,0.12);border-radius:4px;">
                    <div style="color:#5865F2;font-weight:600;margin-bottom:5px;">How to create a Discord webhook</div>
                    <div style="display:flex;flex-direction:column;gap:3px;">
                        <span>1. Open your Discord server and go to the channel you want alerts in</span>
                        <span>2. Click the gear icon (Edit Channel) next to the channel name</span>
                        <span>3. Go to <span style="color:#ccc;">Integrations</span> &rarr; <span style="color:#ccc;">Webhooks</span></span>
                        <span>4. Click <span style="color:#ccc;">New Webhook</span>, give it a name (e.g. "CAT Script")</span>
                        <span>5. Click <span style="color:#ccc;">Copy Webhook URL</span> and paste it below</span>
                    </div>
                    <div style="margin-top:5px;color:#666;font-style:italic;">The Role ID is optional — use it if you want the bot to ping a specific role.</div>
                </div>
                <div style="font-size:11px;color:#888;margin-bottom:8px;">Receive Discord notifications for tactical markers (smoke / tear / help), war events (scheduled, started, ended), chain alerts, and script activation. Marker messages are auto-deleted when the target is hospitalized.</div>
                ${existingWebhook ? `<div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
                    <span style="font-size:10px;color:#aaa;width:55px;flex-shrink:0;">Active</span>
                    <div id="discord-enabled-picker" style="${pickerStyle}">
                        <div class="cat-dc-opt" data-val="on" style="${optBase}${optOn(webhookEnabled)}">On</div>
                        <div class="cat-dc-opt" data-val="off" style="${optBase}${optOn(!webhookEnabled)}">Off</div>
                    </div>
                </div>
                <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
                    <span style="font-size:10px;color:#aaa;width:55px;flex-shrink:0;">When</span>
                    <div id="discord-waronly-picker" style="${pickerStyle}">
                        <div class="cat-dw-opt" data-val="always" style="${optBase}${optOn(!warOnly)}">Always</div>
                        <div class="cat-dw-opt" data-val="war" style="${optBase}${optOn(warOnly)}">War only</div>
                    </div>
                </div>
                <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;">
                    <span style="font-size:10px;color:#aaa;width:55px;flex-shrink:0;">Min chain</span>
                    <input id="discord-min-chain-input" type="number" min="0" max="100000" value="${minChainHits}" style="width:80px;font-size:11px;color:#ddd;font-family:monospace;background:rgba(0,0,0,0.3);padding:4px 6px;border:1px solid rgba(255,255,255,0.06);border-radius:4px;outline:none;" />
                    <span style="font-size:10px;color:#666;">hits (0 = always alert)</span>
                    <button id="discord-min-chain-save" style="background:rgba(88,101,242,0.15);color:#5865F2;border:1px solid rgba(88,101,242,0.2);border-radius:4px;padding:4px 10px;font-weight:600;font-size:10px;cursor:pointer;">Save</button>
                </div>` : ''}
                <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
                    <input id="discord-webhook-input" type="text" placeholder="https://discord.com/api/webhooks/..." value="${this._esc(existingWebhook)}" style="width:80%;font-size:11px;color:#ddd;font-family:monospace;background:rgba(0,0,0,0.3);padding:5px 8px;border:1px solid rgba(255,255,255,0.06);border-radius:4px;outline:none;" />
                    <button id="discord-webhook-save" style="background:rgba(88,101,242,0.15);color:#5865F2;border:1px solid rgba(88,101,242,0.2);border-radius:4px;padding:5px 12px;font-weight:600;font-size:11px;cursor:pointer;white-space:nowrap;">Save</button>
                    <button id="discord-webhook-test" style="background:rgba(255,255,255,0.06);color:#888;border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:5px 12px;font-weight:600;font-size:11px;cursor:pointer;white-space:nowrap;">Test</button>
                </div>
                <div style="display:flex;align-items:center;gap:8px;">
                    <input id="discord-role-id-input" type="text" placeholder="Role ID (optional — digits only)" value="${this._esc(existingRoleId)}" style="width:80%;font-size:11px;color:#ddd;font-family:monospace;background:rgba(0,0,0,0.3);padding:5px 8px;border:1px solid rgba(255,255,255,0.06);border-radius:4px;outline:none;" />
                    <div style="font-size:10px;color:#666;white-space:nowrap;">Ping &lt;@&amp;ID&gt;</div>
                </div>
                <div id="discord-webhook-status" style="font-size:11px;margin-top:5px;min-height:14px;"></div>
                </div>
            </div>`;
            }
            // Delegate Permissions — only for true leaders/admins (not delegates themselves)
            const canManageDelegates = !!(data.isAdmin || data.isLeader) && !data.isDelegate;
            if (canManageDelegates) {
                html += `<div style="margin-top:10px;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;">
                <div class="cat-dropdown-header" data-target="cat-delegate-body" style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;cursor:pointer;user-select:none;">
                    <div style="display:flex;align-items:center;gap:6px;">
                        <span class="cat-dropdown-arrow" style="font-size:8px;color:#888;transition:transform 0.2s;">&#9654;</span>
                        <span style="font-size:10px;color:#D4C07C;text-transform:uppercase;letter-spacing:0.5px;">Delegate Permissions</span>
                    </div>
                    <span style="font-size:9px;color:#888;font-style:italic;">Members with leader-level access</span>
                </div>
                <div id="cat-delegate-body" style="display:none;padding:0 10px 8px 10px;">
                <div id="cat-delegates-list" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;min-height:20px;">
                    <span style="font-size:10px;color:#666;">Loading...</span>
                </div>
                <button id="cat-delegate-add-btn" style="background:rgba(212,192,124,0.1);color:#D4C07C;border:1px solid rgba(212,192,124,0.2);border-radius:4px;padding:4px 12px;font-weight:600;font-size:10px;cursor:pointer;">+ Add delegate</button>
                <div id="cat-delegate-dropdown" style="display:none;margin-top:6px;"></div>
                <div id="cat-delegate-status" style="font-size:11px;margin-top:5px;min-height:14px;"></div>
                </div>
            </div>`;
            }
            loader.style.display = 'none';
            container.style.display = '';
            container.innerHTML = html;
            // Collapsible dropdown toggle handlers
            container.querySelectorAll('.cat-dropdown-header').forEach(header => {
                header.addEventListener('click', (e) => {
                    // Don't toggle if clicking the help "?" button
                    if (e.target.id === 'discord-help-toggle')
                        return;
                    const targetId = header.getAttribute('data-target');
                    if (!targetId)
                        return;
                    const body = document.getElementById(targetId);
                    const arrow = header.querySelector('.cat-dropdown-arrow');
                    if (!body)
                        return;
                    const isOpen = body.style.display !== 'none';
                    body.style.display = isOpen ? 'none' : '';
                    if (arrow)
                        arrow.style.transform = isOpen ? '' : 'rotate(90deg)';
                });
            });
            // Attach activation handlers
            const trialBtn = container.querySelector('#plan-activate-trial');
            if (trialBtn) {
                trialBtn.addEventListener('click', async () => {
                    if (!data?.currentWar) {
                        this.apiManager.showNotification("You're not matched up in a war yet.", 'warning');
                        return;
                    }
                    trialBtn.disabled = true;
                    trialBtn.textContent = '...';
                    await this._activateFromPlanTab(userFactionId, data.currentWar.war_id ?? '', true);
                });
            }
            const paidBtn = container.querySelector('#plan-activate-paid');
            if (paidBtn) {
                paidBtn.addEventListener('click', async () => {
                    paidBtn.disabled = true;
                    paidBtn.textContent = '...';
                    await this._activateFromPlanTab(userFactionId, data.currentWar?.war_id ?? '', false);
                });
            }
            // Viewer dashboard handlers
            const viewerPass = (sub && sub.viewer_token) ? (sub.viewer_pass || '') : '';
            let passVisible = false;
            const togglePassBtn = container.querySelector('#toggle-viewer-pass');
            if (togglePassBtn) {
                togglePassBtn.addEventListener('click', () => {
                    const display = container.querySelector('#viewer-password-display');
                    const eyeIcon = container.querySelector('#viewer-eye-icon');
                    if (!display)
                        return;
                    passVisible = !passVisible;
                    display.textContent = passVisible ? viewerPass : '\u2022'.repeat(10);
                    togglePassBtn.title = passVisible ? 'Hide password' : 'Show password';
                    if (eyeIcon) {
                        eyeIcon.innerHTML = passVisible
                            ? '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>'
                            : '<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/>';
                    }
                });
            }
            const copyUserBtn = container.querySelector('#copy-viewer-user');
            if (copyUserBtn) {
                copyUserBtn.addEventListener('click', () => {
                    const text = container.querySelector('#viewer-username')?.textContent || '';
                    navigator.clipboard.writeText(text).then(() => {
                        copyUserBtn.textContent = 'Copied!';
                        setTimeout(() => { copyUserBtn.textContent = 'Copy'; }, 1500);
                    }).catch(() => { });
                });
            }
            const copyPassBtn = container.querySelector('#copy-viewer-pass');
            if (copyPassBtn) {
                copyPassBtn.addEventListener('click', () => {
                    navigator.clipboard.writeText(viewerPass).then(() => {
                        copyPassBtn.textContent = 'Copied!';
                        setTimeout(() => { copyPassBtn.textContent = 'Copy'; }, 1500);
                    }).catch(() => { });
                });
            }
            // Discord help toggle
            const helpToggle = container.querySelector('#discord-help-toggle');
            if (helpToggle) {
                helpToggle.addEventListener('click', () => {
                    const content = container.querySelector('#discord-help-content');
                    if (!content)
                        return;
                    const visible = content.style.display !== 'none';
                    content.style.display = visible ? 'none' : 'block';
                    helpToggle.style.background = visible ? 'rgba(88,101,242,0.15)' : 'rgba(88,101,242,0.3)';
                });
            }
            // Discord webhook handler
            const webhookSaveBtn = container.querySelector('#discord-webhook-save');
            if (webhookSaveBtn) {
                webhookSaveBtn.addEventListener('click', async () => {
                    const input = container.querySelector('#discord-webhook-input');
                    const roleInput = container.querySelector('#discord-role-id-input');
                    const statusDiv = container.querySelector('#discord-webhook-status');
                    if (!input || !statusDiv)
                        return;
                    const url = input.value.trim();
                    if (url && !/^https:\/\/(discord\.com|discordapp\.com)\/api\/webhooks\/\d+\/[\w-]+$/.test(url)) {
                        statusDiv.style.color = '#ef5350';
                        statusDiv.textContent = 'Invalid Discord webhook URL.';
                        return;
                    }
                    const roleId = (roleInput?.value || '').trim();
                    if (roleId && !/^\d+$/.test(roleId)) {
                        statusDiv.style.color = '#ef5350';
                        statusDiv.textContent = 'Role ID must be digits only.';
                        return;
                    }
                    webhookSaveBtn.disabled = true;
                    webhookSaveBtn.textContent = '...';
                    statusDiv.textContent = '';
                    try {
                        const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/discord-webhook`, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                            body: JSON.stringify({ factionId: targetFactionId, url, roleId: roleId || null })
                        });
                        const result = await resp.json();
                        if (result.success) {
                            statusDiv.style.color = '#4caf50';
                            statusDiv.textContent = url ? 'Webhook saved.' : 'Webhook disabled.';
                        }
                        else {
                            statusDiv.style.color = '#ef5350';
                            statusDiv.textContent = result.error || 'Error saving webhook.';
                        }
                    }
                    catch (err) {
                        statusDiv.style.color = '#ef5350';
                        statusDiv.textContent = 'Network error.';
                    }
                    finally {
                        webhookSaveBtn.disabled = false;
                        webhookSaveBtn.textContent = 'Save';
                    }
                });
            }
            // Discord enabled picker handler (On/Off)
            const enabledPicker = container.querySelector('#discord-enabled-picker');
            if (enabledPicker) {
                const opts = enabledPicker.querySelectorAll('.cat-dc-opt');
                const highlightEnabled = (val) => {
                    opts.forEach(o => {
                        const active = o.dataset.val === val;
                        o.style.background = active ? 'rgba(102,126,234,0.25)' : 'transparent';
                        o.style.color = active ? '#ddd' : '#888';
                        o.style.fontWeight = active ? '600' : '400';
                    });
                };
                opts.forEach(opt => {
                    opt.addEventListener('click', async () => {
                        const newEnabled = opt.dataset.val === 'on';
                        const statusDiv = container.querySelector('#discord-webhook-status');
                        try {
                            const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/discord-webhook/toggle`, {
                                method: 'POST',
                                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                                body: JSON.stringify({ factionId: targetFactionId, enabled: newEnabled })
                            });
                            const result = await resp.json();
                            if (result.success) {
                                highlightEnabled(result.enabled !== false ? 'on' : 'off');
                                if (statusDiv) {
                                    statusDiv.style.color = '#4caf50';
                                    statusDiv.textContent = result.enabled !== false ? 'Notifications enabled.' : 'Notifications disabled.';
                                }
                            }
                        }
                        catch {
                            if (statusDiv) {
                                statusDiv.style.color = '#ef5350';
                                statusDiv.textContent = 'Network error.';
                            }
                        }
                    });
                });
            }
            // Discord war-only picker handler (Always/War only)
            const warOnlyPicker = container.querySelector('#discord-waronly-picker');
            if (warOnlyPicker) {
                const opts = warOnlyPicker.querySelectorAll('.cat-dw-opt');
                const highlightWarOnly = (val) => {
                    opts.forEach(o => {
                        const active = o.dataset.val === val;
                        o.style.background = active ? 'rgba(102,126,234,0.25)' : 'transparent';
                        o.style.color = active ? '#ddd' : '#888';
                        o.style.fontWeight = active ? '600' : '400';
                    });
                };
                opts.forEach(opt => {
                    opt.addEventListener('click', async () => {
                        const newWarOnly = opt.dataset.val === 'war';
                        const statusDiv = container.querySelector('#discord-webhook-status');
                        try {
                            const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/discord-webhook/war-only`, {
                                method: 'POST',
                                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                                body: JSON.stringify({ factionId: targetFactionId, warOnly: newWarOnly })
                            });
                            const result = await resp.json();
                            if (result.success) {
                                highlightWarOnly(result.warOnly ? 'war' : 'always');
                                if (statusDiv) {
                                    statusDiv.style.color = '#4caf50';
                                    statusDiv.textContent = result.warOnly ? 'War only mode.' : 'Always active.';
                                }
                            }
                        }
                        catch {
                            if (statusDiv) {
                                statusDiv.style.color = '#ef5350';
                                statusDiv.textContent = 'Network error.';
                            }
                        }
                    });
                });
            }
            // Discord min chain hits save handler
            const minChainSaveBtn = container.querySelector('#discord-min-chain-save');
            if (minChainSaveBtn) {
                minChainSaveBtn.addEventListener('click', async () => {
                    const input = container.querySelector('#discord-min-chain-input');
                    const statusDiv = container.querySelector('#discord-webhook-status');
                    if (!input || !statusDiv)
                        return;
                    const val = parseInt(input.value, 10);
                    if (isNaN(val) || val < 0) {
                        statusDiv.style.color = '#ef5350';
                        statusDiv.textContent = 'Invalid value.';
                        return;
                    }
                    minChainSaveBtn.disabled = true;
                    minChainSaveBtn.textContent = '...';
                    statusDiv.textContent = '';
                    try {
                        const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/discord-chain-min`, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                            body: JSON.stringify({ factionId: targetFactionId, minChainHits: val })
                        });
                        const result = await resp.json();
                        if (result.success) {
                            statusDiv.style.color = '#4caf50';
                            statusDiv.textContent = val === 0 ? 'Chain alerts: always.' : `Chain alerts: ≥ ${val} hits.`;
                        }
                        else {
                            statusDiv.style.color = '#ef5350';
                            statusDiv.textContent = result.error || 'Error saving.';
                        }
                    }
                    catch {
                        statusDiv.style.color = '#ef5350';
                        statusDiv.textContent = 'Network error.';
                    }
                    finally {
                        minChainSaveBtn.disabled = false;
                        minChainSaveBtn.textContent = 'Save';
                    }
                });
            }
            // Discord webhook test handler
            const webhookTestBtn = container.querySelector('#discord-webhook-test');
            if (webhookTestBtn) {
                webhookTestBtn.addEventListener('click', async () => {
                    const statusDiv = container.querySelector('#discord-webhook-status');
                    if (!statusDiv)
                        return;
                    webhookTestBtn.disabled = true;
                    webhookTestBtn.textContent = '...';
                    statusDiv.textContent = '';
                    try {
                        const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/discord-webhook/test`, {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                            body: JSON.stringify({ factionId: targetFactionId })
                        });
                        const result = await resp.json();
                        if (result.success) {
                            statusDiv.style.color = '#4caf50';
                            statusDiv.textContent = 'Test sent! Check your Discord channel.';
                        }
                        else {
                            statusDiv.style.color = '#ef5350';
                            statusDiv.textContent = result.error || 'Test failed.';
                        }
                    }
                    catch (err) {
                        statusDiv.style.color = '#ef5350';
                        statusDiv.textContent = 'Network error.';
                    }
                    finally {
                        webhookTestBtn.disabled = false;
                        webhookTestBtn.textContent = 'Test';
                    }
                });
            }
            // Hosp uncall delay slider + toggle handler
            const hospSlider = container.querySelector('#hosp-uncall-slider');
            const hospToggle = container.querySelector('#hosp-uncall-toggle');
            const hospSliderRow = container.querySelector('#hosp-uncall-slider-row');
            if (hospSlider && canActivate) {
                const valueLabel = container.querySelector('#hosp-uncall-value');
                const statusDiv = container.querySelector('#hosp-uncall-status');
                let saveTimeout = null;
                const saveDelay = (delay) => {
                    if (saveTimeout)
                        clearTimeout(saveTimeout);
                    saveTimeout = setTimeout(async () => {
                        try {
                            const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/hosp-uncall-delay`, {
                                method: 'POST',
                                headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                                body: JSON.stringify({ factionId: targetFactionId, delay })
                            });
                            const result = await resp.json();
                            if (result.success && statusDiv) {
                                statusDiv.style.color = '#4caf50';
                                statusDiv.textContent = delay === 0 ? 'Disabled' : `Saved: ${result.delay}s`;
                                setTimeout(() => { if (statusDiv)
                                    statusDiv.textContent = ''; }, 2000);
                            }
                            else if (statusDiv) {
                                statusDiv.style.color = '#ef5350';
                                statusDiv.textContent = result.error || 'Error saving.';
                            }
                        }
                        catch {
                            if (statusDiv) {
                                statusDiv.style.color = '#ef5350';
                                statusDiv.textContent = 'Network error.';
                            }
                        }
                    }, 300);
                };
                // Toggle on/off
                if (hospToggle) {
                    hospToggle.addEventListener('change', () => {
                        const on = hospToggle.checked;
                        // Update visual toggle knob
                        const track = hospToggle.nextElementSibling;
                        const knob = track?.nextElementSibling;
                        if (track)
                            track.style.background = on ? '#82C91E' : '#555';
                        if (knob)
                            knob.style.left = on ? '14px' : '2px';
                        // Enable/disable slider row
                        if (hospSliderRow) {
                            hospSliderRow.style.opacity = on ? '1' : '0.4';
                            hospSliderRow.style.pointerEvents = on ? '' : 'none';
                        }
                        saveDelay(on ? Number(hospSlider.value) : 0);
                    });
                }
                hospSlider.addEventListener('input', () => {
                    if (valueLabel)
                        valueLabel.textContent = `${hospSlider.value}s`;
                });
                hospSlider.addEventListener('change', () => {
                    saveDelay(Number(hospSlider.value));
                });
            }
            // Delegate management handlers
            const delegateList = container.querySelector('#cat-delegates-list');
            const delegateAddBtn = container.querySelector('#cat-delegate-add-btn');
            const delegateDropdown = container.querySelector('#cat-delegate-dropdown');
            const delegateStatus = container.querySelector('#cat-delegate-status');
            if (delegateList && delegateAddBtn) {
                const serverUrl = this.apiManager.serverUrl;
                const authToken = this.apiManager.authToken;
                // Fetch full member list from server (more reliable than client-side _memberNames)
                let memberEntries = [];
                let membersFetched = false;
                const renderDelegateChips = (delegates) => {
                    if (delegates.length === 0) {
                        delegateList.innerHTML = '<span style="font-size:10px;color:#666;">No delegates configured</span>';
                        return;
                    }
                    delegateList.innerHTML = delegates.map(d => `<span class="cat-delegate-chip" data-id="${this._esc(d.playerId)}" style="display:inline-flex;align-items:center;gap:4px;background:rgba(212,192,124,0.1);border:1px solid rgba(212,192,124,0.2);border-radius:3px;padding:2px 6px;font-size:10px;color:#D4C07C;">
                        ${this._esc(d.playerName)}
                        <span class="cat-delegate-remove" style="cursor:pointer;color:#888;font-size:12px;line-height:1;" data-cat-tooltip="Remove">\u2716</span>
                    </span>`).join('');
                    // Attach remove handlers
                    delegateList.querySelectorAll('.cat-delegate-remove').forEach(btn => {
                        btn.addEventListener('click', async (e) => {
                            const chip = e.target.closest('.cat-delegate-chip');
                            if (!chip)
                                return;
                            const pid = chip.dataset.id;
                            try {
                                const resp = await this.apiManager.httpRequest(`${serverUrl}/api/subscription/delegates`, {
                                    method: 'DELETE',
                                    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
                                    body: JSON.stringify({ factionId: targetFactionId, playerId: pid })
                                });
                                const result = await resp.json();
                                if (result.success && result.delegates) {
                                    renderDelegateChips(result.delegates);
                                }
                                else if (delegateStatus) {
                                    delegateStatus.style.color = '#ef5350';
                                    delegateStatus.textContent = result.error || 'Error removing delegate';
                                }
                            }
                            catch {
                                if (delegateStatus) {
                                    delegateStatus.style.color = '#ef5350';
                                    delegateStatus.textContent = 'Network error';
                                }
                            }
                        });
                    });
                };
                // Fetch current delegates
                (async () => {
                    try {
                        const resp = await this.apiManager.httpRequest(`${serverUrl}/api/subscription/${encodeURIComponent(targetFactionId)}/delegates`, { method: 'GET', headers: { 'Authorization': `Bearer ${authToken}` } });
                        const result = await resp.json();
                        if (result.success && result.delegates) {
                            renderDelegateChips(result.delegates);
                        }
                        else {
                            delegateList.innerHTML = '<span style="font-size:10px;color:#666;">No delegates configured</span>';
                        }
                    }
                    catch {
                        delegateList.innerHTML = '<span style="font-size:10px;color:#ef5350;">Failed to load</span>';
                    }
                })();
                // Add delegate button → show searchable dropdown (fetches full member list from server)
                delegateAddBtn.addEventListener('click', async () => {
                    if (!delegateDropdown)
                        return;
                    if (delegateDropdown.style.display !== 'none') {
                        delegateDropdown.style.display = 'none';
                        return;
                    }
                    // Fetch full member list from Torn API on first click
                    if (!membersFetched) {
                        try {
                            const factionInfo = await this.apiManager.getFactionInfo(targetFactionId);
                            if (factionInfo && factionInfo.members) {
                                const membersData = factionInfo.members;
                                const membersArray = Array.isArray(membersData) ? membersData : Object.values(membersData);
                                memberEntries = membersArray.map(m => [String(m.id), m.name]).sort((a, b) => a[1].localeCompare(b[1]));
                            }
                        }
                        catch { /* fallback to empty */ }
                        membersFetched = true;
                    }
                    let html2 = `<input id="cat-delegate-search" type="search" autocomplete="off" placeholder="Search member..." style="width:100%;box-sizing:border-box;padding:6px 10px;font-size:11px;background:#1c1c1c;color:#eee;border:1px solid #3a3a3a;border-radius:4px;outline:none;margin-bottom:4px;" />`;
                    html2 += `<div id="cat-delegate-member-list" style="max-height:150px;overflow-y:auto;"></div>`;
                    delegateDropdown.innerHTML = html2;
                    delegateDropdown.style.display = '';
                    const searchInput = delegateDropdown.querySelector('#cat-delegate-search');
                    const memberList = delegateDropdown.querySelector('#cat-delegate-member-list');
                    const renderMembers = (filter) => {
                        if (!memberList)
                            return;
                        const filtered = filter ? memberEntries.filter(([, name]) => name.toLowerCase().includes(filter.toLowerCase())) : memberEntries;
                        if (filtered.length === 0) {
                            memberList.innerHTML = '<div style="padding:5px 10px;font-size:10px;color:#666;">No match</div>';
                            return;
                        }
                        memberList.innerHTML = filtered.map(([id, name]) => `<div class="cat-delegate-option" data-id="${this._esc(id)}" data-name="${this._esc(name)}" style="padding:4px 10px;font-size:11px;color:#ccc;cursor:pointer;border-bottom:1px solid #2a2a2a;">${this._esc(name)}</div>`).join('');
                        memberList.querySelectorAll('.cat-delegate-option').forEach(opt => {
                            opt.addEventListener('click', async () => {
                                const pid = opt.dataset.id;
                                const pname = opt.dataset.name;
                                delegateDropdown.style.display = 'none';
                                if (delegateStatus) {
                                    delegateStatus.style.color = '#D4C07C';
                                    delegateStatus.textContent = `Adding ${pname}...`;
                                }
                                try {
                                    const resp2 = await this.apiManager.httpRequest(`${serverUrl}/api/subscription/delegates`, {
                                        method: 'POST',
                                        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${authToken}` },
                                        body: JSON.stringify({ factionId: targetFactionId, playerId: pid })
                                    });
                                    const result = await resp2.json();
                                    if (result.success && result.delegates) {
                                        renderDelegateChips(result.delegates);
                                        if (delegateStatus) {
                                            delegateStatus.style.color = '#4caf50';
                                            delegateStatus.textContent = `${pname} added`;
                                        }
                                    }
                                    else if (delegateStatus) {
                                        delegateStatus.style.color = '#ef5350';
                                        delegateStatus.textContent = result.error || 'Error';
                                    }
                                    setTimeout(() => { if (delegateStatus)
                                        delegateStatus.textContent = ''; }, 2000);
                                }
                                catch {
                                    if (delegateStatus) {
                                        delegateStatus.style.color = '#ef5350';
                                        delegateStatus.textContent = 'Network error';
                                    }
                                }
                            });
                        });
                    };
                    renderMembers('');
                    if (searchInput) {
                        searchInput.focus();
                        searchInput.addEventListener('input', () => renderMembers(searchInput.value));
                    }
                });
            }
        }
        catch (e) {
            console.log('[CAT] Plan tab error:', e);
            this.apiManager.reportError('planTab', e);
            loader.innerHTML = `<p style="color:#ef5350;">Error: ${this._esc(e instanceof Error ? e.message : String(e))}</p>`;
        }
    }
    async function _activateFromPlanTab(factionId, warId, useTrial) {
        try {
            const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/activate`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.apiManager.authToken}`
                },
                body: JSON.stringify({
                    factionId,
                    warId,
                    useTrial,
                    tornApiKey: this.apiManager.torn_apikey
                })
            });
            const data = await response.json();
            if (data.success) {
                if (this._enhancer) {
                    this._enhancer.activationStatus = 'activated';
                    this._enhancer.removeReadOnlyMode();
                    // Refresh cached subscription data
                    this._enhancer.subscriptionData = null;
                    await this._enhancer.checkActivationStatus();
                }
                this._loadPlanTab();
            }
            else {
                const msg = data.error === 'already_activated' ? 'Already activated!'
                    : data.error === 'trial_already_used' ? 'Free trial already used.'
                        : data.error === 'insufficient_balance' ? 'Insufficient balance.'
                            : data.error === 'Only leader or co-leader can activate' ? 'Only leader or co-leader can activate.'
                                : data.message || data.error || 'Activation failed.';
                alert(msg);
                if (this._enhancer)
                    this._enhancer.subscriptionData = null;
                this._loadPlanTab();
            }
        }
        catch (e) {
            this.apiManager.reportError('activateWarUI', e);
            alert('Error: ' + (e instanceof Error ? e.message : String(e)));
            if (this._enhancer)
                this._enhancer.subscriptionData = null;
            this._loadPlanTab();
        }
    }

    async function startUsersDataLoop() {
        // Don't reset cache or restart if already running
        if (this._usersRefreshInterval)
            return;
        this._usersCache = null;
        if (this.apiManager) {
            await this.apiManager.fetchRankedWarsFromAPI();
        }
        this._fetchUsersData();
        this._usersRefreshInterval = setInterval(() => this._fetchUsersData(), 60 * 1000);
    }
    async function _fetchUsersData() {
        try {
            if (!this.apiManager?.authToken)
                return;
            // Always use the user's own faction from localStorage (not viewingFactionId)
            const raw = StorageUtil.get('cat_user_faction_id', null);
            const userFactionId = raw ? String(raw).replace(/"/g, '') : null;
            const headers = { 'Authorization': `Bearer ${this.apiManager.authToken}` };
            const fetchOpts = { method: 'GET', headers };
            if (!userFactionId) {
                this._usersCache = { membersData: null, activityData: null, leaderboardData: null, warData: null };
                return;
            }
            let warData = null;
            let warStart = null;
            try {
                const warResp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/faction-war/${userFactionId}`, fetchOpts);
                if (!warResp.ok) {
                    // 403/404 are expected when not in a war - don't report
                    if (warResp.status !== 403 && warResp.status !== 404) {
                        throw new Error(`War data HTTP ${warResp.status}`);
                    }
                }
                else {
                    warData = await warResp.json();
                    if (warData && warData.success && warData.war && warData.isActive && warData.war.war_start) {
                        const wsDate = new Date(warData.war.war_start);
                        if (wsDate.getTime() <= Date.now()) {
                            warStart = wsDate.toISOString();
                        }
                    }
                }
            }
            catch (_e) {
                console.log('[CAT] Error fetching war data:', _e);
                this.apiManager.reportError('fetchWarDataUsers', _e);
            }
            const warStartParam = warStart ? `&warStart=${encodeURIComponent(warStart)}` : '';
            const warStartQParam = warStart ? `?warStart=${encodeURIComponent(warStart)}` : '';
            const membersEndpoint = `${this.apiManager.serverUrl}/api/faction-members/${userFactionId}`;
            const activityEndpoint = `${this.apiManager.serverUrl}/api/users/call-activity?factionId=${userFactionId}${warStartParam}`;
            const leaderboardEndpoint = `${this.apiManager.serverUrl}/api/call-leaderboard/${userFactionId}${warStartQParam}`;
            const fetches = [];
            const fetchKeys = [];
            fetches.push(this.apiManager.httpRequest(membersEndpoint, fetchOpts));
            fetchKeys.push('members');
            fetches.push(this.apiManager.httpRequest(activityEndpoint, fetchOpts));
            fetchKeys.push('activity');
            fetches.push(this.apiManager.httpRequest(leaderboardEndpoint, fetchOpts));
            fetchKeys.push('leaderboard');
            const responses = await Promise.all(fetches);
            const results = {};
            for (let i = 0; i < fetchKeys.length; i++) {
                const resp = responses[i];
                if (!resp.ok) {
                    console.log(`[CAT] ${fetchKeys[i]} returned status ${resp.status}`);
                    continue;
                }
                const text = await resp.text();
                if (!text || !text.trim().startsWith('{')) {
                    console.log(`[CAT] ${fetchKeys[i]} returned non-JSON response`);
                    continue;
                }
                try {
                    results[fetchKeys[i]] = JSON.parse(text);
                }
                catch (_e) {
                    console.log(`[CAT] ${fetchKeys[i]} JSON parse error`);
                }
            }
            const membersData = results.members || null;
            const activityData = results.activity || null;
            const leaderboardData = results.leaderboard || null;
            const newCache = { membersData, activityData, leaderboardData, warData: warData };
            const oldJson = this._usersCache ? JSON.stringify(this._usersCache) : '';
            const newJson = JSON.stringify(newCache);
            this._usersCache = newCache;
            if (oldJson !== newJson) {
                const usersTabBtn = document.querySelector('.custom-tab-btn[data-tab="stats"]');
                if (usersTabBtn && usersTabBtn.classList.contains('active')) {
                    this._renderUsersTab();
                }
            }
        }
        catch (err) {
            console.log('[CAT] Error pre-fetching users data:', err);
            this.apiManager.reportError('prefetchUsersData', err);
        }
    }
    function _renderUsersTab() {
        const loader = document.getElementById('users-tab-loader');
        const container = document.getElementById('users-tab-container');
        if (!loader || !container)
            return;
        const lbContentEl = document.getElementById('leaderboard-dropdown-content');
        const usersContentEl = document.getElementById('users-dropdown-content');
        const savedLbOpen = lbContentEl ? lbContentEl.style.display !== 'none' : false;
        const savedUsersOpen = usersContentEl ? usersContentEl.style.display !== 'none' : false;
        if (this._warCountdownInterval) {
            clearInterval(this._warCountdownInterval);
            this._warCountdownInterval = null;
        }
        if (!this._usersCache) {
            loader.style.display = 'block';
            container.style.display = 'none';
            this._fetchUsersData();
            return;
        }
        const membersData = this._usersCache.membersData;
        const activityData = this._usersCache.activityData;
        const leaderboardData = this._usersCache.leaderboardData;
        const warData = this._usersCache.warData;
        if (!membersData || !membersData.success || !membersData.members) {
            loader.innerHTML = '<p style="color: #ffa726;">No member data yet. Open the Faction tab first to load members.</p>';
            return;
        }
        const scriptCount = membersData.members.filter((m) => m.uses_script).length;
        const totalCount = membersData.total;
        const onlineStatuses = this._enhancer?.onlineStatuses || this.onlineStatuses || {};
        const rows = membersData.members.map((m) => {
            const nameColor = m.uses_script ? '#86B202' : '#666';
            const statusIcon = m.uses_script ? '&#10003;' : '&#10007;';
            const statusColor = m.uses_script ? '#86B202' : '#555';
            const olStatus = onlineStatuses[String(m.player_id)] || 'unknown';
            const dotColor = olStatus === 'online' ? '#88B403' : olStatus === 'idle' ? '#E79E00' : '#B1B1B1';
            return `<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
            <td style="padding: 6px 10px;">
                <span id="online-dot-${m.player_id}" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${dotColor};margin-right:6px;vertical-align:middle;"></span><a href="https://www.torn.com/profiles.php?XID=${m.player_id}" target="_blank" style="color: ${nameColor}; text-decoration: none; font-weight: ${m.uses_script ? '600' : '400'};">${this._esc(m.player_name)}</a>
            </td>
            <td style="padding: 6px 10px; color: #a0aec0; text-align: center;">${m.level || '-'}</td>
            <td style="padding: 6px 10px; text-align: center; color: ${statusColor}; font-size: 14px;">${statusIcon}</td>
        </tr>`;
        }).join('');
        let graphHtml = '';
        let startTime = null;
        let callSlots = null;
        let uncallSlots = null;
        let totalMinutes = 1440;
        const warWarStart = warData?.war?.war_start;
        const hasWarData = warData && warData.success && warData.isActive && warData.war && warWarStart;
        const warStartMs = hasWarData && warWarStart ? new Date(warWarStart).getTime() : 0;
        const warHasStarted = hasWarData && warStartMs <= Date.now();
        const warInPreparation = hasWarData && warStartMs > Date.now();
        const hasWarStart = warHasStarted;
        if (activityData && activityData.success && activityData.data) {
            const svgW = 700, svgH = 160, padL = 35, padR = 10, padT = 10, padB = 25;
            const chartW = svgW - padL - padR;
            const chartH = svgH - padT - padB;
            const serverNow = new Date(activityData.server_time);
            if (hasWarStart && warWarStart) {
                const ws = new Date(warWarStart);
                startTime = new Date(Math.floor((ws.getTime() - 60000) / 60000) * 60000);
                totalMinutes = Math.max(10, Math.floor((serverNow.getTime() - startTime.getTime()) / 60000));
            }
            else {
                startTime = new Date(Math.floor((serverNow.getTime() - 24 * 60 * 60 * 1000) / 60000) * 60000);
                totalMinutes = 1440;
            }
            callSlots = new Array(totalMinutes).fill(0);
            uncallSlots = new Array(totalMinutes).fill(0);
            activityData.data.forEach((row) => {
                const t = new Date(row.activity_minute);
                const diffMin = Math.floor((t.getTime() - startTime.getTime()) / 60000);
                if (diffMin >= 0 && diffMin < totalMinutes) {
                    callSlots[diffMin] = row.call_count || 0;
                    uncallSlots[diffMin] = row.uncall_count || 0;
                }
            });
            const rawMax = Math.max(1, ...callSlots, ...uncallSlots);
            const maxVal = rawMax <= 3 ? rawMax + 1
                : rawMax <= 20 ? Math.ceil(rawMax * 1.3)
                    : Math.ceil(rawMax * 1.3 / 5) * 5;
            const lastSlot = totalMinutes - 1 || 1;
            const toPoints = (slots) => {
                const pts = [];
                for (let i = 0; i < totalMinutes; i++) {
                    const x = padL + (i / lastSlot) * chartW;
                    const y = padT + chartH - (slots[i] / maxVal) * chartH;
                    pts.push(`${x.toFixed(1)},${y.toFixed(1)}`);
                }
                return pts.join(' ');
            };
            const callAreaPts = (() => {
                let pts = `${padL},${padT + chartH} `;
                for (let i = 0; i < totalMinutes; i++) {
                    const x = padL + (i / lastSlot) * chartW;
                    const y = padT + chartH - (callSlots[i] / maxVal) * chartH;
                    pts += `${x.toFixed(1)},${y.toFixed(1)} `;
                }
                pts += `${padL + chartW},${padT + chartH}`;
                return pts;
            })();
            const totalHours = Math.ceil(totalMinutes / 60);
            const hourStep = totalHours <= 12 ? 1 : totalHours <= 48 ? 2 : totalHours <= 96 ? 4 : 6;
            const labelStep = totalHours <= 24 ? 2 : totalHours <= 48 ? 4 : totalHours <= 96 ? 8 : 12;
            let gridLines = '';
            let hourLabels = '';
            for (let h = 0; h < totalHours; h += hourStep) {
                const minuteOffset = h * 60;
                if (minuteOffset >= totalMinutes)
                    break;
                const x = padL + (minuteOffset / lastSlot) * chartW;
                gridLines += `<line x1="${x.toFixed(1)}" y1="${padT}" x2="${x.toFixed(1)}" y2="${padT + chartH}" stroke="rgba(255,255,255,0.07)" stroke-width="1"/>`;
                const hourTime = new Date(startTime.getTime() + minuteOffset * 60000);
                const label = hasWarStart && totalHours > 24
                    ? `${String(hourTime.getUTCDate()).padStart(2, '0')}/${String(hourTime.getUTCMonth() + 1).padStart(2, '0')} ${String(hourTime.getUTCHours()).padStart(2, '0')}h`
                    : String(hourTime.getUTCHours()).padStart(2, '0') + 'h';
                if (h % labelStep === 0) {
                    hourLabels += `<text x="${x.toFixed(1)}" y="${svgH - 3}" fill="#718096" font-size="9" text-anchor="middle">${label}</text>`;
                }
            }
            let yLabels = '';
            const ySteps = 4;
            for (let i = 0; i <= ySteps; i++) {
                const val = Math.round((maxVal / ySteps) * i);
                const y = padT + chartH - (i / ySteps) * chartH;
                yLabels += `<text x="${padL - 5}" y="${y + 3}" fill="#718096" font-size="9" text-anchor="end">${val}</text>`;
                if (i > 0) {
                    yLabels += `<line x1="${padL}" y1="${y}" x2="${padL + chartW}" y2="${y}" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>`;
                }
            }
            const savedZoom = parseInt(String(StorageUtil.get('catScriptZoomGraph', '100') || '100'), 10) || 100;
            let activityLabel;
            if (warInPreparation) {
                activityLabel = `<span style="color: #cbd5e0; font-weight: 600;">Call Activity (last 24h)</span><span id="war-countdown" style="color: #ffa726; font-weight: 600; font-size: 12px; margin-left: 8px;"></span>`;
            }
            else if (hasWarStart) {
                activityLabel = `<span style="color: #cbd5e0; font-weight: 600;">Call Activity (since war start)</span><span style="color: #718096; font-size: 11px; margin-left: 8px;">Calls per minute (TCT)</span>`;
            }
            else {
                activityLabel = `<span style="color: #cbd5e0; font-weight: 600;">Call Activity (last 24h)</span><span style="color: #718096; font-size: 11px; margin-left: 8px;">Calls per minute (TCT)</span>`;
            }
            graphHtml = `
            <div style="margin-top: 16px; padding: 10px; background: rgba(102, 126, 234, 0.08); border-left: 3px solid #667eea; border-radius: 4px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between;">
                <div>
                    ${activityLabel}
                </div>
                <div style="display: flex; align-items: center; gap: 6px;">
                    <button id="graph-zoom-out" style="width: 24px; height: 24px; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; background: rgba(255,255,255,0.05); color: #cbd5e0; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1;">-</button>
                    <span id="graph-zoom-label" style="color: #718096; font-size: 11px; min-width: 36px; text-align: center;">${savedZoom}%</span>
                    <button id="graph-zoom-in" style="width: 24px; height: 24px; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; background: rgba(255,255,255,0.05); color: #cbd5e0; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; line-height: 1;">+</button>
                </div>
            </div>
            <div id="call-activity-graph" style="position: relative; background: #2d3748; border-radius: 6px; padding: 8px; overflow-x: auto;">
                <svg id="call-activity-svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}" style="display: block; width: ${savedZoom}%; height: auto;">
                    ${gridLines}
                    ${yLabels}
                    <line x1="${padL}" y1="${padT + chartH}" x2="${padL + chartW}" y2="${padT + chartH}" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>
                    <line x1="${padL}" y1="${padT}" x2="${padL}" y2="${padT + chartH}" stroke="rgba(255,255,255,0.15)" stroke-width="1"/>
                    <polygon points="${callAreaPts}" fill="rgba(154, 230, 180, 0.15)"/>
                    <polyline points="${toPoints(callSlots)}" fill="none" stroke="#9ae6b4" stroke-width="1.5" stroke-linejoin="round"/>
                    ${hourLabels}
                    <line id="graph-hover-line" x1="0" y1="${padT}" x2="0" y2="${padT + chartH}" stroke="rgba(255,255,255,0.4)" stroke-width="1" style="display:none;pointer-events:none;"/>
                </svg>
            </div>
            <div style="margin-top: 6px; display: flex; align-items: center; gap: 16px; font-size: 10px; color: #718096;">
                <span style="display: flex; align-items: center; gap: 4px;">
                    <span style="display: inline-block; width: 16px; height: 2px; background: #9ae6b4;"></span> Calls
                </span>
            </div>
        `;
        }
        let warInfoLabel = '';
        if (warHasStarted && warWarStart) {
            const ws = new Date(warWarStart);
            const pad = (n) => String(n).padStart(2, '0');
            warInfoLabel = `War started: ${pad(ws.getUTCDate())}/${pad(ws.getUTCMonth() + 1)} ${pad(ws.getUTCHours())}:${pad(ws.getUTCMinutes())} TCT`;
        }
        else if (warInPreparation && warWarStart) {
            const ws = new Date(warWarStart);
            const pad = (n) => String(n).padStart(2, '0');
            warInfoLabel = `War starts: ${pad(ws.getUTCDate())}/${pad(ws.getUTCMonth() + 1)} ${pad(ws.getUTCHours())}:${pad(ws.getUTCMinutes())} TCT`;
        }
        let leaderboardHtml = '';
        if (leaderboardData && leaderboardData.success && leaderboardData.leaderboard && leaderboardData.leaderboard.length > 0) {
            const lb = leaderboardData.leaderboard;
            const totalCallsAll = lb.reduce((sum, r) => sum + parseInt(String(r.total_calls || 0)), 0);
            const lbRows = lb.map((r, i) => {
                const rank = i + 1;
                const callsPct = totalCallsAll > 0 ? Math.round((parseInt(String(r.total_calls || 0)) / totalCallsAll) * 100) : 0;
                return `<tr style="border-bottom: 1px solid rgba(255,255,255,0.05);">
                <td style="padding: 6px 10px; text-align: center; color: #718096;">${rank}</td>
                <td style="padding: 6px 10px; color: #e2e8f0; font-weight: 500;">${this._esc(r.caller_name)}</td>
                <td style="padding: 6px 10px; text-align: center; color: #9ae6b4; font-weight: 600;">${r.total_calls}</td>
                <td style="padding: 6px 10px; text-align: center;">
                    <div style="background: rgba(255,255,255,0.1); border-radius: 3px; height: 6px; width: 60px; display: inline-block; vertical-align: middle;">
                        <div style="background: #9ae6b4; border-radius: 3px; height: 100%; width: ${callsPct}%;"></div>
                    </div>
                    <span style="color: #718096; font-size: 10px; margin-left: 4px;">${callsPct}%</span>
                </td>
            </tr>`;
            }).join('');
            leaderboardHtml = `
            <div id="leaderboard-dropdown-header" style="margin: 12px 0 0 0; padding: 10px 12px; background: rgba(154, 230, 180, 0.1); border-left: 3px solid #9ae6b4; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none;">
                <div>
                    <span style="color: #9ae6b4; font-weight: 600;">Call Leaderboard</span>
                    <span style="color: #718096; font-size: 11px; margin-left: 8px;">${totalCallsAll} total calls${warInfoLabel ? ` &middot; ${warInfoLabel}` : ''}</span>
                </div>
                <span id="leaderboard-dropdown-arrow" style="color: #718096; font-size: 14px; transition: transform 0.2s;">&#9660;</span>
            </div>
            <div id="leaderboard-dropdown-content" style="display: none; margin-top: 0; border: 1px solid rgba(154, 230, 180, 0.1); border-top: none; border-radius: 0 0 4px 4px; overflow: hidden;">
                <table style="width: 100%; border-collapse: collapse; font-size: 12px;">
                    <thead>
                        <tr style="border-bottom: 2px solid rgba(255,255,255,0.1);">
                            <th style="padding: 8px 10px; text-align: center; color: #90caf9; font-weight: 600; width: 30px;">#</th>
                            <th style="padding: 8px 10px; text-align: left; color: #90caf9; font-weight: 600;">Caller</th>
                            <th style="padding: 8px 10px; text-align: center; color: #9ae6b4; font-weight: 600;">Calls</th>
                            <th style="padding: 8px 10px; text-align: center; color: #90caf9; font-weight: 600;">Share</th>
                        </tr>
                    </thead>
                    <tbody>${lbRows}</tbody>
                </table>
            </div>
        `;
        }
        container.innerHTML = `
        ${graphHtml}
        ${leaderboardHtml}
        <div id="users-dropdown-header" style="margin: 12px 0 0 0; padding: 10px 12px; background: rgba(102, 126, 234, 0.15); border-left: 3px solid #667eea; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; user-select: none;">
            <div>
                <span style="color: #86B202; font-weight: 600;">${scriptCount}</span>
                <span style="color: #cbd5e0; font-weight: 600;">/ ${totalCount} members using the script</span>
            </div>
            <span id="users-dropdown-arrow" style="color: #718096; font-size: 14px; transition: transform 0.2s;">&#9660;</span>
        </div>
        <div id="users-dropdown-content" style="display: none; margin-top: 0; border: 1px solid rgba(102, 126, 234, 0.15); border-top: none; border-radius: 0 0 4px 4px; overflow: hidden;">
            <table style="width: 100%; border-collapse: collapse; font-size: 12px;">
                <thead>
                    <tr style="border-bottom: 2px solid rgba(255,255,255,0.1);">
                        <th style="padding: 8px 10px; text-align: left; color: #90caf9; font-weight: 600;">Name</th>
                        <th style="padding: 8px 10px; text-align: center; color: #90caf9; font-weight: 600;">Lvl</th>
                        <th style="padding: 8px 10px; text-align: center; color: #90caf9; font-weight: 600;">Script</th>
                    </tr>
                </thead>
                <tbody>${rows}</tbody>
            </table>
        </div>
    `;
        loader.style.display = 'none';
        container.style.display = 'block';
        if (activityData && activityData.success && startTime && callSlots) {
            const graphWrapper = document.getElementById('call-activity-graph');
            const svg = document.getElementById('call-activity-svg');
            const hoverLine = document.getElementById('graph-hover-line');
            let tooltipEl = document.getElementById('call-activity-tooltip');
            if (tooltipEl)
                tooltipEl.remove();
            tooltipEl = document.createElement('div');
            tooltipEl.id = 'call-activity-tooltip';
            tooltipEl.style.cssText = 'display:none; position:fixed; background:#1a202c; border:1px solid rgba(255,255,255,0.2); border-radius:6px; padding:8px 10px; font-size:11px; color:#e2e8f0; pointer-events:none; z-index:100000; max-width:280px; box-shadow:0 4px 12px rgba(0,0,0,0.5); white-space:nowrap;';
            document.body.appendChild(tooltipEl);
            if (graphWrapper && hoverLine && svg) {
                const _svgW = 700, _padL = 35, _padR = 10, _chartW = _svgW - _padL - _padR;
                graphWrapper.style.cursor = 'crosshair';
                graphWrapper.addEventListener('mousemove', (e) => {
                    const svgRect = svg.getBoundingClientRect();
                    const scaleX = _svgW / svgRect.width;
                    const mouseXInSvg = (e.clientX - svgRect.left) * scaleX;
                    const _lastSlot = totalMinutes - 1 || 1;
                    const slotIndex = Math.max(0, Math.min(_lastSlot, Math.round(((mouseXInSvg - _padL) / _chartW) * _lastSlot)));
                    const lineX = _padL + (slotIndex / _lastSlot) * _chartW;
                    hoverLine.setAttribute('x1', lineX.toFixed(1));
                    hoverLine.setAttribute('x2', lineX.toFixed(1));
                    hoverLine.style.display = '';
                    const slotTime = new Date(Math.floor((startTime.getTime() + slotIndex * 60000) / 60000) * 60000);
                    const timeStr = String(slotTime.getUTCHours()).padStart(2, '0') + ':' + String(slotTime.getUTCMinutes()).padStart(2, '0') + ' TCT';
                    const calls = callSlots[slotIndex];
                    const evtKey = slotTime.toISOString();
                    const evts = (activityData.events && activityData.events[evtKey]) || [];
                    let html = `<div style="font-weight:600;color:#cbd5e0;margin-bottom:4px;">${timeStr}</div>`;
                    html += `<div style="color:#9ae6b4;">Calls: ${calls}</div>`;
                    if (evts.length > 0) {
                        const callerStats = {};
                        evts.forEach((evt) => {
                            const key = evt.caller;
                            if (!callerStats[key])
                                callerStats[key] = { calls: 0, uncalls: 0 };
                            if (evt.type === 'call')
                                callerStats[key].calls++;
                            else
                                callerStats[key].uncalls++;
                        });
                        html += `<div style="border-top:1px solid rgba(255,255,255,0.1);margin-top:4px;padding-top:4px;">`;
                        Object.entries(callerStats).forEach(([name, stats]) => {
                            const parts = [];
                            if (stats.calls > 0)
                                parts.push(`<span style="color:#9ae6b4;">${stats.calls} call${stats.calls > 1 ? 's' : ''}</span>`);
                            if (stats.uncalls > 0)
                                parts.push(`<span style="color:#fc8181;">${stats.uncalls} uncall${stats.uncalls > 1 ? 's' : ''}</span>`);
                            html += `<div style="font-size:10px;color:#e2e8f0;">${this._esc(name)}: ${parts.join(', ')}</div>`;
                        });
                        html += `</div>`;
                    }
                    tooltipEl.innerHTML = html;
                    tooltipEl.style.display = 'block';
                    tooltipEl.style.left = (e.clientX + 15) + 'px';
                    tooltipEl.style.top = (e.clientY - 20) + 'px';
                });
                graphWrapper.addEventListener('mouseleave', () => {
                    tooltipEl.style.display = 'none';
                    hoverLine.style.display = 'none';
                });
            }
        }
        const lbHeader = document.getElementById('leaderboard-dropdown-header');
        const lbContent = document.getElementById('leaderboard-dropdown-content');
        const lbArrow = document.getElementById('leaderboard-dropdown-arrow');
        if (lbHeader && lbContent && lbArrow) {
            lbHeader.addEventListener('click', () => {
                const isOpen = lbContent.style.display !== 'none';
                lbContent.style.display = isOpen ? 'none' : 'block';
                lbArrow.style.transform = isOpen ? '' : 'rotate(180deg)';
            });
        }
        const dropdownHeader = document.getElementById('users-dropdown-header');
        const dropdownContent = document.getElementById('users-dropdown-content');
        const dropdownArrow = document.getElementById('users-dropdown-arrow');
        if (dropdownHeader && dropdownContent && dropdownArrow) {
            dropdownHeader.addEventListener('click', () => {
                const isOpen = dropdownContent.style.display !== 'none';
                dropdownContent.style.display = isOpen ? 'none' : 'block';
                dropdownArrow.style.transform = isOpen ? '' : 'rotate(180deg)';
            });
        }
        const zoomIn = document.getElementById('graph-zoom-in');
        const zoomOut = document.getElementById('graph-zoom-out');
        const zoomLabel = document.getElementById('graph-zoom-label');
        const zoomSvg = document.getElementById('call-activity-svg');
        if (zoomIn && zoomOut && zoomLabel && zoomSvg) {
            const applyZoom = (delta) => {
                let current = parseInt(String(StorageUtil.get('catScriptZoomGraph', '100') || '100'), 10) || 100;
                current = Math.max(100, Math.min(500, current + delta));
                StorageUtil.set('catScriptZoomGraph', String(current));
                zoomSvg.style.width = current + '%';
                zoomLabel.textContent = current + '%';
            };
            zoomIn.addEventListener('click', () => applyZoom(50));
            zoomOut.addEventListener('click', () => applyZoom(-50));
        }
        if (savedLbOpen) {
            const lbC = document.getElementById('leaderboard-dropdown-content');
            const lbA = document.getElementById('leaderboard-dropdown-arrow');
            if (lbC)
                lbC.style.display = 'block';
            if (lbA)
                lbA.style.transform = 'rotate(180deg)';
        }
        if (savedUsersOpen) {
            const usC = document.getElementById('users-dropdown-content');
            const usA = document.getElementById('users-dropdown-arrow');
            if (usC)
                usC.style.display = 'block';
            if (usA)
                usA.style.transform = 'rotate(180deg)';
        }
        const countdownEl = document.getElementById('war-countdown');
        if (countdownEl && warInPreparation) {
            const warTarget = warStartMs;
            const updateCountdown = () => {
                const diff = warTarget - Date.now();
                if (diff <= 0) {
                    countdownEl.textContent = 'War started!';
                    countdownEl.style.color = '#9ae6b4';
                    if (this._warCountdownInterval) {
                        clearInterval(this._warCountdownInterval);
                        this._warCountdownInterval = null;
                    }
                    setTimeout(() => this._fetchUsersData(), 2000);
                    return;
                }
                const h = Math.floor(diff / 3600000);
                const m = Math.floor((diff % 3600000) / 60000);
                const s = Math.floor((diff % 60000) / 1000);
                const pad = (n) => String(n).padStart(2, '0');
                countdownEl.textContent = `War start in ${pad(h)}:${pad(m)}:${pad(s)}`;
            };
            updateCountdown();
            this._warCountdownInterval = setInterval(updateCountdown, 1000);
        }
    }

    function setupSettingsTabHandlers() {
        const saveBtn = document.getElementById('tab-setting-save');
        const clearCacheBtn = document.getElementById('tab-setting-clear-cache');
        const apiKeyInput = document.getElementById('tab-setting-torn-apikey');
        const validationDiv = document.getElementById('tab-setting-api-validation');
        const enhancer = this;
        if (!saveBtn || !apiKeyInput)
            return;
        const autoSortCheckbox = document.getElementById('tab-setting-auto-sort');
        if (autoSortCheckbox) {
            autoSortCheckbox.checked = String(StorageUtil.get('cat_auto_sort', 'true')) === 'true';
            autoSortCheckbox.onchange = () => {
                StorageUtil.set('cat_auto_sort', autoSortCheckbox.checked ? 'true' : 'false');
            };
        }
        const levelBarCheckbox = document.getElementById('tab-setting-level-bar');
        if (levelBarCheckbox) {
            levelBarCheckbox.onchange = () => {
                StorageUtil.set('cat_show_level_bar', levelBarCheckbox.checked ? 'true' : 'false');
                const wrap = document.getElementById('cat-level-progress-wrap');
                if (wrap)
                    wrap.style.display = levelBarCheckbox.checked ? '' : 'none';
            };
        }
        const nameColorsCheckbox = document.getElementById('tab-setting-name-colors');
        if (nameColorsCheckbox) {
            nameColorsCheckbox.onchange = () => {
                StorageUtil.set('cat_name_colors', nameColorsCheckbox.checked ? 'true' : 'false');
                if (nameColorsCheckbox.checked) {
                    document.body.classList.remove('cat-no-name-colors');
                }
                else {
                    document.body.classList.add('cat-no-name-colors');
                }
            };
        }
        const callerStatusCheckbox = document.getElementById('tab-setting-caller-status');
        if (callerStatusCheckbox) {
            callerStatusCheckbox.onchange = () => {
                StorageUtil.set('cat_caller_status_color', callerStatusCheckbox.checked ? 'true' : 'false');
                // Invalidate call button states to force re-render
                document.querySelectorAll('.call-button').forEach(btn => {
                    delete btn.dataset.callState;
                    if (!callerStatusCheckbox.checked)
                        btn.style.removeProperty('color');
                });
            };
        }
        // Stat columns checkboxes
        const colBspCb = document.getElementById('tab-setting-col-bsp');
        const colFFCb = document.getElementById('tab-setting-col-ff');
        const colTSCb = document.getElementById('tab-setting-col-ts');
        const colCbs = [colBspCb, colFFCb, colTSCb];
        const colKeys = ['cat_col_show_bsp', 'cat_col_show_ff', 'cat_col_show_ts'];
        colCbs.forEach((cb, i) => {
            if (!cb)
                return;
            cb.onchange = () => {
                const checkedCount = colCbs.filter(c => c?.checked).length;
                if (checkedCount === 0) {
                    cb.checked = true;
                    return;
                } // enforce min 1
                StorageUtil.set(colKeys[i], cb.checked ? 'true' : 'false');
                // Apply column visibility immediately without page reload
                const showBsp = colBspCb?.checked ?? true;
                const showFF = colFFCb?.checked ?? true;
                const showTS = colTSCb?.checked ?? true;
                // For each faction list on the page, check if current col is now hidden
                document.querySelectorAll('.bsp-header').forEach(header => {
                    // bsp-header is in the header row, not inside the ul — find the faction container then the ul
                    const factionContainer = header.closest('.enemy-faction, .your-faction, [class*="tabMenuCont"]');
                    if (!factionContainer)
                        return;
                    const factionList = factionContainer.querySelector('ul, ol');
                    if (!factionList)
                        return;
                    const currentCol = header.getAttribute('data-col') || 'bsp';
                    const colAllowed = { bsp: showBsp, ff: showFF, ts: showTS };
                    // Determine active col: switch to first allowed if current is now disabled
                    const activeCol = colAllowed[currentCol] ? currentCol
                        : (showBsp ? 'bsp' : showFF ? 'ff' : showTS ? 'ts' : 'bsp');
                    // Always persist the preference and update the header
                    const prefKey = factionList.closest('.enemy-faction') ? 'cat_stats_column_enemy' : 'cat_stats_column_your';
                    StorageUtil.set(prefKey, activeCol);
                    header.setAttribute('data-col', activeCol);
                    updateStatsHeaderText(header);
                    // Update column visibility
                    const root = factionList.closest('.enemy-faction') || factionList.closest('[class*="tabMenuCont"]') || factionList.closest('.your-faction') || document;
                    root.querySelectorAll('.bsp-column').forEach(el => el.style.setProperty('display', activeCol === 'bsp' ? 'inline-block' : 'none', 'important'));
                    root.querySelectorAll('.ff-column').forEach(el => el.style.setProperty('display', activeCol === 'ff' ? 'inline-flex' : 'none', 'important'));
                    root.querySelectorAll('.ts-column').forEach(el => el.style.setProperty('display', activeCol === 'ts' ? 'inline-flex' : 'none', 'important'));
                    // If leaving TS, remove the flex layout forced on li rows by addTSColumn
                    if (activeCol !== 'ts') {
                        root.querySelectorAll('li[data-cat-flex-row]').forEach(li => {
                            li.style.removeProperty('display');
                            li.style.removeProperty('align-items');
                            delete li.dataset.catFlexRow;
                        });
                    }
                });
            };
        });
        // CD column toggles
        const cdEnergyCb = document.getElementById('tab-setting-cd-energy');
        const cdDrugCb = document.getElementById('tab-setting-cd-drug');
        const cdMedCb = document.getElementById('tab-setting-cd-med');
        [cdEnergyCb, cdDrugCb, cdMedCb].forEach((cb, i) => {
            if (!cb)
                return;
            const keys = ['cat_cd_show_energy', 'cat_cd_show_drug', 'cat_cd_show_med'];
            const parts = ['cd-energy', 'cd-drug', 'cd-med'];
            const imgParts = ['cd-img-drug', 'cd-img-med'];
            cb.onchange = () => {
                StorageUtil.set(keys[i], cb.checked ? 'true' : 'false');
                // Update visibility immediately
                document.querySelectorAll(`.cd-column .${parts[i]}`).forEach(el => {
                    el.style.display = cb.checked ? '' : 'none';
                });
                // Also hide/show the energy icon
                if (i === 0) {
                    document.querySelectorAll('.cd-column .cd-icon-energy').forEach(el => {
                        el.style.display = cb.checked ? '' : 'none';
                    });
                }
                // Drug/Med icons (index 1 and 2)
                if (i === 1 || i === 2) {
                    document.querySelectorAll(`.cd-column .${imgParts[i - 1]}`).forEach(el => {
                        el.style.display = cb.checked ? '' : 'none';
                    });
                }
            };
        });
        const tornstatsKeyInput = document.getElementById('tab-setting-tornstats-apikey');
        const tornstatsValidationDiv = document.getElementById('tab-setting-tornstats-validation');
        const ffscouterKeyInput = document.getElementById('tab-setting-ffscouter-apikey');
        const ffscouterValidationDiv = document.getElementById('tab-setting-ffscouter-validation');
        // Desktop notification block — inject via DOM for non-PDA
        const _isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        if (!_isPDA && !document.getElementById('tab-setting-pda-notif')) {
            const attackNewtabBlock = document.getElementById('tab-setting-attack-newtab')?.closest('div[style]');
            const insertTarget = attackNewtabBlock?.parentElement || null;
            if (insertTarget) {
                const notifBlock = document.createElement('div');
                notifBlock.style.cssText = 'padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px; margin-top: 8px;';
                const masterChecked = String(StorageUtil.get('cat_pda_notifications', 'true')) === 'true';
                const hospChecked = String(StorageUtil.get('cat_pda_notif_hosp', 'true')) === 'true';
                const callerChecked = String(StorageUtil.get('cat_pda_notif_caller_hosp', 'true')) === 'true';
                const leadVal = String(StorageUtil.get('cat_pda_notif_lead', '20'));
                notifBlock.innerHTML = '<label style="display:flex;align-items:center;gap:8px;cursor:pointer;color:#ccc;font-size:12px;font-weight:500;"><input type="checkbox" id="tab-setting-pda-notif" ' + (masterChecked ? 'checked' : '') + ' style="width:14px;height:14px;cursor:pointer;">Desktop Notifications</label><div id="cat-pda-notif-options" style="display:flex;flex-direction:column;gap:6px;margin:8px 0 0 22px;' + (!masterChecked ? 'opacity:0.4;pointer-events:none;' : '') + '"><label style="display:flex;align-items:center;gap:8px;cursor:pointer;color:#aaa;font-size:11px;"><input type="checkbox" id="tab-setting-pda-notif-hosp" ' + (hospChecked ? 'checked' : '') + ' style="width:13px;height:13px;cursor:pointer;">Target leaving hospital</label><div style="display:flex;align-items:center;gap:8px;margin-left:21px;"><span style="font-size:10px;color:#666;">Alert:</span><select id="tab-setting-pda-lead" style="padding:2px 4px;background:rgba(0,0,0,0.3);border:1px solid #555;border-radius:3px;color:#ddd;font-size:10px;cursor:pointer;"><option value="0" ' + (leadVal === '0' ? 'selected' : '') + '>At Okay</option><option value="10" ' + (leadVal === '10' ? 'selected' : '') + '>10s before</option><option value="20" ' + (leadVal === '20' ? 'selected' : '') + '>20s before</option><option value="30" ' + (leadVal === '30' ? 'selected' : '') + '>30s before</option><option value="60" ' + (leadVal === '60' ? 'selected' : '') + '>1 min before</option></select></div><label style="display:flex;align-items:center;gap:8px;cursor:pointer;color:#aaa;font-size:11px;"><input type="checkbox" id="tab-setting-pda-notif-caller-hosp" ' + (callerChecked ? 'checked' : '') + ' style="width:13px;height:13px;cursor:pointer;">You got hospitalized (med out alert)</label></div><p style="margin:4px 0 0 22px;font-size:9px;color:#555;">If alert is set before Okay, you will also get a confirmation when the target is out</p>';
                if (attackNewtabBlock) {
                    attackNewtabBlock.after(notifBlock);
                }
                else {
                    insertTarget.appendChild(notifBlock);
                }
            }
        }
        // PDA notification master toggle
        const pdaNotifMaster = document.getElementById('tab-setting-pda-notif');
        const pdaNotifOptions = document.getElementById('cat-pda-notif-options');
        if (pdaNotifMaster) {
            pdaNotifMaster.onchange = () => {
                StorageUtil.set('cat_pda_notifications', pdaNotifMaster.checked ? 'true' : 'false');
                if (pdaNotifOptions) {
                    pdaNotifOptions.style.opacity = pdaNotifMaster.checked ? '1' : '0.4';
                    pdaNotifOptions.style.pointerEvents = pdaNotifMaster.checked ? 'auto' : 'none';
                }
            };
        }
        // PDA notification toggles (individual)
        const pdaNotifHospCheckbox = document.getElementById('tab-setting-pda-notif-hosp');
        if (pdaNotifHospCheckbox) {
            pdaNotifHospCheckbox.onchange = () => {
                StorageUtil.set('cat_pda_notif_hosp', pdaNotifHospCheckbox.checked ? 'true' : 'false');
            };
        }
        const pdaNotifCallerHospCheckbox = document.getElementById('tab-setting-pda-notif-caller-hosp');
        if (pdaNotifCallerHospCheckbox) {
            pdaNotifCallerHospCheckbox.onchange = () => {
                StorageUtil.set('cat_pda_notif_caller_hosp', pdaNotifCallerHospCheckbox.checked ? 'true' : 'false');
            };
        }
        const pdaLeadSelect = document.getElementById('tab-setting-pda-lead');
        if (pdaLeadSelect) {
            pdaLeadSelect.onchange = () => {
                StorageUtil.set('cat_pda_notif_lead', pdaLeadSelect.value);
            };
        }
        const pdaPerfCheckbox = document.getElementById('tab-setting-pda-perf');
        if (pdaPerfCheckbox) {
            pdaPerfCheckbox.onchange = () => {
                StorageUtil.set('cat_pda_perf_mode', pdaPerfCheckbox.checked ? 'true' : 'false');
                setTimeout(() => location.reload(), 1000);
            };
        }
        // PDA Performance score live update
        const perfScoreEl = document.getElementById('cat-pda-perf-score');
        const perfBarEl = document.getElementById('cat-pda-perf-bar');
        const perfAvgEl = document.getElementById('cat-pda-perf-avg');
        if (perfScoreEl && perfBarEl && perfAvgEl) {
            // Populate device info line (once)
            const perfDeviceEl = document.getElementById('cat-pda-perf-device');
            if (perfDeviceEl) {
                const tierLabel = { low: 'Low-end', mid: 'Mid-range', high: 'High-end' }[pdaDevice.tier];
                const parts = [pdaDevice.model || 'Unknown'];
                if (pdaDevice.cores > 0)
                    parts.push(`${pdaDevice.cores} cores`);
                if (pdaDevice.memoryGB > 0)
                    parts.push(`${pdaDevice.memoryGB}GB`);
                parts.push(tierLabel);
                perfDeviceEl.textContent = parts.join(' · ');
            }
            // Toggle tooltip on ? click
            const perfHelpBtn = document.getElementById('cat-pda-perf-help');
            const perfTooltip = document.getElementById('cat-pda-perf-tooltip');
            if (perfHelpBtn && perfTooltip) {
                perfHelpBtn.onclick = () => {
                    perfTooltip.style.display = perfTooltip.style.display === 'none' ? 'block' : 'none';
                };
            }
            const updatePerfScore = () => {
                const times = pdaMetrics.responseTimes;
                const elapsed = pdaMetrics._firstRecordTime > 0 ? Date.now() - pdaMetrics._firstRecordTime : 0;
                if (times.length < 3 || elapsed < 15000) {
                    // Need at least 15s of data for stable score
                    perfScoreEl.textContent = '...';
                    perfScoreEl.style.color = '#666';
                    perfBarEl.style.width = '0%';
                    const secsLeft = Math.max(0, Math.ceil((15000 - elapsed) / 1000));
                    perfAvgEl.textContent = times.length === 0 ? 'Measuring...' : `Measuring... ${secsLeft}s`;
                    return;
                }
                const score = pdaMetrics.getScore();
                const avg = Math.round(times.reduce((a, b) => a + b, 0) / times.length);
                const windowSec = Math.max(1, (Date.now() - pdaMetrics._windowStart) / 1000);
                const reqPerSec = pdaMetrics.totalRequests / windowSec;
                // Color based on score
                let color;
                if (score >= 80)
                    color = '#68d391'; // green
                else if (score >= 60)
                    color = '#f6e05e'; // yellow
                else if (score >= 40)
                    color = '#ed8936'; // orange
                else
                    color = '#fc8181'; // red
                perfScoreEl.textContent = String(score);
                perfScoreEl.style.color = color;
                perfBarEl.style.width = score + '%';
                perfBarEl.style.background = color;
                const domPct = Math.round(pdaMetrics.getDomLoad() * 100);
                perfAvgEl.textContent = `${avg}ms · ${reqPerSec.toFixed(1)}/s · DOM ${domPct}%`;
            };
            updatePerfScore();
            setInterval(updatePerfScore, 1000);
        }
        const attackNewTabCheckbox = document.getElementById('tab-setting-attack-newtab');
        if (attackNewTabCheckbox) {
            attackNewTabCheckbox.onchange = () => {
                StorageUtil.set('cat_attack_new_tab', attackNewTabCheckbox.checked ? 'true' : 'false');
            };
        }
        // BS column toggle (War Helper) — shown if cached BS access is true
        const bsToggleContainer = document.getElementById('cat-bs-toggle-container');
        if (bsToggleContainer) {
            const hasBsAccess = String(StorageUtil.get('cat_bs_access', 'false')) === 'true';
            if (hasBsAccess) {
                const showBs = String(StorageUtil.get('cat_show_warhelper_bs', 'true')) === 'true';
                bsToggleContainer.innerHTML = `
                <div style="margin-bottom: 12px; padding: 8px 10px; background: rgba(255,255,255,0.03); border: 1px solid #444; border-radius: 3px;">
                    <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; color: #ccc; font-size: 12px; font-weight: 500;">
                        <input type="checkbox" id="tab-setting-show-bs" ${showBs ? 'checked' : ''} style="width: 14px; height: 14px; cursor: pointer;">
                        Show Battle Stats (BS)
                    </label>
                    <p style="margin: 3px 0 0 22px; font-size: 10px; color: #666;">Show/hide the War Helper BS column</p>
                </div>
            `;
                const bsCheckbox = document.getElementById('tab-setting-show-bs');
                if (bsCheckbox) {
                    bsCheckbox.onchange = () => {
                        StorageUtil.set('cat_show_warhelper_bs', bsCheckbox.checked ? 'true' : 'false');
                        if (bsCheckbox.checked) {
                            document.body.classList.remove('cat-hide-warhelper-bs');
                        }
                        else {
                            document.body.classList.add('cat-hide-warhelper-bs');
                        }
                    };
                }
            }
        }
        const travelModeSelect = document.getElementById('tab-setting-travel-mode');
        if (travelModeSelect) {
            travelModeSelect.onchange = () => {
                StorageUtil.set('cat_travel_eta_mode', travelModeSelect.value);
            };
        }
        const rsPicker = document.getElementById('cat-rs-picker');
        if (rsPicker) {
            const curRS = String(StorageUtil.get('cat_row_style', 'basic') || 'basic');
            const allOpts = rsPicker.querySelectorAll('.cat-rs-opt');
            const highlight = (val) => {
                allOpts.forEach(opt => {
                    const active = opt.dataset.rs === val;
                    opt.style.background = active ? 'rgba(102,126,234,0.25)' : 'transparent';
                    opt.style.color = active ? '#ddd' : '#888';
                    opt.style.fontWeight = active ? '600' : '400';
                });
            };
            highlight(curRS);
            allOpts.forEach(opt => {
                opt.onclick = () => {
                    const val = opt.dataset.rs || 'basic';
                    StorageUtil.set('cat_row_style', val);
                    document.body.classList.remove('cat-row-colors', 'cat-row-bar', 'cat-row-contrast');
                    if (val !== 'basic') {
                        document.body.classList.add(`cat-row-${val}`);
                    }
                    highlight(val);
                };
            });
        }
        const bsPicker = document.getElementById('cat-bs-picker');
        if (bsPicker) {
            const curBS = String(StorageUtil.get('cat_btn_style', 'gradient') || 'gradient');
            const allBsOpts = bsPicker.querySelectorAll('.cat-bs-opt');
            const highlightBs = (val) => {
                allBsOpts.forEach(opt => {
                    const active = opt.dataset.bs === val;
                    opt.style.background = active ? 'rgba(102,126,234,0.25)' : 'transparent';
                    opt.style.color = active ? '#ddd' : '#888';
                    opt.style.fontWeight = active ? '600' : '400';
                });
            };
            highlightBs(curBS);
            allBsOpts.forEach(opt => {
                opt.onclick = () => {
                    const val = opt.dataset.bs || 'gradient';
                    StorageUtil.set('cat_btn_style', val);
                    document.body.classList.remove('cat-btn-flat');
                    if (val === 'flat') {
                        document.body.classList.add('cat-btn-flat');
                    }
                    highlightBs(val);
                };
            });
        }
        const etaTooltipCheckbox = document.getElementById('tab-setting-eta-tooltip');
        if (etaTooltipCheckbox) {
            etaTooltipCheckbox.onchange = () => {
                StorageUtil.set('cat_eta_tooltip', etaTooltipCheckbox.checked ? 'true' : 'false');
            };
        }
        const etaColorInput = document.getElementById('tab-setting-eta-color');
        if (etaColorInput) {
            etaColorInput.oninput = () => {
                StorageUtil.set('cat_eta_color', etaColorInput.value);
                document.querySelectorAll('.cat-travel-eta').forEach(el => {
                    el.style.color = etaColorInput.value;
                });
            };
        }
        const etaColorReset = document.getElementById('tab-setting-eta-color-reset');
        if (etaColorReset && etaColorInput) {
            etaColorReset.onclick = () => {
                const defaultColor = '#FFB74D';
                etaColorInput.value = defaultColor;
                StorageUtil.remove('cat_eta_color');
                document.querySelectorAll('.cat-travel-eta').forEach(el => {
                    el.style.color = defaultColor;
                });
            };
        }
        if (clearCacheBtn) {
            clearCacheBtn.onclick = () => {
                if (confirm('This will clear all cached data. Continue?')) {
                    // Delete user from DB so re-registration creates a clean account
                    const token = StorageUtil.get('cat_auth_token', null);
                    const serverUrl = StorageUtil.get('cat_server_url', null) || 'https://cat-script.com';
                    if (token) {
                        const enhancer = window.FactionWarEnhancer;
                        const req = enhancer?.apiManager
                            ? enhancer.apiManager.httpRequest(`${serverUrl}/api/user/self`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } })
                            : fetch(`${serverUrl}/api/user/self`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } });
                        req.catch(() => { });
                    }
                    // Auth / session
                    StorageUtil.remove('cat_auth_token');
                    StorageUtil.remove('cat_api_key_script');
                    StorageUtil.remove('cat_user_info');
                    StorageUtil.remove('cat_user_faction_id');
                    StorageUtil.remove('cat_enemy_faction_id');
                    StorageUtil.remove('cat_activation_cached');
                    StorageUtil.remove('cat_is_admin_cached');
                    StorageUtil.remove('cat_bs_access');
                    // Cached runtime data
                    StorageUtil.remove('cat_calls_cache');
                    StorageUtil.remove('cat_cached_calls');
                    StorageUtil.remove('cat_online_statuses');
                    StorageUtil.remove('cat_hosp_times');
                    StorageUtil.remove('cat_travel_data');
                    StorageUtil.remove('cat_enemy_chain');
                    StorageUtil.remove('cat_has_active_war');
                    StorageUtil.remove('cat_attacking_signal');
                    StorageUtil.remove('cat_tactical_marker_signal');
                    apiKeyInput.value = '';
                    const settingsBtn = document.getElementById('settings-tab-btn');
                    if (settingsBtn) {
                        settingsBtn.classList.add('blinking');
                    }
                    if (validationDiv) {
                        validationDiv.textContent = 'Cache cleared!';
                        validationDiv.style.color = '#68d391';
                        validationDiv.style.display = 'block';
                    }
                    setTimeout(() => {
                        location.reload();
                    }, 2000);
                }
            };
        }
        const nameFontSelect = document.getElementById('tab-setting-name-font');
        if (nameFontSelect) {
            nameFontSelect.onchange = () => {
                StorageUtil.set('cat_name_font', nameFontSelect.value);
                document.documentElement.style.setProperty('--cat-name-font', nameFontSelect.value || 'inherit');
                if (nameFontSelect.value)
                    loadGoogleFont(nameFontSelect.value);
            };
        }
        const bspFontSelect = document.getElementById('tab-setting-bsp-font');
        if (bspFontSelect) {
            bspFontSelect.onchange = () => {
                StorageUtil.set('cat_bsp_font', bspFontSelect.value);
                document.documentElement.style.setProperty('--cat-bsp-font', bspFontSelect.value || 'inherit');
                if (bspFontSelect.value)
                    loadGoogleFont(bspFontSelect.value);
            };
        }
        saveBtn.onclick = async () => {
            const newKey = apiKeyInput.value.trim();
            const tsKey = tornstatsKeyInput ? tornstatsKeyInput.value.trim() : '';
            const ffsKey = ffscouterKeyInput ? ffscouterKeyInput.value.trim() : '';
            if (!newKey) {
                if (validationDiv) {
                    validationDiv.style.color = '#fc8181';
                    validationDiv.textContent = 'Please enter an API key!';
                    validationDiv.style.display = 'block';
                }
                return;
            }
            saveBtn.disabled = true;
            const originalText = saveBtn.textContent;
            saveBtn.textContent = 'Validating...';
            if (validationDiv) {
                validationDiv.style.color = '#63b3ed';
                validationDiv.textContent = 'Checking Torn API key...';
                validationDiv.style.display = 'block';
            }
            if (tornstatsValidationDiv) {
                tornstatsValidationDiv.style.display = 'none';
            }
            // Run both validations in parallel
            let tornOk = false;
            let tsOk = !tsKey; // skip TS validation if field empty
            let tornProfileName = '';
            let tornErrorMsg = 'Invalid Torn API key!';
            const tornPromise = enhancer.apiManager.httpRequest(`https://api.torn.com/v2/user/profile?striptags=true&key=${newKey}`, { method: 'GET' }).then(async (response) => {
                if (!response.ok) {
                    tornErrorMsg = 'Invalid Torn API key!';
                    return;
                }
                const data = await response.json();
                if (data.profile?.name) {
                    tornOk = true;
                    tornProfileName = data.profile.name;
                    StorageUtil.set('cat_api_key_script', newKey);
                    enhancer.apiManager.torn_apikey = newKey;
                    enhancer.apiManager.playerId = String(data.profile.id || '');
                    enhancer.apiManager.playerName = data.profile.name;
                    StorageUtil.set('cat_user_info', { id: data.profile.id, name: data.profile.name, faction_id: data.profile.faction_id || null, faction_name: 'Your Faction' });
                }
                else if (data.error) {
                    const e = data.error;
                    tornErrorMsg = `Wrong key: ${e.error || 'Unknown'} (${e.code || '?'})`;
                }
                else {
                    tornErrorMsg = 'Unexpected API response';
                }
            }).catch((e) => { tornErrorMsg = e instanceof Error ? e.message : 'Request failed'; });
            let tsErrorMsg = 'Invalid TornStats API key!';
            const tsPromise = tsKey
                ? enhancer.apiManager.httpRequest(`https://www.tornstats.com/api/v2/${tsKey}`, { method: 'GET' }).then(async (response) => {
                    if (!response.ok) {
                        tsErrorMsg = 'TornStats key rejected (HTTP error)';
                        return;
                    }
                    const data = await response.json();
                    if (data?.status === false) {
                        tsErrorMsg = data?.message || 'Invalid TornStats API key';
                    }
                    else {
                        tsOk = true;
                    }
                }).catch((e) => { tsErrorMsg = e instanceof Error ? e.message : 'Request failed'; })
                : Promise.resolve();
            let ffsOk = !ffsKey;
            let ffsErrorMsg = 'Invalid FF Scouter API key!';
            const ffsPromise = ffsKey
                ? enhancer.apiManager.httpRequest(`https://ffscouter.com/api/v1/get-stats?key=${ffsKey}&targets=2353554`, { method: 'GET' }).then(async (response) => {
                    const data = await response.json();
                    // Invalid key → { code: 6, error: "Invalid API key..." }
                    // Valid key → [] or [{ player_id: ... }]
                    if (data?.error || data?.code) {
                        ffsErrorMsg = data?.error || `Error code ${data.code}`;
                    }
                    else {
                        ffsOk = true;
                    }
                }).catch((e) => { ffsErrorMsg = e instanceof Error ? e.message : 'Request failed'; })
                : Promise.resolve();
            await Promise.allSettled([tornPromise, tsPromise, ffsPromise]);
            // Show results
            let canReload = true;
            if (tornOk) {
                document.getElementById('settings-tab-btn')?.classList.remove('blinking');
                if (validationDiv) {
                    validationDiv.style.color = '#68d391';
                    validationDiv.textContent = `Torn key valid — Welcome ${tornProfileName}`;
                    validationDiv.style.display = 'block';
                }
            }
            else {
                canReload = false;
                if (validationDiv) {
                    validationDiv.style.color = '#fc8181';
                    validationDiv.textContent = tornErrorMsg;
                    validationDiv.style.display = 'block';
                }
            }
            if (tsKey) {
                if (tsOk) {
                    StorageUtil.set('cat_tornstats_api_key', tsKey);
                    if (tornstatsValidationDiv) {
                        tornstatsValidationDiv.style.color = '#68d391';
                        tornstatsValidationDiv.textContent = 'TornStats key valid';
                        tornstatsValidationDiv.style.display = 'block';
                    }
                }
                else {
                    canReload = false;
                    if (tornstatsValidationDiv) {
                        tornstatsValidationDiv.style.color = '#fc8181';
                        tornstatsValidationDiv.textContent = tsErrorMsg;
                        tornstatsValidationDiv.style.display = 'block';
                    }
                }
            }
            else {
                StorageUtil.set('cat_tornstats_api_key', '');
            }
            if (ffsKey) {
                if (ffsOk) {
                    StorageUtil.set('cat_ffscouter_api_key', ffsKey);
                    if (ffscouterValidationDiv) {
                        ffscouterValidationDiv.style.color = '#68d391';
                        ffscouterValidationDiv.textContent = 'FF Scouter key valid';
                        ffscouterValidationDiv.style.display = 'block';
                    }
                }
                else {
                    canReload = false;
                    if (ffscouterValidationDiv) {
                        ffscouterValidationDiv.style.color = '#fc8181';
                        ffscouterValidationDiv.textContent = ffsErrorMsg;
                        ffscouterValidationDiv.style.display = 'block';
                    }
                }
            }
            else {
                StorageUtil.set('cat_ffscouter_api_key', '');
            }
            saveBtn.disabled = false;
            saveBtn.textContent = originalText;
            if (canReload) {
                setTimeout(() => location.reload(), 2000);
            }
        };
        apiKeyInput.onkeypress = (e) => {
            if (e.key === 'Enter') {
                saveBtn.click();
            }
        };
        // ToS tooltip is now handled by setupGenericTooltips via data-cat-tooltip attribute — no JS needed here.
        // Score style handlers
        const scoreColorInput = document.getElementById('tab-setting-score-color');
        const scoreSizeInput = document.getElementById('tab-setting-score-size');
        const scoreSizeValue = document.getElementById('tab-setting-score-size-value');
        const scoreShadowInput = document.getElementById('tab-setting-score-shadow');
        const scoreResetBtn = document.getElementById('tab-setting-score-reset');
        if (scoreColorInput) {
            scoreColorInput.oninput = () => {
                StorageUtil.set('cat_score_color', scoreColorInput.value);
                window.FactionWarEnhancer?.applyScoreStyles?.();
            };
        }
        if (scoreSizeInput) {
            scoreSizeInput.oninput = () => {
                const size = scoreSizeInput.value;
                if (scoreSizeValue)
                    scoreSizeValue.textContent = size + 'px';
                StorageUtil.set('cat_score_font_size', parseInt(size, 10));
                window.FactionWarEnhancer?.applyScoreStyles?.();
            };
        }
        if (scoreShadowInput) {
            scoreShadowInput.onchange = () => {
                StorageUtil.set('cat_score_shadow', scoreShadowInput.checked);
                window.FactionWarEnhancer?.applyScoreStyles?.();
            };
        }
        const scoreShadowColorInput = document.getElementById('tab-setting-score-shadow-color');
        if (scoreShadowColorInput) {
            scoreShadowColorInput.oninput = () => {
                StorageUtil.set('cat_score_shadow_color', scoreShadowColorInput.value);
                window.FactionWarEnhancer?.applyScoreStyles?.();
            };
        }
        if (scoreResetBtn) {
            scoreResetBtn.onclick = () => {
                StorageUtil.remove('cat_score_color');
                StorageUtil.remove('cat_score_font_size');
                StorageUtil.remove('cat_score_shadow');
                StorageUtil.remove('cat_score_shadow_color');
                if (scoreColorInput)
                    scoreColorInput.value = '#888888';
                if (scoreSizeInput)
                    scoreSizeInput.value = '12';
                if (scoreSizeValue)
                    scoreSizeValue.textContent = '12px';
                if (scoreShadowInput)
                    scoreShadowInput.checked = false;
                if (scoreShadowColorInput)
                    scoreShadowColorInput.value = '#000000';
                window.FactionWarEnhancer?.applyScoreStyles?.();
            };
        }
    }

    async function loadCallsFromDatabase() {
        try {
            const enhancer = window.FactionWarEnhancer;
            if (!enhancer || !enhancer.apiManager)
                return;
            const calls = await enhancer.apiManager.getCalls();
            if (!calls || calls.length === 0) {
                return;
            }
            const callButtons = document.querySelectorAll('.call-button');
            calls.forEach(callData => {
                callButtons.forEach((btn) => {
                    const memberRow = btn.closest('li') || btn.closest('tr');
                    if (memberRow) {
                        const memberElement = memberRow.querySelector('[class*="member___"], .member');
                        if (memberElement) {
                            const clone = memberElement.cloneNode(true);
                            clone.querySelectorAll('.iconStats, .bsp-value, .bsp-column, [class*="iconStats"]').forEach((el) => el.remove());
                            const rawMemberName = (clone.textContent || '').trim().split('\n')[0].trim();
                            const memberName = enhancer ? enhancer.cleanMemberName(rawMemberName) : rawMemberName;
                            let memberId = null;
                            const attackLink = memberRow.querySelector('a[href*="getInAttack"], a[href*="user2ID"]');
                            if (attackLink) {
                                const match = attackLink.href.match(/user2ID=(\d+)/);
                                if (match) {
                                    memberId = match[1];
                                }
                            }
                            if (!memberId) {
                                const profileLink = memberRow.querySelector('a[href*="profiles.php?XID="]');
                                if (profileLink) {
                                    const m = profileLink.href.match(/XID=(\d+)/);
                                    if (m)
                                        memberId = m[1];
                                }
                            }
                            const isMatch = (callData.memberId && memberId && callData.memberId === memberId) ||
                                (memberName === callData.memberName);
                            if (isMatch) {
                                btn.dataset.callId = callData.id;
                                btn.dataset.memberId = callData.memberId || '';
                                btn.textContent = callData.callerName || '';
                                btn.classList.add('my-call');
                            }
                        }
                    }
                });
            });
        }
        catch (error) {
            console.log('Error loading calls from database:', error);
            this.apiManager.reportError('loadCallsFromDB', error);
        }
    }

    function handleRallyClick(button, memberId, memberName, factionId) {
        if (!memberId || !factionId) {
            console.log('[Rally] Missing memberId or factionId');
            return;
        }
        const enhancer = window.FactionWarEnhancer;
        if (!enhancer || !enhancer.pollingManager) {
            console.log('[Rally] Enhancer or PollingManager not available');
            return;
        }
        // Check if admin viewing another faction
        const isAdmin = enhancer.subscriptionData?.isAdmin || false;
        const viewingOtherFaction = state.catOtherFaction && state.viewingFactionId;
        if (document.body.classList.contains('cat-read-only') && !(isAdmin && viewingOtherFaction)) {
            return;
        }
        let playerId = enhancer.pollingManager.apiManager.playerId;
        const playerName = enhancer.pollingManager.apiManager.playerName;
        // Fallback: extract playerId from page if not available (same as callMember)
        if (!playerId) {
            playerId = enhancer.apiManager.extractPlayerIdFromPage();
            if (playerId) {
                enhancer.pollingManager.apiManager.playerId = playerId;
            }
            else if (playerName && playerName !== 'Unknown') {
                // Try to find from your faction side
                const yourFactionSide = document.querySelector('.your-faction, [class*="your-faction"]');
                if (yourFactionSide) {
                    const profileLinks = yourFactionSide.querySelectorAll('a[href*="profiles.php?XID="]');
                    for (const link of profileLinks) {
                        const container = link.closest('li');
                        if (container && container.textContent?.includes(playerName)) {
                            const match = link.href.match(/XID=(\d+)/);
                            if (match) {
                                playerId = match[1];
                                enhancer.pollingManager.apiManager.playerId = playerId;
                                break;
                            }
                        }
                    }
                }
            }
        }
        if (!playerId) {
            console.log('[Rally] Player ID not available');
            return;
        }
        const currentRally = this.currentRallies.find(r => r.memberId === memberId);
        const isInRally = currentRally?.participants?.some(p => p.playerId === playerId);
        // OPTIMISTIC UI UPDATE
        if (isInRally) {
            // Optimistic leave: remove from local state immediately
            const wasLastInRally = currentRally?.count === 1;
            if (currentRally) {
                currentRally.participants = currentRally.participants.filter(p => p.playerId !== playerId);
                currentRally.count = currentRally.participants.length;
                if (currentRally.count === 0) {
                    this.currentRallies = this.currentRallies.filter(r => r.memberId !== memberId);
                }
            }
            this.updateRallyButtons(this.currentRallies);
            // Optimistic call update: if rally is now empty, remove call immediately
            if (wasLastInRally && enhancer.currentCalls) {
                enhancer.currentCalls = enhancer.currentCalls.filter(c => c.memberId !== memberId);
                enhancer.updateCallButtons(enhancer.currentCalls);
            }
            // Then send to server
            enhancer.pollingManager.leaveRally(memberId).catch(err => {
                enhancer.apiManager.reportError('leaveRally', err);
            });
        }
        else {
            // Optimistic join: add to local state immediately
            // First, leave any other rally optimistically
            const otherRally = this.currentRallies.find(r => r.participants?.some(p => p.playerId === playerId));
            if (otherRally) {
                otherRally.participants = otherRally.participants.filter(p => p.playerId !== playerId);
                otherRally.count = otherRally.participants.length;
                if (otherRally.count === 0) {
                    this.currentRallies = this.currentRallies.filter(r => r.memberId !== otherRally.memberId);
                }
            }
            // Add to this rally
            let targetRally = this.currentRallies.find(r => r.memberId === memberId);
            if (!targetRally) {
                targetRally = {
                    memberId,
                    memberName,
                    factionId,
                    count: 0,
                    participants: [],
                    createdAt: Date.now()
                };
                this.currentRallies.push(targetRally);
            }
            targetRally.participants.push({
                playerId,
                playerName,
                joinedAt: Date.now()
            });
            targetRally.count = targetRally.participants.length;
            this.updateRallyButtons(this.currentRallies);
            // Check if first to rally (for local state only - server will auto-call)
            const existingCallOnTarget = enhancer.currentCalls?.find(c => c.memberId === memberId);
            const playerAlreadyHasCall = enhancer.currentCalls?.find(c => c.callerId === playerId);
            const isFirstToRally = targetRally.count === 1 && !existingCallOnTarget && !playerAlreadyHasCall;
            // OPTIMISTIC CALL: If first to rally and no existing call, create optimistic call immediately
            if (isFirstToRally) {
                const optimisticCall = {
                    id: `optimistic-${Date.now()}`,
                    memberId,
                    memberName,
                    factionId,
                    callerId: playerId,
                    callerName: playerName,
                    targetStatus: null,
                    createdAt: Date.now(),
                };
                if (!enhancer.currentCalls)
                    enhancer.currentCalls = [];
                enhancer.currentCalls.push(optimisticCall);
                enhancer.updateCallButtons(enhancer.currentCalls);
            }
            // Send to server + let server handle auto-call if first (use server response, not local state)
            enhancer.pollingManager.joinRally(factionId, memberId, memberName).then(result => {
                // Check if first to rally based on SERVER response (count === 1)
                const serverIsFirst = result.success && result.data && result.data.count === 1;
                // Also check if there's already a REAL call on this target (ignore optimistic calls)
                const existingCallOnTarget = enhancer.currentCalls?.find(c => c.memberId === memberId &&
                    !c.id?.toString().startsWith('optimistic-'));
                // Also check if player already has a call on any target
                const playerHasCall = enhancer.currentCalls?.find(c => c.callerId === playerId &&
                    !c.id?.toString().startsWith('optimistic-'));
                if (serverIsFirst && !existingCallOnTarget && !playerHasCall) {
                    enhancer.pollingManager.callMember(factionId, memberId, memberName, null).then(callResult => {
                        if (callResult?.success) {
                            // Remove optimistic call before fetching real one
                            if (enhancer.currentCalls) {
                                enhancer.currentCalls = enhancer.currentCalls.filter(c => !c.id?.toString().startsWith('optimistic-'));
                            }
                            // Force immediate UI update with real call from server
                            setTimeout(() => {
                                enhancer.pollingManager?.fetchCalls().catch(() => { });
                            }, 100);
                        }
                        else {
                            console.log('[RALLY] Auto-call failed:', callResult?.error);
                        }
                    }).catch(err => {
                        console.log('[RALLY] Auto-call error:', err);
                    });
                }
                else if (serverIsFirst && existingCallOnTarget) ;
                else if (result?.error === 'not_activated' || result?.error === 'no_active_war') {
                    document.body.classList.add('cat-read-only');
                }
            }).catch(err => {
                console.log('[RALLY] Server error:', err);
                enhancer.apiManager.reportError('joinRally', err);
            });
        }
    }
    function updateRallyButtons(rallies) {
        this.currentRallies = rallies;
        const enhancer = window.FactionWarEnhancer;
        const playerId = enhancer?.pollingManager?.apiManager?.playerId;
        // Check if player is already in any rally
        const playerRally = playerId ? rallies.find(r => r.participants?.some(p => p.playerId === playerId)) : null;
        const playerRallyMemberId = playerRally?.memberId || null;
        document.querySelectorAll('.rally-button').forEach(button => {
            const memberId = button.dataset.memberId;
            if (!memberId)
                return;
            const rally = rallies.find(r => r.memberId === memberId);
            const count = rally?.count || 0;
            const isInThisRally = rally?.participants?.some(p => p.playerId === playerId);
            // If player is in a rally but not THIS one, disable button
            const shouldDisable = playerRallyMemberId && playerRallyMemberId !== memberId;
            button.disabled = !!shouldDisable;
            // Update button appearance — use CSS classes, no inline styles
            // Reset inline styles that may have been set before
            button.style.background = '';
            button.style.opacity = '';
            button.style.cursor = '';
            // Remove old classes
            button.classList.remove('has-ralliers', 'rally-joined', 'rally-disabled');
            // Find the attack link/span in the same container to shorten text when rallied
            const attackContainer = button.closest('[class*="attack"]');
            let attackLink = button._catAttackLink || null;
            if (attackLink && !attackLink.isConnected)
                attackLink = null;
            if (!attackLink) {
                attackLink = attackContainer?.querySelector('a, span.t-gray-9') || null;
                if (attackLink)
                    button._catAttackLink = attackLink;
            }
            if (count > 0) {
                button.classList.add('has-ralliers');
                // Show count inside button (cached ref)
                let countEl = button._catRallyCount || null;
                if (countEl && !countEl.isConnected)
                    countEl = null;
                if (!countEl)
                    countEl = button.querySelector('.rally-count');
                if (countEl) {
                    button._catRallyCount = countEl;
                    countEl.textContent = String(count);
                }
                else {
                    countEl = document.createElement('span');
                    countEl.className = 'rally-count';
                    countEl.textContent = String(count);
                    button.appendChild(countEl);
                    button._catRallyCount = countEl;
                }
                if (isInThisRally) {
                    button.classList.add('rally-joined');
                }
                else if (shouldDisable) {
                    button.classList.add('rally-disabled');
                }
                // Shorten attack button text when rally is active
                if (attackLink) {
                    if (!attackLink.dataset.originalText) {
                        attackLink.dataset.originalText = attackLink.textContent || '';
                    }
                    attackLink.textContent = 'Atk';
                }
                // Add tooltip with participants on hover
                const participants = rally?.participants || [];
                const names = participants.map(p => p.playerName).join(', ');
                button.title = shouldDisable ? `Rally: ${names} (leave your rally first)` : `Rally: ${names}`;
            }
            else {
                // Remove count if no participants (skip querySelector if we know there's none)
                const existingCount = button._catRallyCount;
                if (existingCount) {
                    existingCount.remove();
                    button._catRallyCount = null;
                }
                // Restore attack button original text
                if (attackLink && attackLink.dataset.originalText) {
                    attackLink.textContent = attackLink.dataset.originalText;
                    delete attackLink.dataset.originalText;
                }
                if (shouldDisable) {
                    button.classList.add('rally-disabled');
                    button.title = 'Leave your current rally first';
                }
                else {
                    button.title = 'Rally on this target';
                }
            }
        });
    }

    const DISMISSED_KEY = 'cat_rating_dismissed';
    const RATED_KEY = 'cat_has_rated';
    const SEVEN_DAYS = 7 * 86400000;
    async function showRatingPopupIfNeeded(apiManager) {
        console.log('%c[CAT Rating] %cChecking if rating popup should show...', 'color:#4FC3F7;font-weight:bold', 'color:#E2E8F0');
        // Guard: prevent duplicate popups
        if (document.getElementById('cat-rating-popup')) {
            console.log('%c[CAT Rating] %cPopup already visible, skipping', 'color:#4FC3F7;font-weight:bold', 'color:#718096');
            return;
        }
        // Fast local check: already rated (persisted after successful submit)
        if (StorageUtil.get(RATED_KEY)) {
            console.log('%c[CAT Rating] %cAlready rated (local)', 'color:#4FC3F7;font-weight:bold', 'color:#718096');
            return;
        }
        // Check if dismissed recently (7 days cooldown) — only local UI state
        const dismissedAt = StorageUtil.get(DISMISSED_KEY);
        if (dismissedAt && Date.now() - dismissedAt < SEVEN_DAYS) {
            console.log('%c[CAT Rating] %cDismissed recently, cooldown active', 'color:#4FC3F7;font-weight:bold', 'color:#718096');
            return;
        }
        // Server-side check: has rated + first_seen (all stored in DB)
        try {
            if (!apiManager.authToken || !apiManager.serverUrl) {
                console.log('%c[CAT Rating] %cNo auth/server — skipping', 'color:#4FC3F7;font-weight:bold', 'color:#718096');
                return;
            }
            console.log('%c[CAT Rating] %cChecking server...', 'color:#4FC3F7;font-weight:bold', 'color:#E2E8F0');
            const resp = await apiManager.apiRequest(`${apiManager.serverUrl}/api/has-rated`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiManager.authToken}`,
                },
            });
            if (!resp.ok) {
                // Server error (500, 401, etc.) — don't show popup if we can't confirm status
                console.log('%c[CAT Rating] %cServer returned %s, skipping', 'color:#4FC3F7;font-weight:bold', 'color:#EF5350', resp.status);
                return;
            }
            const data = await resp.json();
            if (data.hasRated) {
                StorageUtil.set(RATED_KEY, true);
                console.log('%c[CAT Rating] %cAlready rated (server)', 'color:#4FC3F7;font-weight:bold', 'color:#718096');
                return;
            }
            // Check install age from DB (first_seen)
            if (data.firstSeen) {
                const installAge = Date.now() - new Date(data.firstSeen).getTime();
                if (installAge < 2 * 86400000) {
                    const hoursAgo = Math.floor(installAge / 3600000);
                    console.log('%c[CAT Rating] %cToo early — installed %sh ago (need 2 days)', 'color:#4FC3F7;font-weight:bold', 'color:#718096', hoursAgo);
                    return;
                }
            }
        }
        catch (e) {
            // Network error — skip silently, will retry next time
            console.log('%c[CAT Rating] %cServer check failed, will retry later', 'color:#4FC3F7;font-weight:bold', 'color:#EF5350');
            return;
        }
        // Find the tab bar to anchor the popup
        const tabsMenu = document.getElementById('custom-tabs-menu');
        if (!tabsMenu) {
            console.log('%c[CAT Rating] %cTab bar not found (#custom-tabs-menu)', 'color:#4FC3F7;font-weight:bold', 'color:#EF5350');
            return;
        }
        console.log('%c[CAT Rating] %cShowing rating popup!', 'color:#4FC3F7;font-weight:bold', 'color:#66BB6A');
        renderPopup(tabsMenu, apiManager);
    }
    function renderPopup(anchor, apiManager) {
        let selectedScore = 0;
        const style = document.createElement('style');
        style.textContent = `
        @keyframes catRatingPop { from { opacity:0; transform:scale(.9); } to { opacity:1; transform:scale(1); } }
        @keyframes catRatingGlow {
            0%, 100% { box-shadow: 0 0 4px rgba(130,201,30,.3), 0 0 8px rgba(130,201,30,.15); }
            50% { box-shadow: 0 0 8px rgba(130,201,30,.6), 0 0 16px rgba(130,201,30,.3); }
        }
        #cat-rating-popup {
            position: fixed; width: 150px; height: 150px; background: #333; border: 1px solid #82C91E; border-radius: 5px;
            padding: 10px 8px 8px; text-align: center; z-index: 10000; box-sizing: border-box;
            animation: catRatingPop .2s ease, catRatingGlow 2s ease-in-out infinite;
            font-family: "Helvetica Neue", Arial, sans-serif; font-size: 12px;
            color: #ddd; -webkit-font-smoothing: antialiased;
        }
        #cat-rating-popup::after {
            content: ''; position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%);
            width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent;
            border-top: 6px solid #333;
        }
        #cat-rating-popup .cat-r-title { color: #ddd; font-size: 11px; font-weight: 700; margin-bottom: 2px; }
        #cat-rating-popup .cat-r-sub { color: #888; font-size: 9px; margin-bottom: 8px; }
        #cat-rating-popup .cat-r-stars { display: flex; justify-content: center; gap: 3px; margin-bottom: 8px; }
        #cat-rating-popup .cat-r-star { font-size: 22px; cursor: pointer; color: #555; transition: color .1s, transform .1s; user-select: none; line-height: 1; }
        #cat-rating-popup .cat-r-star:hover { transform: scale(1.15); }
        #cat-rating-popup .cat-r-star.active { color: #82C91E; }
        #cat-rating-popup .cat-r-star.hover-preview { color: #9BD636; }
        #cat-rating-popup .cat-r-send {
            background: #82C91E; color: #1a1a1a; border: none;
            border-radius: 3px; padding: 4px 16px; font-size: 10px; font-weight: 700; cursor: pointer;
            opacity: .35; pointer-events: none; transition: opacity .15s, background .15s;
            margin-bottom: 6px;
        }
        #cat-rating-popup .cat-r-send.enabled { opacity: 1; pointer-events: auto; }
        #cat-rating-popup .cat-r-send.enabled:hover { background: #9BD636; }
        #cat-rating-popup .cat-r-dismiss {
            display: block; background: none; border: none; color: #888; font-size: 9px;
            cursor: pointer; margin: 0 auto; padding: 0;
        }
        #cat-rating-popup .cat-r-dismiss:hover { color: #aaa; text-decoration: underline; }
    `;
        document.head.appendChild(style);
        const popup = document.createElement('div');
        popup.id = 'cat-rating-popup';
        // Title
        const title = document.createElement('div');
        title.className = 'cat-r-title';
        title.textContent = 'Rate CAT Script!';
        popup.appendChild(title);
        // Subtitle
        const sub = document.createElement('div');
        sub.className = 'cat-r-sub';
        sub.textContent = 'How would you rate this script? (anonymous)';
        popup.appendChild(sub);
        // Stars
        const starsWrap = document.createElement('div');
        starsWrap.className = 'cat-r-stars';
        const stars = [];
        for (let i = 1; i <= 5; i++) {
            const s = document.createElement('span');
            s.className = 'cat-r-star';
            s.textContent = '\u2605';
            s.addEventListener('mouseenter', () => stars.forEach((st, idx) => st.classList.toggle('hover-preview', idx < i)));
            s.addEventListener('click', () => {
                selectedScore = i;
                stars.forEach((st, idx) => st.classList.toggle('active', idx < i));
                sendBtn.classList.add('enabled');
            });
            stars.push(s);
            starsWrap.appendChild(s);
        }
        starsWrap.addEventListener('mouseleave', () => stars.forEach((st, idx) => { st.classList.remove('hover-preview'); st.classList.toggle('active', idx < selectedScore); }));
        popup.appendChild(starsWrap);
        // Send
        const sendBtn = document.createElement('button');
        sendBtn.className = 'cat-r-send';
        sendBtn.textContent = 'Send';
        sendBtn.onclick = async () => {
            if (selectedScore < 1)
                return;
            sendBtn.textContent = '...';
            sendBtn.classList.remove('enabled');
            try {
                const resp = await apiManager.apiRequest(`${apiManager.serverUrl}/api/rate`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiManager.authToken}` },
                    body: JSON.stringify({ score: selectedScore }),
                });
                if (resp.ok) {
                    StorageUtil.set(RATED_KEY, true);
                    const data = await resp.json();
                    showThankYou(popup, data.average, data.count);
                }
                else {
                    showThankYou(popup, null, null);
                }
            }
            catch {
                showThankYou(popup, null, null);
            }
        };
        popup.appendChild(sendBtn);
        // No thanks
        const dismiss = document.createElement('button');
        dismiss.className = 'cat-r-dismiss';
        dismiss.textContent = 'No thanks';
        dismiss.onclick = () => { StorageUtil.set(DISMISSED_KEY, Date.now()); popup.remove(); };
        popup.appendChild(dismiss);
        // Position fixed above the "Help" tab button
        document.body.appendChild(popup);
        const helpBtn = anchor.querySelector('[data-tab="help"]');
        const refEl = helpBtn || anchor;
        const rect = refEl.getBoundingClientRect();
        popup.style.top = (rect.top - popup.offsetHeight - 6) + 'px';
        popup.style.left = (rect.left + rect.width / 2 - popup.offsetWidth / 2) + 'px';
    }
    function showThankYou(popup, average, count) {
        // Keep same size — clear inner content only
        const children = Array.from(popup.children);
        children.forEach(c => c.remove());
        // Stop the glow animation, replace with a green flash
        popup.style.animation = 'catRatingThank .4s ease';
        // Add the thank-you animation style if not already present
        if (!document.getElementById('cat-rating-thank-style')) {
            const thankStyle = document.createElement('style');
            thankStyle.id = 'cat-rating-thank-style';
            thankStyle.textContent = `
            @keyframes catRatingThank {
                0% { box-shadow: 0 0 0 rgba(130,201,30,0); }
                40% { box-shadow: 0 0 20px rgba(130,201,30,.8), 0 0 40px rgba(130,201,30,.4); }
                100% { box-shadow: 0 0 8px rgba(130,201,30,.3), 0 0 16px rgba(130,201,30,.15); }
            }
            #cat-rating-popup .cat-r-thank-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; }
            #cat-rating-popup .cat-r-thank { color: #82C91E; font-size: 12px; font-weight: 700; }
            #cat-rating-popup .cat-r-thank-stats { color: #888; font-size: 9px; margin-top: 4px; }
        `;
            document.head.appendChild(thankStyle);
        }
        const wrap = document.createElement('div');
        wrap.className = 'cat-r-thank-wrap';
        const msg = document.createElement('div');
        msg.className = 'cat-r-thank';
        msg.textContent = 'Thanks for rating!';
        wrap.appendChild(msg);
        if (average !== null && count !== null) {
            const stats = document.createElement('div');
            stats.className = 'cat-r-thank-stats';
            stats.textContent = `${average}/5 \u00b7 ${count} votes`;
            wrap.appendChild(stats);
        }
        popup.appendChild(wrap);
        setTimeout(() => {
            popup.style.transition = 'opacity .3s ease';
            popup.style.opacity = '0';
            setTimeout(() => popup.remove(), 300);
        }, 4000);
    }

    class EnhancementManager {
        constructor() {
            this.observer = null;
            this._enhancer = null;
            this.onlineStatuses = {};
            this._yourFactionEnhanced = false;
            this._enemyFactionEnhanced = false;
            this._noBspChecked = false;
            this._sortDirty = true; // True on first call so initial sort applies
            this._usersCache = null;
            this._usersRefreshInterval = null;
            this._warCountdownInterval = null;
            this._tabsCheckInterval = null;
            this._autoSortInterval = null;
            this._factionCheckInterval = null;
            this._memberNames = {};
            this.currentRallies = [];
            this._sortContainersCache = null;
            this._mutationsPaused = false;
            this.init();
        }
        _esc(str) {
            if (typeof str !== 'string')
                return '';
            return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
        }
        init() {
            this.setupMutationObserver();
            this.enhanceExistingElements();
            this.injectTabsMenu();
            this.loadCallsFromDatabase();
            setTimeout(() => this.restoreSavedSort(), 100);
            setTimeout(() => {
                showRatingPopupIfNeeded(this.apiManager);
            }, 3000);
            // _tabsCheckInterval and _autoSortInterval are now handled by the master loop in dom-observer.ts
        }
        setupMutationObserver() {
            const pendingNodes = [];
            let debounceTimer = null;
            let _sortBusy = false;
            // Core mutation-processing logic, shared by the local observer (waiting mode)
            // and by dom-observer.ts which forwards mutations once desc-wrap is observed.
            const processMutations = (mutations) => {
                if (_sortBusy || this._mutationsPaused)
                    return;
                for (let i = 0; i < mutations.length; i++) {
                    const mutation = mutations[i];
                    if (mutation.type === 'childList') {
                        const target = mutation.target;
                        // Skip our own injected elements (cat- prefix) and call buttons
                        const tCls = target.className;
                        if ((typeof tCls === 'string' && tCls.includes('cat-')) ||
                            target.dataset?.cat ||
                            (target.closest && target.closest('.call-button, .call-column')))
                            continue;
                        const added = mutation.addedNodes;
                        for (let j = 0; j < added.length; j++) {
                            const addedNode = added[j];
                            if (addedNode.nodeType === Node.ELEMENT_NODE) {
                                const nCls = addedNode.className;
                                if ((typeof nCls === 'string' && nCls.includes('cat-')) || addedNode.dataset?.cat)
                                    continue;
                                pendingNodes.push(addedNode);
                            }
                        }
                    }
                }
                if (debounceTimer)
                    clearTimeout(debounceTimer);
                debounceTimer = setTimeout(() => {
                    const nodes = pendingNodes.splice(0);
                    let processed = 0;
                    for (let k = 0; k < nodes.length; k++) {
                        const node = nodes[k];
                        if (!node.isConnected)
                            continue;
                        processed++;
                        if (node.matches && (node.matches('ul') || node.matches('ol'))) {
                            // Only process lists that are in faction war context
                            const inWarContext = node.closest('.desc-wrap') || node.closest('#faction_war_list_id') ||
                                node.closest('.your-faction') || node.closest('.enemy-faction') ||
                                node.closest('[class*="tabMenuCont"]')?.closest('.desc-wrap');
                            if (!inWarContext)
                                continue;
                            const hasMembers = node.querySelectorAll('li').length >= 3;
                            if (hasMembers && !node.hasAttribute('data-enhanced')) {
                                const isYourFaction = node.closest('.your-faction') !== null;
                                if (isYourFaction) {
                                    this.addBspToYourFaction(node);
                                }
                                else {
                                    this.addBspColumn(node);
                                    if (!node.querySelector('.bsp-column') && !node.querySelector('.bsp-header')) {
                                        const parentContainer = node.closest('[class*="tabMenuCont"]') || node.closest('.enemy-faction');
                                        if (parentContainer) {
                                            parentContainer.classList.add('no-bsp');
                                        }
                                    }
                                }
                            }
                        }
                        if (node.matches && (node.closest('.desc-wrap') || node.querySelector('.desc-wrap') ||
                            node.matches('.f-war-list') || node.querySelector('.f-war-list'))) {
                            this.enhanceElement(node);
                        }
                        // Detect BSP loading late (after CAT already set no-bsp)
                        if (node.classList?.contains('iconStats') || node.querySelector?.('.iconStats')) {
                            this._noBspChecked = false;
                            const container = node.closest('[class*="tabMenuCont"]') || node.closest('.enemy-faction') || node.closest('.your-faction');
                            if (container) {
                                container.classList.remove('no-bsp');
                                const list = container.querySelector('ul, ol, .f-war-list');
                                if (list && !list.querySelector('.bsp-column')) {
                                    const isYourFaction = list.closest('.your-faction') !== null;
                                    if (isYourFaction) {
                                        this.addBspToYourFaction(list);
                                    }
                                    else {
                                        this.addBspColumn(list);
                                    }
                                }
                                // Update "Wait" cells to actual BSP values
                                this.updateWaitingBspCells(container);
                            }
                        }
                    }
                    if (processed > 0) {
                        this._checkFactions();
                        this._sortDirty = true;
                        if (this._enhancer) {
                            this._enhancer._hospScanNeeded = true;
                            this._enhancer._travelNodesValid = false;
                            this._enhancer._memberRowsValid = false;
                        }
                        // Trigger sort immediately — don't wait for polling interval
                        if (String(StorageUtil.get('cat_auto_sort', 'true')) === 'true') {
                            _sortBusy = true;
                            this.restoreSavedSort(true);
                            requestAnimationFrame(() => { _sortBusy = false; });
                        }
                    }
                }, 0);
            };
            // Expose for dom-observer to call directly (avoids a second MutationObserver on .desc-wrap)
            this.handleMutations = processMutations;
            // Lightweight observer: only used to detect when .desc-wrap first appears in the DOM.
            // Once it does, we process the initial mutations and disconnect — dom-observer.ts
            // (FactionWarEnhancer.startDomObserver) takes over as the single subtree observer.
            this.observer = new MutationObserver((mutations) => {
                if (!document.querySelector('.desc-wrap'))
                    return;
                // desc-wrap just appeared — process these initial mutations, then stop
                processMutations(mutations);
                this.observer.disconnect();
            });
            // If desc-wrap is already in the DOM, no need to observe at all
            // (dom-observer will handle it). Otherwise watch body shallowly.
            if (!document.querySelector('.desc-wrap')) {
                this.observer.observe(document.body, { childList: true, subtree: false });
            }
        }
        destroy() {
            if (this._tabsCheckInterval) {
                clearInterval(this._tabsCheckInterval);
                this._tabsCheckInterval = null;
            }
            if (this._autoSortInterval) {
                clearInterval(this._autoSortInterval);
                this._autoSortInterval = null;
            }
            if (this._factionCheckInterval) {
                clearInterval(this._factionCheckInterval);
                this._factionCheckInterval = null;
            }
            if (this._usersRefreshInterval) {
                clearInterval(this._usersRefreshInterval);
                this._usersRefreshInterval = null;
            }
            if (this._warCountdownInterval) {
                clearInterval(this._warCountdownInterval);
                this._warCountdownInterval = null;
            }
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }
        }
    }
    // ── Prototype assignments ──
    // faction-detection
    EnhancementManager.prototype._checkFactions = _checkFactions;
    EnhancementManager.prototype._checkYourFaction = _checkYourFaction;
    EnhancementManager.prototype._checkEnemyFaction = _checkEnemyFaction;
    EnhancementManager.prototype._checkNoBsp = _checkNoBsp;
    // dom-enhancement
    EnhancementManager.prototype.enhanceExistingElements = enhanceExistingElements;
    EnhancementManager.prototype.enhanceElement = enhanceElement;
    // bsp-columns
    EnhancementManager.prototype.addBspToYourFaction = addBspToYourFaction;
    EnhancementManager.prototype.addBspHeaderToYourFaction = addBspHeaderToYourFaction;
    EnhancementManager.prototype.addBspColumnToYourFaction = addBspColumnToYourFaction;
    EnhancementManager.prototype.addBspColumn = addBspColumn;
    EnhancementManager.prototype.parseBspValue = parseBspValue;
    EnhancementManager.prototype.updateWaitingBspCells = updateWaitingBspCells;
    EnhancementManager.prototype.addBspHeader = addBspHeader;
    EnhancementManager.prototype.addFFColumn = addFFColumn;
    EnhancementManager.prototype.updateFFColumns = updateFFColumns;
    EnhancementManager.prototype.addFFSwitchArrow = addFFSwitchArrow;
    EnhancementManager.prototype.addTSColumn = addTSColumn;
    EnhancementManager.prototype.updateTSColumns = updateTSColumns;
    // call-buttons
    EnhancementManager.prototype.addCallButtons = addCallButtons;
    // sorting
    EnhancementManager.prototype.sortByBSP = sortByBSP;
    EnhancementManager.prototype.getBSPValue = getBSPValue;
    EnhancementManager.prototype.parseBSPText = parseBSPText;
    EnhancementManager.prototype.addStatusHeaderSorting = addStatusHeaderSorting;
    EnhancementManager.prototype.restoreSavedSort = restoreSavedSort;
    EnhancementManager.prototype.sortByStatus = sortByStatus;
    EnhancementManager.prototype.sortByColumn = sortByColumn;
    EnhancementManager.prototype.getStatusValue = getStatusValue;
    EnhancementManager.prototype.getTravelArea = getTravelArea;
    EnhancementManager.prototype.sortByFF = sortByFF;
    EnhancementManager.prototype.getFFValue = getFFValue;
    EnhancementManager.prototype.sortByTS = sortByTS;
    EnhancementManager.prototype.getTSValue = getTSValue;
    // ui-helpers
    EnhancementManager.prototype.changeLevelToLvl = changeLevelToLvl;
    EnhancementManager.prototype.addLoadingAnimation = addLoadingAnimation;
    // tabs-menu
    EnhancementManager.prototype.injectTabsMenu = injectTabsMenu;
    // plan-tab
    EnhancementManager.prototype._loadPlanTab = _loadPlanTab;
    EnhancementManager.prototype._activateFromPlanTab = _activateFromPlanTab;
    // stats-tab
    EnhancementManager.prototype.startUsersDataLoop = startUsersDataLoop;
    EnhancementManager.prototype._fetchUsersData = _fetchUsersData;
    EnhancementManager.prototype._renderUsersTab = _renderUsersTab;
    // settings-tab
    EnhancementManager.prototype.setupSettingsTabHandlers = setupSettingsTabHandlers;
    // database-calls
    EnhancementManager.prototype.loadCallsFromDatabase = loadCallsFromDatabase;
    // rally
    EnhancementManager.prototype.handleRallyClick = handleRallyClick;
    EnhancementManager.prototype.updateRallyButtons = updateRallyButtons;
    // cd-column
    EnhancementManager.prototype.addCDColumnToYourFaction = addCDColumnToYourFaction;
    EnhancementManager.prototype.updateCDColumns = updateCDColumns;

    const BONUS_HITS$1 = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
    function getNextBonus(chainCount) {
        for (const b of BONUS_HITS$1) {
            if (b > chainCount)
                return b;
        }
        return null;
    }
    function formatBonusNumber(n) {
        if (n >= 1000000)
            return (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1).replace(/\.0$/, '') + 'M';
        if (n >= 1000)
            return (n / 1000).toFixed(n % 1000 === 0 ? 0 : 1).replace(/\.0$/, '') + 'K';
        return String(n);
    }
    function injectChainBoxPanel() {
        const chainBox = document.querySelector('.chain-box');
        if (!chainBox)
            return;
        const title = chainBox.querySelector('.chain-box-title');
        if (!title)
            return;
        if (chainBox.querySelector('.cat-info-panel')) {
            this.updateChainBoxPanelValues(chainBox);
            return;
        }
        const originalContent = document.createElement('div');
        originalContent.className = 'cat-original-content';
        originalContent.style.display = '';
        while (chainBox.firstChild) {
            originalContent.appendChild(chainBox.firstChild);
        }
        // Forward clicks on original content to the parent li (except CAT button)
        // This is needed because wrapping the content breaks Torn's event delegation
        originalContent.addEventListener('click', (e) => {
            const target = e.target;
            if (target.closest('.cat-info-toggle-back'))
                return; // Let CAT button handle itself
            e.stopPropagation();
            e.preventDefault();
            const li = chainBox.closest('li[class*="warListItem___"]');
            if (li) {
                li.click();
            }
        });
        const infoPanel = document.createElement('div');
        infoPanel.className = 'cat-info-panel';
        infoPanel.dataset.cat = '1';
        infoPanel.style.cssText = 'display:none;padding:12px 16px;font-family:Inter,Segoe UI,-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box;overflow:hidden;';
        infoPanel.addEventListener('click', (e) => e.stopPropagation());
        this.renderChainBoxPanelHTML(infoPanel);
        const titleBlock = originalContent.querySelector('.chain-box-title-block');
        if (titleBlock) {
            // Random Target button - use <a> for new tab support
            const randomBtn = document.createElement('a');
            randomBtn.className = 'cat-random-target-btn';
            randomBtn.textContent = 'Random target';
            randomBtn.title = 'Find a newbie target (click or open in new tab)';
            randomBtn.target = '_blank';
            randomBtn.rel = 'noopener';
            randomBtn.style.cssText = 'cursor:pointer;font-size:11px;font-weight:600;color:#ddd;background:linear-gradient(to bottom,#646464,#343434);border:1px solid rgba(255,255,255,0.15);border-radius:4px;padding:3px 8px;margin-left:8px;user-select:none;letter-spacing:0.5px;text-shadow:0 1px 0 rgba(0,0,0,0.75);display:inline-block;vertical-align:middle;line-height:1.3;text-decoration:none;white-space:nowrap;';
            // Generate random ID for href (for middle-click/ctrl+click)
            const generateRandomUrl = () => {
                const minID = 3000000;
                const maxID = 3400000;
                const randID = Math.floor(Math.random() * (maxID - minID + 1)) + minID;
                return `https://www.torn.com/page.php?sid=attack&user2ID=${randID}`;
            };
            randomBtn.href = generateRandomUrl();
            // Update href on mouseenter for fresh random ID each time
            randomBtn.addEventListener('mouseenter', () => {
                randomBtn.href = generateRandomUrl();
            });
            randomBtn.addEventListener('click', (e) => {
                e.stopPropagation();
            });
            titleBlock.appendChild(randomBtn);
            // CAT button
            const backBtn = document.createElement('span');
            backBtn.className = 'cat-info-toggle-back';
            backBtn.textContent = 'CAT';
            backBtn.title = 'Show CAT Script info';
            backBtn.style.cssText = 'cursor:pointer;font-size:11px;font-weight:600;color:#ddd;background:linear-gradient(to bottom,#646464,#343434);border:1px solid rgba(255,255,255,0.15);border-radius:4px;padding:3px 8px;margin-left:8px;user-select:none;letter-spacing:0.5px;text-shadow:0 1px 0 rgba(0,0,0,0.75);display:inline-block;vertical-align:middle;line-height:1.3;';
            backBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                infoPanel.style.cssText = 'padding:12px 16px;font-family:Inter,Segoe UI,-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box;overflow:hidden;';
                originalContent.style.display = 'none';
            });
            titleBlock.appendChild(backBtn);
        }
        chainBox.appendChild(originalContent);
        chainBox.appendChild(infoPanel);
        const toggle = infoPanel.querySelector('.cat-info-toggle');
        if (toggle) {
            toggle.addEventListener('click', (e) => {
                e.stopPropagation();
                infoPanel.style.cssText = 'display:none;padding:12px 16px;font-family:Inter,Segoe UI,-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box;overflow:hidden;';
                originalContent.style.display = '';
            });
        }
        this.setupChainBoxPanelHandlers(infoPanel);
        if (!this.warStatus || this.warStatus === 'checking') {
            this.checkEnlistedStatus();
        }
        // Padding is managed dynamically by updateChainBonusClaimUI based on chain activity
    }
    function renderChainBoxPanelHTML(panel) {
        const apiKey = this.apiManager.torn_apikey || '';
        const maskedKey = apiKey ? '\u2022\u2022\u2022\u2022\u2022\u2022' + apiKey.slice(-4) : 'Not set';
        const serverUrl = (this.apiManager.serverUrl || 'Unknown').replace('https://', '').replace('http://', '');
        const playerName = this.apiManager.playerName || 'Unknown';
        const factionId = StorageUtil.get('cat_user_faction_id', null) || '?';
        const isPollingActive = this.pollingManager && this.pollingManager._isActive;
        const badgeClass = this.warStatus === 'enlisted' ? 'enlisted'
            : this.warStatus === 'checking' ? 'checking'
                : 'not-enlisted';
        const badgeText = this.warStatus === 'enlisted'
            ? 'In War'
            : this.warStatus === 'checking' ? '...'
                : 'No War';
        const badgeBaseStyle = 'display:inline-block;padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;line-height:1.4;vertical-align:middle;';
        const badgeStateStyle = this.warStatus === 'enlisted'
            ? 'background:rgba(72,187,120,0.2);color:#48bb78;border:1px solid rgba(72,187,120,0.4);'
            : this.warStatus === 'checking'
                ? 'background:rgba(237,137,54,0.15);color:#ed8936;border:1px solid rgba(237,137,54,0.3);'
                : 'background:rgba(160,174,192,0.15);color:#718096;border:1px solid rgba(160,174,192,0.3);';
        const dotStyle = isPollingActive
            ? 'width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0;background:#48bb78;box-shadow:0 0 4px rgba(72,187,120,0.6);'
            : 'width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0;background:#fc8181;box-shadow:0 0 4px rgba(252,129,129,0.6);';
        const labelStyle = 'font-size:10px;color:#718096;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;';
        const valueStyle = 'font-size:12px;color:#e2e8f0;font-weight:500;display:flex;align-items:center;gap:5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;';
        const rowStyle = 'display:flex;flex-direction:column;gap:2px;';
        panel.innerHTML = `
        <div class="cat-info-header" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
            <div style="display:flex;align-items:center;gap:8px;">
                <span class="cat-info-title" style="font-size:14px;font-weight:700;color:#e2e8f0;letter-spacing:0.5px;">CAT Script</span>
                <span id="cat-enlisted-badge" class="cat-info-badge ${badgeClass}" style="${badgeBaseStyle}${badgeStateStyle}">${badgeText}</span>
            </div>
            <span class="cat-info-toggle" title="Show chain stats" style="cursor:pointer;font-size:18px;line-height:1;color:#718096;user-select:none;">\u00D7</span>
        </div>
        <div class="cat-info-grid" style="display:grid;grid-template-columns:1fr 1fr;gap:8px 16px;">
            <div class="cat-info-row" style="${rowStyle}">
                <span class="cat-info-label" style="${labelStyle}">API Key</span>
                <span class="cat-info-value" style="${valueStyle}">${maskedKey}</span>
            </div>
            <div class="cat-info-row" style="${rowStyle}">
                <span class="cat-info-label" style="${labelStyle}">Server</span>
                <span class="cat-info-value" style="${valueStyle}">
                    <span class="cat-info-status-dot ${isPollingActive ? 'connected' : 'disconnected'}" style="${dotStyle}"></span>
                    <a href="https://${this._esc(serverUrl)}" target="_blank" style="color:#e2e8f0;text-decoration:none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">${this._esc(serverUrl)}</a>
                </span>
            </div>
            <div class="cat-info-row" style="${rowStyle}">
                <span class="cat-info-label" style="${labelStyle}">Player</span>
                <span class="cat-info-value" style="${valueStyle}">${this._esc(playerName)}</span>
            </div>
            <div class="cat-info-row" style="${rowStyle}">
                <span class="cat-info-label" style="${labelStyle}">Faction ID</span>
                <span class="cat-info-value" style="${valueStyle}">${this._esc(factionId)}</span>
            </div>
        </div>
    `;
    }
    function setupChainBoxPanelHandlers(_panel) {
        // API key edit and clear cache removed — use Settings tab instead
    }
    function applyScoreStyles() {
        const color = StorageUtil.get('cat_score_color', null);
        const fontSize = StorageUtil.get('cat_score_font_size', null);
        const shadow = StorageUtil.get('cat_score_shadow', false);
        const shadowColor = StorageUtil.get('cat_score_shadow_color', null) || '#000000';
        const scoreTarget = StorageUtil.get('cat_war_score_target', null);
        // Skip if nothing to apply and nothing was previously applied
        if (!color && !fontSize && !shadow && scoreTarget == null) {
            if (!this._lastScoreStylesHash)
                return; // never applied — nothing to do
            // Had settings before, now cleared — fall through to strip styles
        }
        // Hash guard — skip if settings unchanged AND no score target active
        // (score target requires re-scan every time since DOM elements can appear late)
        const hash = `${color}|${fontSize}|${shadow}|${shadowColor}|${scoreTarget}`;
        if (hash === this._lastScoreStylesHash && scoreTarget == null)
            return;
        this._lastScoreStylesHash = hash;
        // Always re-query to pick up new DOM nodes
        const scoreElements = document.querySelectorAll('[class*="points___"]');
        scoreElements.forEach(el => {
            // Skip elements outside the faction war list (user stats sidebar, etc.)
            if (!el.closest('#faction_war_list_id') && !el.closest('.f-war-list'))
                return;
            // Skip tab elements
            if (el.closest('.tab___UztMc') || el.closest('[class*="tab___"]'))
                return;
            // Score target highlight: your-faction members at or above target turn green
            const inYourFaction = !!(el.closest('.your-faction') || el.closest('[class*="your-faction"]'));
            if (scoreTarget != null && inYourFaction) {
                const raw = (el.textContent || '').trim().replace(/,/g, '');
                const val = parseInt(raw, 10);
                if (!isNaN(val) && val >= scoreTarget) {
                    if (el.style.getPropertyValue('color') !== '#4caf50')
                        el.style.setProperty('color', '#4caf50', 'important');
                    const wantedSize = fontSize ? fontSize + 'px' : '';
                    if (el.style.getPropertyValue('font-size') !== wantedSize) {
                        if (fontSize)
                            el.style.setProperty('font-size', wantedSize, 'important');
                        else
                            el.style.removeProperty('font-size');
                    }
                    const wantedShadow = shadow ? `0 1px 3px ${shadowColor}` : '';
                    if (el.style.getPropertyValue('text-shadow') !== wantedShadow) {
                        if (shadow)
                            el.style.setProperty('text-shadow', wantedShadow, 'important');
                        else
                            el.style.removeProperty('text-shadow');
                    }
                    return;
                }
            }
            if (color) {
                el.style.setProperty('color', color, 'important');
            }
            else {
                el.style.removeProperty('color');
            }
            if (fontSize) {
                el.style.setProperty('font-size', fontSize + 'px', 'important');
            }
            else {
                el.style.removeProperty('font-size');
            }
            if (shadow) {
                el.style.setProperty('text-shadow', `0 1px 3px ${shadowColor}`, 'important');
            }
            else {
                el.style.removeProperty('text-shadow');
            }
        });
    }
    function updateChainBonusClaimUI() {
        // Cache chainBox ref on instance — static element, never changes
        let chainBox = this._catChainBox || null;
        if (chainBox && !chainBox.isConnected)
            chainBox = null;
        if (!chainBox) {
            chainBox = document.querySelector('.chain-box');
            if (chainBox)
                this._catChainBox = chainBox;
        }
        if (!chainBox)
            return;
        // Don't rebuild if reassign dropdown is open — use cached ref
        let existingDropdown = this._catReassignDropdown || null;
        if (existingDropdown && !existingDropdown.isConnected)
            existingDropdown = null;
        if (!existingDropdown)
            existingDropdown = document.querySelector('.cat-bonus-reassign-dropdown');
        if (existingDropdown)
            return;
        let assignment = this.chainBonusAssignment;
        let container = document.getElementById('cat-bonus-claim-row');
        // Check proximity to next bonus — use the higher of polling data vs DOM (DOM updates faster)
        const pollingChain = this.enemyChainData?.ownChain ?? 0;
        // Cache chain-box-center-stat ref
        let domStat = this._catChainStat || null;
        if (domStat && !domStat.isConnected)
            domStat = null;
        if (!domStat) {
            domStat = chainBox.querySelector('.chain-box-center-stat');
            if (domStat)
                this._catChainStat = domStat;
        }
        const domChain = domStat ? (parseInt((domStat.textContent || '0').replace(/,/g, ''), 10) || 0) : 0;
        const ownChain = Math.max(pollingChain, domChain);
        // Auto-clear assignment if bonus already passed or chain reset to 0
        if (assignment && (assignment.nextBonus <= ownChain || ownChain === 0)) {
            this.chainBonusAssignment = null;
            assignment = null;
        }
        const nextBonus = assignment?.nextBonus ?? getNextBonus(ownChain);
        // Cache .chain-box-general-info ref
        let generalInfoEl = this._catGeneralInfo || null;
        if (generalInfoEl && !generalInfoEl.isConnected)
            generalInfoEl = null;
        if (!generalInfoEl) {
            generalInfoEl = chainBox.querySelector('.chain-box-general-info');
            if (generalInfoEl)
                this._catGeneralInfo = generalInfoEl;
        }
        if (generalInfoEl) {
            generalInfoEl.style.setProperty('padding-top', ownChain > 0 ? '0' : '10px', 'important');
        }
        // Update war list padding based on chain activity — cache warListItems
        let warList = this._catWarList || null;
        if (warList && !warList.isConnected)
            warList = null;
        if (!warList) {
            warList = chainBox.closest('.f-war-list') || document.querySelector('.f-war-list');
            if (warList)
                this._catWarList = warList;
        }
        if (warList) {
            // Cache war list items — only re-query if structural change invalidated them
            let warListItems = this._catWarListItems || null;
            if (!warListItems || (warListItems.length > 0 && !warListItems[0].isConnected)) {
                warListItems = Array.from(warList.querySelectorAll('li[class*="warListItem___"]'));
                this._catWarListItems = warListItems;
            }
            for (let _i = 0; _i < warListItems.length; _i++) {
                const li = warListItems[_i];
                const isActive = li.className.includes('active___');
                li.style.setProperty('padding-bottom', (ownChain > 0 && !isActive) ? '0' : '10px', 'important');
                li.style.setProperty('height', 'auto', 'important');
            }
            warList.querySelectorAll('li.inactive').forEach(li => {
                li.style.paddingBottom = '10px';
            });
        }
        if (!container) {
            // Insert inside .chain-box-general-info (under the timer) for instant visibility
            const generalInfo = generalInfoEl || chainBox.querySelector('.chain-box-general-info');
            if (!generalInfo)
                return;
            container = document.createElement('div');
            container.id = 'cat-bonus-claim-row';
            container.style.cssText = 'font-size:9px;font-weight:700;text-align:center;margin-top:0;line-height:1.3;background:linear-gradient(to bottom,#D4C07C,#C4AD6C);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;';
            container.addEventListener('click', (e) => e.stopPropagation());
            generalInfo.appendChild(container);
        }
        // Hide when no active chain or DOM shows 0
        if (ownChain === 0 || domChain === 0) {
            if (container)
                container.style.display = 'none';
            return;
        }
        if (container)
            container.style.display = '';
        // Fallback to stored user info if apiManager.playerId not yet populated
        const storedUserInfo = StorageUtil.get('cat_user_info', null);
        const playerId = this.apiManager.playerId || (storedUserInfo?.id ? String(storedUserInfo.id) : '') || '';
        const bonusLabel = nextBonus ? `Next bonus (${formatBonusNumber(nextBonus)}):` : 'Next bonus:';
        if (!assignment) {
            // Show "..." until enough time has passed to receive server data (avoid flash of "unclaimed")
            if (this._chainBonusPollCount < 2 || Date.now() - this._chainBonusInitTime < 8000) {
                container.innerHTML = `${bonusLabel}<br><span style="font-size:10px;">Wait...</span>`;
                return;
            }
            // "Next bonus (XXX):" / "unclaimed [claim]"
            let html = `${bonusLabel}<br>`;
            html += '<span style="font-size:10px;">unclaimed </span>';
            html += '<span class="cat-bonus-claim-link" style="cursor:pointer;-webkit-text-fill-color:#aaa;text-decoration:underline;font-size:8px;">[claim]</span>';
            container.innerHTML = html;
            const claimLink = container.querySelector('.cat-bonus-claim-link');
            if (claimLink) {
                claimLink.addEventListener('click', (e) => {
                    e.stopPropagation();
                    this._sendChainBonusClaim();
                });
            }
            return;
        }
        // Two lines: "Next bonus (XXX):" / "PLAYER ✖ [reassign]"
        const isMyClaim = String(assignment.playerId) === String(playerId);
        const isAdmin = !!this.subscriptionData?.isAdmin;
        const canUnclaim = isMyClaim || isAdmin;
        let html = `${bonusLabel}<br>`;
        html += `<span style="font-size:10px;">${assignment.playerName}</span>`;
        if (canUnclaim) {
            html += ' <span class="cat-bonus-unclaim-link" style="cursor:pointer;-webkit-text-fill-color:#D4C07C;font-size:10px;" data-cat-tooltip="Unclaim">\u2716</span>';
        }
        if (isAdmin) {
            html += ' <span class="cat-bonus-override-link" style="cursor:pointer;-webkit-text-fill-color:#aaa;text-decoration:underline;font-size:8px;">[reassign]</span>';
        }
        container.innerHTML = html;
        if (isAdmin) {
            const overrideLink = container.querySelector('.cat-bonus-override-link');
            if (overrideLink) {
                overrideLink.addEventListener('click', (e) => {
                    e.stopPropagation();
                    this._showBonusReassignDropdown(container);
                });
            }
        }
        if (canUnclaim) {
            const unclaimLink = container.querySelector('.cat-bonus-unclaim-link');
            if (unclaimLink) {
                unclaimLink.addEventListener('click', (e) => {
                    e.stopPropagation();
                    this._sendChainBonusUnclaim();
                });
            }
        }
    }
    function _sendChainBonusClaim(targetPlayerId, targetPlayerName) {
        const isAdmin = !!this.subscriptionData?.isAdmin;
        const viewingFaction = (isAdmin && state.catOtherFaction) ? state.viewingFactionId : null;
        const factionId = viewingFaction || StorageUtil.get('cat_user_faction_id', null) || '';
        if (!factionId || !this.apiManager.authToken)
            return;
        const payload = { factionId };
        if (targetPlayerId) {
            payload.targetPlayerId = targetPlayerId;
            if (targetPlayerName)
                payload.targetPlayerName = targetPlayerName;
        }
        // Optimistic update
        const ownChain = this.enemyChainData?.ownChain ?? 0;
        const storedInfo = StorageUtil.get('cat_user_info', null);
        const claimPlayerId = targetPlayerId || this.apiManager.playerId || (storedInfo?.id ? String(storedInfo.id) : '') || '';
        const claimPlayerName = targetPlayerName || this.apiManager.playerName || storedInfo?.name || 'Unknown';
        this.chainBonusAssignment = {
            factionId,
            playerId: claimPlayerId,
            playerName: claimPlayerName,
            nextBonus: getNextBonus(ownChain) || 0,
            assignedBy: this.apiManager.playerId || '',
            assignedByName: this.apiManager.playerName || '',
            createdAt: Date.now()
        };
        this._chainBonusOptimisticUntil = Date.now() + 5000;
        this.updateCallButtons(this.currentCalls);
        this.updateChainBonusClaimUI();
        this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/chain-bonus-claim`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.apiManager.authToken}`
            },
            body: JSON.stringify(payload)
        }).then(async (response) => {
            const data = await response.json();
            if (data.success && data.assignment) {
                // Update with server data (authoritative)
                this.chainBonusAssignment = data.assignment;
                this._chainBonusOptimisticUntil = 0;
                this.updateCallButtons(this.currentCalls);
                this.updateChainBonusClaimUI();
            }
            else if (data.error === 'already_claimed') {
                // Rollback optimistic update
                this.chainBonusAssignment = null;
                this._chainBonusOptimisticUntil = 0;
                this.updateCallButtons(this.currentCalls);
                this.updateChainBonusClaimUI();
                this.apiManager.showNotification(`Already claimed by ${data.claimedBy}`, 'warning');
            }
            else if (data.error === 'no_active_chain') {
                this.chainBonusAssignment = null;
                this._chainBonusOptimisticUntil = 0;
                this.updateCallButtons(this.currentCalls);
                this.updateChainBonusClaimUI();
                this.apiManager.showNotification('No active chain', 'warning');
            }
            else {
                this._chainBonusOptimisticUntil = 0;
            }
        }).catch(() => {
            // Rollback on network error
            this._chainBonusOptimisticUntil = 0;
            this.chainBonusAssignment = null;
            this.updateCallButtons(this.currentCalls);
            this.updateChainBonusClaimUI();
        });
    }
    function _sendChainBonusUnclaim() {
        const isAdmin = !!this.subscriptionData?.isAdmin;
        const viewingFaction = (isAdmin && state.catOtherFaction) ? state.viewingFactionId : null;
        const factionId = viewingFaction || StorageUtil.get('cat_user_faction_id', null) || '';
        if (!factionId || !this.apiManager.authToken)
            return;
        // Optimistic update
        this.chainBonusAssignment = null;
        this._chainBonusOptimisticUntil = Date.now() + 5000;
        this.updateCallButtons(this.currentCalls);
        this.updateChainBonusClaimUI();
        this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/chain-bonus-unclaim`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${this.apiManager.authToken}`
            },
            body: JSON.stringify({ factionId })
        }).catch(() => { });
    }
    async function _showBonusReassignDropdown(container) {
        // Remove existing dropdown if any
        const existing = document.querySelector('.cat-bonus-reassign-dropdown');
        if (existing) {
            existing.remove();
            return;
        }
        // Fetch full member list from Torn API
        const factionId = StorageUtil.get('cat_user_faction_id', null) || '';
        let sorted = [];
        if (factionId && this.apiManager.torn_apikey) {
            try {
                const factionInfo = await this.apiManager.getFactionInfo(factionId);
                if (factionInfo && factionInfo.members) {
                    const membersData = factionInfo.members;
                    const membersArray = Array.isArray(membersData) ? membersData : Object.values(membersData);
                    sorted = membersArray.map(m => [String(m.id), m.name]).sort((a, b) => a[1].localeCompare(b[1]));
                }
            }
            catch { /* fallback below */ }
        }
        // Fallback to client-side cached names
        if (sorted.length === 0) {
            const members = this._memberNames;
            if (!members || Object.keys(members).length === 0) {
                this.apiManager.showNotification('No member list available', 'warning');
                return;
            }
            sorted = Object.entries(members).sort((a, b) => a[1].localeCompare(b[1]));
        }
        const dropdown = document.createElement('div');
        dropdown.className = 'cat-bonus-reassign-dropdown';
        // Position fixed near the container to avoid page scroll
        const rect = container.getBoundingClientRect();
        const dropdownHeight = 240; // approximate height
        const top = Math.max(10, rect.top - dropdownHeight);
        const left = Math.max(10, rect.left + rect.width / 2 - 100);
        dropdown.style.cssText = `
        position:fixed;top:${top}px;left:${left}px;
        background:linear-gradient(to bottom,#333,#242424);
        border:1px solid #444;border-radius:5px;
        padding:0;z-index:9999;width:200px;
        box-shadow:0 6px 20px rgba(0,0,0,0.6),0 0 1px rgba(255,255,255,0.1);
        overflow:hidden;
    `.replace(/\n\s*/g, '');
        dropdown.addEventListener('click', (e) => e.stopPropagation());
        dropdown.addEventListener('mousedown', (e) => e.stopPropagation());
        // Header
        const header = document.createElement('div');
        header.style.cssText = 'padding:6px 10px;font-size:11px;font-weight:700;color:#D4C07C;border-bottom:1px solid #444;-webkit-text-fill-color:#D4C07C;background:rgba(0,0,0,0.2);';
        header.textContent = 'Reassign bonus hit';
        dropdown.appendChild(header);
        // Search input
        const input = document.createElement('input');
        input.type = 'text';
        input.placeholder = 'Search member...';
        input.style.cssText = 'width:100%;box-sizing:border-box;padding:6px 10px;font-size:11px;background:#1c1c1c;color:#eee;border:none;border-bottom:1px solid #3a3a3a;outline:none;-webkit-text-fill-color:#eee;';
        dropdown.appendChild(input);
        const list = document.createElement('div');
        list.className = 'cat-bonus-reassign-list';
        list.style.cssText = 'max-height:160px;overflow-y:auto;';
        const renderList = (filter) => {
            list.innerHTML = '';
            const filtered = filter
                ? sorted.filter(([, name]) => name.toLowerCase().includes(filter.toLowerCase()))
                : sorted;
            if (filtered.length === 0) {
                const empty = document.createElement('div');
                empty.style.cssText = 'padding:8px 10px;font-size:10px;color:#666;-webkit-text-fill-color:#666;text-align:center;';
                empty.textContent = 'No results';
                list.appendChild(empty);
                return;
            }
            for (const [id, name] of filtered) {
                const item = document.createElement('div');
                item.style.cssText = 'padding:5px 10px;font-size:11px;color:#ccc;cursor:pointer;white-space:nowrap;-webkit-text-fill-color:#ccc;transition:background 0.1s;border-bottom:1px solid #2a2a2a;';
                item.textContent = name;
                item.addEventListener('mouseenter', () => { item.style.background = '#3a3a3a'; item.style.color = '#fff'; item.style.webkitTextFillColor = '#fff'; });
                item.addEventListener('mouseleave', () => { item.style.background = ''; item.style.color = '#ccc'; item.style.webkitTextFillColor = '#ccc'; });
                item.addEventListener('click', (e) => {
                    e.stopPropagation();
                    dropdown.remove();
                    this._sendChainBonusClaim(id, name);
                });
                list.appendChild(item);
            }
        };
        renderList('');
        dropdown.appendChild(list);
        document.body.appendChild(dropdown);
        input.addEventListener('input', () => renderList(input.value));
        input.focus();
        // Close on outside click (use mousedown to avoid race with click bubbling)
        const closeHandler = (e) => {
            if (!dropdown.contains(e.target) && !e.target?.closest?.('.cat-bonus-override-link')) {
                dropdown.remove();
                document.removeEventListener('mousedown', closeHandler, true);
            }
        };
        setTimeout(() => document.addEventListener('mousedown', closeHandler, true), 50);
    }
    function updateChainBoxPanelValues(chainBox) {
        const panel = chainBox.querySelector('.cat-info-panel');
        if (!panel || panel.style.display === 'none')
            return;
        const originalContent = chainBox.querySelector('.cat-original-content');
        this.renderChainBoxPanelHTML(panel);
        const toggle = panel.querySelector('.cat-info-toggle');
        if (toggle && originalContent) {
            toggle.addEventListener('click', (e) => {
                e.stopPropagation();
                panel.style.cssText = 'display:none;padding:12px 16px;font-family:Inter,Segoe UI,-apple-system,BlinkMacSystemFont,sans-serif;box-sizing:border-box;overflow:hidden;';
                originalContent.style.display = '';
            });
        }
        this.setupChainBoxPanelHandlers(panel);
    }
    function injectWarStatusBar() {
        // Prefer the factionWarInfo inside .your-faction; fall back to the first one in the document
        const yourFaction = document.querySelector('.your-faction, [class*="your-faction"]');
        const factionWarInfo = (yourFaction?.querySelector('.faction-war-info, [class*="factionWarInfo___"]'))
            ?? document.querySelector('.faction-war-info, [class*="factionWarInfo___"]');
        if (!factionWarInfo)
            return;
        const war = this.subscriptionData?.currentWar;
        const termed = war?.war_termed ?? null;
        const scoreTarget = war?.war_score_target ?? null;
        const canEdit = !!(this.canActivateWar);
        const warId = war?.war_id ? String(war.war_id) : null;
        const factionId = StorageUtil.get('cat_user_faction_id', null);
        // Only show on our own faction war page
        if (!factionId || !window.location.href.includes('step=your')) {
            document.getElementById('cat-war-status-bar')?.remove();
            return;
        }
        // Nothing to show and can't edit — remove and bail
        if (!canEdit && termed === null && scoreTarget === null) {
            document.getElementById('cat-war-status-bar')?.remove();
            return;
        }
        // Sync score target into localStorage for applyScoreStyles, force re-scan
        StorageUtil.set('cat_war_score_target', scoreTarget);
        this._lastScoreStylesHash = '';
        this.applyScoreStyles();
        // Don't re-inject if already present with same data
        const existing = document.getElementById('cat-war-status-bar');
        const hashNow = `${canEdit}|${termed}|${scoreTarget}`;
        if (existing) {
            if (existing.dataset.hash === hashNow)
                return;
            // Don't re-inject while user is typing in the input
            if (existing.querySelector('input:focus'))
                return;
            existing.remove();
        }
        const bar = document.createElement('div');
        bar.id = 'cat-war-status-bar';
        bar.dataset.hash = hashNow;
        bar.style.cssText = 'padding:3px 10px;background:rgba(0,0,0,0.2);border-radius:4px;font-size:11px;display:inline-flex;align-items:center;gap:8px;max-width:fit-content;';
        if (canEdit && warId && factionId) {
            const termedYes = termed === true;
            const termedNo = termed === false;
            const termedUnk = termed === null;
            const showScore = !termedNo;
            const scoreVal = scoreTarget != null ? String(scoreTarget) : '';
            bar.innerHTML = `
            <span style="color:#888;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;flex-shrink:0;">Termed?</span>
            <label style="display:flex;align-items:center;gap:3px;cursor:pointer;">
                <input type="radio" name="cat-wsb-termed" id="cat-wsb-yes" value="yes" ${termedYes ? 'checked' : ''} style="accent-color:#ACEA01;cursor:pointer;margin:0;" />
                <span id="cat-wsb-yes-lbl" style="color:${termedYes ? '#ACEA01' : '#888'};">Yes</span>
            </label>
            <label style="display:flex;align-items:center;gap:3px;cursor:pointer;">
                <input type="radio" name="cat-wsb-termed" id="cat-wsb-no" value="no" ${termedNo ? 'checked' : ''} style="accent-color:#e66;cursor:pointer;margin:0;" />
                <span id="cat-wsb-no-lbl" style="color:${termedNo ? '#e66' : '#888'};">No</span>
            </label>
            <label style="display:flex;align-items:center;gap:3px;cursor:pointer;">
                <input type="radio" name="cat-wsb-termed" id="cat-wsb-unk" value="unk" ${termedUnk ? 'checked' : ''} style="accent-color:#888;cursor:pointer;margin:0;" />
                <span id="cat-wsb-unk-lbl" style="color:${termedUnk ? '#ddd' : '#888'};">?</span>
            </label>
            <span id="cat-wsb-score-wrap" style="display:${showScore ? 'contents' : 'none'};">
                <span style="color:#333;margin:0 2px;">|</span>
                <span style="color:#888;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;flex-shrink:0;">Score target</span>
                <input id="cat-wsb-score" type="number" min="0" max="9999" placeholder="—" value="${scoreVal}"
                    style="width:56px;font-size:11px;color:#ddd;background:rgba(0,0,0,0.2);border:1px solid rgba(255,255,255,0.06);border-radius:4px;padding:3px 6px;outline:none;-moz-appearance:textfield;" />
                <button id="cat-wsb-save"
                    style="background:rgba(255,255,255,0.1);color:#ddd;border:1px solid rgba(255,255,255,0.08);border-radius:4px;padding:3px 10px;font-size:10px;font-weight:600;cursor:pointer;">
                    Save
                </button>
            </span>
            <span id="cat-wsb-status" style="font-size:11px;"></span>`;
            const saveWarStatus = async (patch) => {
                try {
                    const resp = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/war-status`, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiManager.authToken}` },
                        body: JSON.stringify({ factionId, warId, ...patch })
                    });
                    const result = await resp.json();
                    if (result.success && this.subscriptionData?.currentWar) {
                        if ('termed' in patch)
                            this.subscriptionData.currentWar.war_termed = patch.termed;
                        if ('scoreTarget' in patch) {
                            this.subscriptionData.currentWar.war_score_target = patch.scoreTarget;
                            StorageUtil.set('cat_war_score_target', patch.scoreTarget ?? null);
                            this.applyScoreStyles();
                        }
                        const el = document.getElementById('cat-war-status-bar');
                        if (el)
                            el.dataset.hash = '';
                    }
                    return result.success ?? false;
                }
                catch {
                    return false;
                }
            };
            bar.querySelectorAll('input[name="cat-wsb-termed"]').forEach(radio => {
                radio.addEventListener('change', async () => {
                    const val = bar.querySelector('input[name="cat-wsb-termed"]:checked')?.value;
                    const yesLbl = bar.querySelector('#cat-wsb-yes-lbl');
                    const noLbl = bar.querySelector('#cat-wsb-no-lbl');
                    const unkLbl = bar.querySelector('#cat-wsb-unk-lbl');
                    const scoreWrap = bar.querySelector('#cat-wsb-score-wrap');
                    const scoreInp = bar.querySelector('#cat-wsb-score');
                    const statusEl = bar.querySelector('#cat-wsb-status');
                    if (yesLbl)
                        yesLbl.style.color = val === 'yes' ? '#ACEA01' : '#888';
                    if (noLbl)
                        noLbl.style.color = val === 'no' ? '#e66' : '#888';
                    if (unkLbl)
                        unkLbl.style.color = val === 'unk' ? '#ddd' : '#888';
                    if (val === 'no') {
                        if (scoreWrap)
                            scoreWrap.style.display = 'none';
                        if (scoreInp)
                            scoreInp.value = '';
                        const ok = await saveWarStatus({ termed: false, scoreTarget: null });
                        if (statusEl) {
                            statusEl.style.color = ok ? '#ACEA01' : '#e66';
                            statusEl.textContent = ok ? '✓' : '✗';
                            setTimeout(() => { statusEl.textContent = ''; }, 1500);
                        }
                    }
                    else if (val === 'unk') {
                        if (scoreWrap)
                            scoreWrap.style.display = 'contents';
                        const ok = await saveWarStatus({ termed: null });
                        if (statusEl) {
                            statusEl.style.color = ok ? '#ACEA01' : '#e66';
                            statusEl.textContent = ok ? '✓' : '✗';
                            setTimeout(() => { statusEl.textContent = ''; }, 1500);
                        }
                    }
                    else {
                        if (scoreWrap)
                            scoreWrap.style.display = 'contents';
                        const ok = await saveWarStatus({ termed: true });
                        if (statusEl) {
                            statusEl.style.color = ok ? '#ACEA01' : '#e66';
                            statusEl.textContent = ok ? '✓' : '✗';
                            setTimeout(() => { statusEl.textContent = ''; }, 1500);
                        }
                    }
                });
            });
            const saveBtn = bar.querySelector('#cat-wsb-save');
            const scoreInput = bar.querySelector('#cat-wsb-score');
            const statusEl = bar.querySelector('#cat-wsb-status');
            if (saveBtn && scoreInput) {
                saveBtn.addEventListener('click', async () => {
                    const raw = scoreInput.value.trim();
                    const num = raw === '' ? null : parseInt(raw, 10);
                    if (raw !== '' && (isNaN(num) || num < 0)) {
                        if (statusEl) {
                            statusEl.style.color = '#e66';
                            statusEl.textContent = '?';
                        }
                        return;
                    }
                    saveBtn.disabled = true;
                    saveBtn.textContent = '…';
                    const ok = await saveWarStatus({ scoreTarget: num });
                    if (statusEl) {
                        statusEl.style.color = ok ? '#ACEA01' : '#e66';
                        statusEl.textContent = ok ? '✓' : '✗';
                        setTimeout(() => { statusEl.textContent = ''; }, 1500);
                    }
                    saveBtn.disabled = false;
                    saveBtn.textContent = 'Save';
                });
                scoreInput.addEventListener('keydown', (e) => { if (e.key === 'Enter')
                    saveBtn.click(); });
            }
        }
        else {
            // Read-only display for regular members
            const parts = [];
            if (termed !== null)
                parts.push(`<span style="color:#888;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;">Termed:</span> <span style="color:${termed ? '#ACEA01' : '#e66'};font-weight:700;">${termed ? 'Yes' : 'No'}</span>`);
            if (scoreTarget !== null)
                parts.push(`<span style="color:#888;font-size:10px;text-transform:uppercase;letter-spacing:0.5px;font-weight:600;">Score target:</span> <span style="color:#ddd;font-weight:700;">${scoreTarget}</span>`);
            if (parts.length === 0)
                return;
            bar.innerHTML = parts.join('<span style="color:#333;margin:0 4px;">|</span>');
        }
        // Insert at the top of factionWarInfo
        factionWarInfo.insertBefore(bar, factionWarInfo.firstChild);
    }

    function startPeriodicEnhancement() {
    }
    async function checkEnlistedStatus() {
        if (!this.apiManager.torn_apikey) {
            this.warStatus = 'not_enlisted';
            return;
        }
        const factionId = StorageUtil.get('cat_user_faction_id', null);
        if (!factionId) {
            this.warStatus = 'not_enlisted';
            return;
        }
        this.warStatus = 'checking';
        const badge = document.getElementById('cat-enlisted-badge');
        if (badge) {
            badge.className = 'cat-info-badge checking';
            badge.textContent = '...';
        }
        try {
            const rankedwars = await this.apiManager.fetchRankedWarsData();
            if (rankedwars && rankedwars.length > 0) {
                const currentWar = rankedwars.find(w => !w.end && !w.winner);
                if (currentWar) {
                    this.warStatus = 'enlisted';
                    const factionList = currentWar.factions
                        ? (Array.isArray(currentWar.factions) ? currentWar.factions : Object.values(currentWar.factions))
                        : [];
                    const enemyFaction = factionList.find(f => String(f.id) !== String(factionId));
                    this.warEnemyName = enemyFaction?.name ?? null;
                }
                else {
                    this.warStatus = 'not_enlisted';
                    this.warEnemyName = null;
                }
            }
            else {
                this.warStatus = 'not_enlisted';
                this.warEnemyName = null;
            }
        }
        catch (e) {
            console.log('[CAT] Error checking enlisted status:', e);
            this.apiManager.reportError('checkEnlistedStatus', e);
            this.warStatus = 'not_enlisted';
            this.warEnemyName = null;
        }
        this.updateEnlistedBadge();
    }
    function updateEnlistedBadge() {
        const badge = document.getElementById('cat-enlisted-badge');
        if (!badge)
            return;
        const baseStyle = 'display:inline-block;padding:2px 8px;border-radius:3px;font-size:10px;font-weight:700;letter-spacing:0.5px;text-transform:uppercase;line-height:1.4;vertical-align:middle;';
        if (this.warStatus === 'enlisted') {
            badge.className = 'cat-info-badge enlisted';
            badge.style.cssText = baseStyle + 'background:rgba(72,187,120,0.2);color:#48bb78;border:1px solid rgba(72,187,120,0.4);';
            badge.textContent = 'In War';
        }
        else if (this.warStatus === 'checking') {
            badge.className = 'cat-info-badge checking';
            badge.style.cssText = baseStyle + 'background:rgba(237,137,54,0.15);color:#ed8936;border:1px solid rgba(237,137,54,0.3);';
            badge.textContent = '...';
        }
        else {
            badge.className = 'cat-info-badge not-enlisted';
            badge.style.cssText = baseStyle + 'background:rgba(160,174,192,0.15);color:#718096;border:1px solid rgba(160,174,192,0.3);';
            badge.textContent = 'No War';
        }
    }
    async function checkActivationStatus(retryCount = 0) {
        const userFactionId = StorageUtil.get('cat_user_faction_id', null);
        if (!userFactionId || !this.apiManager.authToken) {
            this.activationStatus = 'activated';
            return;
        }
        this.activationStatus = 'loading';
        try {
            const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/${encodeURIComponent(userFactionId)}`, {
                method: 'GET',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.apiManager.authToken}`
                }
            });
            if (!response.ok) {
                console.log('[CAT] Subscription check failed, fail-closed');
                // Retry once after 2s on mobile (network timing issues)
                if (retryCount < 1) {
                    console.log('[CAT] Retrying subscription check in 2s...');
                    setTimeout(() => this.checkActivationStatus(retryCount + 1), 2000);
                    return;
                }
                this.activationStatus = 'error';
                this.applyReadOnlyMode();
                return;
            }
            const data = await response.json();
            if (!data.success) {
                console.log('[CAT] Subscription data.success=false, fail-closed');
                // Retry once after 2s
                if (retryCount < 1) {
                    console.log('[CAT] Retrying subscription check in 2s...');
                    setTimeout(() => this.checkActivationStatus(retryCount + 1), 2000);
                    return;
                }
                this.activationStatus = 'error';
                this.applyReadOnlyMode();
                return;
            }
            this.subscriptionData = data;
            // Update BS access cache from server, then apply visibility
            const hasBsAccess = !!data.hasBsAccess;
            StorageUtil.set('cat_bs_access', hasBsAccess ? 'true' : 'false');
            const userWantsBs = String(StorageUtil.get('cat_show_warhelper_bs', 'true')) === 'true';
            if (hasBsAccess && userWantsBs) {
                document.body.classList.remove('cat-hide-warhelper-bs');
            }
            else {
                document.body.classList.add('cat-hide-warhelper-bs');
            }
            // Determine canActivateWar: admin, leader/co-leader, or delegate (from server DB)
            this.canActivateWar = !!data.isAdmin || !!data.isLeader || !!data.isDelegate;
            // Check if admin viewing another faction
            const isAdmin = !!data.isAdmin;
            // Store war status in localStorage for cross-page info
            localStorage.setItem('cat_has_active_war', data.currentWar ? '1' : '0');
            // Parse viewingFactionId directly from URL if not set yet (timing issue)
            let viewingFactionId = state.viewingFactionId;
            if (!viewingFactionId && window.location.search.includes('step=profile')) {
                const idMatch = window.location.search.match(/ID=(\d+)/);
                if (idMatch) {
                    viewingFactionId = idMatch[1];
                }
            }
            const viewingOtherFaction = state.catOtherFaction && viewingFactionId && viewingFactionId !== userFactionId;
            if (isAdmin && viewingOtherFaction) {
                // Cache isAdmin for instant button display on next page load (skips API round-trip)
                StorageUtil.set('cat_is_admin_cached', 'true');
                // Admin viewing another faction → allow editing, skip read-only mode
                this.activationStatus = 'activated';
                this.removeReadOnlyMode();
                // Force re-check of factions to create call buttons now that we know user is admin
                // removeReadOnlyMode() is synchronous — no delay needed
                if (this.enhancementManager) {
                    this.enhancementManager._enemyFactionEnhanced = false;
                    this.enhancementManager._checkFactions();
                }
                return;
            }
            // Not admin viewing another faction → clear admin cache
            StorageUtil.set('cat_is_admin_cached', 'false');
            if (!data.currentWar) {
                StorageUtil.set('cat_activation_cached', 'false');
                this.activationStatus = 'no_war';
                this.applyReadOnlyMode();
                return;
            }
            if (data.isActivatedForCurrentWar) {
                StorageUtil.set('cat_activation_cached', 'true');
                this.activationStatus = 'activated';
                this.removeReadOnlyMode();
            }
            else {
                StorageUtil.set('cat_activation_cached', 'false');
                this.activationStatus = 'not_activated';
                this.applyReadOnlyMode();
            }
        }
        catch (e) {
            console.log('[CAT] Subscription check error, fail-closed:', e);
            // Retry once after 2s on error (timeout, network issues on mobile)
            if (retryCount < 1) {
                console.log('[CAT] Retrying subscription check in 2s...');
                setTimeout(() => this.checkActivationStatus(retryCount + 1), 2000);
                return;
            }
            this.apiManager.reportError('checkActivationStatus', e);
            this.activationStatus = 'error';
            this.applyReadOnlyMode();
        }
    }
    function applyReadOnlyMode() {
        document.body.classList.add('cat-read-only');
        this.showActivationBanner();
    }
    function removeReadOnlyMode() {
        document.body.classList.remove('cat-read-only');
        document.documentElement.classList.remove('cat-other-faction');
        this.hideActivationBanner();
    }
    async function fetchDynamicPrice() {
        try {
            const factionId = StorageUtil.get('cat_user_faction_id', null);
            const apiKey = this.apiManager.torn_apikey;
            if (!factionId || !apiKey) {
                console.log('[Price] Missing factionId or apiKey, using default');
                this.currentPrice = 30;
                this.currentRankTier = 'gold';
                return;
            }
            const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/price?factionId=${factionId}&apiKey=${apiKey}`, { method: 'GET', headers: { 'Authorization': `Bearer ${this.apiManager.authToken}` } });
            const res = await response.json();
            if (res.success && res.price !== undefined && res.rankTier) {
                this.currentPrice = res.price;
                this.currentRankTier = res.rankTier;
                // Re-render banner only if it already exists (avoid triggering fetchDynamicPrice again)
                if (document.getElementById('cat-activation-banner')) {
                    this._priceAlreadyFetched = true;
                    this.showActivationBanner();
                }
            }
        }
        catch (err) {
            console.log('[Price] Fetch failed, using default', err);
            this.currentPrice = 30;
            this.currentRankTier = 'gold';
        }
    }
    function showActivationBanner() {
        this.hideActivationBanner();
        // Only show on faction pages (not profiles, etc.)
        if (!window.location.pathname.includes('factions.php'))
            return;
        // Fetch dynamic price in background (will re-render when complete) — only once
        if ((!this.currentPrice || this.currentPrice === 30) && !this._priceAlreadyFetched) {
            this.fetchDynamicPrice().catch(() => {
                // Silent catch - errors already logged in fetchDynamicPrice
            });
        }
        const container = document.querySelector('.desc-wrap') || document.querySelector('#faction_war_list_id');
        if (!container)
            return;
        const sub = this.subscriptionData?.subscription;
        const hasData = !!sub;
        const balance = sub?.xanax_balance ?? 0;
        const trialAvailable = !sub?.trial_used;
        const card = document.createElement('div');
        card.id = 'cat-activation-banner';
        card.style.cssText = 'display:flex;align-items:center;gap:8px;padding:5px 10px;background:#333;border:1px solid rgba(255,255,255,0.06);border-radius:4px;margin:0 0 4px 0;flex-wrap:wrap;';
        const btnStyle = 'cursor:pointer;font-size:9px;font-weight:600;color:#ddd;background:linear-gradient(#646464,#343434);border:1px solid rgba(255,255,255,0.15);border-radius:4px;padding:2px 6px;user-select:none;letter-spacing:0.3px;text-shadow:0 1px 0 rgba(0,0,0,0.75);display:inline-block;vertical-align:middle;line-height:1.3;white-space:nowrap;';
        let html = `<span style="font-size:10px;font-weight:600;color:#FF794C;white-space:nowrap;">\u26A0\uFE0F CAT Script not activated</span>`;
        // Show balance only if we have data, otherwise show loading indicator
        if (hasData) {
            const hasEnough = balance >= this.currentPrice;
            html += `<span style="font-size:9px;color:#888;white-space:nowrap;"><strong style="color:${hasEnough ? '#ACEA01' : '#e66'};">${balance}</strong> xanax</span>`;
            // Add rank badge
            const rankColors = {
                metal: '#8B7355',
                bronze: '#CD7F32',
                silver: '#C0C0C0',
                gold: '#FFD700',
                platinum: '#E5E4E2'
            };
            const rankColor = rankColors[this.currentRankTier] || '#FFD700';
            html += `<span style="font-size:9px;color:${rankColor};white-space:nowrap;opacity:0.8;">${this.currentRankTier.toUpperCase()}</span>`;
        }
        else {
            html += `<span style="font-size:9px;color:#888;white-space:nowrap;">Loading...</span>`;
        }
        const isAdmin = !!this.subscriptionData?.isAdmin;
        if (this.canActivateWar && (hasData || isAdmin)) {
            if (trialAvailable || isAdmin) {
                html += `<button id="cat-activate-trial" style="${btnStyle}">Trial</button>`;
            }
            if (balance >= this.currentPrice) {
                html += `<button id="cat-activate-paid" style="${btnStyle}">Activate (${this.currentPrice})</button>`;
            }
            if (!trialAvailable && !isAdmin && balance < this.currentPrice) {
                html += `<span style="font-size:10px;color:#e66;white-space:nowrap;">Insufficient balance (need ${this.currentPrice} xanax).</span>`;
            }
        }
        else if (!hasData) {
            html += `<span style="font-size:10px;color:#888;white-space:nowrap;">Open Plan tab to load data.</span>`;
        }
        else {
            html += `<span style="font-size:10px;color:#888;white-space:nowrap;">Ask your leader to activate.</span>`;
        }
        card.innerHTML = html;
        container.insertBefore(card, container.firstChild);
        const trialBtn = card.querySelector('#cat-activate-trial');
        if (trialBtn)
            trialBtn.addEventListener('click', () => this.activateWar(true));
        const paidBtn = card.querySelector('#cat-activate-paid');
        if (paidBtn)
            paidBtn.addEventListener('click', () => this.activateWar(false));
    }
    function hideActivationBanner() {
        const existing = document.getElementById('cat-activation-banner');
        if (existing)
            existing.remove();
    }
    async function activateWar(useTrial) {
        const sub = this.subscriptionData;
        const isAdmin = !!sub?.isAdmin;
        if (!sub || (!sub.currentWar && !isAdmin)) {
            if (useTrial)
                this.apiManager.showNotification("You're not matched up in a war yet.", 'warning');
            return;
        }
        const userFactionId = StorageUtil.get('cat_user_faction_id', null);
        if (!userFactionId)
            return;
        const trialBtn = document.getElementById('cat-activate-trial');
        const paidBtn = document.getElementById('cat-activate-paid');
        if (trialBtn) {
            trialBtn.disabled = true;
            trialBtn.textContent = '...';
        }
        if (paidBtn) {
            paidBtn.disabled = true;
            paidBtn.textContent = '...';
        }
        try {
            const response = await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/subscription/activate`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.apiManager.authToken}`
                },
                body: JSON.stringify({
                    factionId: userFactionId,
                    warId: sub.currentWar?.war_id ?? null,
                    useTrial: useTrial,
                    tornApiKey: this.apiManager.torn_apikey
                })
            });
            const data = await response.json();
            if (data.success) {
                console.log(`%c[CAT] War activated (${data.activation?.activation_type})`, 'color:#ACEA01;font-weight:bold;');
                this.activationStatus = 'activated';
                this.removeReadOnlyMode();
            }
            else {
                const errorMsg = data.error === 'already_activated' ? 'Already activated!'
                    : data.error === 'trial_already_used' ? 'Free trial already used.'
                        : data.error === 'insufficient_balance' ? 'Insufficient balance.'
                            : data.error === 'Only leader or co-leader can activate' ? 'Only leader or co-leader can activate.'
                                : (data.error || '').toLowerCase().includes('war') ? "You're not matched up in a war yet."
                                    : data.message || data.error || 'Activation failed.';
                this.apiManager.showNotification(errorMsg, 'warning');
                await this.checkActivationStatus();
            }
        }
        catch (e) {
            console.log('[CAT] Activation error:', e);
            this.apiManager.reportError('activateWar', e);
            alert('Activation error. Please try again.');
            if (trialBtn) {
                trialBtn.disabled = false;
                trialBtn.textContent = 'Free trial (1st war)';
            }
            if (paidBtn) {
                paidBtn.disabled = false;
                paidBtn.textContent = `Activate (${this.currentPrice} xanax)`;
            }
        }
    }

    const TRAVEL_MODE_KEY = 'cat_travel_eta_mode';
    const MODE_LABELS = {
        standard: 'Standard',
        airstrip: 'PI',
        wlt: 'WLT',
        bct: 'BCT',
    };
    /**
     * Read the user's preferred travel speed mode from storage.
     */
    function getTravelMode() {
        const stored = StorageUtil.get(TRAVEL_MODE_KEY, 'airstrip');
        if (stored === 'standard' || stored === 'airstrip' || stored === 'wlt' || stored === 'bct')
            return stored;
        return 'airstrip';
    }
    /**
     * Compute arrival timestamps for ALL travel modes at once (for tooltip).
     */
    function computeAllETAs(area, departedAt, fromArea) {
        const result = { standard: null, airstrip: null, wlt: null, bct: null };
        if (!departedAt)
            return result;
        const lookupArea = area === 1 ? fromArea : area;
        if (!lookupArea)
            return result;
        const times = CONFIG.travelTimes[lookupArea];
        if (!times)
            return result;
        const now = Date.now();
        for (const mode of ['standard', 'airstrip', 'wlt', 'bct']) {
            const arrivalMs = departedAt + times[mode] * 1000;
            result[mode] = arrivalMs > now ? arrivalMs : null;
        }
        return result;
    }
    /**
     * Build tooltip text showing all 4 mode ETAs.
     * Active mode is marked with ▸, passed modes show "landed".
     */
    function buildEtaTooltip(allEtas, activeMode) {
        const modes = ['bct', 'wlt', 'airstrip', 'standard'];
        const now = Date.now();
        const lines = [];
        for (const mode of modes) {
            const etaMs = allEtas[mode];
            const label = MODE_LABELS[mode].padEnd(8);
            const prefix = mode === activeMode ? '\u25B8 ' : '  ';
            if (etaMs && etaMs > now) {
                const formatted = formatETA(etaMs);
                lines.push(`${prefix}${label} ~${formatted}`);
            }
            else {
                lines.push(`${prefix}${label} landed`);
            }
        }
        return lines.join('\n');
    }
    /**
     * Format remaining milliseconds into a human-readable countdown.
     * Returns "HH:MM" or "MM min" or "" if already arrived.
     * No seconds — travel times have ~3% variance so precision is approximate.
     */
    function formatETA(arrivalMs) {
        const remaining = arrivalMs - Date.now();
        if (remaining <= 0)
            return '';
        const totalMinutes = Math.ceil(remaining / 60000);
        const hours = Math.floor(totalMinutes / 60);
        const minutes = totalMinutes % 60;
        if (hours > 0) {
            return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
        }
        return `${minutes}min`;
    }

    function registerHospNode(id, node) {
        if (!node || !id)
            return;
        const idx = this.hospNodes.findIndex((h) => h[0] == id);
        if (idx !== -1) {
            if (!this.hospNodes[idx][1].isConnected) {
                this.hospNodes[idx][1] = node;
            }
        }
        else {
            this.hospNodes.push([id, node]);
        }
        if (this.previousStatus[id]) {
            const areaName = CONFIG.areas[this.previousStatus[id].area] || 'Unknown';
            const li = node._catLi || (node._catLi = node.closest('li'));
            if (li) {
                li.setAttribute('data-abroad-hosp', '1');
                li.setAttribute('data-abroad-dest', areaName);
                li.title = `Was Abroad (${areaName})`;
            }
            node.setAttribute('data-abroad-dest', areaName);
        }
    }
    function injectLevelIndicators() {
        // Use cached member rows if valid, otherwise re-query
        if (!this._memberRowsValid || !this._cachedMemberRows) {
            this._cachedMemberRows = Array.from(document.querySelectorAll('.desc-wrap li[class*="member___"], .desc-wrap li.enemy, .desc-wrap li.your'));
            this._memberRowsValid = true;
            // Invalidate global level hash so all rows re-check
            this._catLevelGlobalHash = '';
        }
        const memberRows = this._cachedMemberRows;
        // Global version-guard: skip entire loop if revivableCache + onlineStatuses version unchanged
        // Callers bump _revivableVersion / _onlineStatusVersion whenever those objects change
        const revV = this._revivableVersion || 0;
        const onlineV = this._onlineStatusVersion || 0;
        const globalHash = `${memberRows.length}|${revV}|${onlineV}`;
        if (globalHash === this._catLevelGlobalHash)
            return;
        // If only onlineV changed (revV + count same), skip the indicator rebuild loop.
        // Interceptors already applied online status directly to DOM elements — no loop needed.
        const prevHash = this._catLevelGlobalHash || '';
        const onlyOnlineChanged = prevHash.length > 0 && prevHash.split('|')[1] === String(revV) && prevHash.split('|')[0] === String(memberRows.length);
        this._catLevelGlobalHash = globalHash;
        if (onlyOnlineChanged)
            return;
        for (let i = 0; i < memberRows.length; i++) {
            const row = memberRows[i];
            if (!row.isConnected) {
                this._memberRowsValid = false;
                continue;
            }
            // Cache levelEl + honorWrap as direct JS refs on row (faster than getElementById)
            let levelEl = row._catLevelEl || null;
            if (levelEl && !levelEl.isConnected)
                levelEl = null;
            if (!levelEl) {
                levelEl = row.querySelector('[class*="level___"]');
                if (levelEl)
                    row._catLevelEl = levelEl;
            }
            let honorWrap = row._catHonorWrap || null;
            if (honorWrap && !honorWrap.isConnected)
                honorWrap = null;
            if (!honorWrap) {
                honorWrap = row.querySelector('[class*="honorWrap___"]');
                if (honorWrap)
                    row._catHonorWrap = honorWrap;
            }
            if (!levelEl || !honorWrap)
                continue;
            const level = (levelEl.textContent || '').trim();
            if (!level)
                continue;
            // Extract userId from row — cache in data-cat-uid
            let userId = row.dataset.catUid || null;
            if (!userId) {
                const profileLink = row.querySelector('a[href*="profiles.php?XID="]');
                if (profileLink) {
                    const m = profileLink.href.match(/XID=(\d+)/);
                    if (m)
                        userId = m[1];
                }
                if (!userId) {
                    const attackLink = row.querySelector('a[href*="user2ID"]');
                    if (attackLink) {
                        const m = attackLink.href.match(/user2ID=(\d+)/);
                        if (m)
                            userId = m[1];
                    }
                }
                if (userId)
                    row.dataset.catUid = userId;
            }
            // Determine suffix: abroad destination (only for abroad-hosp rows, skip querySelector otherwise)
            let suffix = '';
            let suffixColor = '';
            if (row.hasAttribute('data-abroad-hosp')) {
                // Use cached statusEl abroad dest to avoid querySelector
                const abroadDest = row._catAbroadDest
                    || row.querySelector('[data-abroad-dest]')?.dataset.abroadDest;
                if (abroadDest) {
                    row._catAbroadDest = abroadDest;
                    suffix = `- ${abroadDest}`;
                    suffixColor = '#4FC4F7';
                }
            }
            else {
                row._catAbroadDest = undefined;
            }
            const isRevivable = !!(userId && this.revivableCache?.[userId] === true);
            // Hash-guard: skip DOM update if nothing changed since last inject
            const rowHash = `${level}|${suffix}|${userId}|${isRevivable ? '1' : '0'}`;
            if (row._catLevelHash === rowHash)
                continue;
            row._catLevelHash = rowHash;
            // Build target content
            let targetHTML;
            const hasHTML = suffix.length > 0;
            if (hasHTML) {
                targetHTML = `Lvl ${level} <span style="color:${suffixColor}!important;font-size:7px!important;font-weight:bold!important">${suffix}</span>`;
            }
            else {
                targetHTML = `Lvl ${level}`;
            }
            // Create or update indicator (cached on row to avoid querySelector every second)
            let indicator = row._catIndicator || null;
            if (indicator && !indicator.isConnected)
                indicator = null;
            if (!indicator) {
                indicator = document.createElement('span');
                indicator.className = 'cat-level-indicator';
                indicator.dataset.cat = '1';
                indicator.style.cssText = 'font-size:7px!important;text-decoration:none!important;border:none!important;display:block!important;line-height:1!important;margin:0!important;padding:0!important;background:none!important;position:absolute!important;bottom:-8px!important;left:0!important;overflow:visible!important;';
                honorWrap.style.position = 'relative';
                honorWrap.style.overflow = 'visible';
                honorWrap.appendChild(indicator);
                row._catIndicator = indicator;
            }
            // Update indicator text content
            if (hasHTML) {
                if (indicator.innerHTML !== targetHTML)
                    indicator.innerHTML = targetHTML;
            }
            else {
                const textNode = indicator.firstChild;
                if (textNode && textNode.nodeType === Node.TEXT_NODE) {
                    if (textNode.textContent !== targetHTML)
                        textNode.textContent = targetHTML;
                }
                else {
                    indicator.innerHTML = targetHTML;
                }
            }
            // Inject revive status icon into indicator
            injectReviveIconInIndicator(row, indicator, isRevivable);
            // Inject loadout button as sibling after honorWrap (inside userInfoBox flex row) — re-inject if disconnected or missing
            let loadoutBtn = row._catLoadoutBtn || null;
            if (loadoutBtn && !loadoutBtn.isConnected)
                loadoutBtn = null;
            if (userId && honorWrap && !loadoutBtn) {
                const btn = document.createElement('span');
                btn.className = 'cat-loadout-btn';
                btn.dataset.catLoadoutId = userId;
                btn.innerHTML = `<img src="${LOADOUT_ICON_B64}" width="12" height="12" style="display:block;image-rendering:auto;">`;
                // Show immediately if loadout already cached (in-memory cache with war-key validation)
                btn.style.display = hasLoadoutCached(userId) ? 'flex' : 'none';
                honorWrap.insertAdjacentElement('afterend', btn);
                row._catLoadoutBtn = btn;
            }
            // Apply online status color if we have data
            if (userId && this.onlineStatuses) {
                const status = this.onlineStatuses[userId];
                if (status) {
                    let nameEl = row._catNameEl || null;
                    if (nameEl && (!nameEl.isConnected || (!nameEl.className.includes('honor-text-wrap') && !nameEl.className.includes('honorWrap___'))))
                        nameEl = null;
                    if (!nameEl) {
                        // Prefer .honor-text-wrap — CSS targets [data-online-status] on this element
                        nameEl = row.querySelector('.honor-text-wrap') ||
                            row.querySelector('[class*="honorWrap___"]');
                        if (nameEl)
                            row._catNameEl = nameEl;
                    }
                    if (nameEl && nameEl.dataset.onlineStatus !== status) {
                        nameEl.dataset.onlineStatus = status;
                    }
                    if (row.dataset.catStatus !== status) {
                        row.dataset.catStatus = status;
                    }
                }
            }
        }
    }
    function scanHospitalizedMembers() {
        // Fast-path: all known hosp nodes are connected — nothing new to discover.
        // This guard applies whether _hospScanNeeded is true or false:
        // if allConnected, we already have every hospitalized player registered,
        // and updateHospTimers handles the display. Skip the full scan.
        if (this._hospNodesAllConnected && this.hospNodes.length > 0 && !(this._hospPass2Needed)) {
            this._hospScanNeeded = false;
            return;
        }
        if (!this._hospScanNeeded) {
            // No hosp data at all — nothing to scan
            // Avoid Object.keys() allocation: use for...in to check emptiness in O(1) best case
            let _htEmpty = true;
            // eslint-disable-next-line no-unreachable-loop
            for (const _k in this.hospTime) {
                _htEmpty = false;
                break;
            }
            if (_htEmpty && this.hospNodes.length === 0)
                return;
            // hospTime has entries but no DOM nodes yet (or all disconnected) — only re-scan
            // when pass2 is needed (structural DOM change), not on every tick.
            if (!_htEmpty && this.hospNodes.length === 0 && this._hospPass2Needed === false)
                return;
        }
        // Reset the "no patients found" sentinel when entering via a real event (_hospScanNeeded was true)
        // so new patients can be discovered by pass2.
        this._hospPass2DoneClean = false;
        this._hospScanNeeded = false;
        // Get member rows — prefer cache, fall back to live DOM query
        const allRows = (this._memberRowsValid && this._cachedMemberRows && this._cachedMemberRows.length > 0)
            ? this._cachedMemberRows
            : Array.from(document.querySelectorAll('.desc-wrap li[class*="member___"], .desc-wrap li.enemy, .desc-wrap li.your'));
        // If DOM has no rows yet but we have hospTime entries, retry at 100ms intervals (max 30 tries)
        if (allRows.length === 0 && Object.keys(this.hospTime).length > 0) {
            this._hospScanNeeded = true;
            const retries = (this._hospDomRetries || 0) + 1;
            this._hospDomRetries = retries;
            if (retries <= 30) {
                setTimeout(() => {
                    this._hospScanNeeded = true;
                    this.scanHospitalizedMembers();
                }, 100);
            }
            return;
        }
        this._hospDomRetries = 0;
        // Build uid→row index once — O(N rows), then O(1) per lookup.
        // injectLevelIndicators + scanAllFactionStatuses pre-populate row.dataset.catUid on most rows,
        // so we only fall back to querySelector for rows that don't have it yet.
        let rowByUid = this._catRowByUid || null;
        if (!rowByUid || !this._memberRowsValid) {
            rowByUid = {};
            for (let _i = 0; _i < allRows.length; _i++) {
                const row = allRows[_i];
                let uid = row.dataset.catUid;
                if (!uid) {
                    // Fallback: extract from link (first-run or rows not yet visited by injectLevel)
                    const a = row.querySelector('a[href*="profiles.php?XID="], a[href*="user2ID"]');
                    if (a) {
                        const m2 = a.href.match(/(?:XID|user2ID)=(\d+)/);
                        if (m2) {
                            uid = m2[1];
                            row.dataset.catUid = uid;
                        }
                    }
                }
                if (uid)
                    rowByUid[uid] = row;
            }
            this._catRowByUid = rowByUid;
        }
        // Pass 1: process known hospTime entries — O(hospTime)
        const hospIds = Object.keys(this.hospTime);
        // If rows are present but rowByUid has none of our hospTime IDs, links may not be rendered yet.
        // Retry at 100ms until resolved (max 20 tries = 2s).
        if (hospIds.length > 0 && allRows.length > 0 && Object.keys(rowByUid).length === 0) {
            const retries = (this._hospDomRetries || 0) + 1;
            this._hospDomRetries = retries;
            if (retries <= 20) {
                this._catRowByUid = null; // force rebuild next time
                this._memberRowsValid = false;
                setTimeout(() => {
                    this._hospScanNeeded = true;
                    this.scanHospitalizedMembers();
                }, 100);
            }
            return;
        }
        let _pass1NewNodes = 0;
        for (let _hi = 0; _hi < hospIds.length; _hi++) {
            const memberId = hospIds[_hi];
            const row = rowByUid[memberId];
            if (!row || !row.isConnected)
                continue;
            let statusEl = row._catStatusEl || null;
            if (statusEl && !statusEl.isConnected)
                statusEl = null;
            if (!statusEl) {
                statusEl = row.querySelector('[class*="status___"], .status.left');
                if (statusEl)
                    row._catStatusEl = statusEl;
            }
            if (!statusEl)
                continue;
            const text = (statusEl.textContent || '').trim();
            // If CAT's countdown is already showing, re-register the node if not yet in hospNodes
            // (happens on page refresh: text persists but hospNodes is freshly empty)
            if (/^\d{2}:\d{2}:\d{2}$/.test(text)) {
                if (text === '00:00:00') {
                    const cn = statusEl.className;
                    const isNonHosp = cn.includes('okay') || cn.includes('traveling') || cn.includes('abroad') ||
                        cn.includes('jail') || cn.includes('federal') || cn.includes('fallen');
                    if (isNonHosp) {
                        const correctStatus = cn.includes('okay') ? 'Okay' : cn.includes('traveling') ? 'Traveling' :
                            cn.includes('abroad') ? 'Abroad' : cn.includes('jail') ? 'Jail' :
                                cn.includes('federal') ? 'Federal' : 'Fallen';
                        statusEl.textContent = correctStatus;
                        delete statusEl.dataset.catHospStatus;
                        delete this.hospTime[memberId];
                        delete this.previousStatus[memberId];
                    }
                    continue;
                }
                // Countdown text in DOM + hospTime entry exists → CAT owns this node.
                // Re-register if missing from hospNodes (page refresh: dataset lost but hospTime persists).
                // catHospStatus may be absent after refresh (datasets don't survive page reload).
                const alreadyRegistered = this.hospNodes.some(([nid]) => nid === memberId);
                if (!alreadyRegistered) {
                    // Claim ownership before registering so updateHospTimers won't skip it
                    statusEl.dataset.catHospStatus = 'Hospital';
                    const prevLen = this.hospNodes.length;
                    this.registerHospNode(memberId, statusEl);
                    if (this.hospNodes.length > prevLen)
                        _pass1NewNodes++;
                }
                continue;
            }
            const cn = statusEl.className;
            const isNonHospClass = cn.includes('okay') || cn.includes('traveling') || cn.includes('abroad') ||
                cn.includes('jail') || cn.includes('federal') || cn.includes('fallen');
            if (isNonHospClass) {
                delete this.hospTime[memberId];
                delete this.previousStatus[memberId];
                continue;
            }
            if (text !== 'Hospital')
                statusEl.textContent = 'Hospital';
            const prevLen = this.hospNodes.length;
            this.registerHospNode(memberId, statusEl);
            if (this.hospNodes.length > prevLen)
                _pass1NewNodes++;
        }
        // Pass 2: scan DOM for "Hospital" text not yet in hospTime — discovers new patients.
        // Skip when all hospTime entries were found in pass 1 and no new nodes were registered
        // (meaning no unknown-hospital players exist). Set _hospPass2Needed=true from outside
        // when a structural change may have introduced new hospitalized players.
        // Pass2: scan all rows for "Hospital" text not yet in hospTime.
        // Run when: explicitly flagged (_hospPass2Needed), new nodes found in pass1,
        // or first-ever scan (no hospTime and no nodes and not yet done once clean).
        const _pass2FirstRun = hospIds.length === 0 && this.hospNodes.length === 0 && !this._hospPass2DoneClean;
        const needPass2 = this._hospPass2Needed !== false || _pass1NewNodes > 0 || _pass2FirstRun;
        if (needPass2) {
            this._hospPass2Needed = false;
            let _pass2Found = 0;
            for (let _ri = 0; _ri < allRows.length; _ri++) {
                const row = allRows[_ri];
                if (!row.isConnected)
                    continue;
                const uid = row.dataset.catUid;
                if (uid && this.hospTime[uid])
                    continue; // already handled in pass 1
                let statusEl = row._catStatusEl || null;
                if (statusEl && !statusEl.isConnected)
                    statusEl = null;
                if (!statusEl) {
                    statusEl = row.querySelector('[class*="status___"], .status.left');
                    if (statusEl)
                        row._catStatusEl = statusEl;
                }
                if (!statusEl)
                    continue;
                const p2text = (statusEl.textContent || '').trim();
                // Skip nodes with an active countdown — PDA or CAT already owns them
                if (/^\d{2}:\d{2}:\d{2}$/.test(p2text))
                    continue;
                const p2isHosp = p2text === 'Hospital' || statusEl.className.includes('hospital');
                if (!p2isHosp)
                    continue;
                if (!uid)
                    continue;
                // Normalize text so updateHospTimers recognizes it (e.g. stale travel text)
                if (p2text !== 'Hospital')
                    statusEl.textContent = 'Hospital';
                this.registerHospNode(uid, statusEl);
                _pass2Found++;
            }
            // Mark clean if this was a first-run pass2 that found nothing — skip future empty scans
            if (_pass2FirstRun && _pass2Found === 0) {
                this._hospPass2DoneClean = true;
            }
        }
        // Hospital timers with missing endTime will be filled when Torn's own
        // periodic refresh (getwarusers / getwardata) is intercepted passively.
    }
    function updateHospTimers() {
        if (this.hospNodes.length === 0)
            return;
        for (let _fi = this.hospNodes.length - 1; _fi >= 0; _fi--) {
            if (!this.hospNodes[_fi][1] || !this.hospNodes[_fi][1].isConnected) {
                this.hospNodes.splice(_fi, 1);
            }
        }
        // Signal to scanHospitalizedMembers (runs before us next tick) that all nodes are connected.
        // Even after a prune, the remaining nodes are by definition all connected (we just removed the bad ones).
        this._hospNodesAllConnected = this.hospNodes.length > 0;
        if (this.hospNodes.length === 0)
            return;
        // Gate: only run the timer-display loop when the wall-clock second has changed.
        // Countdowns change 1×/s — no need to loop 49 nodes at 10 ticks/s.
        // Exception: run immediately if any timer is expired so we clean up without 1s delay.
        const _nowMs = Date.now();
        const _nowSec = Math.floor(_nowMs / 1000);
        if (this._catLastHospSec === _nowSec) {
            let _hasExpired = false;
            for (let _ei = 0; _ei < this.hospNodes.length; _ei++) {
                const _eid = this.hospNodes[_ei][0];
                if (this.hospTime[_eid] && Math.floor(((this.hospTime[_eid] > 9999999999 ? this.hospTime[_eid] : this.hospTime[_eid] * 1000) - _nowMs) / 1000) <= 0) {
                    _hasExpired = true;
                    break;
                }
            }
            if (!_hasExpired)
                return;
        }
        this._catLastHospSec = _nowSec;
        const _now = Date.now();
        for (let i = 0; i < this.hospNodes.length; i++) {
            const [id, node] = this.hospNodes[i];
            if (!this.hospTime[id]) {
                // No timer yet — check if still showing Hospital (timer may arrive via updateIcons shortly)
                const nodeText = node.textContent?.trim() || '';
                const stillHosp = nodeText === 'Hospital' || nodeText === '00:00:00' ||
                    node.dataset.catHospStatus === 'Hospital';
                if (!stillHosp) {
                    // Status changed — drop the node
                    this.hospNodes.splice(i, 1);
                    i--;
                }
                // Keep node in hospNodes even without timer — updateIcons may deliver timer soon
                continue;
            }
            // Once CAT owns the display (catHospStatus set), only check the CSS class for early-release
            // detection on seconds-boundaries — className read is cheap but doing it 10×/s × 70 nodes adds up.
            const nodeEl2 = node;
            const catOwns = !!nodeEl2.dataset.catHospStatus;
            const endMs = this.hospTime[id] > 9999999999 ? this.hospTime[id] : this.hospTime[id] * 1000;
            const floorSecs = Math.floor((endMs - _now) / 1000);
            // Compare as number — avoid String() allocation on every node every tick
            const prevSecs = catOwns ? nodeEl2._catHospSecsNum ?? -999 : -999;
            const secondChanged = catOwns && prevSecs !== floorSecs;
            // Skip className check on sub-second ticks when CAT owns and display is up to date
            const checkClass = !catOwns || secondChanged;
            const cn = checkClass ? node.className : '';
            const isNonHospCn = checkClass && (cn.includes('okay') || cn.includes('traveling') || cn.includes('abroad') ||
                cn.includes('jail') || cn.includes('federal') || cn.includes('fallen'));
            if (isNonHospCn) {
                // Torn changed CSS class — player left hospital
                const isNowAbroad = cn.includes('abroad');
                const isNowTraveling = cn.includes('traveling');
                const prevStatus = this.previousStatus[id];
                if (isNowAbroad || isNowTraveling) {
                    const cleanLiEarly = node._catLi || (node._catLi = node.closest('li'));
                    // Resolve area: travelData set by interceptor → previousStatus → data-abroad-dest → cat-level-indicator
                    let areaNum = this.travelData[id]?.area || prevStatus?.area;
                    if (!areaNum) {
                        const destName = node.dataset.abroadDest ||
                            (cleanLiEarly && cleanLiEarly.dataset.abroadDest);
                        if (destName) {
                            const found = Object.entries(CONFIG.areas).find(([, n]) => n === destName);
                            if (found)
                                areaNum = Number(found[0]);
                        }
                    }
                    if (!areaNum && cleanLiEarly) {
                        // Last resort: read country from cat-level-indicator "Lvl N - Country"
                        const lvlEl = cleanLiEarly.querySelector('.cat-level-indicator span[style*="color"]');
                        if (lvlEl) {
                            const lvlText = (lvlEl.textContent || '').replace(/^-\s*/, '').trim();
                            const found = Object.entries(CONFIG.areas).find(([, n]) => n === lvlText);
                            if (found)
                                areaNum = Number(found[0]);
                        }
                    }
                    // Ensure travelData is populated for updateTravelingStatus
                    if (!this.travelData[id] && areaNum) {
                        this.travelData[id] = {
                            area: areaNum,
                            status: isNowAbroad ? 'Abroad' : 'Traveling',
                            departedAt: Date.now()
                        };
                    }
                    // Write area name directly — don't wait for updateTravelingStatus
                    const resolvedName = areaNum ? (CONFIG.areas[areaNum] || `Area ${areaNum}`) : (isNowAbroad ? 'Abroad' : 'Traveling');
                    node.textContent = resolvedName;
                    delete node.dataset.travelUpdated;
                    delete node.dataset.originalStatus;
                }
                else {
                    const correctStatus = cn.includes('okay') ? 'Okay' :
                        cn.includes('jail') ? 'Jail' :
                            cn.includes('federal') ? 'Federal' : 'Fallen';
                    node.textContent = correctStatus;
                }
                delete this.hospTime[id];
                delete this.previousStatus[id];
                delete nodeEl2.dataset.catHospStatus;
                delete nodeEl2._catAttackEnabled;
                // Remove early-attack link if Torn hasn't replaced it yet
                const earlyAtk = node._catLi?.querySelector?.('.cat-early-attack');
                if (earlyAtk)
                    earlyAtk.remove();
                const cleanLi = node._catLi || (node._catLi = node.closest('li'));
                if (cleanLi) {
                    cleanLi.removeAttribute('data-abroad-hosp');
                    cleanLi.removeAttribute('data-abroad-dest');
                    cleanLi.removeAttribute('data-cat-hosp-ready');
                    cleanLi.title = '';
                }
                this.hospNodes.splice(i, 1);
                i--;
                // Trigger sort update — status changed
                if (this.enhancementManager) {
                    this.enhancementManager._sortDirty = true;
                    this.enhancementManager._lastSortHash_your = '';
                    this.enhancementManager._lastSortHash_enemy = '';
                    setTimeout(() => {
                        if (this.enhancementManager)
                            this.enhancementManager.restoreSavedSort();
                    }, 50);
                }
                continue;
            }
            if (!catOwns) {
                // CAT hasn't claimed this node yet — check text too
                const currentText = node.textContent.trim();
                if (currentText !== 'Hospital' && !/^\d{2}:\d{2}:\d{2}$/.test(currentText)) {
                    // Neither "Hospital" nor our countdown — skip (Torn showing something else)
                    continue;
                }
            }
            // floorSecs already computed above for the className-check gate — reuse endTime
            const endTime = this.hospTime[id] > 9999999999 ? this.hospTime[id] : this.hospTime[id] * 1000;
            const totalSeconds = (endTime - _now) / 1000;
            if (floorSecs <= 1 && !nodeEl2._catExpiredPollActive) {
                nodeEl2._catExpiredPollActive = true;
                const pollNode = nodeEl2;
                const enhancer = this;
                let pollCount = 0;
                const pollId = setInterval(() => {
                    pollCount++;
                    const cn = pollNode.className;
                    const isStillHosp = cn.includes('hospital') && !cn.includes('okay') && !cn.includes('traveling') && !cn.includes('abroad');
                    if (!isStillHosp || pollCount >= 80) {
                        clearInterval(pollId);
                        pollNode._catExpiredPollActive = false;
                        if (!isStillHosp && enhancer.enhancementManager) {
                            enhancer.enhancementManager._lastSortHash_your = '';
                            enhancer.enhancementManager._lastSortHash_enemy = '';
                            enhancer.enhancementManager._sortDirty = true;
                            enhancer.enhancementManager.restoreSavedSort();
                        }
                    }
                }, 250);
            }
            if (floorSecs <= 0) {
                // Timer expired — freeze display at 00:00:00 and wait for Torn's CSS class change.
                // Keep node in hospNodes and keep hospTime so sort stays stable.
                // isNonHospCn check at top of loop handles cleanup when Torn confirms new status.
                node.textContent = '00:00:00';
                nodeEl2._catHospSecsNum = 0;
                // Throttled refresh: ask CAT server for updated statuses
                if (this.pollingManager?.isActive() && (!this._lastExpiredRefresh || _now - this._lastExpiredRefresh > 5000)) {
                    this._lastExpiredRefresh = _now;
                    this.pollingManager.requestCalls();
                }
                // Enable attack button early — only for regular hosp (not abroad-hosp)
                // Torn confirms ~1.75s later via WS; clicking early sends the request sooner.
                if (!nodeEl2._catAttackEnabled) {
                    nodeEl2._catAttackEnabled = true;
                    const li = node._catLi || (node._catLi = node.closest('li'));
                    if (li) {
                        const graySpan = li.querySelector('span.t-gray-9');
                        if (graySpan && (graySpan.textContent || '').trim().toLowerCase().includes('attack')) {
                            graySpan.classList.remove('t-gray-9');
                            graySpan.classList.add('t-blue', 'h', 'c-pointer', 'cat-early-attack');
                            graySpan.addEventListener('click', () => {
                                window.location.href = `/page.php?sid=attack&user2ID=${id}`;
                            }, { once: true });
                        }
                    }
                }
                continue;
            }
            const hours = Math.floor(totalSeconds / 3600);
            const remaining = totalSeconds % 3600;
            const minutes = Math.floor(remaining / 60);
            const seconds = Math.floor(remaining % 60);
            const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
            // Skip DOM write when display unchanged — textContent write is expensive (triggers layout)
            // secondChanged already compared floorSecs vs dataset — reuse it
            if (secondChanged || !catOwns) {
                node.textContent = timeStr;
                nodeEl2._catHospSecsNum = floorSecs;
            }
            // Mark node so DOM scanner reads 'Hospital' instead of the countdown text
            if (!nodeEl2.dataset.catHospStatus)
                nodeEl2.dataset.catHospStatus = 'Hospital';
            const li = node._catLi || (node._catLi = node.closest('li'));
            if (this.previousStatus[id]) {
                const areaName = CONFIG.areas[this.previousStatus[id].area] || 'Unknown';
                node.setAttribute('data-abroad-dest', areaName);
                if (li && !li.hasAttribute('data-abroad-hosp')) {
                    li.setAttribute('data-abroad-hosp', '1');
                    li.setAttribute('data-abroad-dest', areaName);
                    li.title = `Was Abroad (${areaName})`;
                }
            }
            // Don't remove data-abroad-hosp if previousStatus[id] is missing
            // Once set, it stays until status changes or timer expires
        }
        if (this.hospNodes.length > 0)
            this.hospLoopCounter++;
        // Persist hospTime to localStorage — throttled to once per 5s to avoid thrashing storage
        const now = Date.now();
        if (now - (this._lastHospTimerWrite || 0) > 5000) {
            this._lastHospTimerWrite = now;
            StorageUtil.set('cat_hosp_times', this.hospTime);
        }
    }
    function updateTravelingStatus() {
        const travelIds = Object.keys(this.travelData);
        if (travelIds.length === 0)
            return;
        // Build uid→row index from cached member rows (O(N) once, then O(1) lookup per traveler)
        let rowByUid = this._catRowByUid || null;
        if (!rowByUid || !this._memberRowsValid) {
            rowByUid = {};
            // Re-query all rows fresh when cache is invalid (panel re-opened etc.)
            const freshRows = Array.from(document.querySelectorAll('.desc-wrap li[class*="member___"], .desc-wrap li.enemy, .desc-wrap li.your, .desc-wrap li[class*="your___"]'));
            if (freshRows.length > 0) {
                this._cachedMemberRows = freshRows;
                this._memberRowsValid = true;
            }
            const rows = freshRows.length > 0 ? freshRows : (this._cachedMemberRows || []);
            for (let _i = 0; _i < rows.length; _i++) {
                const row = rows[_i];
                let uid = row.dataset.catUid;
                if (!uid) {
                    const link = row.querySelector('a[href*="XID="], a[href*="user2ID="]');
                    if (link) {
                        const m = link.href.match(/(?:XID|user2ID)=(\d+)/);
                        if (m) {
                            uid = m[1];
                            row.dataset.catUid = uid;
                        }
                    }
                }
                if (uid)
                    rowByUid[uid] = row;
            }
            this._catRowByUid = rowByUid;
        }
        // Read shared settings once outside loop — avoids localStorage reads per traveler
        const travelMode = getTravelMode();
        const etaColorPref = StorageUtil.get('cat_eta_color', null);
        const etaColorDefault = document.body.classList.contains('dark-mode') ? CONFIG.colors.travelEta : '#E65100';
        const cachedEtaColor = etaColorPref || etaColorDefault;
        // Iterate only known travelers — O(travelers) not O(all rows)
        for (let _ti = 0; _ti < travelIds.length; _ti++) {
            const userId = travelIds[_ti];
            const memberRow = rowByUid[userId];
            if (!memberRow || !memberRow.isConnected)
                continue;
            // Get or cache statusEl on the row
            let statusEl = memberRow._catStatusEl || null;
            if (statusEl && !statusEl.isConnected)
                statusEl = null;
            if (!statusEl) {
                statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                if (statusEl)
                    memberRow._catStatusEl = statusEl;
            }
            if (!statusEl)
                continue;
            const travelInfo = this.travelData[userId];
            if (!travelInfo || travelInfo.area === undefined) {
                // Player no longer traveling — clean up our modifications
                if (statusEl.dataset.travelUpdated === 'true') {
                    delete statusEl.dataset.travelUpdated;
                    delete statusEl.dataset.originalStatus;
                    statusEl.style.removeProperty('text-transform');
                    statusEl.style.removeProperty('font-size');
                    statusEl.style.removeProperty('white-space');
                    statusEl.style.removeProperty('overflow');
                    statusEl.style.removeProperty('text-overflow');
                }
                continue;
            }
            const text = (statusEl.textContent || '').trim();
            // Detect real status from Torn's CSS class (covers BEM classes like traveling___xyz)
            const isTravelingClass = statusEl.classList.contains('traveling') || statusEl.className.includes('traveling');
            const isAbroadClass = statusEl.classList.contains('abroad') || statusEl.className.includes('abroad');
            const isOkayClass = statusEl.classList.contains('okay') || statusEl.className.includes('okay');
            const isHospClass = statusEl.classList.contains('hospital') || statusEl.className.includes('hospital');
            // Player landed or is hospitalized — clear stale travelData and travel display
            if (isOkayClass || isHospClass) {
                delete this.travelData[userId];
                if (statusEl.dataset.travelUpdated === 'true') {
                    delete statusEl.dataset.travelUpdated;
                    delete statusEl.dataset.originalStatus;
                    statusEl.style.removeProperty('text-transform');
                    statusEl.style.removeProperty('font-size');
                    statusEl.style.removeProperty('white-space');
                    statusEl.style.removeProperty('overflow');
                    statusEl.style.removeProperty('text-overflow');
                }
                continue;
            }
            if (!statusEl.dataset.originalStatus) {
                statusEl.dataset.originalStatus = isTravelingClass ? 'Traveling' : isAbroadClass ? 'Abroad' : text;
            }
            else if (statusEl.dataset.originalStatus === 'Abroad' && isTravelingClass) {
                // Torn updated element in-place: Abroad → Traveling
                statusEl.dataset.originalStatus = 'Traveling';
            }
            else if (statusEl.dataset.originalStatus === 'Traveling' && isAbroadClass) {
                // Torn updated element in-place: Traveling → Abroad
                statusEl.dataset.originalStatus = 'Abroad';
            }
            const areaName = CONFIG.areas[travelInfo.area] || `Area ${travelInfo.area}`;
            const originalText = statusEl.dataset.originalStatus;
            let displayText = '';
            if (originalText === 'Abroad') {
                displayText = areaName;
            }
            else if (travelInfo.area === 1) {
                displayText = '\u27A4 Torn';
            }
            else {
                displayText = `\u27A4 ${areaName}`;
            }
            if (!statusEl.dataset.travelUpdated) {
                statusEl.style.textTransform = 'none';
                statusEl.style.fontSize = '0.75em';
                statusEl.dataset.travelUpdated = 'true';
            }
            // Compute ETA for traveling players (multi-mode)
            let etaLine = '';
            let etaColor = '';
            let tooltipText = '';
            if (originalText === 'Traveling' || statusEl.dataset.originalStatus === 'Traveling') {
                if (travelInfo.departedAt) {
                    const mode = travelMode;
                    const allEtas = computeAllETAs(travelInfo.area, travelInfo.departedAt, travelInfo.fromArea);
                    const primaryEtaMs = allEtas[mode];
                    const now = Date.now();
                    if (primaryEtaMs && primaryEtaMs > now) {
                        const remaining = primaryEtaMs - now;
                        const lookupArea = travelInfo.area === 1 ? travelInfo.fromArea : travelInfo.area;
                        const totalDuration = (lookupArea && CONFIG.travelTimes[lookupArea])
                            ? CONFIG.travelTimes[lookupArea][mode] * 1000
                            : remaining;
                        const landingThreshold = totalDuration * 0.03;
                        if (remaining <= landingThreshold) {
                            etaLine = '~Landing';
                            etaColor = '#4ecdc4';
                        }
                        else {
                            const formatted = formatETA(primaryEtaMs);
                            if (formatted) {
                                etaLine = `ETA ~${formatted}`;
                                etaColor = cachedEtaColor;
                            }
                        }
                    }
                    tooltipText = buildEtaTooltip(allEtas, mode);
                }
            }
            const fullKey = displayText + '|' + etaLine;
            if (statusEl.dataset.catContent !== fullKey) {
                statusEl.dataset.catContent = fullKey;
                if (etaLine) {
                    // Try to update existing ETA span in-place to avoid DOM flicker
                    const existingEta = statusEl.querySelector('.cat-travel-eta');
                    const existingText = statusEl.querySelector('.cat-travel-text');
                    if (existingEta && existingText) {
                        existingText.textContent = displayText;
                        existingEta.textContent = etaLine;
                        existingEta.style.color = etaColor;
                    }
                    else {
                        statusEl.textContent = '';
                        const textSpan = document.createElement('span');
                        textSpan.className = 'cat-travel-text';
                        textSpan.dataset.cat = '1';
                        textSpan.style.cssText = 'margin-top:7px;display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.2;';
                        textSpan.textContent = displayText;
                        const etaSpan = document.createElement('span');
                        etaSpan.className = 'cat-travel-eta';
                        etaSpan.dataset.cat = '1';
                        etaSpan.style.cssText = `display:block;font-size:0.75em;color:${etaColor};cursor:default;white-space:nowrap;line-height:1.2;`;
                        etaSpan.textContent = etaLine;
                        statusEl.appendChild(textSpan);
                        statusEl.appendChild(etaSpan);
                    }
                    statusEl.style.whiteSpace = 'normal';
                    statusEl.style.overflow = 'hidden';
                    statusEl.style.lineHeight = '1.1';
                    statusEl.style.transform = '';
                    if (tooltipText)
                        statusEl.dataset.etaTooltip = tooltipText;
                }
                else {
                    statusEl.textContent = displayText;
                    statusEl.style.whiteSpace = 'nowrap';
                    statusEl.style.overflow = 'hidden';
                    statusEl.style.textOverflow = 'ellipsis';
                    statusEl.style.lineHeight = '';
                    statusEl.style.transform = '';
                    delete statusEl.dataset.etaTooltip;
                }
            }
        }
        // Persist travelData to localStorage — throttled to once per 5s
        const _tNow = Date.now();
        if (_tNow - (this._lastTravelWrite || 0) > 5000) {
            this._lastTravelWrite = _tNow;
            StorageUtil.set('cat_travel_data', this.travelData);
        }
    }

    // Module-level state (survives across calls, not per-instance)
    // _lastDomStatuses: stored as JS property (_catLastStatus) on each row element to avoid Map overhead
    const _serverStatuses = new Map(); // memberId → last known server status
    const isPDA = typeof window.flutter_inappwebview !== 'undefined';
    const pdaPerfMode = isPDA && String(StorageUtil.get('cat_pda_perf_mode', 'false')) === 'true';
    const SCAN_INTERVAL_MS = pdaPerfMode ? 5000 : 3000;
    /**
     * Called by onStatusesUpdate to keep track of what the server knows.
     * This lets us avoid sending redundant updates.
     */
    function updateServerStatuses(statuses) {
        for (let i = 0; i < statuses.length; i++) {
            const s = statuses[i];
            _serverStatuses.set(String(s.memberId), s.status);
        }
    }
    /**
     * Start the periodic DOM scanner for all war member statuses (both factions).
     * Runs every 3s, lightweight: only reads text content from cached rows.
     */
    function startDomStatusScanner() {
        this._domScanInterval = setInterval(() => {
            // Skip if tab is hidden — no DOM updates happen when hidden
            if (document.hidden)
                return;
            // Skip if polling not active
            if (!this.pollingManager?.isActive())
                return;
            scanAllFactionStatuses.call(this);
        }, SCAN_INTERVAL_MS);
    }
    function scanAllFactionStatuses() {
        // Reuse enhancer's cached member rows — avoids querySelectorAll every 3s
        let rows;
        if (this._memberRowsValid && this._cachedMemberRows && this._cachedMemberRows.length > 0) {
            rows = this._cachedMemberRows;
        }
        else {
            const queried = Array.from(document.querySelectorAll('.desc-wrap li[class*="member___"], .desc-wrap li.enemy, .desc-wrap li.your, .desc-wrap li[class*="your___"]'));
            // Store in enhancer cache so other code can reuse it
            this._cachedMemberRows = queried;
            this._memberRowsValid = true;
            rows = queried;
        }
        if (rows.length === 0)
            return;
        for (let i = 0; i < rows.length; i++) {
            const row = rows[i];
            // Get cached member ID (set by other code: hospital-timers, call-buttons-update)
            let memberId = row.dataset.catUid || null;
            if (!memberId) {
                const link = row.querySelector('a[href*="profiles.php?XID="], a[href*="user2ID"]');
                if (link) {
                    const m = link.href.match(/(?:XID|user2ID)=(\d+)/);
                    if (m) {
                        memberId = m[1];
                        row.dataset.catUid = memberId;
                    }
                }
            }
            if (!memberId)
                continue;
            // Read status from DOM (prefer originalStatus dataset to avoid reading our modified text)
            let statusEl = row._catStatusEl || null;
            if (statusEl && !statusEl.isConnected)
                statusEl = null;
            if (!statusEl) {
                statusEl = row.querySelector('[class*="status___"], .status.left');
                if (statusEl)
                    row._catStatusEl = statusEl;
            }
            if (!statusEl)
                continue;
            const domStatus = statusEl.dataset.catHospStatus || statusEl.dataset.originalStatus || (statusEl.textContent || '').trim();
            if (!domStatus)
                continue;
            // Check if DOM status changed since last scan — stored on row as JS prop (faster than Map.get/set)
            const prevDomStatus = row._catLastStatus;
            row._catLastStatus = domStatus;
            // Compare with what the server knows — skip if server is already up to date
            const serverStatus = _serverStatuses.get(memberId);
            if (domStatus === serverStatus)
                continue;
            // Skip if DOM didn't change AND server doesn't know yet (already sent, waiting for server ack)
            if (domStatus === prevDomStatus)
                continue;
            // Enrich with local data depending on status
            let until = null;
            let previousArea = null;
            let departedAt = null;
            if (domStatus === 'Hospital') {
                if (this.hospTime[memberId])
                    until = this.hospTime[memberId];
                if (this.previousStatus[memberId]?.area)
                    previousArea = this.previousStatus[memberId].area;
            }
            else if (domStatus === 'Traveling' || domStatus === 'Abroad') {
                const td = this.travelData[memberId];
                if (td) {
                    previousArea = td.area ?? null;
                    departedAt = td.departedAt ?? null;
                }
            }
            // Detect faction: enemy rows have class "enemy", own rows have "your"
            const isEnemy = row.classList.contains('enemy');
            let factionId = null;
            if (isEnemy) {
                const cached = StorageUtil.get('cat_enemy_faction_id', null);
                if (cached?.id)
                    factionId = cached.id;
            }
            // Status changed in DOM and differs from server — send update
            this.pollingManager.queueStatusUpdate(memberId, {
                status: domStatus,
                details: null,
                until,
                previousStatus: serverStatus || prevDomStatus || null,
                previousArea,
                departedAt,
                factionId
            });
        }
    }

    // Perf counters — module-level so startTimerRefresh and startDomObserver share them
    let _mutationCount = 0;
    // Detailed per-function timing accumulators (reset every 10s with perf log)
    const _t = {
        // tick-loop functions
        scanHosp: 0, updateHospTimers: 0, updateTravel: 0, injectLevel: 0,
        restoreSort: 0, scanStatuses: 0, checkFactions: 0, chainBox: 0, tabsMenu: 0,
        // flushVisuals sub-costs
        vScoreStyles: 0, vScanHosp: 0, vHospTimers: 0, vTravel: 0, vLevel: 0, vTactical: 0, vSort: 0,
    };
    function startCallRefresh() {
        if (this.pollingManager && this.pollingManager.isActive()) {
            this.pollingManager.requestCalls();
        }
    }
    function startTimerRefresh() {
        // Apply score styles immediately on start
        this.applyScoreStyles();
        // On TornPDA Android, document.hidden can be stuck true even when page is visible
        const isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        const pdaPerfMode = isPDA && String(StorageUtil.get('cat_pda_perf_mode', 'false')) === 'true';
        let tick = 0;
        // Cache auto-sort setting — re-read every 10s to pick up changes without hitting localStorage every tick
        let _autoSort = String(StorageUtil.get('cat_auto_sort', 'true')) === 'true';
        const doWork = () => {
            const t0 = performance.now();
            // Every tick (1s): hosp timers only — must be precise
            let _ts = performance.now();
            this.scanHospitalizedMembers();
            _t.scanHosp += performance.now() - _ts;
            _ts = performance.now();
            this.updateHospTimers();
            _t.updateHospTimers += performance.now() - _ts;
            // Every 3 ticks (3s): travel + level indicators — change rarely, no need for 1s precision
            if (tick % 3 === 0) {
                _ts = performance.now();
                this.updateTravelingStatus();
                _t.updateTravel += performance.now() - _ts;
                _ts = performance.now();
                this.injectLevelIndicators();
                _t.injectLevel += performance.now() - _ts;
            }
            // Every tick: auto-sort if dirty (guard inside restoreSavedSort)
            if (this.enhancementManager && _autoSort) {
                _ts = performance.now();
                this.enhancementManager.restoreSavedSort();
                _t.restoreSort += performance.now() - _ts;
            }
            // Every 10 ticks (10s): refresh cached settings
            if (tick % 10 === 0) {
                _autoSort = String(StorageUtil.get('cat_auto_sort', 'true')) === 'true';
            }
            // Every 3 ticks (3s): DOM status scanner
            if (tick % 3 === 0 && this.pollingManager?.isActive()) {
                _ts = performance.now();
                scanAllFactionStatuses.call(this);
                _t.scanStatuses += performance.now() - _ts;
            }
            // Every tick: flush BSP "Wait" cells — BSP Extension fills .iconStats via textContent
            // mutation (not childList/attribute), so MutationObserver misses it. Poll instead.
            if (this.enhancementManager && document.querySelector('.bsp-column .bsp-wait')) {
                const containers = document.querySelectorAll('.your-faction, .enemy-faction, [class*="tabMenuCont"]');
                containers.forEach(c => this.enhancementManager.updateWaitingBspCells(c));
            }
            // Every tick: flush TS columns showing "-" — .iconStats may arrive after addTSColumn ran.
            if (this.enhancementManager && document.querySelector('.ts-column[style*="inline-flex"] .ts-value')) {
                const hasDash = Array.from(document.querySelectorAll('.ts-column[style*="inline-flex"] .ts-value'))
                    .some(el => el.textContent?.trim() === '-');
                if (hasDash)
                    this.enhancementManager.updateTSColumns();
            }
            // Every 2 ticks (2s): faction check — re-injects call buttons if Torn re-rendered
            if (tick % 2 === 0 && this.enhancementManager) {
                _ts = performance.now();
                this.enhancementManager._checkFactions();
                _t.checkFactions += performance.now() - _ts;
            }
            // Every 5 ticks (5s): chain box + tabs menu check
            if (tick % 5 === 0) {
                _ts = performance.now();
                // Cache chain-box ref — only re-query when disconnected
                let _cb = this._catChainBox2 || null;
                if (!_cb || !_cb.isConnected) {
                    _cb = document.querySelector('.chain-box');
                    this._catChainBox2 = _cb;
                }
                if (_cb && !document.querySelector('.cat-info-panel')) {
                    this.injectChainBoxPanel();
                }
                if (_cb && !document.getElementById('cat-bonus-claim-row')) {
                    this.updateChainBonusClaimUI();
                }
                this.injectWarStatusBar();
                _t.chainBox += performance.now() - _ts;
            }
            if (tick % 5 === 0 && this.enhancementManager) {
                _ts = performance.now();
                // Cache war container ref — re-query only when disconnected
                let _cachedWarEl = this._catWarEl || null;
                if (!_cachedWarEl || !_cachedWarEl.isConnected) {
                    _cachedWarEl = document.querySelector('.faction-war-info') || document.getElementById('faction_war_list_id');
                    this._catWarEl = _cachedWarEl;
                }
                const hasWar = _cachedWarEl;
                if (hasWar) {
                    const existingMenu = document.getElementById('custom-tabs-menu');
                    if (!existingMenu) {
                        this.enhancementManager.injectTabsMenu();
                    }
                    else if (StorageUtil.get('cat_api_key_script', '') &&
                        (document.querySelector('.faction-war-info') || document.querySelector('[data-warid], [class*="rankBox"]')) &&
                        !existingMenu.querySelector('[data-tab="faction"]')) {
                        if (!state.catOtherFaction) {
                            const activeBtn = existingMenu.querySelector('.custom-tab-btn.active');
                            const activeTabName = activeBtn?.getAttribute('data-tab') || null;
                            existingMenu.remove();
                            document.querySelectorAll('.custom-tab-content').forEach(c => c.remove());
                            window._factionStatsLoaded = false;
                            this.enhancementManager.injectTabsMenu();
                            if (activeTabName) {
                                const newBtn = document.querySelector(`#custom-tabs-menu .custom-tab-btn[data-tab="${activeTabName}"]`);
                                if (newBtn)
                                    newBtn.click();
                            }
                        }
                    }
                }
                _t.tabsMenu += performance.now() - _ts;
            }
            tick++;
            const tickMs = performance.now() - t0;
            pdaMetrics.recordDomWork(tickMs);
            // Reset accumulators every 10s
            if (tick % 10 === 0) {
                _mutationCount = 0;
                for (const k in _t)
                    _t[k] = 0;
            }
        };
        this.refreshInterval = setInterval(() => {
            if (isPDA) {
                doWork();
            }
            else {
                if (document.hidden)
                    return;
                requestAnimationFrame(doWork);
            }
        }, pdaPerfMode ? 2000 : 1000);
    }
    function startDomObserver() {
        if (this.domObserver) {
            this.domObserver.disconnect();
        }
        let retries = 0;
        const maxRetries = 30;
        const observeWarPage = () => {
            // Watch the broadest container that covers both factions
            const descWrap = document.querySelector('.desc-wrap');
            if (!descWrap) {
                if (++retries < maxRetries) {
                    setTimeout(observeWarPage, 1000);
                }
                return;
            }
            let warUpdateDebounce = null;
            let isApplyingVisuals = false;
            const sendDomUpdate = () => {
                if (warUpdateDebounce)
                    clearTimeout(warUpdateDebounce);
                warUpdateDebounce = setTimeout(() => {
                    const targets = this.parseTargetsFromDOM();
                    const userFaction = StorageUtil.get('cat_user_faction_id', null);
                    if (targets.length > 0 && this.pollingManager && this.pollingManager.isActive() && userFaction) {
                        this.pollingManager.sendWarUpdate(userFaction, targets);
                    }
                }, 200);
            };
            let _hasHospChange = false;
            let _hasStructuralChange = false;
            let _hasScoreChange = false;
            let _rafPending = false;
            let _scoreThrottle = null;
            let _structuralDebounce = null;
            const flushVisuals = () => {
                _rafPending = false;
                _scoreThrottle = null;
                isApplyingVisuals = true;
                const t0 = performance.now();
                const structural = _hasStructuralChange;
                const hospChange = _hasHospChange;
                const scoreChange = _hasScoreChange;
                _hasStructuralChange = false;
                _hasHospChange = false;
                _hasScoreChange = false;
                let _vs = 0;
                if (structural) {
                    // Full re-scan: nodes added/removed
                    this._travelNodesValid = false;
                    this._memberRowsValid = false;
                    this._hospScanNeeded = true;
                    this._hospPass2Needed = true;
                    // Invalidate chain-box cached refs — war list items may have changed
                    this._catWarListItems = null;
                    this._catChainBox = null;
                    this._catGeneralInfo = null;
                    this._catChainStat = null;
                    this._catScoreEls = null;
                    _vs = performance.now();
                    this.updateTravelingStatus();
                    _t.vTravel += performance.now() - _vs;
                    _vs = performance.now();
                    this.injectLevelIndicators();
                    _t.vLevel += performance.now() - _vs;
                    _vs = performance.now();
                    this.scanHospitalizedMembers();
                    _t.vScanHosp += performance.now() - _vs;
                    _vs = performance.now();
                    this.updateHospTimers();
                    _t.vHospTimers += performance.now() - _vs;
                    _vs = performance.now();
                    this.updateTacticalMarkers();
                    _t.vTactical += performance.now() - _vs;
                    _vs = performance.now();
                    this.applyScoreStyles();
                    _t.vScoreStyles += performance.now() - _vs;
                }
                else if (hospChange) {
                    // Always scan on any hospital class change — a new patient may have appeared
                    // (e.g. abroad → hospital abroad) even when all existing nodes are still connected.
                    this._hospScanNeeded = true;
                    this._hospPass2Needed = true;
                    _vs = performance.now();
                    this.scanHospitalizedMembers();
                    _t.vScanHosp += performance.now() - _vs;
                    _vs = performance.now();
                    this.updateHospTimers();
                    _t.vHospTimers += performance.now() - _vs;
                    _vs = performance.now();
                    this.applyScoreStyles();
                    _t.vScoreStyles += performance.now() - _vs;
                }
                else if (scoreChange) {
                    // Generic class toggle (score, status): score styles only
                    _vs = performance.now();
                    this.applyScoreStyles();
                    _t.vScoreStyles += performance.now() - _vs;
                }
                const visualCost = performance.now() - t0;
                pdaMetrics.recordDomWork(visualCost);
                if (this.enhancementManager) {
                    // Only trigger re-sort for changes that affect sort order (structural/hosp).
                    // Pure score class-toggle mutations don't change member order — skip sort to avoid
                    // thrashing during pushes where score DOM mutations fire at high frequency.
                    if (structural || hospChange) {
                        if (structural) {
                            this.enhancementManager._sortContainersCache = null;
                            this.enhancementManager._catOrderApplied_your = false;
                            this.enhancementManager._catOrderApplied_enemy = false;
                        }
                        this.enhancementManager._lastSortHash_your = '';
                        this.enhancementManager._lastSortHash_enemy = '';
                        this.enhancementManager._sortDirty = true;
                        _vs = performance.now();
                        this.enhancementManager.restoreSavedSort();
                        _t.vSort += performance.now() - _vs;
                    }
                }
                requestAnimationFrame(() => { isApplyingVisuals = false; });
            };
            const applyVisuals = (isStructural, isHospChange, isScoreChange) => {
                // Accumulate flags across rapid-fire mutations
                if (isStructural)
                    _hasStructuralChange = true;
                if (isHospChange)
                    _hasHospChange = true;
                if (isScoreChange)
                    _hasScoreChange = true;
                if (isStructural) {
                    // Structural: debounce 50ms to batch DOM work
                    if (_structuralDebounce)
                        clearTimeout(_structuralDebounce);
                    _structuralDebounce = setTimeout(() => {
                        _structuralDebounce = null;
                        flushVisuals();
                    }, 50);
                }
                else if (isHospChange && !_rafPending && !_structuralDebounce) {
                    // Hosp: fast path — next animation frame
                    _rafPending = true;
                    requestAnimationFrame(() => {
                        if (_structuralDebounce) {
                            _rafPending = false;
                            return;
                        }
                        flushVisuals();
                    });
                }
                else if (!_scoreThrottle && !_rafPending && !_structuralDebounce) {
                    // Score-only: throttle to max 1 flush per 100ms — push batches arrive at ~3/s,
                    // score styles are cosmetic, 100ms lag is imperceptible
                    _scoreThrottle = setTimeout(() => {
                        if (_structuralDebounce) {
                            _scoreThrottle = null;
                            return;
                        }
                        _rafPending = true;
                        requestAnimationFrame(flushVisuals);
                    }, 100);
                }
            };
            this.domObserver = new MutationObserver((mutations) => {
                _mutationCount += mutations.length;
                if (isApplyingVisuals)
                    return;
                let hasRelevantChange = false;
                let hasStructuralChange = false;
                let hasHospChange = false;
                // Cap scan at 32 mutations — beyond that it's a high-frequency push batch.
                // Score-only mutations dominate pushes; hospital changes are caught by the 1s timer.
                const scanLen = mutations.length > 32 ? 32 : mutations.length;
                for (let _mi = 0; _mi < scanLen; _mi++) {
                    const mutation = mutations[_mi];
                    const target = mutation.target;
                    // Skip mutations on our own elements — cat- class prefix or data-cat attribute
                    const cls = target.className;
                    if ((typeof cls === 'string' && cls.includes('cat-')) ||
                        target.dataset?.cat ||
                        target.closest?.('[data-cat]')) {
                        continue;
                    }
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        hasRelevantChange = true;
                        // Structural only if a member row was added/removed — not arbitrary Torn DOM updates
                        const added = mutation.addedNodes;
                        for (let _ni = 0; _ni < added.length; _ni++) {
                            const n = added[_ni];
                            if (n.nodeType === 1) {
                                const nc = n.className;
                                if (typeof nc === 'string' && (nc.includes('member') || nc.includes('enemy') || nc.includes('your___') || nc.includes('warList'))) {
                                    hasStructuralChange = true;
                                    break;
                                }
                            }
                        }
                        if (hasStructuralChange)
                            break;
                        continue; // childList but no member rows added — not structural
                    }
                    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                        hasRelevantChange = true;
                        // Detect hospital class toggle — needs hosp scan, not full scan.
                        // Also triggers when status transitions OUT of hospital (e.g. medded → okay),
                        // detected by catHospStatus dataset still present on the element.
                        const isStatusEl = target.className.includes('status___') || target.classList.contains('status');
                        if (isStatusEl && (target.className.includes('hospital') || target.dataset.catHospStatus)) {
                            hasHospChange = true;
                            const memberRow = target.closest('li');
                            const memberId = memberRow?.dataset?.catUid;
                            if (memberId && this.hospTime[memberId]) {
                                this.registerHospNode(memberId, target);
                            }
                        }
                        else if (isStatusEl && (target.className.includes('okay') || target.className.includes('traveling') || target.className.includes('abroad'))) {
                            // Status changed away from hospital (or player landed) — trigger immediate sort
                            hasHospChange = true;
                            if (this.enhancementManager) {
                                this.enhancementManager._lastSortHash_your = '';
                                this.enhancementManager._lastSortHash_enemy = '';
                            }
                        }
                    }
                }
                if (hasRelevantChange) {
                    sendDomUpdate();
                    applyVisuals(hasStructuralChange, hasHospChange, !hasStructuralChange && !hasHospChange);
                }
                // Forward mutations to EnhancementManager only if relevant
                if (hasRelevantChange && this.enhancementManager) {
                    this.enhancementManager.handleMutations(mutations);
                }
            });
            this.domObserver.observe(descWrap, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['class']
            });
            // Also observe faction_war_list_id if it's outside desc-wrap (rank wars)
            const warList = document.getElementById('faction_war_list_id');
            if (warList && !warList.closest('.desc-wrap')) {
                const warListParent = warList.parentElement;
                if (warListParent) {
                    this.domObserver.observe(warListParent, {
                        childList: true,
                        subtree: true,
                        attributes: true,
                        attributeFilter: ['class']
                    });
                }
            }
            // Initial application
            sendDomUpdate();
        };
        setTimeout(observeWarPage, 0);
    }

    function _factionCard(s, color, leaderHtml, coLeaderHtml) {
        const c = 'cat-fc-box';
        return `
        <div class="${c}">
            <div class="cat-fc-label">Faction</div>
            <div class="cat-fc-accent" style="font-size:14px;font-weight:700;color:${color};">${this._esc(s.name)}</div>
        </div>
        <div class="${c}">
            <div class="cat-fc-label">Leadership</div>
            <div class="cat-fc-value" style="margin-top:4px;">Leader: ${leaderHtml}</div>
            <div class="cat-fc-value" style="margin-top:2px;">Co-Leader: ${coLeaderHtml}</div>
        </div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px;">
            <div class="${c}" style="margin-bottom:0;">
                <div class="cat-fc-label">Members</div>
                <div class="cat-fc-accent" style="font-size:16px;font-weight:700;color:${color};">${s.memberCount}</div>
            </div>
            <div class="${c}" style="margin-bottom:0;">
                <div class="cat-fc-label">Founded</div>
                <div class="cat-fc-value" style="font-size:12px;font-weight:600;">${s.founded}</div>
            </div>
        </div>
        <div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;">
            <div class="${c}" style="margin-bottom:0;">
                <div class="cat-fc-label">Wins</div>
                <div class="cat-fc-accent" style="font-size:16px;font-weight:700;color:${color};">${s.wins}</div>
            </div>
            <div class="${c}" style="margin-bottom:0;">
                <div class="cat-fc-label">Best Chain</div>
                <div class="cat-fc-value" style="font-size:16px;font-weight:700;">${s.bestWin}</div>
            </div>
        </div>
    `;
    }
    function renderFactionStats(factionInfo, container, loader) {
        try {
            loader.style.display = 'none';
            container.style.display = 'block';
            const data = (factionInfo && factionInfo.basic) || {};
            const s = {
                name: data.name || 'Unknown',
                memberCount: data.members || 0,
                founded: data.days_old ? `${data.days_old} days` : 'Unknown',
                wins: (data.rank && data.rank.wins) || 0,
                bestWin: data.best_chain || 0
            };
            const leaderId = data.leader_id;
            const coLeaderId = data.co_leader_id;
            const leaderHtml = leaderId ? `<a href="https://www.torn.com/profiles.php?XID=${leaderId}" target="_blank" style="color:#74BEF9;text-decoration:none;">[${leaderId}]</a>` : 'Unknown';
            const coLeaderHtml = coLeaderId ? `<a href="https://www.torn.com/profiles.php?XID=${coLeaderId}" target="_blank" style="color:#74BEF9;text-decoration:none;">[${coLeaderId}]</a>` : 'Unknown';
            container.innerHTML = `<div style="padding:0;">${this._factionCard(s, '#FF794C', leaderHtml, coLeaderHtml)}</div>`;
        }
        catch (error) {
            console.log('Error rendering faction stats:', error);
            this.apiManager.reportError('renderFactionStats', error);
            loader.innerHTML = '<p style="color: #ef5350;">Error rendering faction stats</p>';
        }
    }
    async function renderDualFactionStats(enemyFactionInfo, userFactionInfo, container, loader, cachedLeaders) {
        try {
            loader.style.display = 'none';
            container.style.display = 'block';
            const getStatsFromData = (factionInfo) => {
                const data = (factionInfo && factionInfo.basic) || {};
                return {
                    name: data.name || 'Unknown',
                    leader_id: data.leader_id || null,
                    coLeader_id: data.co_leader_id || null,
                    memberCount: data.members || 0,
                    founded: data.days_old ? `${data.days_old} days` : 'Unknown',
                    wins: (data.rank && data.rank.wins) || 0,
                    bestWin: data.best_chain || 0
                };
            };
            const enemy = getStatsFromData(enemyFactionInfo);
            const user = getStatsFromData(userFactionInfo);
            const idLink = (id) => id ? `<a href="https://www.torn.com/profiles.php?XID=${id}" target="_blank" style="color:#74BEF9;text-decoration:none;">[${id}]</a>` : '<span style="color:#ccc;">Unknown</span>';
            const headerStyle = 'background:linear-gradient(180deg,#666 0%,#333 100%);color:#fff;text-shadow:0 0 2px #000;padding:6px 10px;font-size:12px;font-weight:600;border-radius:4px 4px 0 0;letter-spacing:0.3px;border-top:1px solid #555;border-bottom:1px solid #222;box-shadow:0 0 2px rgba(0,0,0,0.25);';
            const renderCards = (el, ecl, ul, ucl) => {
                container.innerHTML = `
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
                    <div>
                        <div style="${headerStyle}">Enemy Faction</div>
                        ${this._factionCard(enemy, '#FF794C', el, ecl)}
                    </div>
                    <div>
                        <div style="${headerStyle}">Your Faction</div>
                        ${this._factionCard(user, '#ACEA01', ul, ucl)}
                    </div>
                </div>
            `;
            };
            if (cachedLeaders) {
                renderCards(cachedLeaders.enemyLeader, cachedLeaders.enemyCoLeader, cachedLeaders.userLeader, cachedLeaders.userCoLeader);
                this._lastLeaderHtml = cachedLeaders;
            }
            else {
                // Phase 1 : affichage immédiat avec IDs
                renderCards(idLink(enemy.leader_id), idLink(enemy.coLeader_id), idLink(user.leader_id), idLink(user.coLeader_id));
                // Phase 2 : résolution des noms en arrière-plan
                const getLeaderHtml = async (leaderId) => {
                    if (!leaderId)
                        return '<span style="color:#ccc;">Unknown</span>';
                    try {
                        const userInfo = await this.apiManager.getUserInfo(leaderId);
                        const username = userInfo?.profile?.name || userInfo?.profile?.username || null;
                        if (username)
                            return `<a href="https://www.torn.com/profiles.php?XID=${leaderId}" target="_blank" style="color:#74BEF9;text-decoration:none;">${this._esc(username)}</a>`;
                    }
                    catch (e) {
                        this.apiManager.reportError('getLeaderInfo', e);
                    }
                    return idLink(leaderId);
                };
                Promise.all([
                    getLeaderHtml(enemy.leader_id),
                    getLeaderHtml(enemy.coLeader_id),
                    getLeaderHtml(user.leader_id),
                    getLeaderHtml(user.coLeader_id)
                ]).then(([el, ecl, ul, ucl]) => {
                    this._lastLeaderHtml = { enemyLeader: el, enemyCoLeader: ecl, userLeader: ul, userCoLeader: ucl };
                    renderCards(el, ecl, ul, ucl);
                });
            }
        }
        catch (error) {
            console.log('Error rendering dual faction stats:', error);
            this.apiManager.reportError('renderDualFactionStats', error);
            loader.innerHTML = '<p style="color: #ef5350;">Error rendering faction stats</p>';
        }
    }
    async function getPredictions(enemyFactionId, userFactionId, enemyWins = 0, enemyLosses = 0, userWins = 0, userLosses = 0) {
        try {
            const params = new URLSearchParams({
                enemyWins: String(enemyWins || 0),
                enemyLosses: String(enemyLosses || 0),
                userWins: String(userWins || 0),
                userLosses: String(userLosses || 0)
            });
            const url = `${this.apiManager.serverUrl}/api/predictions/${enemyFactionId}/${userFactionId}?${params.toString()}`;
            const response = await this.apiManager.httpRequest(url, {
                method: 'GET',
                headers: {
                    'Authorization': `Bearer ${this.apiManager.authToken}`
                }
            });
            if (!response.ok) {
                console.log('\u274C Failed to fetch predictions:', response.status);
                return null;
            }
            const data = await response.json();
            return data.success ? data : null;
        }
        catch (error) {
            console.log('\u274C Error getting predictions:', error);
            this.apiManager.reportError('getPredictions', error);
            return null;
        }
    }
    async function sendFactionMembersToServer(factionId, factionInfo, label) {
        try {
            const membersData = factionInfo.members;
            if (!membersData || typeof membersData !== 'object')
                return;
            const membersArray = Array.isArray(membersData) ? membersData : Object.values(membersData);
            const members = membersArray.map(member => ({
                id: String(member.id),
                name: member.name || `Player${member.id}`,
                level: member.level || 0
            }));
            if (members.length === 0)
                return;
            for (const m of members) {
                this._memberNames[m.id] = m.name;
            }
            await this.apiManager.httpRequest(`${this.apiManager.serverUrl}/api/faction-members`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${this.apiManager.authToken}`
                },
                body: JSON.stringify({
                    factionId: String(factionId),
                    members: members
                })
            });
            const name = factionInfo?.basic?.name || factionId;
            console.log(`%c[CAT] Members in ${label || 'faction'} [${name}] : ${members.length}`, 'color:#ACEA01;');
        }
        catch (error) {
            this.apiManager.reportError('sendFactionMembers', error);
        }
    }

    function setupEventListeners() {
        let wasPaused = false;
        const isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        const checkFocus = () => {
            const hasFocus = document.hasFocus() && document.visibilityState === 'visible';
            if (!hasFocus && !wasPaused) {
                wasPaused = true;
                this.pause();
            }
            else if (hasFocus && wasPaused) {
                wasPaused = false;
                this.resume();
            }
        };
        window.addEventListener('blur', checkFocus);
        window.addEventListener('focus', checkFocus);
        document.addEventListener('visibilitychange', checkFocus);
        // PDA: visibility APIs are broken (hasFocus stuck false, visibilitychange unreliable)
        // Use multiple signals to detect user return from background
        if (isPDA) {
            let lastResumeTime = 0;
            const pdaForceResume = (source) => {
                if (!wasPaused)
                    return;
                const now = Date.now();
                if (now - lastResumeTime < 3000)
                    return;
                lastResumeTime = now;
                wasPaused = false;
                this.resume();
            };
            // Signal 1: user touches or scrolls the page
            document.addEventListener('scroll', () => pdaForceResume(), { passive: true });
            document.addEventListener('touchstart', () => pdaForceResume(), { passive: true });
            // Signal 2: pageshow fires when page becomes visible (back from background, bfcache restore)
            window.addEventListener('pageshow', () => {
                wasPaused = true; // force resume even if not previously paused
                pdaForceResume();
            });
            // Signal 3: IntersectionObserver on war container — re-attach if DOM re-rendered
            let _io = null;
            const watchDescWrap = () => {
                const descWrap = document.querySelector('.desc-wrap');
                if (!descWrap) {
                    setTimeout(watchDescWrap, 1000);
                    return;
                }
                if (_io)
                    _io.disconnect();
                _io = new IntersectionObserver((entries) => {
                    if (entries[0]?.isIntersecting) {
                        pdaForceResume();
                    }
                }, { threshold: 0.1 });
                _io.observe(descWrap);
            };
            watchDescWrap();
            // Signal 4: re-attach IntersectionObserver if desc-wrap is re-rendered by Torn
            const rewatchObserver = new MutationObserver(() => {
                if (document.querySelector('.desc-wrap') && (!_io)) {
                    watchDescWrap();
                }
            });
            rewatchObserver.observe(document.body, { childList: true, subtree: false });
        }
        if (!isPDA) {
            document.addEventListener('click', (e) => {
                if (String(StorageUtil.get('cat_attack_new_tab', 'true')) !== 'true')
                    return;
                const link = e.target.closest('a[href*="getInAttack"], a[href*="user2ID"], a[href*="sid=attack"]');
                if (link) {
                    e.preventDefault();
                    window.open(link.href, '_blank', 'noopener');
                }
            }, true);
        }
        this.setupCallButtonTooltips();
        this.setupEtaTooltips();
        this.setupGenericTooltips();
        this.setupLoadoutTooltips();
    }
    function setupCallButtonTooltips() {
        document.addEventListener('mouseover', (e) => {
            const target = e.target;
            if (target && target.classList.contains('call-button') && target.dataset.tooltip) {
                this.showTooltip(target);
            }
        });
        document.addEventListener('mouseout', (e) => {
            const target = e.target;
            if (target && target.classList.contains('call-button')) {
                this.hideTooltip();
            }
        });
    }
    function showTooltip(button) {
        this.hideTooltip();
        const tooltip = document.createElement('div');
        tooltip.className = 'call-button-tooltip';
        tooltip.textContent = button.dataset.tooltip || '';
        tooltip.style.cssText = `
        position: fixed;
        background: #333;
        color: #fff;
        padding: 6px 10px;
        border-radius: 4px;
        font-size: 12px;
        font-weight: 500;
        white-space: nowrap;
        z-index: 10000;
        pointer-events: none;
        animation: tooltipFadeIn 0.2s ease-in;
    `;
        document.body.appendChild(tooltip);
        this.currentTooltip = tooltip;
        const rect = button.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();
        const left = rect.left + (rect.width - tooltipRect.width) / 2;
        const top = rect.top - tooltipRect.height - 8;
        tooltip.style.left = left + 'px';
        tooltip.style.top = top + 'px';
        // Auto-hide if source element is removed from DOM or mouse leaves it
        const cleanup = (e) => {
            if (!button.isConnected || !button.contains(document.elementFromPoint(e.clientX, e.clientY))) {
                document.removeEventListener('mousemove', cleanup);
                this.hideTooltip();
            }
        };
        document.addEventListener('mousemove', cleanup);
    }
    function hideTooltip() {
        if (this.currentTooltip) {
            this.currentTooltip.remove();
            this.currentTooltip = null;
        }
    }
    function setupEtaTooltips() {
        document.addEventListener('mouseover', (e) => {
            if (String(StorageUtil.get('cat_eta_tooltip', 'false')) === 'false')
                return;
            const target = e.target;
            const statusEl = target.classList.contains('cat-travel-eta')
                ? target.parentElement
                : target.hasAttribute('data-eta-tooltip') ? target : null;
            if (statusEl?.dataset.etaTooltip) {
                this.showEtaTooltip(statusEl);
            }
        });
        document.addEventListener('mouseout', (e) => {
            const target = e.target;
            if (target.classList.contains('cat-travel-eta') || target.hasAttribute('data-eta-tooltip')) {
                this.hideEtaTooltip();
            }
        });
    }
    function showEtaTooltip(el) {
        this.hideEtaTooltip();
        const text = el.dataset.etaTooltip;
        if (!text)
            return;
        const tooltip = document.createElement('div');
        tooltip.className = 'cat-eta-tooltip';
        tooltip.style.cssText = `
        position: fixed;
        background: #1a1a2e;
        color: #ccc;
        padding: 8px 12px;
        border-radius: 4px;
        border: 1px solid #444;
        font-size: 11px;
        font-family: monospace;
        white-space: pre;
        z-index: 10000;
        pointer-events: none;
        animation: tooltipFadeIn 0.2s ease-in;
        line-height: 1.6;
    `;
        tooltip.textContent = text;
        document.body.appendChild(tooltip);
        this._currentEtaTooltip = tooltip;
        const rect = el.getBoundingClientRect();
        const tooltipRect = tooltip.getBoundingClientRect();
        const left = Math.max(4, rect.left + (rect.width - tooltipRect.width) / 2);
        const top = rect.top - tooltipRect.height - 6;
        tooltip.style.left = left + 'px';
        tooltip.style.top = (top > 0 ? top : rect.bottom + 6) + 'px';
        // Auto-hide if source element is removed from DOM or mouse leaves it
        const cleanup = (e) => {
            if (!el.isConnected || !el.contains(document.elementFromPoint(e.clientX, e.clientY))) {
                document.removeEventListener('mousemove', cleanup);
                this.hideEtaTooltip();
            }
        };
        document.addEventListener('mousemove', cleanup);
    }
    function hideEtaTooltip() {
        if (this._currentEtaTooltip) {
            this._currentEtaTooltip.remove();
            this._currentEtaTooltip = null;
        }
    }
    /** Generic tooltip for any element with data-cat-tooltip attribute (replaces native title to avoid stuck tooltips). */
    function setupGenericTooltips() {
        let genericTooltip = null;
        const hide = () => {
            if (genericTooltip) {
                genericTooltip.remove();
                genericTooltip = null;
            }
        };
        document.addEventListener('mouseover', (e) => {
            const target = e.target.closest('[data-cat-tooltip]');
            if (!target)
                return;
            hide();
            const text = target.dataset.catTooltip;
            if (!text)
                return;
            const tip = document.createElement('div');
            tip.className = 'call-button-tooltip';
            // Compact rendering for TS stat tooltips (contain STR:/DEF: lines)
            if (text.includes('STR:') || text.includes('DEF:')) {
                tip.textContent = text;
                tip.style.cssText = `
                position: fixed;
                background: #222;
                color: #ccc;
                padding: 5px 8px;
                border-radius: 4px;
                font-size: 10px;
                font-family: Monaco, Menlo, monospace;
                white-space: pre;
                line-height: 1.5;
                z-index: 10000;
                pointer-events: none;
                animation: tooltipFadeIn 0.15s ease-in;
            `;
            }
            else {
                tip.textContent = text;
                tip.style.cssText = `
                position: fixed;
                background: #333;
                color: #fff;
                padding: 6px 10px;
                border-radius: 4px;
                font-size: 12px;
                font-weight: 500;
                white-space: pre;
                z-index: 10000;
                pointer-events: none;
                animation: tooltipFadeIn 0.2s ease-in;
            `;
            }
            document.body.appendChild(tip);
            genericTooltip = tip;
            const rect = target.getBoundingClientRect();
            const tipRect = tip.getBoundingClientRect();
            tip.style.left = (rect.left + (rect.width - tipRect.width) / 2) + 'px';
            tip.style.top = (rect.top - tipRect.height - 6) + 'px';
            const cleanup = (ev) => {
                const over = document.elementFromPoint(ev.clientX, ev.clientY);
                // Accept hover on target itself, its children, or any ancestor that contains target (e.g. SVG with pointer-events:none)
                if (!target.isConnected || (!target.contains(over) && !over?.contains(target))) {
                    document.removeEventListener('mousemove', cleanup);
                    hide();
                }
            };
            document.addEventListener('mousemove', cleanup);
        });
        document.addEventListener('mouseout', (e) => {
            const target = e.target.closest('[data-cat-tooltip]');
            if (target)
                hide();
        });
    }

    function pause() {
        console.log('%c[CAT] Page lost focus — interceptors paused (Torn rules)', 'color:#FFA500;font-weight:bold;');
    }
    function resume() {
        console.log('%c[CAT] Page regained focus — interceptors resumed', 'color:#FFA500;font-weight:bold;');
        this._travelNodesValid = false;
        this._memberRowsValid = false;
        this._hospScanNeeded = true;
        // Force re-scan — timers may have expired while tab was in background
        this._lastExpiredRefresh = 0;
        // Run updateHospTimers first so expired timers are cleaned up while hospNodes is still populated
        this.updateHospTimers();
        // For any remaining frozen countdowns (tab was in background, Torn didn't tick):
        // clear expired timers, keep valid ones in place to avoid flash
        this.hospNodes.forEach(([id, node]) => {
            if (!node || !node.isConnected)
                return;
            const endTime = this.hospTime[id];
            if (endTime) {
                const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                if (endMs <= Date.now()) {
                    // Expired — clear
                    node.textContent = '';
                    delete node.dataset.catHospStatus;
                    delete node._catHospSecsNum;
                    delete this.hospTime[id];
                }
                // Still running — leave countdown text in place, updateHospTimers will refresh it
            }
            else {
                // No hospTime — check CSS class, otherwise clear
                const cn = node.className;
                const isNonHosp = cn.includes('okay') || cn.includes('traveling') || cn.includes('abroad') ||
                    cn.includes('jail') || cn.includes('federal') || cn.includes('fallen');
                if (isNonHosp) {
                    const correctStatus = cn.includes('okay') ? 'Okay' : cn.includes('traveling') ? 'Traveling' :
                        cn.includes('abroad') ? 'Abroad' : cn.includes('jail') ? 'Jail' :
                            cn.includes('federal') ? 'Federal' : 'Fallen';
                    node.textContent = correctStatus;
                }
                else if (/^\d{2}:\d{2}:\d{2}$/.test((node.textContent || '').trim())) {
                    node.textContent = '';
                }
                delete node.dataset.catHospStatus;
            }
        });
        // Don't wipe hospNodes — keep valid ones so updateHospTimers can tick immediately
        this.hospNodes = this.hospNodes.filter(([id, node]) => node && node.isConnected && this.hospTime[id]);
        this._hospNodesAllConnected = this.hospNodes.length > 0;
        this.scanHospitalizedMembers();
        this.updateHospTimers();
        // Reset all hosp-ready markers on tab return — status may have changed while in background.
        // updateHospTimers() above will re-set them if the timer is still expired and member is still out.
        document.querySelectorAll('li[data-cat-hosp-ready]').forEach(li => {
            li.removeAttribute('data-cat-hosp-ready');
        });
        this.updateTravelingStatus();
        this.injectLevelIndicators();
        this.updateTacticalMarkers();
        this.applyScoreStyles();
        // Re-sync WS state with background — messages sent while tab was in background
        // may have been lost (Firefox: sendMessage to inactive tab fails silently)
        if (isExtensionMode()) {
            extensionWSRequestStatus();
        }
        // Force re-sort on tab return
        if (this.enhancementManager) {
            this.enhancementManager._sortDirty = true;
        }
        // If Torn re-rendered the war DOM while the tab was in background,
        // call buttons and timers may be gone. Re-inject if needed.
        const descWrap = document.querySelector('.desc-wrap');
        if (descWrap && !descWrap.querySelector('.call-button')) {
            if (this.enhancementManager) {
                this.enhancementManager._enemyFactionEnhanced = false;
                this.enhancementManager._yourFactionEnhanced = false;
                this.enhancementManager._checkFactions();
            }
            this.startDomObserver();
        }
        if (this.pollingManager?.isActive()) {
            // Small delay to let WS status sync arrive before HTTP fetch
            // avoids double-update clignotement when WS reconnects simultaneously (Chrome)
            setTimeout(() => {
                if (this.pollingManager?.isActive()) {
                    this.pollingManager.requestCalls();
                }
            }, 300);
            const userFaction = StorageUtil.get('cat_user_faction_id', null);
            const targets = this.parseTargetsFromDOM();
            if (targets.length > 0 && userFaction) {
                this.pollingManager.sendWarUpdate(userFaction, targets);
            }
        }
    }
    function updateTheme(colors) {
        if (this.cssManager) {
            this.cssManager.updateColors(colors);
        }
    }
    function configure(config) {
        if (config.serverUrl) {
            this.apiManager.setServerUrl(config.serverUrl);
        }
        if (config.authToken) {
            this.apiManager.setAuthToken(config.authToken);
        }
        if (config.factionId) {
            this.apiManager.setFactionId(config.factionId);
        }
    }
    function disableAllCallButtons() {
        document.body.classList.add('hide-call-buttons');
    }
    function enableAllCallButtons() {
        if (state.updateRequired)
            return;
        document.body.classList.remove('hide-call-buttons');
    }

    class FactionWarEnhancer {
        constructor() {
            this._lastExpiredRefresh = 0;
            this.cssManager = null;
            this.enhancementManager = null;
            this.throttleManager = new ThrottleManager();
            this.apiManager = new APIManager();
            this.pollingManager = null;
            this.refreshInterval = null;
            this.isUpdatingButtons = false;
            // Load cached hospTimes so PDA Android script reloads don't lose timers
            const cachedHosp = StorageUtil.get('cat_hosp_times', null);
            if (cachedHosp) {
                const now = Date.now();
                this.hospTime = {};
                for (const [id, t] of Object.entries(cachedHosp)) {
                    const ms = t > 9999999999 ? t : t * 1000;
                    if (ms > now)
                        this.hospTime[id] = t;
                }
            }
            else {
                this.hospTime = {};
            }
            // Load cached online statuses for instant display
            const cachedStatuses = StorageUtil.get('cat_online_statuses', null);
            this.onlineStatuses = cachedStatuses || {};
            this.hospNodes = [];
            this._hospScanNeeded = true;
            this.hospLoopCounter = 0;
            // Load cached calls for instant display (admin fast path on reload)
            this.currentCalls = StorageUtil.get('cat_cached_calls', []) || [];
            // Load cached travelData so refresh doesn't lose Traveling/Abroad destinations
            const cachedTravel = StorageUtil.get('cat_travel_data', null);
            if (cachedTravel) {
                const now = Date.now();
                this.travelData = {};
                for (const [id, entry] of Object.entries(cachedTravel)) {
                    // Keep entries with no expiry, or not yet expired
                    const expiryMs = entry.updateAt ? (entry.updateAt > 9999999999 ? entry.updateAt : entry.updateAt * 1000) : null;
                    if (!expiryMs || expiryMs > now)
                        this.travelData[id] = entry;
                }
            }
            else {
                this.travelData = {};
            }
            this.previousStatus = {};
            this.domObserver = null;
            this.factionTags = [];
            this._memberNames = {};
            this._pendingHospFetch = new Set();
            this.warStatus = null;
            this.warEnemyName = null;
            this.activationStatus = null;
            this.subscriptionData = null;
            this.canActivateWar = false;
            this.currentPrice = 30;
            this.currentRankTier = 'gold';
            this._priceAlreadyFetched = false;
            this._lastCallsHash = undefined;
            this._lastBtnCount = undefined;
            this.currentTooltip = null;
            this._travelNodesValid = false;
            this._cachedTravelNodes = null;
            this._lastLeaderHtml = null;
            this._memberCallIds = new Map();
            this.revivableCache = {};
            // Load cached enemy chain for instant display
            this.enemyChainData = StorageUtil.get('cat_enemy_chain', null);
            this.chainBonusAssignment = null;
            this._chainBonusPollCount = 0;
            this._chainBonusInitTime = Date.now();
            this._chainBonusOptimisticUntil = 0;
            this.tacticalMarkers = {};
            this.softUncalls = [];
            this._notifiedSoftUncalls = new Set();
            this.ffStats = {};
            this.memberBarsCache = new Map();
            this._currentEtaTooltip = null;
            this._cachedMemberRows = null;
            this._memberRowsValid = false;
            this._domScanInterval = null;
            this._chainBoxInterval = null;
            this._memberBarsInterval = null;
            this._lastScoreStylesHash = undefined;
            this._lastHospTimerWrite = 0;
            this.init();
        }
        _esc(str) {
            if (str === null || str === undefined)
                return '';
            if (typeof str === 'number')
                return String(str);
            if (typeof str !== 'string')
                return '';
            return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
        }
        // Member call ID mapping (for attack icon)
        setMemberCallId(memberId, callId) {
            this._memberCallIds.set(memberId, callId);
        }
        getMemberCallId(memberId) {
            return this._memberCallIds.get(memberId);
        }
        clearMemberCallId(memberId) {
            this._memberCallIds.delete(memberId);
        }
        formatNumber(num) {
            if (!num || isNaN(Number(num)))
                return '0';
            const number = parseFloat(String(num));
            if (number === 0)
                return '0';
            const absNum = Math.abs(number);
            if (absNum >= 1e9) {
                return (number / 1e9).toFixed(1) + 'B';
            }
            else if (absNum >= 1e6) {
                return (number / 1e6).toFixed(1) + 'M';
            }
            else if (absNum >= 1e3) {
                return (number / 1e3).toFixed(1) + 'K';
            }
            else {
                return number.toFixed(0);
            }
        }
        cleanMemberName(name) {
            if (!name)
                return name;
            name = name.replace(/^[^a-zA-Z0-9_-]+/, '');
            for (const tag of this.factionTags) {
                if (name.startsWith(tag) && name.length > tag.length) {
                    return name.slice(tag.length);
                }
            }
            return name;
        }
        init() {
            this.cssManager = new CSSManager();
            this.pollingManager = new PollingManager(this.apiManager);
            this.pollingManager._enhancer = this;
            this._setupPollingCallbacks();
            this.pollingManager.start();
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => this.start());
            }
            else {
                this.start();
            }
        }
        _setupPollingCallbacks() {
            this.pollingManager.onCallsUpdate = (calls) => {
                const normalized = normalizeCallsArray(calls);
                this.currentCalls = normalized;
                StorageUtil.set('cat_cached_calls', normalized);
                this.updateCallButtons(normalized);
            };
            this.pollingManager.onStatusesUpdate = (statuses) => {
                if (!statuses || !Array.isArray(statuses))
                    return;
                // Feed server-known statuses to DOM scanner for comparison
                updateServerStatuses(statuses);
                statuses.forEach(s => {
                    const id = String(s.memberId);
                    if (s.status === 'Hospital' && s.until) {
                        if (!this.hospTime[id] && !tornConfirmedOkay.has(id)) {
                            const endTime = s.until;
                            const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                            if (endMs > Date.now()) {
                                this.hospTime[id] = endTime;
                            }
                        }
                        // Don't trust server's previousStatus — it can be stale.
                        // Only WS updateIcons/updateStatus provide reliable abroad info.
                    }
                    else if (s.status === 'Traveling' || s.status === 'Abroad') {
                        const fromArea = s.lastDestination ?? undefined;
                        // tornConfirmedOkay: WS already confirmed player landed — server data is stale, skip
                        if (!this.travelData[id] && s.previousArea != null && !tornConfirmedOkay.has(id)) {
                            this.travelData[id] = { area: s.previousArea, status: s.status, updateAt: s.until ?? undefined, departedAt: s.departedAt ?? undefined, fromArea };
                        }
                        else if (this.travelData[id]) {
                            if (!this.travelData[id].departedAt && s.departedAt) {
                                this.travelData[id].departedAt = s.departedAt;
                            }
                            if (!this.travelData[id].fromArea && fromArea) {
                                this.travelData[id].fromArea = fromArea;
                            }
                        }
                    }
                });
                // Re-scan after server provides hospTimes (nodes may already be registered but without timer)
                this._hospScanNeeded = true;
                this.scanHospitalizedMembers();
                this.updateHospTimers();
                // Trigger immediate re-sort so status changes (e.g. med → Okay) reflect instantly
                if (this.enhancementManager) {
                    this.enhancementManager.restoreSavedSort(true);
                }
            };
            this.pollingManager.onConnectionChange = (connected) => {
                const dot = document.querySelector('.cat-info-status-dot');
                if (dot) {
                    if (connected) {
                        dot.style.cssText = 'width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0;background:#48bb78;box-shadow:0 0 4px rgba(72,187,120,0.6);';
                        dot.className = 'cat-info-status-dot connected';
                    }
                    else {
                        dot.style.cssText = 'width:7px;height:7px;border-radius:50%;display:inline-block;flex-shrink:0;background:#fc8181;box-shadow:0 0 4px rgba(252,129,129,0.6);';
                        dot.className = 'cat-info-status-dot disconnected';
                    }
                }
                // Update sidebar badge dot
                const sDot = document.querySelector('.cat-sidebar-dot');
                if (sDot) {
                    if (connected) {
                        sDot.style.cssText = 'width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0;background:#48bb78;margin-left:6px;margin-right:4px;animation:catDotBlink 2s ease-in-out infinite;';
                    }
                    else {
                        sDot.style.cssText = 'width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0;background:#fc8181;margin-left:6px;margin-right:4px;animation:none;';
                    }
                }
            };
            this.pollingManager.onRalliesUpdate = (rallies) => {
                if (this.enhancementManager) {
                    this.enhancementManager.updateRallyButtons(rallies);
                }
            };
            this.pollingManager.onMemberBarsUpdate = (bars) => {
                this.memberBarsCache = new Map(bars.map(b => [b.player_id, b]));
                if (this.enhancementManager) {
                    this.enhancementManager.updateCDColumns();
                }
            };
            this.pollingManager.onRevivableDataUpdate = (data) => {
                for (const [playerId, isRevivable] of Object.entries(data)) {
                    this.revivableCache[playerId] = isRevivable;
                }
                this._revivableVersion = (this._revivableVersion || 0) + 1;
                invalidateReviveCache();
            };
            this.pollingManager.onTacticalMarkersUpdate = (markers) => {
                this.tacticalMarkers = {};
                for (const m of markers) {
                    const id = m.targetId || m.target_id;
                    if (id) {
                        this.tacticalMarkers[id] = {
                            targetId: id,
                            markerType: m.markerType || m.marker_type || '',
                            setBy: m.setBy || m.set_by || '',
                            setByName: m.setByName || m.set_by_name || ''
                        };
                    }
                }
                this.updateTacticalMarkers();
            };
            this.pollingManager.onSoftUncallsUpdate = (softUncalls) => {
                const isPDA = typeof window.flutter_inappwebview !== 'undefined';
                const masterNotifOn = String(StorageUtil.get('cat_pda_notifications', 'true')) === 'true';
                if (isPDA && masterNotifOn) {
                    const myId = this.apiManager.playerId;
                    // Notification: your called target is now Okay
                    // Only send if hosp timer is enabled AND lead time > 0 (otherwise the hosp timer
                    // already fires "At Okay" so this would be a duplicate)
                    const hospTimerOn = String(StorageUtil.get('cat_pda_notif_hosp', 'true')) === 'true';
                    const leadTime = parseInt(String(StorageUtil.get('cat_pda_notif_lead', '20')), 10);
                    if (hospTimerOn && leadTime > 0) {
                        for (const su of softUncalls) {
                            if (su.type === 'target-okay' && su.callerId === myId && !this._notifiedSoftUncalls.has(su.callId)) {
                                this._notifiedSoftUncalls.add(su.callId);
                                const notifId = Math.abs(parseInt(su.memberId, 10)) % 10000;
                                try {
                                    window.flutter_inappwebview.callHandler('cancelNotification', { id: notifId });
                                    window.flutter_inappwebview.callHandler('scheduleNotification', {
                                        title: `${su.memberName} is now Okay!`,
                                        subtitle: 'Your called target left hospital — attack now!',
                                        id: notifId,
                                        timestamp: Date.now() + 1000,
                                        urlCallback: `https://www.torn.com/page.php?sid=attack&user2ID=${su.memberId}`,
                                        launchNativeToast: true,
                                        toastMessage: `${su.memberName} is now Okay!`,
                                        toastColor: 'green',
                                        toastDurationSeconds: 5,
                                    });
                                }
                                catch { /* PDA handler not available */ }
                            }
                        }
                    }
                    // Notification: YOU got hospitalized while having an active call
                    const hospNotifOn = String(StorageUtil.get('cat_pda_notif_caller_hosp', 'true')) === 'true';
                    if (hospNotifOn && myId) {
                        for (const su of softUncalls) {
                            if (su.type === 'caller-hosp' && su.callerId === myId && !this._notifiedSoftUncalls.has('hosp-' + su.callId)) {
                                this._notifiedSoftUncalls.add('hosp-' + su.callId);
                                const secsLeft = Math.max(0, Math.round((su.expiresAt - Date.now()) / 1000));
                                const notifId = (Math.abs(parseInt(su.memberId, 10)) % 10000) + 5000;
                                try {
                                    window.flutter_inappwebview.callHandler('scheduleNotification', {
                                        title: `You're hospitalized!`,
                                        subtitle: `Med out within ${secsLeft}s to keep your call on ${su.memberName}`,
                                        id: notifId,
                                        timestamp: Date.now() + 500,
                                        urlCallback: 'https://www.torn.com/item.php#medical-tab',
                                        launchNativeToast: true,
                                        toastMessage: `Med out within ${secsLeft}s to keep your call on ${su.memberName}!`,
                                        toastColor: 'red',
                                        toastDurationSeconds: 8,
                                    });
                                }
                                catch { /* PDA handler not available */ }
                            }
                        }
                    }
                }
                // Clean up old notification IDs
                const activeIds = new Set(softUncalls.map(su => su.callId));
                const activeHospIds = new Set(softUncalls.map(su => 'hosp-' + su.callId));
                this._notifiedSoftUncalls.forEach(id => {
                    if (!activeIds.has(id) && !activeHospIds.has(id))
                        this._notifiedSoftUncalls.delete(id);
                });
                this.softUncalls = softUncalls;
                this.updateCallButtons(this.currentCalls);
            };
            this.pollingManager.onEnemyChainUpdate = (data) => {
                this.enemyChainData = data;
                StorageUtil.set('cat_enemy_chain', data);
                this.updateEnemyChainDisplay();
                // Re-evaluate bonus badge visibility (depends on chain proximity)
                if (this.chainBonusAssignment) {
                    this.updateCallButtons(this.currentCalls);
                }
                this.updateChainBonusClaimUI();
            };
            this.pollingManager.onChainBonusAssignmentUpdate = (assignment) => {
                // Skip polling updates briefly after an optimistic claim/unclaim to prevent flicker
                if (this._chainBonusOptimisticUntil && Date.now() < this._chainBonusOptimisticUntil) {
                    this._chainBonusPollCount++;
                    return;
                }
                this.chainBonusAssignment = assignment;
                this._chainBonusPollCount++;
                // No localStorage cache — server is the source of truth
                this.updateCallButtons(this.currentCalls);
                this.updateChainBonusClaimUI();
            };
            this.pollingManager.onFFStatsUpdate = (data) => {
                // Merge local FFS cache on top of server data (local takes priority if key set)
                this.ffStats = mergeFFSLocal(data);
                if (this.enhancementManager) {
                    this.enhancementManager.updateFFColumns();
                    this.enhancementManager.updateTSColumns();
                }
                const httpFn = this.apiManager.httpRequest.bind(this.apiManager);
                const rawEnemyFid = this.apiManager.factionId;
                const enemyFid = rawEnemyFid ? String(rawEnemyFid).replace(/\D/g, '') : null;
                const userFid = StorageUtil.get('cat_user_faction_id', null);
                // Fetch TornStats client-side (never sent to server)
                const tsKey = StorageUtil.get('cat_tornstats_api_key', '') || '';
                if (tsKey) {
                    if (enemyFid)
                        fetchTornStatsFaction(enemyFid, httpFn).then(() => { this.enhancementManager?.updateTSColumns(); }).catch(() => { });
                    if (userFid)
                        fetchTornStatsFaction(userFid, httpFn).then(() => { this.enhancementManager?.updateTSColumns(); }).catch(() => { });
                }
                // Fetch FF Scouter client-side (never sent to server, overrides server data)
                const ffsKey = (StorageUtil.get('cat_ffscouter_api_key', '') || '').trim();
                if (ffsKey) {
                    // Collect member IDs for both factions from DOM
                    const getMemberIds = () => {
                        const ids = [];
                        document.querySelectorAll('li[class*="enemy"], li[class*="your"], li[class*="member"]').forEach(li => {
                            const link = li.querySelector('a[href*="XID="]');
                            const m = link?.href.match(/XID=(\d+)/);
                            if (m)
                                ids.push(m[1]);
                        });
                        return [...new Set(ids)];
                    };
                    const memberIds = getMemberIds();
                    const factionIds = [enemyFid, userFid].filter((f) => !!f);
                    factionIds.forEach(fid => {
                        fetchFFScouterFaction(fid, memberIds, httpFn).then(() => {
                            this.ffStats = mergeFFSLocal(data);
                            this.enhancementManager?.updateFFColumns();
                            this.enhancementManager?.updateTSColumns();
                        }).catch(() => { });
                    });
                }
            };
        }
        async start() {
            if (!this.apiManager.torn_apikey) {
                this.disableAllCallButtons();
            }
            this.enhancementManager = new EnhancementManager();
            Object.assign(this.enhancementManager, { apiManager: this.apiManager, _enhancer: this });
            // Apply cached calls immediately if buttons already exist (before async detectFaction)
            if (this.currentCalls && this.currentCalls.length > 0) {
                this.updateCallButtons(this.currentCalls);
            }
            this.setupEventListeners();
            this.startPeriodicEnhancement();
            this.startTimerRefresh();
            this.startDomObserver();
            // Display cached enemy chain immediately (before first poll returns)
            if (this.enemyChainData) {
                this.updateEnemyChainDisplay();
            }
            // DOM status scanner + chain box interval now handled by master loop in dom-observer.ts
            // Fetch member bars (drug/medical CD) immediately then every 60s
            this.pollingManager?.fetchMemberBars();
            this._memberBarsInterval = setInterval(() => this.pollingManager?.fetchMemberBars(), 60000);
            this.injectChainBoxPanel();
            this._injectSidebarBadge();
            // Fast path: if cached activation + viewing other faction,
            // show call buttons immediately without waiting for checkActivationStatus() API round-trip
            const cachedIsAdmin = StorageUtil.get('cat_is_admin_cached', '') === 'true';
            const cachedActivated = StorageUtil.get('cat_activation_cached', '') === 'true';
            if ((cachedIsAdmin || cachedActivated) && state.catOtherFaction) {
                if (cachedIsAdmin) {
                    if (!this.subscriptionData)
                        this.subscriptionData = { success: true, isAdmin: true };
                    else
                        this.subscriptionData.isAdmin = true;
                }
                this.activationStatus = 'activated';
                this.removeReadOnlyMode();
                if (this.enhancementManager) {
                    this.enhancementManager._checkFactions();
                }
                if (this.currentCalls.length > 0) {
                    this.updateCallButtons(this.currentCalls);
                }
            }
            // Pre-fetch fresh call data immediately for ALL users — ensures onCallsUpdate
            // fires with correct data before/when buttons render (~500ms later)
            // Skip if viewing another faction without admin confirmed — would fetch wrong faction's calls
            const _prefetchIsAdmin = !!(this.subscriptionData?.isAdmin) || StorageUtil.get('cat_is_admin_cached', '') === 'true';
            if (this.pollingManager && (!state.catOtherFaction || _prefetchIsAdmin)) {
                this.pollingManager.lastCallsHash = null;
                this.pollingManager.fetchCalls();
            }
            this.apiManager.detectFactionAutomatically().then(async () => {
                if (this.currentCalls && this.currentCalls.length > 0) {
                    this.updateCallButtons(this.currentCalls);
                }
                try {
                    const enemyFactionId = this.apiManager.factionId;
                    const userFactionId = StorageUtil.get('cat_user_faction_id', null);
                    if (enemyFactionId && userFactionId && this.apiManager.torn_apikey) {
                        const [enemyInfo, userInfo] = await Promise.all([
                            this.apiManager.getFactionInfo(enemyFactionId),
                            this.apiManager.getFactionInfo(userFactionId)
                        ]);
                        if (enemyInfo && enemyInfo.basic && enemyInfo.basic.tag)
                            this.factionTags.push(enemyInfo.basic.tag);
                        if (userInfo && userInfo.basic && userInfo.basic.tag)
                            this.factionTags.push(userInfo.basic.tag);
                        if (userInfo)
                            this.sendFactionMembersToServer(userFactionId, userInfo, 'your faction');
                        if (enemyInfo)
                            this.sendFactionMembersToServer(enemyFactionId, enemyInfo, 'opposite faction');
                    }
                }
                catch (e) {
                    console.log('[MEMBERS] Auto-sync error:', e);
                    this.apiManager.reportError('autoSyncMembers', e);
                }
                // Fetch FF Scouter client-side if user has their own key (works for any faction, including other factions)
                const _ffsKey = (StorageUtil.get('cat_ffscouter_api_key', '') || '').trim();
                if (_ffsKey && state.catOtherFaction) {
                    const _httpFn = this.apiManager.httpRequest.bind(this.apiManager);
                    const _viewingFid = state.viewingFactionId;
                    const _memberIds = [];
                    document.querySelectorAll('a[href*="XID="]').forEach(a => {
                        const m = a.href.match(/XID=(\d+)/);
                        if (m)
                            _memberIds.push(m[1]);
                    });
                    const _uniqueIds = [...new Set(_memberIds)];
                    if (_viewingFid && _uniqueIds.length > 0) {
                        fetchFFScouterFaction(_viewingFid, _uniqueIds, _httpFn).then(() => {
                            if (this.ffStats) {
                                this.ffStats = mergeFFSLocal(this.ffStats);
                            }
                            else {
                                // No server data yet — build ffStats from local cache only
                                this.ffStats = mergeFFSLocal({});
                            }
                            this.enhancementManager?.updateFFColumns();
                            this.enhancementManager?.updateTSColumns();
                        }).catch(() => { });
                    }
                }
                this.injectChainBoxPanel();
                this.updateChainBonusClaimUI();
                await this.checkActivationStatus();
                // Reconnect WS with correct faction now that isAdmin/viewingFactionId are known
                if (this.pollingManager) {
                    this.pollingManager.reconnectWS();
                }
                // Rebuild tabs menu now that canActivateWar is known
                if (this.enhancementManager) {
                    const existingMenu = document.getElementById('custom-tabs-menu');
                    if (existingMenu) {
                        existingMenu.remove();
                        document.querySelectorAll('.custom-tab-content').forEach(c => c.remove());
                    }
                    this.enhancementManager.injectTabsMenu();
                }
            });
            console.log('%c[CAT] CAT Script loaded successfully \uD83D\uDD25', 'color:#ACEA01;font-weight:bold;font-size:13px;');
        }
        destroy() {
            if (this.refreshInterval) {
                clearInterval(this.refreshInterval);
                this.refreshInterval = null;
            }
            if (this._domScanInterval) {
                clearInterval(this._domScanInterval);
                this._domScanInterval = null;
            }
            if (this._chainBoxInterval) {
                clearInterval(this._chainBoxInterval);
                this._chainBoxInterval = null;
            }
            if (this._memberBarsInterval) {
                clearInterval(this._memberBarsInterval);
                this._memberBarsInterval = null;
            }
            if (this.domObserver) {
                this.domObserver.disconnect();
                this.domObserver = null;
            }
            if (this.enhancementManager) {
                this.enhancementManager.destroy();
            }
        }
        _injectSidebarBadge() {
            // Desktop only — sidebar doesn't exist on mobile
            const sidebar = document.querySelector('#sidebar');
            if (!sidebar || document.querySelector('#catSidebarBadge'))
                return;
            // Clone classes from a native sidebar link for identical styling
            const nativeLink = sidebar.querySelector('a[class*="desktopLink___"]');
            const nativeSvgWrap = nativeLink?.querySelector('span[class*="svgIconWrap___"]');
            const nativeDefaultIcon = nativeLink?.querySelector('span[class*="defaultIcon___"]');
            const nativeLinkName = nativeLink?.querySelector('span[class*="linkName___"]');
            if (!nativeLink)
                return; // sidebar not ready
            const badge = document.createElement('div');
            badge.id = 'catSidebarBadge';
            const isDark = document.body.classList.contains('dark-mode');
            badge.style.cssText = `background:${isDark ? '#333' : '#F2F2F2'};padding:0;border-radius:0 5px 5px 0;width:172px;height:23px;box-sizing:border-box;margin-top:2px;display:flex;align-items:center;`;
            const link = document.createElement('a');
            link.href = '#';
            link.className = nativeLink.className;
            link.style.cssText = 'display:flex;align-items:center;height:100%;filter:none !important;-webkit-filter:none !important;';
            link.addEventListener('click', (e) => {
                e.preventDefault();
                const settingsBtn = document.getElementById('settings-tab-btn');
                if (settingsBtn) {
                    settingsBtn.click();
                    settingsBtn.scrollIntoView({ behavior: 'smooth', block: 'center' });
                }
            });
            // Icon wrapper — reuse native classes but kill extra margin/padding
            const iconWrap = document.createElement('span');
            if (nativeSvgWrap)
                iconWrap.className = nativeSvgWrap.className;
            iconWrap.style.cssText = 'margin:0;padding:0;line-height:0;display:inline-flex;align-items:center;filter:none !important;-webkit-filter:none !important;';
            const innerIcon = document.createElement('span');
            if (nativeDefaultIcon)
                innerIcon.className = nativeDefaultIcon.className;
            innerIcon.style.cssText = 'margin:0;padding:0;line-height:0;display:inline-flex;align-items:center;filter:none !important;-webkit-filter:none !important;';
            // Animated cat face SVG with theme-aware gradient
            const gradTop = isDark ? '#787878' : '#9C9C9C';
            const gradBot = isDark ? '#676767' : '#CBCBCB';
            innerIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="23" height="20" viewBox="0 0 24 24" fill="none"><style>@keyframes catBreathe{0%,100%{transform:scale(1);transform-origin:center}50%{transform:scale(1.03);transform-origin:center}}@keyframes catBlink{0%,90%,100%{opacity:1}95%{opacity:0}}@keyframes catTwitch{0%,100%{transform:rotate(0deg);transform-origin:center}50%{transform:rotate(2deg);transform-origin:center}}.cat-body{animation:catBreathe 3s ease-in-out infinite}.cat-eyes{animation:catBlink 4s infinite}.wh-r{animation:catTwitch 2s ease-in-out infinite}.wh-l{animation:catTwitch 2s ease-in-out infinite reverse}</style><defs><linearGradient id="catGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="${gradTop}"/><stop offset="100%" stop-color="${gradBot}"/></linearGradient></defs><g class="cat-body"><path d="M19.9801 9.0625L20.7301 9.06545V9.0625H19.9801ZM4.01995 9.0625H3.26994L3.26995 9.06545L4.01995 9.0625ZM19.0993 10.6602L18.5268 11.1447L18.6114 11.2447L18.725 11.3101L19.0993 10.6602ZM18.8279 9.39546C18.494 9.15031 18.0246 9.22224 17.7795 9.55611C17.5343 9.88999 17.6063 10.3594 17.9401 10.6045L18.8279 9.39546ZM4.01994 15L3.26994 15V15H4.01994ZM6.05987 10.6045C6.39375 10.3594 6.46568 9.88999 6.22053 9.55612C5.97538 9.22224 5.50598 9.15031 5.1721 9.39546L6.05987 10.6045ZM12 5.65636C11.2279 5.65636 10.7904 5.69743 10.4437 5.74003C10.1041 5.78176 9.93161 5.8125 9.60601 5.8125V7.3125C10.0465 7.3125 10.3308 7.26518 10.6266 7.22883C10.9153 7.19336 11.2918 7.15636 12 7.15636V5.65636ZM12 7.15636C12.7083 7.15636 13.0847 7.19336 13.3734 7.22883C13.6692 7.26518 13.9536 7.3125 14.394 7.3125V5.8125C14.0684 5.8125 13.896 5.78176 13.5563 5.74003C13.2097 5.69743 12.7721 5.65636 12 5.65636V7.15636ZM14.394 7.3125C14.6069 7.3125 14.8057 7.25192 14.9494 7.19867C15.1051 7.14099 15.2662 7.06473 15.4208 6.98509C15.7257 6.82803 16.0797 6.61814 16.4042 6.43125C16.7431 6.23612 17.064 6.0575 17.3512 5.92771C17.6589 5.78868 17.8349 5.75011 17.9053 5.75011V4.25011C17.4968 4.25011 17.0743 4.40685 16.7336 4.56076C16.3725 4.72392 15.9951 4.9359 15.6557 5.13136C15.3019 5.33508 14.9976 5.51578 14.7338 5.65167C14.6041 5.7185 14.5034 5.7643 14.4284 5.79206C14.3415 5.82426 14.3408 5.8125 14.394 5.8125V7.3125ZM17.9053 5.75011C18.2495 5.75011 18.58 5.85266 18.8122 6.0527C19.0237 6.23486 19.2301 6.56231 19.2301 7.18761H20.7301C20.7301 6.18792 20.3778 5.42162 19.7913 4.91628C19.2255 4.42882 18.5186 4.25011 17.9053 4.25011V5.75011ZM19.2301 7.18761V9.0625H20.7301V7.18761H19.2301ZM9.60601 5.8125C9.65925 5.8125 9.65855 5.82426 9.57164 5.79206C9.49668 5.7643 9.39595 5.71849 9.26624 5.65166C9.00249 5.51576 8.69813 5.33504 8.34437 5.13132C8.00493 4.93584 7.62754 4.72384 7.26643 4.56067C6.92577 4.40675 6.5032 4.25 6.09476 4.25V5.75C6.16512 5.75 6.34105 5.78856 6.64878 5.92761C6.93605 6.05741 7.25693 6.23603 7.5958 6.43118C7.92035 6.61808 8.27434 6.82799 8.57919 6.98506C8.73377 7.06471 8.89488 7.14098 9.05059 7.19866C9.19436 7.25191 9.39317 7.3125 9.60601 7.3125V5.8125ZM6.09476 4.25C5.48139 4.25 4.77453 4.42871 4.20872 4.91616C3.62216 5.4215 3.26995 6.18781 3.26995 7.1875H4.76995C4.76995 6.56219 4.97634 6.23475 5.18778 6.05259C5.41998 5.85254 5.75053 5.75 6.09476 5.75V4.25ZM3.26995 7.1875V9.0625H4.76995V7.1875H3.26995ZM12 20.75C13.431 20.75 15.5401 20.4654 17.3209 19.6462C19.1035 18.8262 20.7301 17.3734 20.7301 15H19.2301C19.2301 16.5328 18.2232 17.58 16.694 18.2835C15.1631 18.9877 13.2822 19.25 12 19.25V20.75ZM19.6719 10.1758C19.437 9.89818 19.1575 9.63749 18.8279 9.39546L17.9401 10.6045C18.1808 10.7813 18.3726 10.9625 18.5268 11.1447L19.6719 10.1758ZM19.2301 9.05955C19.2293 9.25778 19.1888 9.67007 19.0916 9.95501C19.0374 10.1139 19.0062 10.1101 19.0627 10.0649C19.1075 10.0289 19.1902 9.98403 19.3002 9.97847C19.4051 9.97317 19.468 10.007 19.4737 10.0103L18.725 11.3101C18.9057 11.4142 19.1272 11.4891 19.3759 11.4766C19.6297 11.4637 19.8412 11.3633 20.0013 11.2349C20.2881 11.0048 20.4331 10.6686 20.5113 10.4392C20.679 9.94758 20.7289 9.35941 20.7301 9.06545L19.2301 9.05955ZM12 19.25C10.7178 19.25 8.83685 18.9877 7.30594 18.2835C5.7768 17.5801 4.76994 16.5328 4.76994 15H3.26994C3.26994 17.3734 4.89649 18.8262 6.67907 19.6462C8.45988 20.4654 10.5689 20.75 12 20.75V19.25ZM4.76994 15C4.76994 14.2119 4.71349 13.5629 4.7889 12.8724C4.85939 12.227 5.04214 11.6541 5.47321 11.1447L4.32811 10.1758C3.64728 10.9804 3.38966 11.8682 3.29777 12.7095C3.2108 13.5058 3.26994 14.3696 3.26994 15L4.76994 15ZM5.47321 11.1447C5.62738 10.9625 5.81916 10.7813 6.05987 10.6045L5.1721 9.39546C4.84248 9.63749 4.56299 9.89818 4.32811 10.1758L5.47321 11.1447ZM3.26995 9.06545C3.27111 9.35941 3.32101 9.94757 3.48871 10.4392C3.56694 10.6686 3.71186 11.0048 3.99873 11.2349C4.15878 11.3633 4.3703 11.4637 4.62412 11.4766C4.87277 11.4891 5.0943 11.4142 5.27501 11.3101L4.52631 10.0103C4.53204 10.007 4.59487 9.97317 4.69976 9.97847C4.80981 9.98403 4.89252 10.0289 4.93734 10.0649C4.99376 10.1101 4.96261 10.1139 4.9084 9.95501C4.81121 9.67007 4.77072 9.25778 4.76994 9.05955L3.26995 9.06545Z" fill="url(#catGrad)"/><path d="M12.826 16C12.826 16.1726 12.465 16.3125 12.0196 16.3125C11.5742 16.3125 11.2131 16.1726 11.2131 16C11.2131 15.8274 11.5742 15.6875 12.0196 15.6875C12.465 15.6875 12.826 15.8274 12.826 16Z" stroke="url(#catGrad)" stroke-width="1.5"/></g><g class="cat-eyes"><path d="M15.5 13.5938C15.5 14.0252 15.2834 14.375 15.0161 14.375C14.7489 14.375 14.5323 14.0252 14.5323 13.5938C14.5323 13.1623 14.7489 12.8125 15.0161 12.8125C15.2834 12.8125 15.5 13.1623 15.5 13.5938Z" stroke="url(#catGrad)" stroke-width="1.5"/><path d="M9.5 13.5938C9.5 14.0252 9.28336 14.375 9.01613 14.375C8.74889 14.375 8.53226 14.0252 8.53226 13.5938C8.53226 13.1623 8.74889 12.8125 9.01613 12.8125C9.28336 12.8125 9.5 13.1623 9.5 13.5938Z" stroke="url(#catGrad)" stroke-width="1.5"/></g><g class="wh-r"><path d="M22.0004 15.4688C21.5165 15.1562 19.4197 14.375 18.6133 14.375" stroke="url(#catGrad)" stroke-width="1.5" stroke-linecap="round"/><path d="M20.3871 17.9688C19.9033 17.6562 18.7742 16.875 17.9678 16.875" stroke="url(#catGrad)" stroke-width="1.5" stroke-linecap="round"/></g><g class="wh-l"><path d="M2 15.4688C2.48387 15.1562 4.58065 14.375 5.3871 14.375" stroke="url(#catGrad)" stroke-width="1.5" stroke-linecap="round"/><path d="M3.61279 17.9688C4.09667 17.6562 5.2257 16.875 6.03215 16.875" stroke="url(#catGrad)" stroke-width="1.5" stroke-linecap="round"/></g></svg>`;
            iconWrap.appendChild(innerIcon);
            link.appendChild(iconWrap);
            // Inject blink keyframes once
            if (!document.querySelector('#catSidebarDotAnim')) {
                const style = document.createElement('style');
                style.id = 'catSidebarDotAnim';
                style.textContent = '@keyframes catDotBlink{0%,100%{opacity:1}50%{opacity:.3}}';
                document.head.appendChild(style);
            }
            // Text — same class as native link names
            const text = document.createElement('span');
            if (nativeLinkName)
                text.className = nativeLinkName.className;
            text.textContent = `CAT Script: ${VERSION}`;
            link.appendChild(text);
            // Status dot — after text. Green blinking if online, red if offline
            const alreadyConnected = !!(this.pollingManager?._isActive);
            const dot = document.createElement('span');
            dot.className = 'cat-sidebar-dot';
            dot.style.cssText = alreadyConnected
                ? 'width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0;background:#48bb78;margin-left:6px;margin-right:4px;animation:catDotBlink 2s ease-in-out infinite;'
                : 'width:6px;height:6px;border-radius:50%;display:inline-block;flex-shrink:0;background:#fc8181;margin-left:6px;margin-right:4px;';
            link.appendChild(dot);
            badge.appendChild(link);
            // React to dark/light mode changes
            const updateBadgeBg = () => {
                const dark = document.body.classList.contains('dark-mode');
                badge.style.background = dark ? '#333' : '#F2F2F2';
                // Update SVG gradient colors
                const svg = badge.querySelector('svg');
                if (svg) {
                    const stops = svg.querySelectorAll('linearGradient stop');
                    if (stops[0])
                        stops[0].setAttribute('stop-color', dark ? '#787878' : '#9C9C9C');
                    if (stops[1])
                        stops[1].setAttribute('stop-color', dark ? '#676767' : '#CBCBCB');
                }
            };
            const observer = new MutationObserver(updateBadgeBg);
            observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
            // Insert just after #nav-calendar
            const navCalendar = sidebar.querySelector('#nav-calendar');
            if (navCalendar && navCalendar.parentElement) {
                navCalendar.parentElement.insertBefore(badge, navCalendar.nextSibling);
            }
            else {
                // Fallback: before first area-desktop element
                const areaDesktop = sidebar.querySelector('[class*="area-desktop___"]');
                if (areaDesktop && areaDesktop.parentElement) {
                    areaDesktop.parentElement.insertBefore(badge, areaDesktop);
                }
                else {
                    const areasContainer = sidebar.querySelector('[class*="areas___"]');
                    if (areasContainer) {
                        areasContainer.appendChild(badge);
                    }
                    else {
                        sidebar.appendChild(badge);
                    }
                }
            }
            // Avoid double spacing: no margin-top if prev sibling has margin-bottom, and vice versa
            const prev = badge.previousElementSibling;
            const next = badge.nextElementSibling;
            if (prev) {
                const prevMb = parseFloat(getComputedStyle(prev).marginBottom) || 0;
                if (prevMb > 0)
                    badge.style.marginTop = '0';
            }
            if (next) {
                const nextMt = parseFloat(getComputedStyle(next).marginTop) || 0;
                if (nextMt > 0)
                    badge.style.marginBottom = '0';
            }
        }
        updateEnemyChainDisplay() {
            const BONUS_HITS = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
            const chain = this.enemyChainData?.enemyChain ?? 0;
            const nextBonus = BONUS_HITS.find(b => b > chain) || BONUS_HITS[BONUS_HITS.length - 1];
            const broken = this.enemyChainData?.chainBroken ?? false;
            const text = broken ? `\u26D3\uFE0F\u200D\uD83D\uDCA5 Enemy Chain: ${chain}/${nextBonus}` : `Enemy Chain: ${chain}/${nextBonus}`;
            // Place below the timer info block's parent row
            const timerBlock = document.querySelector('[class*="infoBlock___"][class*="timer___"]');
            if (timerBlock) {
                const row = timerBlock.closest('[class*="warInfo___"]') || timerBlock.parentElement;
                if (row) {
                    let el = document.getElementById('cat-enemy-chain');
                    if (!el) {
                        el = document.createElement('div');
                        el.id = 'cat-enemy-chain';
                        el.style.cssText = 'color:#FF794C !important;font-size:10px !important;font-weight:600 !important;text-align:center !important;padding:0 !important;margin-top:-4px !important;width:100% !important;';
                        row.parentElement.insertBefore(el, row.nextSibling);
                    }
                    if (el.textContent !== text)
                        el.textContent = text;
                    return;
                }
            }
            // Fallback: inject into opponent block
            const parent = document.querySelector('.opponentBlock___XPro7') || document.querySelector('[class*="opponentBlock"]');
            if (!parent)
                return;
            if (parent.textContent !== text) {
                parent.textContent = text;
                parent.style.cssText = 'color:#FF794C !important;font-size:10px !important;font-weight:600 !important;';
            }
        }
    }
    // Prototype assignments
    FactionWarEnhancer.prototype.injectChainBoxPanel = injectChainBoxPanel;
    FactionWarEnhancer.prototype.renderChainBoxPanelHTML = renderChainBoxPanelHTML;
    FactionWarEnhancer.prototype.setupChainBoxPanelHandlers = setupChainBoxPanelHandlers;
    FactionWarEnhancer.prototype.applyScoreStyles = applyScoreStyles;
    FactionWarEnhancer.prototype.updateChainBoxPanelValues = updateChainBoxPanelValues;
    FactionWarEnhancer.prototype.injectWarStatusBar = injectWarStatusBar;
    FactionWarEnhancer.prototype.updateChainBonusClaimUI = updateChainBonusClaimUI;
    FactionWarEnhancer.prototype._sendChainBonusClaim = _sendChainBonusClaim;
    FactionWarEnhancer.prototype._sendChainBonusUnclaim = _sendChainBonusUnclaim;
    FactionWarEnhancer.prototype._showBonusReassignDropdown = _showBonusReassignDropdown;
    FactionWarEnhancer.prototype.checkEnlistedStatus = checkEnlistedStatus;
    FactionWarEnhancer.prototype.updateEnlistedBadge = updateEnlistedBadge;
    FactionWarEnhancer.prototype.checkActivationStatus = checkActivationStatus;
    FactionWarEnhancer.prototype.applyReadOnlyMode = applyReadOnlyMode;
    FactionWarEnhancer.prototype.removeReadOnlyMode = removeReadOnlyMode;
    FactionWarEnhancer.prototype.fetchDynamicPrice = fetchDynamicPrice;
    FactionWarEnhancer.prototype.showActivationBanner = showActivationBanner;
    FactionWarEnhancer.prototype.hideActivationBanner = hideActivationBanner;
    FactionWarEnhancer.prototype.activateWar = activateWar;
    FactionWarEnhancer.prototype.startPeriodicEnhancement = startPeriodicEnhancement;
    FactionWarEnhancer.prototype.registerHospNode = registerHospNode;
    FactionWarEnhancer.prototype.injectLevelIndicators = injectLevelIndicators;
    FactionWarEnhancer.prototype.scanHospitalizedMembers = scanHospitalizedMembers;
    FactionWarEnhancer.prototype.updateHospTimers = updateHospTimers;
    FactionWarEnhancer.prototype.updateTravelingStatus = updateTravelingStatus;
    FactionWarEnhancer.prototype.startCallRefresh = startCallRefresh;
    FactionWarEnhancer.prototype.startTimerRefresh = startTimerRefresh;
    FactionWarEnhancer.prototype.startDomObserver = startDomObserver;
    FactionWarEnhancer.prototype.startDomStatusScanner = startDomStatusScanner;
    FactionWarEnhancer.prototype.parseTargetsFromDOM = parseTargetsFromDOM;
    FactionWarEnhancer.prototype._applyCallsToButtons = _applyCallsToButtons;
    FactionWarEnhancer.prototype.updateCallButtons = updateCallButtons;
    FactionWarEnhancer.prototype.updateCallButtonsFromServer = updateCallButtonsFromServer;
    FactionWarEnhancer.prototype.updateTacticalMarkers = updateTacticalMarkers;
    FactionWarEnhancer.prototype._factionCard = _factionCard;
    FactionWarEnhancer.prototype.renderFactionStats = renderFactionStats;
    FactionWarEnhancer.prototype.renderDualFactionStats = renderDualFactionStats;
    FactionWarEnhancer.prototype.getPredictions = getPredictions;
    FactionWarEnhancer.prototype.sendFactionMembersToServer = sendFactionMembersToServer;
    FactionWarEnhancer.prototype.setupEventListeners = setupEventListeners;
    FactionWarEnhancer.prototype.setupCallButtonTooltips = setupCallButtonTooltips;
    FactionWarEnhancer.prototype.showTooltip = showTooltip;
    FactionWarEnhancer.prototype.hideTooltip = hideTooltip;
    FactionWarEnhancer.prototype.setupEtaTooltips = setupEtaTooltips;
    FactionWarEnhancer.prototype.showEtaTooltip = showEtaTooltip;
    FactionWarEnhancer.prototype.hideEtaTooltip = hideEtaTooltip;
    FactionWarEnhancer.prototype.setupGenericTooltips = setupGenericTooltips;
    FactionWarEnhancer.prototype.setupLoadoutTooltips = setupLoadoutTooltips;
    FactionWarEnhancer.prototype.pause = pause;
    FactionWarEnhancer.prototype.resume = resume;
    FactionWarEnhancer.prototype.updateTheme = updateTheme;
    FactionWarEnhancer.prototype.configure = configure;
    FactionWarEnhancer.prototype.disableAllCallButtons = disableAllCallButtons;
    FactionWarEnhancer.prototype.enableAllCallButtons = enableAllCallButtons;

    function exposeGlobalAPI(enhancer) {
        window.FactionWarEnhancer = enhancer;
        window.setTornApiKey = function (apiKey) {
            if (enhancer.apiManager) {
                enhancer.apiManager.saveTornApiKey(apiKey);
                enhancer.apiManager.detectFactionAutomatically();
            }
        };
        window.getTornApiKey = function () {
            if (enhancer.apiManager) {
                return enhancer.apiManager.torn_apikey ? '✅ API Key est configurée' : '❌ API Key non configurée';
            }
            return '❌ API Manager non disponible';
        };
        window.resetTornApiKey = function () {
            StorageUtil.remove('cat_api_key_script');
        };
        window.showBspCacheKeys = function () {
            for (let i = 0; i < localStorage.length; i++) {
                const key = localStorage.key(i);
                if (key && (key.includes('bsp') || key.includes('cache') || key.includes('battle') || key.includes('stats'))) {
                    const size = localStorage.getItem(key)?.length ?? 0;
                    console.log(`  ${key}: ${size} chars`);
                }
            }
        };
        window.clearFactionCache = function () {
            StorageUtil.remove('cat_enemy_faction_id');
            return 'Faction cache cleared. Reload the page to re-detect faction.';
        };
        window.getFactionCacheStatus = function () {
            const cached = StorageUtil.get('cat_enemy_faction_id', null);
            if (!cached) {
                return { status: 'no_cache', message: 'No faction cache found' };
            }
            const now = Date.now();
            const remaining = cached.expiresAt - now;
            const remainingMinutes = Math.round(remaining / 60000);
            if (remaining > 0) {
                return {
                    status: 'valid',
                    id: cached.id,
                    name: cached.name,
                    expiresIn: `${remainingMinutes} minutes`
                };
            }
            else {
                return { status: 'expired', message: 'Cache has expired' };
            }
        };
    }

    const BONUS_HITS = [10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 10000, 25000, 50000, 100000];
    const WARN_DISTANCE = 3;
    // Helper: send HTTP request using extension bridge or GM_xmlhttpRequest
    function _catRequest(details) {
        // Re-check bridge dynamically — _bridgeReady is evaluated at module load time and may be stale
        const bridgeNow = typeof document !== 'undefined' && document.documentElement.getAttribute('data-cat-bridge') === 'ready';
        if (bridgeNow || isExtensionMode()) {
            extensionFetch(details.url, {
                method: details.method,
                headers: details.headers || {},
                body: details.data || undefined,
            }).then(resp => {
                if (details.onload) {
                    resp.text().then(text => {
                        details.onload({ status: resp.status, responseText: text });
                    });
                }
            }).catch(() => {
                if (details.onerror)
                    details.onerror();
            });
        }
        else if (typeof GM_xmlhttpRequest !== 'undefined') {
            GM_xmlhttpRequest(details);
        }
        else if (typeof window.PDA_httpPost === 'function' && typeof window.PDA_httpGet === 'function') {
            const method = (details.method || 'GET').toUpperCase();
            const pdaCall = method === 'POST'
                ? window.PDA_httpPost(details.url, details.headers || {}, details.data || '')
                : window.PDA_httpGet(details.url, details.headers || {});
            pdaCall.then((resp) => {
                if (details.onload) {
                    const text = typeof resp === 'string' ? resp : (resp?.responseText || JSON.stringify(resp));
                    const status = typeof resp === 'object' && resp?.status ? resp.status : 200;
                    details.onload({ status, responseText: text });
                }
            }).catch(() => {
                if (details.onerror)
                    details.onerror();
            });
        }
        else {
            // Fallback: native fetch (cross-origin works as cat.dgh.sh has CORS for torn.com)
            fetch(details.url, {
                method: details.method,
                headers: details.headers || {},
                body: details.data || undefined,
            }).then(resp => {
                if (details.onload) {
                    resp.text().then(text => {
                        details.onload({ status: resp.status, responseText: text });
                    });
                }
            }).catch(() => {
                if (details.onerror)
                    details.onerror();
            });
        }
    }
    const MARKER_ICONS = {
        smoke: {
            img: 'smoke.png',
            color: '#90A4AE',
            label: 'Smoke'
        },
        tear: {
            img: 'tear.png',
            color: '#4FC3F7',
            label: 'Tear'
        },
        help: {
            img: 'help.png',
            color: '#EF5350',
            label: 'Help'
        },
    };
    class ChainWarning {
        constructor() {
            this.chain = 0;
            this.el = null;
            this.activeMarker = null;
            this.activeMarkerSetBy = null;
            this.markerContainer = null;
            this.fightMarkerContainer = null;
            this.canUseMarkers = false;
            this.hospUntilMs = 0;
            this.hospEls = [];
            this.hospInterval = null;
            // ── Chain timer relay to server ─────────────────────────────────
            this._lastRelayedChainEnd = 0;
        }
        init() {
            // Reset loadout upload flag for each new attack page
            window.__CAT_LOADOUT_UPLOADED = false;
            // Install fetch intercept only once per script version (survives SPA re-evals)
            const interceptVersion = 'v2-loadout';
            if (window.__CAT_CHAIN_INIT !== interceptVersion) {
                window.__CAT_CHAIN_INIT = interceptVersion;
                this.installFetchIntercept();
            }
            // Always (re)inject UI — handles SPA navigation and dynamic DOM
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => {
                    this.injectUI();
                    this.loadCurrentMarker();
                    this.fetchHospIfNeeded();
                });
            }
            else {
                this.injectUI();
                this.loadCurrentMarker();
                this.fetchHospIfNeeded();
            }
        }
        // ── Fetch Intercept ─────────────────────────────────────────────
        installFetchIntercept() {
            const self = this;
            const targetWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
            const oldFetch = targetWindow.fetch;
            targetWindow.fetch = async (...args) => {
                const firstArg = args[0];
                const url = (typeof firstArg === 'object' && firstArg !== null && 'url' in firstArg)
                    ? firstArg.url
                    : (typeof firstArg === 'string' ? firstArg : undefined);
                const response = await oldFetch(...args);
                if (typeof url === 'string' && url.includes('sid=attackData')) {
                    const clone = response.clone();
                    clone.json().then((json) => {
                        // attackData JSON is flat (no DB wrapper)
                        const db = json?.DB ?? json;
                        const attacker = db?.attackerUser;
                        const defender = db?.defenderUser;
                        const chain = attacker?.chain ?? 0;
                        self.onChainUpdate(chain);
                        self.checkAttackResult(json, db);
                        // Capture defender loadout on first data (before fight starts)
                        if (db)
                            self.uploadDefenderLoadout(db);
                        // Hospital countdown: fetch from Torn API when defender is in hospital
                        const errorMsg = json?.error ?? '';
                        const attackStatus = json?.attackStatus ?? '';
                        const defId = defender?.userID ?? 0;
                        if (defId > 0 && (errorMsg.toLowerCase().includes('hospital') || attackStatus === 'notStarted')) {
                            self.fetchDefenderHospUntil(defId);
                        }
                        // Relay chain timer to server for Discord chain monitor
                        const chainEndSec = attacker?.chainEnd ?? 0;
                        if (chainEndSec > 0 && chain > 0) {
                            self.relayChainTimer(chain, chainEndSec);
                        }
                    }).catch(() => { });
                }
                return response;
            };
        }
        // ── Defender loadout upload ─────────────────────────────────────
        uploadDefenderLoadout(db) {
            // Only upload once per attack page load
            if (window.__CAT_LOADOUT_UPLOADED)
                return;
            const defenderItems = db.defenderItems;
            const defenderUser = db.defenderUser;
            const attackerUser = db.attackerUser;
            if (!defenderItems || typeof defenderItems !== 'object')
                return;
            if (!defenderUser?.userID || !attackerUser?.userID)
                return;
            // Don't upload if Torn hasn't revealed items yet (only Fists slot present)
            const hasRealItems = Object.keys(defenderItems).some(k => k !== '999');
            if (!hasRealItems)
                return;
            const factionId = localStorage.getItem('cat_user_faction_id');
            const authToken = localStorage.getItem('cat_auth_token');
            const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat.dgh.sh';
            if (!factionId || !authToken)
                return;
            window.__CAT_LOADOUT_UPLOADED = true;
            _catRequest({
                method: 'POST',
                url: `${serverUrl}/api/loadout/upload`,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${authToken}`
                },
                data: JSON.stringify({
                    factionId,
                    targetId: defenderUser.userID,
                    targetName: defenderUser.playername || null,
                    targetFactionId: defenderUser.factionID ? String(defenderUser.factionID) : null,
                    attackerId: attackerUser.userID,
                    attackerName: attackerUser.playername || null,
                    items: defenderItems,
                }),
                onerror: () => { window.__CAT_LOADOUT_UPLOADED = false; }
            });
        }
        relayChainTimer(chainCount, chainEndSeconds) {
            // chainEndSeconds is a countdown (seconds remaining), convert to unix timestamp ms
            const chainEnd = Date.now() + chainEndSeconds * 1000;
            // Don't spam — only relay if chainEnd changed significantly (>5s diff)
            if (Math.abs(chainEnd - this._lastRelayedChainEnd) < 5000)
                return;
            this._lastRelayedChainEnd = chainEnd;
            const factionId = localStorage.getItem('cat_user_faction_id');
            const authToken = localStorage.getItem('cat_auth_token');
            const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat-script.com';
            if (!factionId || !authToken)
                return;
            _catRequest({
                method: 'POST',
                url: `${serverUrl}/api/chain-timer`,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${authToken}`
                },
                data: JSON.stringify({ factionId, chainEnd, chainCount })
            });
        }
        // ── Hospital countdown ─────────────────────────────────────────
        fetchDefenderHospUntil(defId) {
            this.resolveHospUntil(defId).then(untilMs => {
                if (untilMs > Date.now()) {
                    this.onDefenderHospUpdate(untilMs);
                    this.injectHospDialog();
                }
            }).catch(() => { });
        }
        onDefenderHospUpdate(untilMs) {
            this.hospUntilMs = untilMs;
            if (untilMs <= 0) {
                this.clearHospCountdown();
                return;
            }
            this.renderHospCountdown();
            if (!this.hospInterval) {
                this.hospInterval = setInterval(() => this.renderHospCountdown(), 1000);
            }
        }
        renderHospCountdown() {
            if (this.hospEls.length === 0)
                return;
            const remaining = this.hospUntilMs - Date.now();
            if (remaining <= 0) {
                this.clearHospCountdown();
                return;
            }
            const totalSec = Math.ceil(remaining / 1000);
            const h = Math.floor(totalSec / 3600);
            const m = Math.floor((totalSec % 3600) / 60);
            const s = totalSec % 60;
            const fmt = h > 0
                ? `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`
                : `${m}:${String(s).padStart(2, '0')}`;
            for (const el of this.hospEls) {
                el.textContent = `Hospital: ${fmt}`;
                el.style.display = '';
            }
        }
        clearHospCountdown() {
            if (this.hospInterval) {
                clearInterval(this.hospInterval);
                this.hospInterval = null;
            }
            for (const el of this.hospEls) {
                el.textContent = '';
                el.style.display = 'none';
            }
        }
        fetchHospIfNeeded() {
            const defIdStr = new URLSearchParams(window.location.search).get('user2ID');
            console.log('[CAT Hosp] fetchHospIfNeeded — defId:', defIdStr);
            if (!defIdStr)
                return;
            this.resolveHospUntil(Number(defIdStr)).then(untilMs => {
                console.log('[CAT Hosp] resolveHospUntil result — untilMs:', untilMs, '| now:', Date.now(), '| in hosp:', untilMs > Date.now());
                if (untilMs > Date.now()) {
                    this.onDefenderHospUpdate(untilMs);
                    this.injectHospDialog();
                }
            }).catch(err => { console.log('[CAT Hosp] resolveHospUntil error:', err); });
        }
        resolveHospUntil(defId) {
            // 1. Check cat_hosp_times (set by hospital-timers module on faction page)
            try {
                const hospTimes = JSON.parse(localStorage.getItem('cat_hosp_times') || '{}');
                const raw = hospTimes[String(defId)];
                const untilMs = typeof raw === 'number' ? (raw > 9999999999 ? raw : raw * 1000) : 0;
                console.log('[CAT Hosp] cat_hosp_times for', defId, '— raw:', raw, '→ ms:', untilMs);
                if (untilMs > Date.now()) {
                    console.log('[CAT Hosp] Using cached hosp time');
                    return Promise.resolve(untilMs);
                }
            }
            catch (_) { }
            // 2. Fetch from Torn API v2 using native fetch
            const apiKey = localStorage.getItem('cat_api_key_script');
            console.log('[CAT Hosp] API key present:', !!apiKey);
            if (!apiKey)
                return Promise.resolve(0);
            const url = `https://api.torn.com/v2/user/${defId}?selections=profile&key=${apiKey}&comment=CAT`;
            console.log('[CAT Hosp] Fetching Torn API:', url.replace(apiKey, '***'));
            return fetch(url)
                .then(r => r.json())
                .then(data => {
                console.log('[CAT Hosp] Torn API response status:', data?.status);
                const st = data?.status;
                if (st?.state === 'Hospital' && st?.until > 0) {
                    return st.until * 1000;
                }
                return 0;
            })
                .catch(err => { console.log('[CAT Hosp] Torn API fetch error:', err); return 0; });
        }
        injectHospDialog() {
            const doInject = () => {
                const title = document.querySelector('[class*="dialog___"] [class*="title___"]');
                if (!title)
                    return null;
                const el = document.createElement('div');
                el.id = 'cat-hosp-dialog';
                el.className = 'cat-hosp-countdown cat-hosp-dialog';
                title.appendChild(el);
                return el;
            };
            const el = doInject();
            if (el) {
                this.hospEls.push(el);
                this.renderHospCountdown();
            }
            // Re-inject if React removes our element (title re-render)
            const obs = new MutationObserver(() => {
                if (!document.getElementById('cat-hosp-dialog') && this.hospUntilMs > Date.now()) {
                    // Remove stale ref and re-inject
                    this.hospEls = this.hospEls.filter(e => e.id !== 'cat-hosp-dialog');
                    const newEl = doInject();
                    if (newEl) {
                        this.hospEls.push(newEl);
                        this.renderHospCountdown();
                    }
                }
            });
            obs.observe(document.body || document.documentElement, { childList: true, subtree: true });
            // Stop observing when hospital time expires
            const stopObs = setInterval(() => {
                if (this.hospUntilMs <= Date.now()) {
                    obs.disconnect();
                    clearInterval(stopObs);
                }
            }, 5000);
        }
        // ── Chain update handler ───────────────────────────────────────
        onChainUpdate(chain) {
            this.chain = chain;
            if (chain > 0) {
                this.getNextBonus(chain);
            }
            this.updateDisplay();
        }
        // ── Attack result handler ──────────────────────────────────────
        checkAttackResult(json, db) {
            // Check if combat ended
            if (db?.attackStatus !== 'end' || !db?.usersLife) {
                return;
            }
            const usersLife = db.usersLife;
            const defender = usersLife.defender;
            const attacker = usersLife.attacker;
            const defenderDied = defender?.currentLife === 0;
            const attackerDied = attacker?.currentLife === 0;
            if (!defenderDied && !attackerDied) {
                return;
            }
            // Get target user ID from URL
            const urlParams = new URLSearchParams(window.location.search);
            const targetUserId = urlParams.get('user2ID');
            if (!targetUserId) {
                return;
            }
            console.log(`[CAT] Combat ended - defender died: ${defenderDied}, attacker died: ${attackerDied}`);
            // Get necessary data from localStorage
            const factionId = localStorage.getItem('cat_user_faction_id');
            let playerId = null;
            try {
                const userInfo = JSON.parse(localStorage.getItem('cat_user_info') || '{}');
                playerId = userInfo.id ? String(userInfo.id) : null;
            }
            catch (_) { /* ignore */ }
            const apiKey = localStorage.getItem('cat_auth_token');
            if (!factionId || !apiKey) {
                console.log('[CAT] Missing factionId or authToken for status update');
                return;
            }
            // Send status update to server
            const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat.dgh.sh';
            _catRequest({
                method: 'POST',
                url: `${serverUrl}/api/status-update`,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${apiKey}`
                },
                data: JSON.stringify({
                    factionId: factionId,
                    statuses: [{
                            memberId: targetUserId,
                            status: 'Hospital',
                            details: 'Attacked',
                            until: Date.now() + 300000
                        }],
                    updatedBy: playerId || null
                }),
                onload: (response) => {
                    if (response.status === 200) {
                        console.log('[CAT] Attack result sent successfully');
                    }
                    else {
                        console.log('[CAT] Failed to send attack result:', response.status);
                    }
                },
                onerror: () => {
                    console.log('[CAT] Error sending attack result');
                }
            });
        }
        // ── Bonus logic ────────────────────────────────────────────────
        getNextBonus(current) {
            for (const b of BONUS_HITS) {
                if (b > current)
                    return b;
            }
            return null;
        }
        getLevel(current) {
            if (current <= 0)
                return 'none';
            const next = this.getNextBonus(current);
            if (!next)
                return 'info';
            const left = next - current;
            if (left <= 1)
                return 'bonus';
            if (left <= WARN_DISTANCE)
                return 'warning';
            return 'info';
        }
        // ── UI ─────────────────────────────────────────────────────────
        injectUI() {
            // Reuse existing element if already in DOM
            const existing = document.getElementById('cat-chain-warning');
            if (existing) {
                this.el = existing;
                this.markerContainer = document.getElementById('cat-tactical-markers');
                return;
            }
            // Reset stale references (element was detached by SPA navigation)
            this.el = null;
            this.markerContainer = null;
            this.injectStyles();
            this.el = document.createElement('div');
            this.el.id = 'cat-chain-warning';
            const hospEl = document.createElement('div');
            hospEl.id = 'cat-hosp-countdown';
            hospEl.style.display = 'none';
            this.hospEls = [hospEl];
            // Create marker container
            this.markerContainer = document.createElement('div');
            this.markerContainer.id = 'cat-tactical-markers';
            const title = document.createElement('div');
            title.className = 'cat-marker-title';
            title.textContent = 'Request assist';
            this.markerContainer.appendChild(title);
            const btnRow = document.createElement('div');
            btnRow.className = 'cat-marker-row';
            this.markerContainer.appendChild(btnRow);
            for (const [type, icon] of Object.entries(MARKER_ICONS)) {
                const btn = document.createElement('button');
                btn.className = 'cat-marker-btn';
                btn.dataset.type = type;
                const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat-script.com';
                btn.title = icon.label;
                btn.style.color = icon.color;
                btn.innerHTML = `<img src="${serverUrl}/assets/${icon.img}" alt="${icon.label}"><span class="cat-marker-label">${icon.label}</span>`;
                btn.addEventListener('click', () => this.sendTacticalMarker(type));
                btnRow.appendChild(btn);
            }
            // Insert after dialogButtons___ (pre-fight) or above appHeaderWrapper___ (fight in progress)
            let fightMarkersInjected = false;
            const tryInsert = () => {
                if (this.el?.isConnected)
                    return true;
                // Case 1: Pre-fight page → insert chain warning after "Start fight" buttons
                const buttons = document.querySelector('[class*="dialogButtons___"]');
                if (buttons && this.el) {
                    buttons.after(this.el);
                    const hospEl = this.hospEls[0];
                    if (hospEl)
                        this.el.after(hospEl);
                    this.updateDisplay();
                    this.hookStartFightButton(buttons);
                    return true;
                }
                // Case 2: Fight already in progress → inject fight markers above header
                // Don't return true — keep observer alive in case dialogButtons appears later
                if (!fightMarkersInjected) {
                    const header = document.querySelector('[class*="appHeaderWrapper___"]');
                    if (header && !document.getElementById('cat-fight-markers')) {
                        this.injectFightMarkers(header);
                        fightMarkersInjected = true;
                    }
                }
                return false;
            };
            if (!tryInsert()) {
                const obs = new MutationObserver(() => {
                    if (tryInsert())
                        obs.disconnect();
                });
                obs.observe(document.body || document.documentElement, { childList: true, subtree: true });
                setTimeout(() => obs.disconnect(), 30000);
            }
        }
        injectStyles() {
            const style = document.createElement('style');
            style.textContent = `
            #cat-chain-warning {
                font-size: 12px;
                font-weight: bold;
                text-align: center;
                padding: 4px 0;
            }
            #cat-chain-warning.cat-cw-none { color: #999; }
            #cat-chain-warning.cat-cw-info { color: #8899ee; }
            #cat-chain-warning.cat-cw-warning { color: #ffb733; }
            #cat-chain-warning.cat-cw-bonus { color: #44cc44; }

            #cat-tactical-markers {
                display: flex;
                flex-direction: column;
                align-items: center;
                padding: 4px 0;
            }
            .cat-marker-title {
                font-size: 11px;
                color: #aaa;
                margin-bottom: 4px;
            }
            .cat-marker-row {
                display: flex;
                justify-content: center;
                gap: 6px;
            }
            .cat-marker-btn {
                width: 64px;
                height: 72px;
                border: 2px solid transparent;
                border-radius: 4px;
                background: rgba(255,255,255,0.08);
                cursor: pointer;
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: center;
                opacity: 1;
                transition: border-color 0.15s, background 0.15s;
                padding: 4px 8px;
                gap: 2px;
            }
            .cat-marker-btn img { width: 44px; height: 44px; object-fit: contain; display: block; }
            .cat-marker-label { font-size: 10px; color: #ccc; line-height: 1; }
            .cat-marker-btn:hover { background: rgba(255,255,255,0.15); }
            .cat-marker-btn.active {
                border-color: currentColor;
                background: rgba(255,255,255,0.2);
            }
            .cat-marker-btn.locked {
                cursor: not-allowed;
                opacity: 0.25;
            }
            .cat-marker-error {
                font-size: 10px;
                color: #EF5350;
                text-align: center;
                padding: 2px 0;
            }

            #cat-hosp-countdown, .cat-hosp-countdown {
                font-size: 12px;
                font-weight: bold;
                text-align: center;
                color: #ef5350;
                padding: 2px 0;
            }
            .cat-hosp-dialog {
                font-size: inherit;
                font-weight: inherit;
                font-family: inherit;
                color: inherit;
                text-shadow: inherit;
                text-align: center;
                margin-top: 6px;
            }

            #cat-fight-markers {
                display: flex;
                justify-content: center;
                padding: 6px 0 2px;
            }
            .cat-fight-marker-row {
                display: flex;
                justify-content: center;
                gap: 6px;
            }
            .cat-fight-marker-btn {
                width: 52px;
                height: 56px;
                padding: 3px 6px;
            }
            .cat-fight-marker-btn img { width: 34px; height: 34px; }
        `;
            document.head.appendChild(style);
        }
        updateDisplay() {
            if (!this.el)
                return;
            const level = this.getLevel(this.chain);
            this.el.className = `cat-cw-${level}`;
            if (level === 'none') {
                this.el.textContent = 'No active chain';
                return;
            }
            const next = this.getNextBonus(this.chain);
            const left = next ? next - this.chain : null;
            if (level === 'bonus' && next) {
                this.el.textContent = `Chain: ${this.chain}/${next} — NEXT BONUS HIT!`;
            }
            else if (next && left !== null) {
                this.el.textContent = `Chain: ${this.chain}/${next} — ${left} hits left`;
            }
            else {
                this.el.textContent = `Chain: ${this.chain}`;
            }
        }
        // ── Tactical Markers ────────────────────────────────────────────
        getServerInfo() {
            const factionId = localStorage.getItem('cat_user_faction_id');
            const apiKey = localStorage.getItem('cat_auth_token');
            const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat.dgh.sh';
            const targetId = new URLSearchParams(window.location.search).get('user2ID');
            let playerId = null;
            try {
                const userInfo = JSON.parse(localStorage.getItem('cat_user_info') || '{}');
                playerId = userInfo.id ? String(userInfo.id) : null;
            }
            catch (_) { /* ignore */ }
            // Extract defender name from DOM (attack page — 2nd player panel is the defender)
            let targetName = null;
            const defenderPanel = document.querySelectorAll('[class*="player___"]')[1];
            const nameEl = defenderPanel?.querySelector('[class*="userName___"]');
            if (nameEl) {
                targetName = (nameEl.textContent || '').trim() || null;
            }
            if (!factionId || !apiKey || !targetId)
                return null;
            return { serverUrl, apiKey, factionId, targetId, playerId, targetName };
        }
        loadCurrentMarker() {
            const info = this.getServerInfo();
            if (!info)
                return;
            _catRequest({
                method: 'GET',
                url: `${info.serverUrl}/api/tactical-marker?factionId=${info.factionId}&targetId=${info.targetId}${info.playerId ? '&playerId=' + info.playerId : ''}`,
                headers: { 'Authorization': `Bearer ${info.apiKey}` },
                onload: (response) => {
                    if (response.status === 200) {
                        try {
                            const data = JSON.parse(response.responseText);
                            // Server says this player cannot use markers → hide them
                            if (data.canUseMarkers === false) {
                                this.hideMarkersUI();
                            }
                            else {
                                this.canUseMarkers = true;
                            }
                            if (data.marker) {
                                this.activeMarker = data.marker.markerType || data.marker.marker_type;
                                this.activeMarkerSetBy = data.marker.setBy || data.marker.set_by;
                                this.updateMarkerButtons();
                            }
                        }
                        catch (_) { /* silent */ }
                    }
                }
            });
        }
        hideMarkersUI() {
            if (this.markerContainer) {
                this.markerContainer.style.display = 'none';
            }
            const fightMarkers = document.getElementById('cat-fight-markers');
            if (fightMarkers) {
                fightMarkers.style.display = 'none';
            }
        }
        sendTacticalMarker(type) {
            const info = this.getServerInfo();
            if (!info) {
                // Missing server info — cannot send marker
                return;
            }
            // Send marker request to server
            // If locked by another player, do nothing
            if (this.activeMarker && this.activeMarkerSetBy && info.playerId && String(this.activeMarkerSetBy) !== String(info.playerId)) {
                // Marker locked by another player
                return;
            }
            // Toggle: same type → send null to remove
            const sendType = (this.activeMarker === type) ? null : type;
            // Optimistic update — apply immediately before server response
            const prevMarker = this.activeMarker;
            const prevSetBy = this.activeMarkerSetBy;
            if (sendType) {
                this.activeMarker = sendType;
                this.activeMarkerSetBy = info.playerId;
            }
            else {
                this.activeMarker = null;
                this.activeMarkerSetBy = null;
            }
            this.updateMarkerButtons();
            localStorage.setItem('cat_tactical_marker_signal', String(Date.now()));
            _catRequest({
                method: 'POST',
                url: `${info.serverUrl}/api/tactical-marker`,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${info.apiKey}`
                },
                data: JSON.stringify({
                    factionId: info.factionId,
                    targetId: info.targetId,
                    markerType: sendType,
                    userFactionId: info.factionId,
                    targetName: info.targetName
                }),
                onload: (response) => {
                    if (response.status === 200) {
                        // Server confirmed — signal again to pick up server state
                        localStorage.setItem('cat_tactical_marker_signal', String(Date.now()));
                    }
                    else if (response.status === 403) {
                        // Revert optimistic update
                        this.activeMarker = prevMarker;
                        this.activeMarkerSetBy = prevSetBy;
                        this.updateMarkerButtons();
                        localStorage.setItem('cat_tactical_marker_signal', String(Date.now()));
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.error === 'marker_set_by_other') {
                                this.showMarkerError(`Set by ${data.setByName}`);
                            }
                        }
                        catch (_) { /* silent */ }
                    }
                    else {
                        // Revert on unexpected error
                        this.activeMarker = prevMarker;
                        this.activeMarkerSetBy = prevSetBy;
                        this.updateMarkerButtons();
                        localStorage.setItem('cat_tactical_marker_signal', String(Date.now()));
                    }
                },
                onerror: () => {
                    // Revert on network error
                    this.activeMarker = prevMarker;
                    this.activeMarkerSetBy = prevSetBy;
                    this.updateMarkerButtons();
                    localStorage.setItem('cat_tactical_marker_signal', String(Date.now()));
                }
            });
        }
        updateMarkerButtons() {
            const info = this.getServerInfo();
            const isMyMarker = !this.activeMarkerSetBy || (info?.playerId && String(this.activeMarkerSetBy) === String(info.playerId));
            const containers = [this.markerContainer, this.fightMarkerContainer].filter(Boolean);
            for (const container of containers) {
                const btns = container.querySelectorAll('.cat-marker-btn');
                btns.forEach(btn => {
                    const type = btn.dataset.type;
                    const isActive = type === this.activeMarker;
                    btn.classList.toggle('active', isActive);
                    btn.classList.toggle('locked', !!this.activeMarker && !isActive && !isMyMarker);
                    if (isActive && !isMyMarker) {
                        btn.title = `${MARKER_ICONS[type]?.label} (by ${this.activeMarkerSetBy})`;
                    }
                    else if (isActive) {
                        btn.title = `${MARKER_ICONS[type]?.label} (click to remove)`;
                    }
                    else {
                        btn.title = MARKER_ICONS[type]?.label || type || '';
                    }
                });
            }
        }
        hookStartFightButton(buttonsContainer) {
            const startBtn = buttonsContainer.querySelector('button[type="submit"]');
            if (!startBtn)
                return;
            startBtn.addEventListener('click', () => {
                const info = this.getServerInfo();
                if (!info)
                    return;
                // Mark as attacking on the server
                _catRequest({
                    method: 'POST',
                    url: `${info.serverUrl}/api/call/mark-attacking`,
                    headers: {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${info.apiKey}`
                    },
                    data: JSON.stringify({
                        factionId: info.factionId,
                        targetId: info.targetId
                    }),
                    onload: (response) => {
                        if (response.status === 200) {
                            localStorage.setItem('cat_attacking_signal', String(Date.now()));
                        }
                    },
                    onerror: () => { }
                });
                // Watch for fight page header to appear → move markers above it
                this.waitForFightHeader();
            });
        }
        waitForFightHeader() {
            const tryInject = () => {
                const header = document.querySelector('[class*="appHeaderWrapper___"]');
                if (!header)
                    return false;
                // Already injected in fight header
                if (document.getElementById('cat-fight-markers'))
                    return true;
                this.injectFightMarkers(header);
                return true;
            };
            if (!tryInject()) {
                const obs = new MutationObserver(() => {
                    if (tryInject())
                        obs.disconnect();
                });
                obs.observe(document.body || document.documentElement, { childList: true, subtree: true });
                setTimeout(() => obs.disconnect(), 30000);
            }
        }
        injectFightMarkers(headerEl) {
            const container = document.createElement('div');
            container.id = 'cat-fight-markers';
            const btnRow = document.createElement('div');
            btnRow.className = 'cat-fight-marker-row';
            container.appendChild(btnRow);
            for (const [type, icon] of Object.entries(MARKER_ICONS)) {
                const btn = document.createElement('button');
                btn.className = 'cat-marker-btn cat-fight-marker-btn';
                btn.dataset.type = type;
                const serverUrl = localStorage.getItem('cat_server_url') || 'https://cat-script.com';
                btn.title = icon.label;
                btn.style.color = icon.color;
                btn.innerHTML = `<img src="${serverUrl}/assets/${icon.img}" alt="${icon.label}"><span class="cat-marker-label">${icon.label}</span>`;
                btn.addEventListener('click', () => this.sendTacticalMarker(type));
                btnRow.appendChild(btn);
            }
            // Store as secondary marker container for updateMarkerButtons
            this.fightMarkerContainer = container;
            // Insert inside the header wrapper, before the <hr> delimiter
            const delimiter = headerEl.querySelector('[class*="delimiter___"]');
            if (delimiter) {
                delimiter.before(container);
            }
            else {
                headerEl.appendChild(container);
            }
            this.updateMarkerButtons();
        }
        showMarkerError(msg) {
            if (!this.markerContainer)
                return;
            let errEl = this.markerContainer.parentElement?.querySelector('.cat-marker-error');
            if (!errEl) {
                errEl = document.createElement('div');
                errEl.className = 'cat-marker-error';
                this.markerContainer.after(errEl);
            }
            errEl.textContent = msg;
            setTimeout(() => errEl?.remove(), 3000);
        }
    }

    // Dedicated PDA queue for level-progress — separate from main http-client queue
    // to avoid competing with polling requests at page load
    Promise.resolve();
    const CACHE_KEY = 'cat_level_progress';
    const INACTIVE_THRESHOLD = 200 * 24 * 60 * 60; // 200 days in seconds
    /**
     * Returns the timestamp (ms) of the most recent 00:05 UTC reset.
     * HOF level rankings update at Torn's daily reset (00:00 UTC); we add 5 min buffer.
     */
    function _lastResetTs() {
        const now = new Date();
        const reset = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 5, 0, 0));
        if (reset.getTime() > now.getTime()) {
            // Today's 00:05 UTC hasn't happened yet — use yesterday's
            reset.setUTCDate(reset.getUTCDate() - 1);
        }
        return reset.getTime();
    }
    async function _sleep(ms) {
        return new Promise(r => setTimeout(r, ms));
    }
    async function _apiFetch(url) {
        // Extension (Chrome/Firefox/Safari extension bridge)
        if (isExtensionMode()) {
            const res = await extensionFetch(url);
            if (!res.ok)
                throw new Error(`fetch failed: ${res.status}`);
            return res.json();
        }
        // TornPDA — native fetch works directly in Flutter webview
        const _isPDA = typeof window.flutter_inappwebview !== 'undefined' || typeof window.PDA_httpGet !== 'undefined';
        if (_isPDA) {
            const res = await fetch(url);
            if (!res.ok)
                throw new Error(`fetch failed: ${res.status}`);
            return res.json();
        }
        // Safari (customFetch)
        const _customFetch = typeof window.customFetch === 'function' ? window.customFetch : undefined;
        if (_customFetch) {
            const res = await _customFetch(url);
            if (!res.ok)
                throw new Error(`fetch failed: ${res.status}`);
            return res.json();
        }
        // Tampermonkey (GM_xmlhttpRequest)
        if (typeof GM_xmlhttpRequest !== 'undefined') {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url,
                    onload: (r) => {
                        try {
                            resolve(JSON.parse(r.responseText));
                        }
                        catch (e) {
                            reject(e);
                        }
                    },
                    onerror: () => reject(new Error('GM_xmlhttpRequest failed')),
                    ontimeout: () => reject(new Error('GM_xmlhttpRequest timeout'))
                });
            });
        }
        // Native fetch fallback
        const res = await fetch(url);
        if (!res.ok)
            throw new Error(`fetch failed: ${res.status}`);
        return res.json();
    }
    async function _getHofPage(apiKey, offset) {
        const url = `https://api.torn.com/v2/torn/hof?limit=100&offset=${offset}&cat=level&key=${apiKey}`;
        const data = await _apiFetch(url);
        return data.hof ?? [];
    }
    /**
     * Binary search to find the approximate offset where `targetLevel` players start,
     * then linear scan to find the first player at that level inactive for 365+ days.
     * Returns their HOF position, or null if none found within maxScanPages pages.
     */
    async function _findInactivePos(apiKey, targetLevel, searchRight) {
        const nowSec = Math.floor(Date.now() / 1000);
        const maxScanPages = 40; // safety limit for linear scan
        // Binary search phase — find approximate zone start
        let left = 1;
        let right = searchRight;
        let approxOffset = Math.max(0, Math.floor((left + right) / 2) - 50);
        for (let i = 0; i < 14 && left + 100 <= right; i++) {
            await _sleep(350);
            const mid = Math.max(0, Math.floor((left + right) / 2) - 50);
            const page = await _getHofPage(apiKey, mid);
            if (page.length === 0) {
                right = mid;
                continue;
            }
            const hasTarget = page.some(p => p.level === targetLevel);
            const hasHigher = page.some(p => p.level > targetLevel);
            const hasLower = page.some(p => p.level < targetLevel);
            if (hasHigher && !hasTarget && !hasLower) {
                left = mid + 100; // zone is to the right
            }
            else if (hasHigher && hasTarget && !hasLower) {
                approxOffset = mid; // right at the boundary
                break;
            }
            else {
                right = mid - 1;
            }
            approxOffset = mid;
        }
        // Linear scan phase — find first inactive player at targetLevel
        let offset = Math.max(0, approxOffset - 100);
        for (let page = 0; page < maxScanPages; page++) {
            await _sleep(350);
            const entries = await _getHofPage(apiKey, offset);
            if (entries.length === 0)
                break;
            for (const p of entries) {
                if (p.level === targetLevel && p.last_action !== undefined) {
                    const inactiveSec = nowSec - p.last_action;
                    if (inactiveSec > INACTIVE_THRESHOLD) {
                        return p.position;
                    }
                }
            }
            // If we've passed the level zone entirely, stop scanning
            const allLower = entries.every(p => p.level < targetLevel);
            if (allLower)
                break;
            offset += 100;
        }
        return null;
    }
    /**
     * Fallback: find the HOF rank of the first player at `targetLevel`.
     * Uses binary search — returns null if not found.
     */
    async function _findLevelBoundary(apiKey, targetLevel, searchRight) {
        let left = 1;
        let right = searchRight;
        let bestPos = null;
        for (let i = 0; i < 16 && left + 100 <= right; i++) {
            await _sleep(350);
            const mid = Math.max(0, Math.floor((left + right) / 2) - 50);
            const page = await _getHofPage(apiKey, mid);
            if (page.length === 0) {
                right = mid;
                continue;
            }
            const hasTarget = page.some(p => p.level === targetLevel);
            const hasHigher = page.some(p => p.level > targetLevel);
            const hasLower = page.some(p => p.level < targetLevel);
            if (hasHigher && !hasTarget && !hasLower) {
                left = mid + 100;
            }
            else if (hasTarget) {
                // Find the first (lowest rank = highest position) player at targetLevel in this page
                const first = page.filter(p => p.level === targetLevel).sort((a, b) => a.position - b.position)[0];
                if (first)
                    bestPos = first.position;
                if (hasHigher)
                    left = mid + 100; // boundary is further right
                else
                    right = mid - 1;
            }
            else {
                right = mid - 1;
            }
        }
        return bestPos;
    }
    async function _calcProgress(apiKey) {
        const hofData = await _apiFetch(`https://api.torn.com/v2/user/hof?key=${apiKey}`);
        const userLevel = hofData.hof?.level?.value;
        const userRank = hofData.hof?.level?.rank;
        if (!userLevel || !userRank)
            return null;
        if (userLevel >= 100)
            return { finalLevel: 100, pct: 100 };
        // --- Method 1: inactive anchors ---
        const currentInactivePos = await _findInactivePos(apiKey, userLevel, userRank + 3000);
        const lowerInactivePos = await _findInactivePos(apiKey, userLevel - 1, userRank + 5000);
        if (currentInactivePos && lowerInactivePos && lowerInactivePos > currentInactivePos) {
            const relativePos = (lowerInactivePos - userRank) / (lowerInactivePos - currentInactivePos);
            const clamped = Math.min(Math.max(relativePos, 0.01), 0.99);
            const pct = Math.round(clamped * 100);
            return { finalLevel: userLevel + pct / 100, pct };
        }
        console.log('[LevelXP] inactive anchors not found, falling back to level boundary method');
        // --- Method 2: level boundaries (fallback) ---
        const rankFirstCurrentLevel = await _findLevelBoundary(apiKey, userLevel, userRank + 1000);
        const rankFirstNextLevel = await _findLevelBoundary(apiKey, userLevel + 1, userRank);
        if (!rankFirstCurrentLevel || !rankFirstNextLevel || rankFirstNextLevel >= rankFirstCurrentLevel)
            return null;
        const relativePos = (userRank - rankFirstCurrentLevel) / (rankFirstNextLevel - rankFirstCurrentLevel);
        const clamped = Math.min(Math.max(relativePos, 0.01), 0.99);
        const pct = Math.round(clamped * 100);
        return { finalLevel: userLevel + pct / 100, pct };
    }
    function _formatLabel(pct, finalLevel, isMobile) {
        if (pct == null || finalLevel == null)
            return 'wait';
        const currentLvl = Math.floor(finalLevel);
        if (currentLvl >= 100)
            return isMobile ? 'Lvl : 100' : '100';
        const pctRounded = Math.round(pct);
        if (isMobile)
            return `XP ${pctRounded}/100`;
        return `${currentLvl} \u2192 ${currentLvl + 1} \u00B7 ${pctRounded}%`;
    }
    function _isMobile() {
        return !!(document.querySelector('p.wai')) || window.innerWidth < 600;
    }
    function _injectBar(pct, finalLevel) {
        const mobile = _isMobile();
        const existing = document.getElementById('cat-level-progress-wrap');
        if (existing) {
            const bar = existing.querySelector('#cat-level-progress-fill');
            const label = existing.querySelector('#cat-level-progress-label');
            const nameEl2 = existing.querySelector('[class*="bar-name"], p.wai');
            const isMax2 = finalLevel != null && Math.floor(finalLevel) >= 100;
            if (nameEl2 && !mobile)
                nameEl2.textContent = isMax2 ? 'Lvl:' : 'Lvl XP:';
            if (bar)
                bar.style.width = isMax2 ? '100%' : (pct != null ? pct.toFixed(1) + '%' : '0%');
            if (label) {
                if (mobile) {
                    label.style.cssText += ';display:flex;align-items:center;';
                    label.innerHTML = `<img src="data:image/png;base64,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" width="11" height="11" style="display:block;margin-right:2px;flex-shrink:0;">${_formatLabel(pct, finalLevel, mobile)}`;
                }
                else {
                    label.textContent = _formatLabel(pct, finalLevel, mobile);
                }
            }
            return;
        }
        const lifeBar = document.querySelector('[class*="life___"]');
        if (!lifeBar || !lifeBar.parentNode)
            return;
        const statsEl = lifeBar.querySelector('[class*="bar-stats"]');
        const nameEl = lifeBar.querySelector('[class*="bar-name"]') || lifeBar.querySelector('p.wai');
        const valueEl = lifeBar.querySelector('[class*="bar-value"]');
        const progressEl = lifeBar.querySelector('[class*="progress___"]');
        const lineEl = lifeBar.querySelector('[class*="progress-line___"]');
        if (!statsEl || !nameEl || !valueEl || !progressEl || !lineEl)
            return;
        const wrap = document.createElement('div');
        wrap.id = 'cat-level-progress-wrap';
        wrap.className = lifeBar.className;
        wrap.style.cssText = 'cursor:default;';
        wrap.setAttribute('tabindex', '0');
        wrap.setAttribute('role', 'region');
        wrap.setAttribute('aria-roledescription', 'progress indicator');
        const stats = document.createElement('div');
        stats.className = statsEl.className;
        const name = document.createElement('p');
        name.className = nameEl.className;
        const isMax = finalLevel != null && Math.floor(finalLevel) >= 100;
        name.textContent = mobile ? '' : (isMax ? 'Lvl:' : 'Lvl XP:');
        const value = document.createElement('p');
        value.id = 'cat-level-progress-label';
        value.className = valueEl.className;
        if (mobile) {
            value.style.cssText += ';display:flex;align-items:center;';
            value.innerHTML = `<img src="data:image/png;base64,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" width="11" height="11" style="display:block;margin-right:2px;flex-shrink:0;">${_formatLabel(pct, finalLevel, mobile)}`;
        }
        else {
            value.textContent = _formatLabel(pct, finalLevel, mobile);
        }
        stats.appendChild(name);
        stats.appendChild(value);
        const progressWrap = document.createElement('div');
        progressWrap.className = progressEl.className;
        progressWrap.style.cssText = 'border-color:#7048e8;background:rgba(132,94,247,.12);';
        const fill = document.createElement('div');
        fill.id = 'cat-level-progress-fill';
        fill.className = lineEl.className;
        fill.style.width = isMax ? '100%' : (pct != null ? pct.toFixed(1) + '%' : '0%');
        fill.style.background = 'linear-gradient(90deg,#7048e8,#9775fa)';
        progressWrap.appendChild(fill);
        wrap.appendChild(stats);
        wrap.appendChild(progressWrap);
        lifeBar.parentNode.insertBefore(wrap, lifeBar.nextSibling);
    }
    function _waitForLifeBar() {
        return new Promise(resolve => {
            const existing = document.querySelector('[class*="life___"]');
            if (existing) {
                resolve(existing);
                return;
            }
            const obs = new MutationObserver(() => {
                const el = document.querySelector('[class*="life___"]');
                if (el) {
                    obs.disconnect();
                    resolve(el);
                }
            });
            obs.observe(document.body, { childList: true, subtree: true });
            setTimeout(() => obs.disconnect(), 30000);
        });
    }
    async function injectLevelProgressBar(apiKey) {
        console.log('[LevelXP] injectLevelProgressBar called, apiKey:', apiKey ? 'set' : 'missing');
        if (!apiKey)
            return;
        await _waitForLifeBar();
        console.log('[LevelXP] life bar found');
        const now = new Date();
        const lastReset = _lastResetTs();
        console.log('[LevelXP] now UTC:', now.toUTCString(), '| lastReset:', new Date(lastReset).toUTCString());
        const _isPDA = typeof window.PDA_httpGet !== 'undefined' || typeof window.flutter_inappwebview !== 'undefined';
        const cached = StorageUtil.get(CACHE_KEY, null);
        const cachedLevel = cached ? Math.floor(cached.finalLevel) : 0;
        const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
        const cacheExpired = !cached || !cached.ts || cached.ts < lastReset || (Date.now() - cached.ts) > CACHE_TTL_MS;
        // Level detection: DOM on desktop, API call on mobile
        let currentLevel = 0;
        if (_isPDA) {
            // Mobile: call /v2/user on every page load (lightweight)
            try {
                const userData = await _apiFetch(`https://api.torn.com/v2/user?key=${apiKey}&selections=basic`);
                currentLevel = userData?.profile?.level ?? 0;
            }
            catch (e) { /* silent */ }
        }
        else {
            // Desktop: read level from DOM
            const domLevelEl = Array.from(document.querySelectorAll('.name___ChDL3'))
                .find(el => el.textContent?.trim() === 'Level:');
            currentLevel = domLevelEl
                ? parseInt(domLevelEl.nextElementSibling?.textContent || '0', 10)
                : 0;
        }
        const levelChanged = currentLevel > 0 && cachedLevel > 0 && currentLevel !== cachedLevel;
        if (!cacheExpired && !levelChanged) {
            console.log('[LevelXP] cache valid, cached at:', new Date(cached.ts).toUTCString());
            _injectBar(cached.pct, cached.finalLevel);
            return;
        }
        if (levelChanged)
            console.log('[LevelXP] level changed:', cachedLevel, '->', currentLevel, '— recalculating');
        else
            console.log('[LevelXP] cache expired or missing, recalculating...');
        // Show stale cache while recalculating — better than "wait"
        if (cached)
            _injectBar(cached.pct, cached.finalLevel);
        else
            _injectBar(null, null);
        // Delay on TornPDA to let initial page requests finish before queuing HOF calls
        if (_isPDA)
            await _sleep(3000);
        try {
            const result = await _calcProgress(apiKey);
            if (!result) {
                // HOF calc failed — show level without % if we know the level
                if (currentLevel > 0 && !cached) {
                    _injectBar(0, currentLevel);
                }
                return;
            }
            StorageUtil.set(CACHE_KEY, { ...result, ts: Date.now() });
            _injectBar(result.pct, result.finalLevel);
        }
        catch (e) {
            // silent
        }
    }

    // ── Guard: prevent double-init when both extension and userscript are active ──
    // Extension sets __catScriptExtension at document_start — always wins over userscript
    const _isExtensionBundle = !!window.__catScriptExtension;
    if (window.__catScriptLoaded && !_isExtensionBundle) {
        console.log('[CAT] Already loaded (userscript) — skipping duplicate init');
        // @ts-expect-error -- intentional throw to halt script execution
        throw new Error('[CAT] duplicate init guard');
    }
    if (window.__catScriptLoaded && _isExtensionBundle) {
        console.log('[CAT] Extension overriding prior userscript load');
    }
    window.__catScriptLoaded = true;
    // ── Attack page: chain warning only ──
    const isAttackPage = (window.location.pathname === '/loader.php' || window.location.pathname === '/page.php')
        && window.location.search.includes('sid=attack');
    if (isAttackPage) {
        new ChainWarning().init();
    }
    else {
        // ── Faction / war pages: full CAT Script ──
        state.catBlocked = window.location.search.includes('step=rankreport');
        if (state.catBlocked) {
            console.log('[CAT] BLOCKED on rankreport page - exiting');
        }
        if (window.location.search.includes('step=profile')) {
            const idMatch = window.location.search.match(/ID=(\d+)/);
            if (idMatch) {
                const pageFactionId = idMatch[1];
                const userFactionId = (localStorage.getItem('cat_user_faction_id') || '').replace(/"/g, '');
                state.viewingFactionId = pageFactionId;
                if (userFactionId && pageFactionId !== userFactionId) {
                    state.catOtherFaction = true;
                    document.documentElement.classList.add('cat-other-faction');
                }
            }
        }
        // Inject level XP bar on all Torn pages
        const _apiKey = localStorage.getItem('cat_api_key_script') || localStorage.getItem('cat_torn_api_key') || '';
        const _showLevelBar = String(StorageUtil.get('cat_show_level_bar', 'true')) === 'true';
        if (_apiKey && _showLevelBar)
            injectLevelProgressBar(_apiKey);
        if (!state.catBlocked) {
            checkForUpdate();
            setupUrlListeners();
            try {
                const storedUser = JSON.parse(localStorage.getItem('cat_user_info') || 'null');
                const storedFaction = localStorage.getItem('cat_user_faction_id');
                if (storedUser && storedUser.id && storedUser.name && storedFaction) {
                    const pingUrl = 'https://cat-script.com/api/ping';
                    const pingHeaders = {
                        'Content-Type': 'application/json',
                        'Authorization': `Bearer ${localStorage.getItem('cat_auth_token') || ''}`
                    };
                    const pingBody = JSON.stringify({
                        playerId: String(storedUser.id),
                        playerName: storedUser.name,
                        factionId: String(storedFaction),
                        scriptVersion: VERSION,
                        tornApiKey: localStorage.getItem('cat_api_key_script') || undefined
                    });
                    if (isExtensionMode()) {
                        extensionFetch(pingUrl, { method: 'POST', headers: pingHeaders, body: pingBody }).catch(() => { });
                    }
                    else if (typeof GM_xmlhttpRequest !== 'undefined') {
                        GM_xmlhttpRequest({ method: 'POST', url: pingUrl, headers: pingHeaders, data: pingBody });
                    }
                    else if (typeof PDA_httpPost === 'function') {
                        PDA_httpPost(pingUrl, pingHeaders, pingBody).catch(() => { });
                    }
                    else if (typeof window !== 'undefined' && typeof window.customFetch === 'function') {
                        window.customFetch(pingUrl, { method: 'POST', headers: pingHeaders, body: pingBody }).catch(() => { });
                    }
                }
            }
            catch (e) { /* silent */ }
            // On non-war pages, poll the server every 3s to keep cat_calls_cache fresh
            const _isWarPage = window.location.hash.startsWith('#/war') || window.location.hash === '#/'
                || window.location.search.includes('step=profile');
            if (!_isWarPage) {
                const _bgPoll = () => {
                    try {
                        const _token = localStorage.getItem('cat_auth_token') || '';
                        const _serverUrl = localStorage.getItem('cat_server_url') || 'https://cat-script.com';
                        const _factionId = (localStorage.getItem('cat_user_faction_id') || '').replace(/"/g, '');
                        if (!_token || !_factionId)
                            return;
                        const _url = `${_serverUrl}/api/calls?factionId=${encodeURIComponent(_factionId)}`;
                        const _headers = { 'Authorization': `Bearer ${_token}`, 'Content-Type': 'application/json' };
                        let _lastCallCount = -1;
                        const _store = (data) => {
                            try {
                                localStorage.setItem('cat_calls_cache', JSON.stringify(data));
                                const _calls = data?.calls;
                                const _count = _calls?.length ?? 0;
                                if (_count !== _lastCallCount) {
                                    _lastCallCount = _count;
                                    console.log(`%c[CAT BG] ${_count} active call(s)`, 'color:#4fc3f7;font-weight:600;');
                                }
                            }
                            catch (_e) { /* quota */ }
                        };
                        if (isExtensionMode()) {
                            extensionFetch(_url, { method: 'GET', headers: _headers }).then(r => r.json()).then(_store).catch(() => { });
                        }
                        else if (typeof GM_xmlhttpRequest !== 'undefined') {
                            GM_xmlhttpRequest({ method: 'GET', url: _url, headers: _headers, onload: (r) => { try {
                                    _store(JSON.parse(r.responseText));
                                }
                                catch (_e) { } } });
                        }
                        else if (typeof PDA_httpGet === 'function') {
                            PDA_httpGet(_url, _headers).then((r) => r.json()).then(_store).catch(() => { });
                        }
                        else if (typeof window !== 'undefined' && typeof window.customFetch === 'function') {
                            window.customFetch(_url, { method: 'GET', headers: _headers }).then((r) => r.json()).then(_store).catch(() => { });
                        }
                    }
                    catch (_e) { /* silent */ }
                };
                _bgPoll();
                setInterval(_bgPoll, 3000);
            }
            try {
                installInterceptors();
                state.enhancer = new FactionWarEnhancer();
                exposeGlobalAPI(state.enhancer);
            }
            catch (e) {
                console.log('[CAT] Initialization error:', e);
            }
        }
    }

})();