CAT Script v3

CAT Script Beta - Fluffy Kittens

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         CAT Script v3
// @namespace    http://tampermonkey.net/
// @version      1.9.15
// @description  CAT Script Beta - Fluffy Kittens
// @author       JESUUS [2353554]
// @license      MIT
// @match        https://www.torn.com/war.php?step=rankreport&rankID=*
// @match        https://www.torn.com/factions.php?*
// @match        https://www.torn.com/loader.php?sid=attack&user*
// @grant        GM_xmlhttpRequest
// @connect      cat-script.com
// @connect      142.44.247.35
// @connect      localhost
// @connect      api.torn.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.15';
    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;
                }
            }
        }
        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 isAllowed = !isTerritoryWarProfile && !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();
                }
            }
            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.warn(`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.warn(`Error writing to localStorage [${key}]:`, e);
                return false;
            }
        },
        remove(key) {
            try {
                localStorage.removeItem(key);
                return true;
            }
            catch (e) {
                console.warn(`Error removing from localStorage [${key}]:`, e);
                return false;
            }
        }
    };

    function pageFocus() {
        return document.hasFocus();
    }
    const _w = [];
    const _wi = [];
    let _r = false;
    function _h(enhancer, su) {
        if (!su.status)
            return;
        const id = String(su.userId);
        const status = su.status;
        const statusText = (status.text || '').trim();
        enhancer._hospScanNeeded = true;
        const isHospital = statusText === 'Hospital' || statusText.toLowerCase().includes('hospital');
        if (isHospital) {
            const area = status.area || (enhancer.travelData[id] && enhancer.travelData[id].area);
            if (area && area !== 1) {
                enhancer.previousStatus[id] = { status: 'Abroad', area: area };
            }
            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') {
            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);
        }
        enhancer.updateHospTimers();
        // Deferred scan: Torn's React DOM update happens after the WS event.
        // Always re-scan 200ms later — ensures member is in hospNodes even if hospTime is not yet
        // available (missing `until` in WS), so the timer shows as soon as the next fetch provides it.
        if (isHospital) {
            setTimeout(() => {
                enhancer._hospScanNeeded = true;
                enhancer.scanHospitalizedMembers();
                enhancer.updateHospTimers();
            }, 200);
        }
    }
    /**
     * 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);
                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();
                    // Find area number from CONFIG.areas
                    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();
                }, 200);
            }
        }
        else if (enhancer.hospTime[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') {
            // 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 {
            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 {
                            _w.push(statusUpdate);
                        }
                    }
                    // 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 },
        });
        targetWindow.addEventListener('focus', () => {
            if (!state.enhancer)
                return;
            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);
                }
            }
        });
        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);
                            }, 300);
                        }).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 = {};
                                Object.entries(json.rankedWarMembers).forEach(([id, data]) => {
                                    const st = data?.onlineStatus?.status;
                                    if (st)
                                        enhancer.onlineStatuses[id] = st;
                                    if (data?.status?.text) {
                                        processBulkMemberStatus(enhancer, id, data.status);
                                    }
                                });
                                Object.entries(json.rankedWarMembers).forEach(([id, data]) => {
                                    const st = data?.onlineStatus?.status || 'offline';
                                    const memberRow = document.querySelector(`.desc-wrap a[href*="profiles.php?XID=${id}"]`)?.closest('li');
                                    if (memberRow) {
                                        const nameEl = memberRow.querySelector('.honor-text-wrap, .honor-text:not(.honor-text-svg)');
                                        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);
                                    console.log(`%c[CAT Chain] %c${chainCount} hits %c· expires in ${remaining}s %c(faction page)`, 'color:#4FC3F7;font-weight:bold', 'color:#E2E8F0', 'color:#718096', 'color:#4FC3F7;font-style:italic');
                                }
                                else {
                                    console.log(`%c[CAT Chain] %cNo active chain %c(faction page)`, 'color:#4FC3F7;font-weight:bold', 'color:#718096', 'color:#4FC3F7;font-style:italic');
                                }
                                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();
                            }, 300);
                        }).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) {
                                Object.entries(json).forEach(([id, st]) => {
                                    // Color the player name based on online status via data attribute
                                    const memberRow = document.querySelector(`.desc-wrap a[href*="profiles.php?XID=${id}"]`)?.closest('li');
                                    if (memberRow) {
                                        const nameEl = memberRow.querySelector('.honor-text-wrap, .honor-text:not(.honor-text-svg)');
                                        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 so next poll re-renders caller colors
                                document.querySelectorAll('.call-button.other-call').forEach(btn => {
                                    btn.dataset.callState = '';
                                });
                            }
                        }).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 !important;
                    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,
                html.cat-other-faction .desc-wrap [class*="attack___"],
                html.cat-other-faction .attack,
                html.cat-other-faction [class*="attack___"] {
                    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;
                }

                /* 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: 380px !important;
                    max-width: 380px !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;
                }

                /* No API key: push attack link to the right */
                body.cat-no-api-key .t-blue.h.c-pointer {
                    margin-left: 100% !important;
                }
                @media (max-width: 784px) {
                    body.cat-no-api-key .t-blue.h.c-pointer {
                        margin-left: 2% !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;
                    margin-top: 10px !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;
                }

                /* 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 !important;
                    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;
                }

    `;
    }

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

                /* Tabs (Faction / Plan / Help / Settings) — smaller on Android */
                body.cat-android .custom-tab-btn {
                    font-size: 9px !important;
                    padding: 4px 6px !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(),
            ].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.warn(`[CAT] ${url} unreachable, trying next...`);
            }
        }
        this.serverUrl = this.serverUrls[0];
        console.warn('[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.warn('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.warn('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.warn('Error accessing localStorage');
        }
        console.warn('\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.error('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.warn('[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.warn('[CAT] User has no faction ID in profile');
                }
            }
            else {
                console.warn('[CAT] Failed to fetch user profile:', response.status);
            }
        }
        catch (e) {
            console.warn('[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 faction ID from registration response
                    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.warn('[CAT] Registration failed:', response.status);
            }
        }
        catch (e) {
            console.warn('[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.warn(`[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.warn('[CAT] Failed to refresh token, returning 401');
                }
            }
            catch (e) {
                console.error('[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.warn('[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.error('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;
                    return userInfo;
                }
            }
        }
        catch (error) {
            this.reportError('fetchUserInfo', error);
        }
        return null;
    }
    async function fetchEnemyFactionIdFromAPI() {
        if (!this.torn_apikey) {
            console.warn('\u274C [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.warn('\u274C [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.warn('\u274C [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.warn('\u274C [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.warn('\u26A0\uFE0F [Step 3] No active war found');
            return null;
        }
        catch (error) {
            console.error('\u274C 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.warn('Failed to fetch faction info:', response.status);
                return null;
            }
            const data = await response.json();
            return data;
        }
        catch (error) {
            console.error('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.warn('Failed to fetch user info:', response.status);
                return null;
            }
            const data = await response.json();
            return data;
        }
        catch (error) {
            console.error('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.warn('[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.warn('[WAR DOM] Error scanning war from DOM:', error);
            this.reportError('scanWarFromDOM', error);
        }
    }
    async function fetchRankedWarsFromAPI() {
        try {
            if (!this.torn_apikey) {
                console.warn('[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.warn('[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.error('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;
    }

    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 = {}) {
        return new Promise((resolve, reject) => {
            const method = (options.method || 'GET').toUpperCase();
            const headers = _sanitizeHeaders((options.headers || {}));
            const hasPDAHttp = typeof PDA_httpGet === 'function' && typeof PDA_httpPost === 'function';
            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 PDA_httpPost(url, headers, body)
                                : await PDA_httpGet(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.warn('[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.error('[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.error('[PDA] Request failed after retries:', error);
                            resolve({
                                ok: false,
                                status: 0,
                                text: () => Promise.resolve(''),
                                json: () => Promise.resolve({})
                            });
                        }
                    }
                }, isRead).catch((e) => {
                    console.error('[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.warn('[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.error('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.error('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.warn('[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.error('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.error('[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.warn('[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.error('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.error('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.error('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'))
            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(() => { });
    }

    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.showNotification = showNotification;
    APIManager.prototype.reportError = reportError;

    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.lastCallsHash = null;
            this._pendingStatuses = [];
            this._statusFlushTimer = null;
            this._fetchingCalls = false;
            this._pollingActive = false;
            this._pollingTimeout = null;
            this._pdaWaitCount = 0;
            this._rallyInFlight = false;
            this._callInFlight = 0;
        }
        start() {
            if (this._isActive) {
                return;
            }
            const pdaReady = 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.warn('[HTTP] PDA functions not available after 10s, trying anyway');
            }
            // Restore cached calls instantly before first network fetch
            this._restoreCallsFromCache();
            // 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();
            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.warn('[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);
            }
        }
        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 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.error('[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.error('[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 */ }
                        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);
                        }
                        return true;
                    }
                }
            }
            catch (error) {
                console.error('[HTTP] Error fetching calls:', error);
                this.apiManager.reportError('fetchCalls', error);
            }
            finally {
                this._fetchingCalls = false;
            }
            return false;
        }
        startPolling() {
            this.stopPolling();
            this._pollingActive = true;
            const poll = async () => {
                if (!this._pollingActive)
                    return;
                await this.fetchCalls();
                if (this._pollingActive) {
                    this._pollingTimeout = setTimeout(poll, this.pollRate);
                }
            };
            this._pollingTimeout = setTimeout(poll, 0);
        }
        stopPolling() {
            this._pollingActive = false;
            if (this._pollingTimeout) {
                clearTimeout(this._pollingTimeout);
                this._pollingTimeout = null;
            }
            if (this.pollingInterval) {
                clearInterval(this.pollingInterval);
                this.pollingInterval = null;
            }
        }
        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'
                        })
                    });
                }
                catch (e) {
                    this.apiManager.reportError('heartbeat', e);
                }
            }, 30000);
        }
        stopHeartbeat() {
            if (this.heartbeatInterval) {
                clearInterval(this.heartbeatInterval);
                this.heartbeatInterval = null;
            }
        }
        _restoreCallsFromCache() {
            try {
                const raw = localStorage.getItem('cat_calls_cache');
                if (!raw)
                    return;
                const data = JSON.parse(raw);
                if (!data?.success || !data.calls)
                    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);
                const revivableData = data.revivableData;
                if (revivableData && this.onRevivableDataUpdate)
                    this.onRevivableDataUpdate(revivableData);
                const tacticalMarkers = data.tacticalMarkers;
                if (tacticalMarkers && this.onTacticalMarkersUpdate)
                    this.onTacticalMarkersUpdate(tacticalMarkers);
                const chainBonusAssignment = data.chainBonusAssignment;
                if (chainBonusAssignment && this.onChainBonusAssignmentUpdate)
                    this.onChainBonusAssignmentUpdate(chainBonusAssignment);
            }
            catch (_e) { /* corrupted cache, ignore */ }
        }
        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);
            }
            if (this._statusFlushTimer)
                clearTimeout(this._statusFlushTimer);
            this._statusFlushTimer = setTimeout(() => this.flushStatusUpdates(), 500);
        }
        async flushStatusUpdates() {
            this._statusFlushTimer = null;
            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;
        }
    }

    function _checkFactions() {
        this._checkYourFaction();
        this._checkEnemyFaction();
        this._checkNoBsp();
        document.querySelectorAll('.finally-bs-col, .finally-bs-filter, .finally-bs-swap').forEach(el => el.remove());
        // Strip inherited level classes from War Helper BS elements to avoid CSS conflicts
        document.querySelectorAll('.__warhelper.bs.level').forEach(el => {
            el.classList.remove('level');
            el.className = el.className.replace(/\blevel___\S*/g, '');
        });
    }
    function _checkYourFaction() {
        const yourFactionContainer = document.querySelector('.your-faction[class*="tabMenuCont"]');
        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);
                }
            }
            else if (hasFFData && !list.querySelector('.ff-column')) {
                // FF data arrived after initial enhancement — add FF column now
                this.addFFColumn(list);
                this.addFFSwitchArrow(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() {
        const enemyFactionContainer = document.querySelector('.enemy-faction[class*="tabMenuCont"], .enemy-faction.left');
        if (!enemyFactionContainer) {
            return;
        }
        // If latched, verify the DOM still has our enhancements (panel may have been re-opened)
        if (this._enemyFactionEnhanced) {
            const hasCallButtons = enemyFactionContainer.querySelector('.call-button');
            const lists = enemyFactionContainer.querySelectorAll('ul, ol');
            let hasMembers = false;
            lists.forEach(l => { if (l.querySelectorAll('li').length >= 3)
                hasMembers = true; });
            if (hasMembers && !hasCallButtons) {
                // DOM was recreated — reset latch
                this._enemyFactionEnhanced = false;
                this._noBspChecked = false;
            }
            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 allEnhanced = 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;
            if (hasBspColumn && hasCallButtons && (!hasFFData || hasFFColumn))
                return;
            const hasMembers = list.querySelectorAll('li').length >= 3;
            if (!hasMembers) {
                // List exists but members not rendered yet (SPA two-phase render) — don't latch
                allEnhanced = false;
                return;
            }
            if (hasMembers) {
                if (!hasBspColumn) {
                    allEnhanced = false;
                    this.addBspHeader(list);
                    this.addBspColumn(list);
                }
                if (!hasCallButtons) {
                    allEnhanced = 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');
                    }
                }
            }
        });
        if (allEnhanced && 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);
        this._factionCheckInterval = setInterval(() => this._checkFactions(), 2000);
    }
    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);
        }
    }

    // ── 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↑ → BSP↓ ──
    // Each click advances to the next state. If FF data is not available, stays on BSP.
    function handleStatsHeaderClick(header, factionList) {
        let memberContainer = 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;
        }
        const hasFF = this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0;
        const hasBsp = factionList.querySelectorAll('.iconStats').length > 0
            || factionList.closest('.enemy-faction')?.querySelectorAll('.iconStats').length
            || factionList.closest('[class*="tabMenuCont"]')?.querySelectorAll('.iconStats').length;
        const currentCol = header.getAttribute('data-col') || (StorageUtil.get('cat_stats_column', 'bsp'));
        const currentSort = header.getAttribute('data-sort') || 'none';
        // Determine next state in the cycle
        let nextCol;
        let nextSort;
        if (currentCol === 'bsp') {
            if (currentSort === 'asc') {
                // BSP↑ → next: if FF available, switch to FF↓, else BSP↓
                if (hasFF) {
                    nextCol = 'ff';
                    nextSort = 'desc';
                }
                else {
                    nextCol = 'bsp';
                    nextSort = 'desc';
                }
            }
            else {
                // BSP↓ or none → BSP↑
                nextCol = 'bsp';
                nextSort = 'asc';
            }
        }
        else {
            // col === 'ff'
            if (currentSort === 'asc') {
                // FF↑ → next: if BSP available, switch to BSP↓, else FF↓
                if (hasBsp) {
                    nextCol = 'bsp';
                    nextSort = 'desc';
                }
                else {
                    nextCol = 'ff';
                    nextSort = 'desc';
                }
            }
            else {
                // FF↓ or none → FF↑
                nextCol = 'ff';
                nextSort = 'asc';
            }
        }
        // Save column preference
        if (nextCol !== currentCol) {
            StorageUtil.set('cat_stats_column', nextCol);
            header.setAttribute('data-col', nextCol);
            // Toggle 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', nextCol === 'bsp' ? 'inline-block' : 'none', 'important'));
            root.querySelectorAll('.ff-column').forEach(el => el.style.setProperty('display', nextCol === 'ff' ? 'inline-flex' : 'none', 'important'));
        }
        // Set opposite direction so the sort function toggles to the desired one
        header.setAttribute('data-sort', nextSort === 'asc' ? 'desc' : 'asc');
        if (nextCol === 'ff') {
            this.sortByFF(memberContainer, header);
        }
        else {
            this.sortByBSP(memberContainer, header);
        }
        // Update header text to show current column + arrow
        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' : 'BSP';
        // Update only the text node, preserving any child elements
        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.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
            const existingBsp = headerContainer?.querySelector('.bsp-header');
            if (existingBsp) {
                const newBsp = existingBsp.cloneNode(true);
                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';
            bspHeader.textContent = 'BSP';
            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')) {
                const bspElement = member.querySelector('.iconStats');
                let bspValue = 'Wait';
                let bspClass = 'bsp-wait';
                if (bspElement) {
                    bspValue = (bspElement.textContent || '').trim() || 'Wait';
                    if (bspValue !== 'Wait') {
                        const bspStyle = bspElement.style.background || bspElement.style.backgroundColor;
                        if (bspStyle.includes('#FF0000') || bspStyle.includes('rgb(255, 0, 0)')) {
                            bspClass = 'bsp-red';
                        }
                        else if (bspStyle.includes('#FFB30F') || bspStyle.includes('rgb(255, 179, 15)')) {
                            bspClass = 'bsp-orange';
                        }
                        else if (bspStyle.includes('#47A6FF') || bspStyle.includes('rgb(71, 166, 255)')) {
                            bspClass = 'bsp-blue';
                        }
                        else if (bspStyle.includes('#73DF5D') || bspStyle.includes('rgb(115, 223, 93)')) {
                            bspClass = 'bsp-green';
                        }
                        else if (bspStyle.includes('#FFFFFF') || bspStyle.includes('rgb(255, 255, 255)')) {
                            bspClass = 'bsp-white';
                        }
                        else if (bspStyle.includes('#949494') || bspStyle.includes('rgb(148, 148, 148)')) {
                            bspClass = 'bsp-gray';
                        }
                        else {
                            bspClass = 'bsp-default';
                        }
                    }
                }
                const bspColumn = document.createElement('div');
                bspColumn.className = 'bsp-column left';
                bspColumn.innerHTML = `<span class="bsp-value ${bspClass}">${bspValue}</span>`;
                const activeColYF = StorageUtil.get('cat_stats_column', 'bsp');
                const hideForFF = activeColYF === 'ff' && this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0;
                bspColumn.style.cssText = `
                color: #ffffff !important;
                padding: 2px 4px !important;
                font-size: 1em !important;
                font-weight: 700 !important;
                display: ${hideForFF ? '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')) {
                const bspElement = member.querySelector('.iconStats');
                let bspValue = 'Wait';
                let bspClass = 'bsp-wait';
                if (bspElement) {
                    bspValue = (bspElement.textContent || '').trim() || 'Wait';
                    if (bspValue !== 'Wait') {
                        const bspStyle = bspElement.style.background || bspElement.style.backgroundColor;
                        if (bspStyle.includes('#FF0000') || bspStyle.includes('rgb(255, 0, 0)')) {
                            bspClass = 'bsp-red';
                        }
                        else if (bspStyle.includes('#FFB30F') || bspStyle.includes('rgb(255, 179, 15)')) {
                            bspClass = 'bsp-orange';
                        }
                        else if (bspStyle.includes('#47A6FF') || bspStyle.includes('rgb(71, 166, 255)')) {
                            bspClass = 'bsp-blue';
                        }
                        else if (bspStyle.includes('#73DF5D') || bspStyle.includes('rgb(115, 223, 93)')) {
                            bspClass = 'bsp-green';
                        }
                        else if (bspStyle.includes('#FFFFFF') || bspStyle.includes('rgb(255, 255, 255)')) {
                            bspClass = 'bsp-white';
                        }
                        else if (bspStyle.includes('#949494') || bspStyle.includes('rgb(148, 148, 148)')) {
                            bspClass = 'bsp-gray';
                        }
                        else {
                            bspClass = 'bsp-default';
                        }
                    }
                }
                const bspColumn = document.createElement('div');
                bspColumn.className = 'bsp-column left';
                bspColumn.innerHTML = `<span class="bsp-value ${bspClass}">${bspValue}</span>`;
                // Hide if FF column is currently active
                const activeCol = StorageUtil.get('cat_stats_column', 'bsp');
                if (activeCol === 'ff' && this._enhancer?.ffStats && Object.keys(this._enhancer.ffStats).length > 0) {
                    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;
            let bspClass = 'bsp-default';
            const bspStyle = bspElement.style.background || bspElement.style.backgroundColor;
            if (bspStyle.includes('#FF0000') || bspStyle.includes('rgb(255, 0, 0)')) {
                bspClass = 'bsp-red';
            }
            else if (bspStyle.includes('#FFB30F') || bspStyle.includes('rgb(255, 179, 15)')) {
                bspClass = 'bsp-orange';
            }
            else if (bspStyle.includes('#47A6FF') || bspStyle.includes('rgb(71, 166, 255)')) {
                bspClass = 'bsp-blue';
            }
            else if (bspStyle.includes('#73DF5D') || bspStyle.includes('rgb(115, 223, 93)')) {
                bspClass = 'bsp-green';
            }
            else if (bspStyle.includes('#FFFFFF') || bspStyle.includes('rgb(255, 255, 255)')) {
                bspClass = 'bsp-white';
            }
            else if (bspStyle.includes('#949494') || bspStyle.includes('rgb(148, 148, 148)')) {
                bspClass = 'bsp-gray';
            }
            cell.textContent = bspValue;
            cell.className = `bsp-value ${bspClass}`;
        });
    }
    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) {
            const parentContainer = factionList.closest('.enemy-faction') ||
                factionList.closest('[class*="faction"]') ||
                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';
            bspHeader.textContent = 'BSP';
            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 = StorageUtil.get('cat_stats_column', 'bsp');
        const hasBsp = factionList.querySelectorAll('.iconStats').length > 0;
        // If user prefers BSP and BSP is available, hide FF column
        const showFF = preference === 'ff' || !hasBsp;
        // 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';
                        ffHeader.textContent = 'FF';
                        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', 'ff');
                        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
        const parentContainer = factionList.closest('.enemy-faction') ||
            factionList.closest('[class*="tabMenuCont"]') ||
            factionList.closest('.your-faction') ||
            factionList.parentElement;
        const bspHeader = parentContainer?.querySelector('.bsp-header');
        if (!bspHeader)
            return;
        // Set data-col attribute for cycling logic
        const preference = StorageUtil.get('cat_stats_column', 'bsp');
        if (!bspHeader.getAttribute('data-col')) {
            bspHeader.setAttribute('data-col', preference || 'bsp');
        }
        updateStatsHeaderText(bspHeader);
    }

    // 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*="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;
                        }
                        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.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') {
                                // Call is still in-flight — mark for auto-uncall when server responds
                                callButton.dataset.pendingUncall = 'true';
                                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;
                                });
                                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);
                                }
                                // PDA: 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 (_) { /* ignore */ }
                                }
                                enqueueCallOp(async () => {
                                    try {
                                        const result = await enhancer.pollingManager.cancelCall(existingCallId, enhancer.apiManager.playerName, uncallMemberName);
                                        if (!result || !result.success) {
                                            console.error('❌ [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 = false;
                            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) {
                                        // Check if user clicked uncall while call was in-flight
                                        if (callButton.dataset.pendingUncall === 'true') {
                                            delete callButton.dataset.pendingUncall;
                                            // Auto-cancel the call that just succeeded
                                            try {
                                                await enhancer.pollingManager.cancelCall(result.data.id, enhancer.apiManager.playerName, memberName);
                                            }
                                            catch (_) { /* best effort */ }
                                            // Ensure button stays in "Call" state
                                            callButton.textContent = 'Call';
                                            callButton.className = 'call-button';
                                            if (callButton.style.length > 0)
                                                callButton.removeAttribute('style');
                                            delete callButton.dataset.callId;
                                            callButton.dataset.callState = '';
                                            // Remove from local state
                                            const cancelMemberId = result.data.memberId || memberId || '';
                                            if (cancelMemberId) {
                                                enhancer.currentCalls = enhancer.currentCalls.filter(c => c.memberId !== cancelMemberId);
                                                enhancer.updateCallButtons(enhancer.currentCalls);
                                            }
                                            document.querySelectorAll('.call-button.call-locked').forEach(btn => {
                                                btn.className = 'call-button';
                                                btn.disabled = false;
                                                btn.dataset.callState = '';
                                            });
                                            return;
                                        }
                                        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);
                                        // PDA: schedule notification when target is hosp with known timer
                                        const isPDA = typeof window.flutter_inappwebview !== 'undefined';
                                        const pdaMasterOn = String(StorageUtil.get('cat_pda_notifications', 'true')) === 'true';
                                        const pdaNotifEnabled = String(StorageUtil.get('cat_pda_notif_hosp', 'true')) === 'true';
                                        const pdaLeadTime = parseInt(String(StorageUtil.get('cat_pda_notif_lead', '20')), 10) * 1000;
                                        if (isPDA && pdaMasterOn && pdaNotifEnabled && memberId && enhancer.hospTime[memberId]) {
                                            const endTime = enhancer.hospTime[memberId];
                                            const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                                            const notifTime = pdaLeadTime > 0 ? endMs - pdaLeadTime : endMs;
                                            if (notifTime > Date.now()) {
                                                const notifId = Math.abs(parseInt(memberId, 10)) % 10000;
                                                const cleanName = result.data.memberName || memberName;
                                                const leadLabel = pdaLeadTime > 0 ? `${pdaLeadTime / 1000}s` : 'now';
                                                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/loader.php?sid=attack&user2ID=${memberId}`,
                                                        launchNativeToast: false,
                                                    });
                                                }
                                                catch (_) { /* PDA handler not available */ }
                                            }
                                        }
                                    }
                                    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;
                if (enhancer && enhancer.currentCalls && enhancer.currentCalls.length > 0) {
                    enhancer.updateCallButtons(enhancer.currentCalls);
                }
                else if (enhancer && enhancer.pollingManager) {
                    enhancer.pollingManager.lastCallsHash = null;
                    enhancer.pollingManager.fetchCalls();
                }
            }, 0);
        }
    }

    function sortByBSP(factionList, headerElement) {
        // Detect faction
        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';
        }
        // 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;
        }
        members.sort((a, b) => {
            const bspA = this.getBSPValue(a);
            const bspB = this.getBSPValue(b);
            if (newSort === 'asc') {
                return bspA - bspB;
            }
            else {
                return 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');
            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');
            setupColumnSort(scoreHeader, 'score');
        }
    }
    function restoreSavedSort(force = false) {
        // Skip if nothing changed since last sort (dirty flag cleared after processing)
        if (!force && !this._sortDirty)
            return;
        this._sortDirty = false;
        // Handle status sorting
        const allStatusHeaders = document.querySelectorAll('.white-grad [class*="status___"]');
        for (const statusHeader of allStatusHeaders) {
            // Find the member container that is a sibling of the white-grad header row
            const headerRow = statusHeader.closest('.white-grad');
            let memberContainer = null;
            if (headerRow) {
                // The member list is typically the next sibling of the header row
                let sibling = headerRow.nextElementSibling;
                while (sibling) {
                    if (sibling.tagName === 'UL' || sibling.classList.contains('f-war-list')) {
                        memberContainer = sibling;
                        break;
                    }
                    sibling = sibling.nextElementSibling;
                }
            }
            // Fallback to old method if sibling search fails
            if (!memberContainer) {
                const tabContainer = statusHeader.closest('[class*="tabMenuCont"]');
                memberContainer = tabContainer?.querySelector('.f-war-list') || tabContainer?.querySelector('ul') || null;
            }
            if (!memberContainer)
                continue;
            // Detect faction by checking member classes: li.your = your faction, li.enemy = enemy faction
            const hasYourMembers = memberContainer.querySelector('li.your, li[class*="your___"]') !== null;
            const hasEnemyMembers = memberContainer.querySelector('li.enemy, li[class*="enemy"]') !== null;
            // If has your-class members, it's your faction. If has enemy-class members, it's enemy.
            // Fallback to position classes if neither found.
            let isYourFaction = false;
            if (hasYourMembers && !hasEnemyMembers) {
                isYourFaction = true;
            }
            else if (hasEnemyMembers && !hasYourMembers) {
                isYourFaction = false;
            }
            else {
                // Fallback: check container classes
                const container = statusHeader.closest('[class*="tabMenuCont"]') || statusHeader.closest('.your-faction') || statusHeader.closest('.enemy-faction');
                isYourFaction = !!(statusHeader.closest('.your-faction') ||
                    statusHeader.closest('[class*="your-faction"]') ||
                    (container && container.classList.contains('right')));
            }
            const factionKey = isYourFaction ? 'your' : 'enemy';
            const savedSort = StorageUtil.get(`cat_sort_preference_${factionKey}`, null);
            if (!savedSort || savedSort.column !== 'status')
                continue;
            const members = memberContainer.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]');
            const statusTexts = [];
            for (let i = 0; i < members.length; i++) {
                const row = members[i];
                // Cache status element ref via dataset ID
                let el = row.dataset.catStatusId ? document.getElementById(row.dataset.catStatusId) : null;
                if (!el) {
                    el = row.querySelector('.status.left, [class*="status___"]');
                    if (el) {
                        if (!el.id)
                            el.id = `_cat_st_s${i}`;
                        row.dataset.catStatusId = el.id;
                    }
                }
                if (el) {
                    const txt = (el.textContent || '').trim();
                    // Cache svg ref on row
                    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.substring(0, 3) + ol);
                }
                else {
                    statusTexts.push('');
                }
            }
            const statusHash = statusTexts.join('|');
            const hashKey = `_lastSortHash_${factionKey}`;
            if (statusHash === this[hashKey])
                continue;
            this[hashKey] = statusHash;
            statusHeader.setAttribute('data-sort', savedSort.direction);
            this.sortByStatus(memberContainer, statusHeader, savedSort.direction);
        }
        // Handle score/level/name sorting
        // Search for all tabMenuCont containers (war page structure)
        const factionContainers = document.querySelectorAll('[class*="tabMenuCont"]');
        const processedContainers = new Set();
        for (const container of factionContainers) {
            // Skip if already processed
            if (processedContainers.has(container))
                continue;
            processedContainers.add(container);
            const memberContainer = container.querySelector('.f-war-list') || container.querySelector('ul');
            if (!memberContainer)
                continue;
            // Detect faction by checking member classes
            const hasYourMembers = memberContainer.querySelector('li.your, li[class*="your___"]') !== null;
            const hasEnemyMembers = memberContainer.querySelector('li.enemy, li[class*="enemy"]') !== null;
            let isYourFaction = false;
            if (hasYourMembers && !hasEnemyMembers) {
                isYourFaction = true;
            }
            else if (hasEnemyMembers && !hasYourMembers) {
                isYourFaction = false;
            }
            else {
                // Fallback: check container classes
                isYourFaction = !!(container.classList.contains('your-faction') ||
                    container.className.includes('your-faction') ||
                    container.classList.contains('right'));
            }
            const factionKey = isYourFaction ? 'your' : 'enemy';
            const savedSort = StorageUtil.get(`cat_sort_preference_${factionKey}`, null);
            if (!savedSort || savedSort.column === 'status')
                continue;
            // Check if sort needs to be re-applied by checking if order is correct
            const members = Array.from(memberContainer.querySelectorAll('li[class*="member"], li.enemy, li[class*="enemy"], li.your, li[class*="your___"]'));
            if (members.length === 0)
                continue;
            // Check if flexbox ordering is still applied
            const hasOrder = members.some(m => m.style.order && m.style.order !== '0');
            if (!hasOrder) {
                if (savedSort.column === 'bsp') {
                    // Re-apply BSP sort
                    const bspHeader = container.querySelector('.bsp-header');
                    if (bspHeader) {
                        bspHeader.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                        this.sortByBSP(memberContainer, bspHeader);
                    }
                }
                else if (savedSort.column === 'ff') {
                    // Re-apply FF sort
                    const bspHeader = container.querySelector('.bsp-header');
                    if (bspHeader) {
                        bspHeader.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                        this.sortByFF(memberContainer, bspHeader);
                    }
                }
                else {
                    // Re-apply the sort
                    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) {
                        // Set opposite direction because sortByColumn toggles it
                        header.setAttribute('data-sort', savedSort.direction === 'asc' ? 'desc' : 'asc');
                        this.sortByColumn(memberContainer, header, savedSort.column);
                    }
                }
            }
        }
    }
    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;
        }
        members.sort((a, b) => {
            const statusA = this.getStatusValue(a);
            const statusB = this.getStatusValue(b);
            const isOkayA = statusA >= 40000 && statusA <= 40002;
            const isOkayB = statusB >= 40000 && statusB <= 40002;
            if (isOkayA && !isOkayB)
                return -1;
            if (!isOkayA && isOkayB)
                return 1;
            if (newSort === 'asc') {
                return statusA - statusB;
            }
            else {
                return 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;
        members.sort((a, b) => {
            let valA = '';
            let valB = '';
            if (column === 'name') {
                const nameElA = a.querySelector('[class*="honorWrap___"] a, [class*="name___"] a, a[href*="profiles.php"]');
                const nameElB = b.querySelector('[class*="honorWrap___"] a, [class*="name___"] a, a[href*="profiles.php"]');
                valA = (nameElA?.textContent || '').trim().toLowerCase();
                valB = (nameElB?.textContent || '').trim().toLowerCase();
            }
            else if (column === 'level') {
                const levelElA = a.querySelector('[class*="level___"]');
                const levelElB = b.querySelector('[class*="level___"]');
                valA = parseInt((levelElA?.textContent || '0').replace(/\D/g, '')) || 0;
                valB = parseInt((levelElB?.textContent || '0').replace(/\D/g, '')) || 0;
            }
            else if (column === 'score') {
                const scoreElA = a.querySelector('[class*="points___"]');
                const scoreElB = b.querySelector('[class*="points___"]');
                valA = parseInt((scoreElA?.textContent || '0').replace(/\D/g, '')) || 0;
                valB = parseInt((scoreElB?.textContent || '0').replace(/\D/g, '')) || 0;
            }
            if (typeof valA === 'string' && typeof valB === 'string') {
                return newSort === 'asc' ? valA.localeCompare(valB) : valB.localeCompare(valA);
            }
            else {
                const numA = valA;
                const numB = valB;
                return newSort === 'asc' ? numA - numB : numB - numA;
            }
        });
        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;
        }
        const statusText = statusElement.dataset.originalStatus || (statusElement.textContent || '').trim();
        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;
            }
            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 (statusText.includes('Okay') || statusText.includes('Online') || statusText.includes('Idle') || statusText.includes('Offline')) {
            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;
        members.sort((a, b) => {
            const ffA = this.getFFValue(a);
            const ffB = this.getFFValue(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 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;
    }

    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;
                }
                let nameEl = row.dataset.catNameId ? document.getElementById(row.dataset.catNameId) : null;
                if (!nameEl) {
                    nameEl = memberEl.querySelector('.honor-text, [class*="honorText"]');
                    if (nameEl) {
                        if (!nameEl.id)
                            nameEl.id = `_cat_ne_p${targets.length}`;
                        row.dataset.catNameId = nameEl.id;
                    }
                }
                const name = nameEl ? (nameEl.textContent || '').trim() : null;
                let statusEl = row.dataset.catStatusId ? document.getElementById(row.dataset.catStatusId) : null;
                if (!statusEl) {
                    statusEl = memberEl.querySelector('.status.left, [class*="status___"]');
                    if (statusEl) {
                        if (!statusEl.id)
                            statusEl.id = `_cat_st_p${targets.length}`;
                        row.dataset.catStatusId = statusEl.id;
                    }
                }
                const status = statusEl ? (statusEl.textContent || '').trim() : 'Unknown';
                let levelEl = row.dataset.catLevelId ? document.getElementById(row.dataset.catLevelId) : null;
                if (!levelEl) {
                    levelEl = memberEl.querySelector('.level.left, [class*="level___"]');
                    if (levelEl) {
                        if (!levelEl.id)
                            levelEl.id = `_cat_lv_p${targets.length}`;
                        row.dataset.catLevelId = levelEl.id;
                    }
                }
                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 insertSoftUncallBadge(memberRow, badgeText, callerName = '', shortenAttack = false, isTargetOkay = false) {
        // 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 = isTargetOkay
            ? `Target is now Okay — auto-uncall in ${badgeText}`
            : callerName
                ? `${callerName} hospitalized — auto-uncall in ${badgeText}`
                : `Caller hospitalized — auto-uncall in ${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 playerHasActiveCall = !!playerNameLower && factionCalls.some(call => {
            const callerName = (call.callerName || '').replace(/^\u2694\uFE0F/, '').trim().toLowerCase();
            return callerName === playerNameLower;
        });
        const callButtons = document.querySelectorAll('.call-button');
        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 = 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;
            }
            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;
                            // Ensure correct color class
                            existingBadge.classList.toggle('cat-hosp-uncall', isGreen);
                            existingBadge.dataset.catTooltip = isGreen
                                ? `Target is now Okay — auto-uncall in ${badgeText}`
                                : `${softUncall.callerName || 'Caller'} hospitalized — auto-uncall in ${badgeText}`;
                            // Shorten Attack → Atk if not already done
                            const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                            if (atkText && atkText.textContent?.trim() === 'Attack')
                                atkText.textContent = ' Atk ';
                        }
                        else {
                            insertSoftUncallBadge(memberRow, badgeText, softUncall.callerName || '', true, isGreen);
                        }
                    }
                    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) {
                        const statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                        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;
                        }
                        // Reset margins that were set for bonus badge
                        const statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                        if (statusEl)
                            statusEl.style.removeProperty('margin-left');
                        button.style.removeProperty('margin-left');
                        // Restore Attack from Atk
                        const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                        if (atkText && atkText.textContent?.trim() === 'Atk')
                            atkText.textContent = ' Attack ';
                    }
                    // Re-apply bonus glow class
                    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) {
                    const statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                    if (statusEl) {
                        const pistolSpan = document.createElement('span');
                        pistolSpan.className = 'cat-pistol-icon';
                        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 = isGreen
                            ? `Target is now Okay — auto-uncall in ${badgeText}`
                            : `${softUncall.callerName || 'Caller'} hospitalized — auto-uncall in ${badgeText}`;
                        // Shorten Attack → Atk if not already done
                        const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                        if (atkText && atkText.textContent?.trim() === 'Attack')
                            atkText.textContent = ' Atk ';
                    }
                    else {
                        insertSoftUncallBadge(memberRow, badgeText, softUncall.callerName || '', true, isGreen);
                    }
                }
                else if (existingBadge) {
                    removeSoftUncallBadge(memberRow);
                }
                // Chain bonus assignment: golden border + badge ONLY when within threshold hits of bonus
                const pollingChain = this.enemyChainData?.ownChain ?? 0;
                const domStat = document.querySelector('.chain-box-center-stat');
                const domChain = domStat ? (parseInt((domStat.textContent || '0').replace(/,/g, ''), 10) || 0) : 0;
                const ownChain = Math.max(pollingChain, domChain);
                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;
                            // Shift timer left and call button right to make room for badge
                            const statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                            if (statusEl)
                                statusEl.style.setProperty('margin-left', '-6px', 'important');
                            button.style.setProperty('margin-left', '18px', 'important');
                            // Shorten Attack → Atk to save space
                            const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                            if (atkText && atkText.textContent?.trim() === 'Attack')
                                atkText.textContent = ' Atk ';
                        }
                    }
                    else {
                        existingBonusBadge.dataset.catTooltip = `Bonus ${this.chainBonusAssignment.nextBonus} hit assigned to ${this.chainBonusAssignment.playerName}`;
                    }
                }
                else {
                    // Bonus not assigned (or no longer assigned) — clean up badge, margins, and text
                    if (existingBonusBadge) {
                        existingBonusBadge.remove();
                        memberRow._catBonusBadge = null;
                    }
                    // Reset margins
                    const statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                    if (statusEl)
                        statusEl.style.removeProperty('margin-left');
                    button.style.removeProperty('margin-left');
                    // Restore Attack from Atk
                    const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                    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;
                    // Reset margins that were set for bonus badge
                    const statusEl = memberRow.querySelector('[class*="status___"], .status.left');
                    if (statusEl)
                        statusEl.style.removeProperty('margin-left');
                    button.style.removeProperty('margin-left');
                }
                // Restore Attack from Atk
                const atkText = memberRow.querySelector('.t-gray-9, [class*="t-blue"]');
                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;
        const btnCount = document.querySelectorAll('.call-button').length;
        if (callsHash === this._lastCallsHash && btnCount === this._lastBtnCount)
            return;
        this._lastCallsHash = callsHash;
        this._lastBtnCount = btnCount;
        // 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.error('[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.error('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.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 16px !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: 12px !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;
            }

            .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;
        let tabs;
        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', 'Help', 'Settings'] : ['Help', 'Settings'];
        }
        else if (hasWar) {
            tabs = showPlan ? ['Faction', 'Plan', 'Help', 'Settings'] : ['Faction', 'Help', 'Settings'];
        }
        else {
            // No war: hide Faction tab (no data to show)
            tabs = showPlan ? ['Plan', 'Help', 'Settings'] : ['Help', 'Settings'];
        }
        const enhancer = this;
        const canActivate = enhancer._enhancer?.canActivateWar || false;
        if (hasWar && canActivate && !state.catOtherFaction) {
            tabs.push('Stats');
        }
        const tabContents = {};
        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="margin-bottom: 12px;">
                            <div style="display: flex; align-items: center; gap: 6px; margin-bottom: 8px;">
                                <div style="font-size: 11px; color: #86B202; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600;">API Key</div>
                                <div id="cat-tos-trigger" style="position: relative; display: inline-flex; cursor: help;">
                                    <span 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;">?</span>
                                    <div id="cat-tos-tooltip" style="display: none; position: absolute; top: 22px; left: -10px; z-index: 100000; width: 380px; background: #1a1a1a; border: 1px solid rgba(255,255,255,0.12); border-radius: 6px; box-shadow: 0 8px 24px rgba(0,0,0,0.5); padding: 12px; font-size: 10px; color: #bbb; line-height: 1.5;">
                                        <div style="font-size: 11px; font-weight: 600; color: #FF794C; margin-bottom: 10px;">API Terms of Service</div>
                                        <div style="display:flex;flex-direction:column;gap:7px;">
                                            <div><span style="color:#86B202;font-weight:600;">Data Storage</span><br>Player ID, name, faction (15 days). Auth token (90 days). Statuses (2h), logs (48h), errors (7 days). Calls & wars: permanent.</div>
                                            <div style="height:1px;background:rgba(255,255,255,0.06);"></div>
                                            <div><span style="color:#86B202;font-weight:600;">Data Sharing</span><br>Faction members see calls. Leaders/co-leaders see statuses, leaderboard & dashboard during wars. No third-party sharing.</div>
                                            <div style="height:1px;background:rgba(255,255,255,0.06);"></div>
                                            <div><span style="color:#86B202;font-weight:600;">Purpose of Use</span><br>Faction war coordination: real-time statuses, call system, chain tracking, leaderboards, and dashboard.</div>
                                            <div style="height:1px;background:rgba(255,255,255,0.06);"></div>
                                            <div><span style="color:#86B202;font-weight:600;">Key Storage & Sharing</span><br>Stored locally in browser (localStorage). Sent once during registration to verify identity via the Torn API \u2014 never stored in the database. Not shared with any third party.</div>
                                            <div style="height:1px;background:rgba(255,255,255,0.06);"></div>
                                            <div><span style="color:#86B202;font-weight:600;">Key Access Level</span><br>Public Access</div>
                                        </div>
                                    </div>
                                </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;">Terms of Service & Privacy Policy</a>
                            </div>
                            <div style="display: flex; gap: 8px; align-items: center;">
                                <input type="password" id="tab-setting-torn-apikey" placeholder="Paste your PUBLIC API key here" value="${this._esc(currentKey)}"
                                    style="flex: 1; padding: 8px 10px; border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; box-sizing: border-box; font-size: 12px; background: rgba(0,0,0,0.3); color: #ddd;" class="api-key-input">
                                <button id="tab-setting-save" style="padding: 8px 12px; background: #555; color: #fff; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; font-size: 11px; white-space: nowrap;">Test & Save</button>
                            </div>
                            <p style="margin: 4px 0 0 0; font-size: 10px; color: #666;">Stored locally in your browser only</p>
                            <div id="tab-setting-api-validation" style="margin-top: 6px; font-size: 11px; display: none; padding: 6px 8px; border-radius: 3px; background: rgba(0,0,0,0.3);"></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-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>

                        <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;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>
                                </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() === 'help') {
                content.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>
                `;
            }
            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>
                `;
            }
            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();
                    }
                }
            };
            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;">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>
                </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';
                }
                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(() => { });
                });
            }
            supportBtn.onclick = (e) => {
                e.preventDefault();
                if (supportBtn.classList.contains('active')) {
                    supportBtn.classList.remove('active');
                    supportContent.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'));
                    supportBtn.classList.add('active');
                    supportContent.classList.add('active');
                    refreshDebugDisplay();
                }
            };
            tabsMenu.appendChild(supportBtn);
            // ── 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: 12px;">What's New \u2014 v${VERSION}</div>
                <div class="cat-wn-body" style="font-size: 12px; color: #ccc; line-height: 1.7;">
                    <div style="margin-bottom: 14px;">
                        <div style="color: #ACEA01; font-weight: 600; margin-bottom: 6px; font-size: 13px;">Chain Bonus Hit</div>
                        <div style="color: #bbb;">You can now claim the next bonus hit. A "Next bonus (XX)" shows up in the chain box.</div>
                        <div style="color: #bbb; margin-top: 4px;">Click [claim] and everyone sees a golden HIT BONUS marker on your call button (only when you\u2019re close to the bonus hit, 5 hits away).</div>
                        <div style="color: #bbb; margin-top: 4px;">Leaders/co-leaders/delegates can reassign it to anyone.</div>
                        <div style="color: #bbb; margin-top: 4px;">Works even after the war ends if the chain is still going.</div>
                    </div>
                    <div style="margin-bottom: 14px;">
                        <div style="color: #ACEA01; font-weight: 600; margin-bottom: 6px; font-size: 13px;">Delegate Permissions</div>
                        <div style="color: #bbb;">Leaders can give delegate access to trusted members from the Plan tab.</div>
                        <div style="color: #bbb; margin-top: 4px;">Delegates can activate wars, configure Discord, reassign bonus hits (same as leaders/co-leaders).</div>
                        <div style="color: #bbb; margin-top: 4px;">They just can\u2019t add/remove other delegates.</div>
                    </div>
                    <div style="margin-bottom: 14px;">
                        <div style="color: #4FC3F7; font-weight: 600; margin-bottom: 6px; font-size: 13px;">Hospital Timers (PDA)</div>
                        <div style="color: #bbb;">Fixed timers showing "HOSPITAL" or stuck timers on PDA.</div>
                    </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);
            }
        }
        // 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 && 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:#ccc;margin-bottom:8px;">Activate script for this war:</div>
                <div style="display:flex;gap:8px;flex-wrap:wrap;">`;
                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 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: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 () => {
                    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 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.error('[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;
            const userFactionId = StorageUtil.get('cat_user_faction_id', 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.warn('[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.warn(`[CAT] ${fetchKeys[i]} returned status ${resp.status}`);
                    continue;
                }
                const text = await resp.text();
                if (!text || !text.trim().startsWith('{')) {
                    console.warn(`[CAT] ${fetchKeys[i]} returned non-JSON response`);
                    continue;
                }
                try {
                    results[fetchKeys[i]] = JSON.parse(text);
                }
                catch (_e) {
                    console.warn(`[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.error('[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 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');
                });
            };
        }
        // 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');
                    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?')) {
                    StorageUtil.remove('cat_api_key_script');
                    StorageUtil.remove('cat_user_info');
                    StorageUtil.remove('cat_user_faction_id');
                    StorageUtil.remove('cat_enemy_faction_id');
                    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();
            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 API key...';
                validationDiv.style.display = 'block';
            }
            try {
                const response = await enhancer.apiManager.httpRequest(`https://api.torn.com/v2/user/profile?striptags=true&key=${newKey}`, { method: 'GET' });
                if (response.ok) {
                    const data = await response.json();
                    if (data.profile && data.profile.name) {
                        StorageUtil.set('cat_api_key_script', newKey);
                        enhancer.apiManager.torn_apikey = newKey;
                        const userInfo = {
                            id: data.profile.id,
                            name: data.profile.name,
                            faction_id: data.profile.faction_id || null,
                            faction_name: 'Your Faction'
                        };
                        StorageUtil.set('cat_user_info', userInfo);
                        enhancer.apiManager.playerId = String(data.profile.id || '');
                        enhancer.apiManager.playerName = data.profile.name;
                        const settingsBtn = document.getElementById('settings-tab-btn');
                        if (settingsBtn) {
                            settingsBtn.classList.remove('blinking');
                        }
                        if (validationDiv) {
                            validationDiv.style.color = '#68d391';
                            validationDiv.textContent = `Success! Welcome ${data.profile.name}`;
                            validationDiv.style.display = 'block';
                        }
                        setTimeout(() => {
                            location.reload();
                        }, 2000);
                    }
                    else if (data.error) {
                        const tornErr = data.error;
                        throw new Error(`Wrong API Key: ${tornErr.error || 'Unknown error'} (code ${tornErr.code || '?'})`);
                    }
                    else {
                        throw new Error('Invalid API response: unexpected format');
                    }
                }
                else {
                    if (validationDiv) {
                        validationDiv.style.color = '#fc8181';
                        validationDiv.textContent = 'Invalid API key!';
                        validationDiv.style.display = 'block';
                    }
                }
            }
            catch (error) {
                console.error('API Validation error:', error);
                this.apiManager.reportError('apiKeyValidationUI', error);
                if (error instanceof Error)
                    console.error('Error stack:', error.stack);
                if (validationDiv) {
                    validationDiv.style.color = '#fc8181';
                    validationDiv.textContent = 'Error validating API key: ' + (error instanceof Error ? error.message : String(error));
                    validationDiv.style.display = 'block';
                }
            }
            finally {
                saveBtn.disabled = false;
                saveBtn.textContent = originalText;
            }
        };
        apiKeyInput.onkeypress = (e) => {
            if (e.key === 'Enter') {
                saveBtn.click();
            }
        };
        // ToS tooltip hover
        const tosTrigger = document.getElementById('cat-tos-trigger');
        const tosTooltip = document.getElementById('cat-tos-tooltip');
        if (tosTrigger && tosTooltip) {
            tosTrigger.addEventListener('mouseenter', () => { tosTooltip.style.display = 'block'; });
            tosTrigger.addEventListener('mouseleave', () => { tosTooltip.style.display = 'none'; });
        }
        // 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.warn('Error loading calls from database:', error);
            this.apiManager.reportError('loadCallsFromDB', error);
        }
    }

    function handleRallyClick(button, memberId, memberName, factionId) {
        if (!memberId || !factionId) {
            console.warn('[Rally] Missing memberId or factionId');
            return;
        }
        const enhancer = window.FactionWarEnhancer;
        if (!enhancer || !enhancer.pollingManager) {
            console.warn('[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.warn('[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.error('[RALLY] Auto-call failed:', callResult?.error);
                        }
                    }).catch(err => {
                        console.error('[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.error('[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._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);
            this._tabsCheckInterval = setInterval(() => {
                if (document.querySelector('.faction-war-info, [class*="factionWarInfo"]') || document.getElementById('faction_war_list_id')) {
                    const existingMenu = document.getElementById('custom-tabs-menu');
                    if (!existingMenu) {
                        this.injectTabsMenu();
                    }
                    else if (!state.catOtherFaction && StorageUtil.get('cat_api_key_script', '') && (document.querySelector('.faction-war-info, [class*="factionWarInfo"]') || document.querySelector('[data-warid], [class*="rankBox"]')) && !existingMenu.querySelector('[data-tab="faction"]')) {
                        // Only recreate tabs to add Faction tab if we're on OUR faction page (not another faction)
                        // Save active tab state before recreating
                        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.injectTabsMenu();
                        // Restore active tab if there was one
                        if (activeTabName) {
                            const newBtn = document.querySelector(`#custom-tabs-menu .custom-tab-btn[data-tab="${activeTabName}"]`);
                            if (newBtn)
                                newBtn.click();
                        }
                    }
                }
            }, 5000);
            const isPDA = typeof window.flutter_inappwebview !== 'undefined';
            const pdaPerfMode = isPDA && String(StorageUtil.get('cat_pda_perf_mode', 'false')) === 'true';
            this._autoSortInterval = setInterval(() => {
                if (String(StorageUtil.get('cat_auto_sort', 'true')) === 'true') {
                    this.restoreSavedSort();
                }
            }, pdaPerfMode ? 2000 : 1000);
        }
        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;
                        if (target.classList && (target.classList.contains('call-button') || target.classList.contains('call-column') ||
                            target.classList.contains('cat-level-indicator') || target.classList.contains('cat-travel-eta') ||
                            target.classList.contains('cat-travel-text') || target.classList.contains('cat-pistol-icon') ||
                            target.classList.contains('cat-tactical-marker') || target.classList.contains('cat-info-panel')))
                            continue;
                        if (target.closest && target.closest('.call-button, .call-column, .cat-level-indicator, .cat-travel-eta, .cat-pistol-icon, .cat-tactical-marker, .cat-info-panel'))
                            continue;
                        const added = mutation.addedNodes;
                        for (let j = 0; j < added.length; j++) {
                            const addedNode = added[j];
                            if (addedNode.nodeType === Node.ELEMENT_NODE) {
                                if (addedNode.classList?.contains('cat-level-indicator') ||
                                    addedNode.classList?.contains('cat-travel-eta') ||
                                    addedNode.classList?.contains('cat-travel-text') ||
                                    addedNode.classList?.contains('cat-pistol-icon') ||
                                    addedNode.classList?.contains('cat-tactical-marker'))
                                    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 instead of waiting for the 1s poll
                        if (!_sortBusy && String(StorageUtil.get('cat_auto_sort', 'true')) === 'true') {
                            _sortBusy = true;
                            this.restoreSavedSort();
                            requestAnimationFrame(() => { _sortBusy = false; });
                        }
                    }
                }, 50);
            };
            // 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;
    // 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;
    // 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;

    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.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/loader.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';
        // Apply to all score elements (points___) - use setProperty with important to override BSP
        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;
            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() {
        const chainBox = document.querySelector('.chain-box');
        if (!chainBox)
            return;
        // Don't rebuild if reassign dropdown is open
        const existingDropdown = document.querySelector('.cat-bonus-reassign-dropdown');
        if (existingDropdown)
            return;
        const 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;
        const domStat = chainBox.querySelector('.chain-box-center-stat');
        const domChain = domStat ? (parseInt((domStat.textContent || '0').replace(/,/g, ''), 10) || 0) : 0;
        const ownChain = Math.max(pollingChain, domChain);
        const nextBonus = assignment?.nextBonus ?? getNextBonus(ownChain);
        // Override Torn's padding-top:0 on .chain-box-general-info when no active chain
        const generalInfoEl = chainBox.querySelector('.chain-box-general-info');
        if (generalInfoEl) {
            generalInfoEl.style.setProperty('padding-top', ownChain > 0 ? '0' : '10px', 'important');
        }
        // Update war list padding based on chain activity
        const warList = chainBox.closest('.f-war-list') || document.querySelector('.f-war-list');
        if (warList) {
            warList.querySelectorAll('li[class*="warListItem___"]').forEach(li => {
                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 = 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 = '';
        const playerId = this.apiManager.playerId || '';
        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 claimPlayerId = targetPlayerId || this.apiManager.playerId || '';
        const claimPlayerName = targetPlayerName || this.apiManager.playerName || '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();
            this._chainBonusOptimisticUntil = 0;
            if (data.success && data.assignment) {
                // Update with server data (authoritative)
                this.chainBonusAssignment = data.assignment;
                this.updateCallButtons(this.currentCalls);
                this.updateChainBonusClaimUI();
            }
            else if (data.error === 'already_claimed') {
                // Rollback optimistic update
                this.chainBonusAssignment = null;
                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.updateCallButtons(this.currentCalls);
                this.updateChainBonusClaimUI();
                this.apiManager.showNotification('No active chain', 'warning');
            }
        }).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 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.warn('[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.warn('[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.warn('[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.warn('[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.warn('[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 with updated price
                this.showActivationBanner();
            }
        }
        catch (err) {
            console.warn('[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)
        if (!this.currentPrice || this.currentPrice === 30) {
            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>`;
        }
        if (this.canActivateWar && hasData) {
            if (trialAvailable) {
                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 && 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;
        if (!sub || !sub.currentWar)
            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,
                    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.message || data.error || 'Activation failed.';
                alert(errorMsg);
                await this.checkActivationStatus();
            }
        }
        catch (e) {
            console.error('[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;
        }
        const memberRows = this._cachedMemberRows;
        for (let i = 0; i < memberRows.length; i++) {
            const row = memberRows[i];
            if (!row.isConnected) {
                this._memberRowsValid = false;
                continue;
            }
            // Cache levelEl reference via ID to avoid repeated querySelector each second
            let levelEl = null;
            if (row.dataset.catLevelId) {
                levelEl = document.getElementById(row.dataset.catLevelId);
            }
            if (!levelEl) {
                levelEl = row.querySelector('.level___g3CWR, [class*="level___"]');
                if (levelEl) {
                    if (!levelEl.id)
                        levelEl.id = `_cat_lv_${i}`;
                    row.dataset.catLevelId = levelEl.id;
                }
            }
            // Cache honorWrap reference via ID
            let honorWrap = null;
            if (row.dataset.catHonorId) {
                honorWrap = document.getElementById(row.dataset.catHonorId);
            }
            if (!honorWrap) {
                honorWrap = row.querySelector('[class*="honorWrap___"]');
                if (honorWrap) {
                    if (!honorWrap.id)
                        honorWrap.id = `_cat_hw_${i}`;
                    row.dataset.catHonorId = honorWrap.id;
                }
            }
            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')) {
                const abroadEl = row.querySelector('[data-abroad-dest]');
                const abroadDest = abroadEl?.dataset.abroadDest;
                if (abroadDest) {
                    suffix = `- ${abroadDest}`;
                    suffixColor = '#4FC4F7';
                }
            }
            // Build target content
            let targetHTML;
            let 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}`;
            }
            // Add revivable indicator if player is revivable
            if (userId && this.revivableCache?.[userId] === true) {
                targetHTML += ' <svg xmlns="http://www.w3.org/2000/svg" width="9" height="9" viewBox="0 0 506 506" style="vertical-align:-1px;margin-left:1px;display:inline-block" data-cat-tooltip="Revivable"><path fill="#E57373" d="M475.6,123.4H351.2v12.4c0,8-2.4,14.4-10,14.4s-14-6.4-14-14.4v-12.4H182v16c0,8-6.4,14.4-14,14.4s-14-6.4-14-14.4v-16H36.4c-15.2,0-31.6,11.2-31.6,28.8V185h500v-32.8C504,135.8,492,123.4,475.6,123.4z"/><path fill="#BF360C" d="M4,181.4v246.8c0,16.4,15.2,32,31.2,32h434.4c16.8,0,28.4-16.4,28.4-32V181.4H4z"/><path fill="#795548" d="M304.4,45.8H201.2c-25.2,0-45.6,18.8-45.6,41.6V141c0,5.6,4.4,10,10,10s10-4.4,10-10V87.4c0-13.6,13.2-22,25.6-22h103.2c13.6,0,22.4,8.8,22.4,22v50c0,5.6,4.4,10,10,10s10-4.4,10-10v-50C346.8,63.4,328.8,45.8,304.4,45.8z"/><path fill="#EEE" d="M348.8,299h-58c-2.4,0-6-3.6-6-6v-58.4c0-2.8-.8-3.2-4-3.2H232c-2.8,0-7.2.4-7.2,3.2V293c0,2.4.4,6-2,6H164c-2.8,0-6.8.4-6.8,3.2v41.2c0,3.2,4,7.2,6.8,7.2h58.8c2.4,0,2-.4,2,2v58c0,3.2,4.4,7.6,7.2,7.6h48.8c3.2,0,4-4.8,4-7.6v-58c0-2,3.6-2,6-2h58c3.2,0,3.6-4.4,3.6-7.2v-41.2C352.8,299.4,352,299,348.8,299z"/></svg>';
                hasHTML = true;
            }
            // 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.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:-2px!important;left:0!important;overflow:visible!important;';
                honorWrap.style.position = 'relative';
                honorWrap.style.overflow = 'visible';
                honorWrap.appendChild(indicator);
                row._catIndicator = indicator;
            }
            if (hasHTML) {
                if (indicator.innerHTML !== targetHTML) {
                    indicator.innerHTML = targetHTML;
                }
            }
            else {
                if (indicator.textContent !== targetHTML) {
                    indicator.textContent = targetHTML;
                }
            }
            // Apply online status color if we have data
            if (userId && this.onlineStatuses) {
                const status = this.onlineStatuses[userId];
                if (status) {
                    let nameEl = null;
                    if (row.dataset.catNameId) {
                        nameEl = document.getElementById(row.dataset.catNameId);
                    }
                    if (!nameEl) {
                        nameEl = row.querySelector('.honor-text-wrap, .honor-text:not(.honor-text-svg)');
                        if (nameEl) {
                            if (!nameEl.id)
                                nameEl.id = `_cat_ne_${i}`;
                            row.dataset.catNameId = nameEl.id;
                        }
                    }
                    if (nameEl && nameEl.dataset.onlineStatus !== status) {
                        nameEl.dataset.onlineStatus = status;
                    }
                    if (row.dataset.catStatus !== status) {
                        row.dataset.catStatus = status;
                    }
                }
            }
        }
    }
    function scanHospitalizedMembers() {
        if (!this._hospScanNeeded) {
            let allConnected = true;
            for (let i = 0; i < this.hospNodes.length; i++) {
                if (!this.hospNodes[i][1] || !this.hospNodes[i][1].isConnected) {
                    allConnected = false;
                    break;
                }
            }
            if (allConnected && this.hospNodes.length > 0) {
                return;
            }
        }
        this._hospScanNeeded = false;
        // Reuse cached member rows instead of querying all status nodes across the page
        const rows = 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'));
        for (let ri = 0; ri < rows.length; ri++) {
            const row = rows[ri];
            if (!row.isConnected)
                continue;
            let statusEl = null;
            if (row.dataset.catStatusId) {
                statusEl = document.getElementById(row.dataset.catStatusId);
            }
            if (!statusEl) {
                statusEl = row.querySelector('[class*="status___"], .status.left');
                if (statusEl) {
                    if (!statusEl.id)
                        statusEl.id = `_cat_st_${ri}`;
                    row.dataset.catStatusId = statusEl.id;
                }
            }
            if (!statusEl)
                continue;
            const text = (statusEl.textContent || '').trim();
            // memberId already cached by dom-status-scanner or injectLevelIndicators
            let memberId = row.dataset.catUid || null;
            if (!memberId) {
                const profileLink = row.querySelector('a[href*="profiles.php?XID="]');
                if (profileLink) {
                    const m = profileLink.href.match(/XID=(\d+)/);
                    if (m)
                        memberId = m[1];
                }
                if (!memberId) {
                    const attackLink = row.querySelector('a[href*="user2ID"]');
                    if (attackLink) {
                        const m = attackLink.href.match(/user2ID=(\d+)/);
                        if (m)
                            memberId = m[1];
                    }
                }
                if (memberId)
                    row.dataset.catUid = memberId;
            }
            if (!memberId)
                continue;
            // Register node if text is "Hospital" OR if we already have a hospTime for this player
            const hasHospTime = !!this.hospTime[memberId];
            // If Torn is already showing a countdown (PDA), skip — Torn manages its own timer
            if (/^\d{2}:\d{2}:\d{2}$/.test(text))
                continue;
            if (text === 'Hospital' || hasHospTime) {
                // Check CSS class — if Torn already changed to okay/traveling/etc, clean up hospTime
                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 (hasHospTime && isNonHospClass) {
                    delete this.hospTime[memberId];
                    delete this.previousStatus[memberId];
                    continue;
                }
                // WS may set hospTime before Torn updates DOM text — force Hospital
                if (hasHospTime && text !== 'Hospital') {
                    statusEl.textContent = 'Hospital';
                }
                this.registerHospNode(memberId, statusEl);
            }
        }
        // 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;
        this.hospNodes = this.hospNodes.filter(([, n]) => n && n.isConnected);
        for (let i = 0; i < this.hospNodes.length; i++) {
            const [id, node] = this.hospNodes[i];
            if (!this.hospTime[id]) {
                continue;
            }
            const currentText = node.textContent.trim();
            if (currentText !== 'Hospital') {
                const cn = node.className;
                if (cn.includes('okay') || cn.includes('traveling') || cn.includes('abroad') ||
                    cn.includes('jail') || cn.includes('federal') || cn.includes('fallen')) {
                    // Restore textContent — Torn changed the class but our countdown overwrote the text
                    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;
                    delete this.hospTime[id];
                    delete this.previousStatus[id];
                    delete node.dataset.catHospStatus;
                    const cleanLi = node._catLi || (node._catLi = node.closest('li'));
                    if (cleanLi) {
                        cleanLi.removeAttribute('data-abroad-hosp');
                        cleanLi.removeAttribute('data-abroad-dest');
                        cleanLi.title = '';
                    }
                    this.hospNodes.splice(i, 1);
                    i--;
                    continue;
                }
            }
            const endTime = this.hospTime[id] > 9999999999
                ? this.hospTime[id]
                : this.hospTime[id] * 1000;
            const totalSeconds = (endTime - new Date().getTime()) / 1000;
            if (!totalSeconds || totalSeconds <= 0) {
                // Timer expired — restore "Hospital" text and ask server for fresh status
                node.textContent = 'Hospital';
                delete this.hospTime[id];
                delete this.previousStatus[id];
                delete node.dataset.catHospStatus;
                const expLi = node._catLi || (node._catLi = node.closest('li'));
                if (expLi) {
                    expLi.removeAttribute('data-abroad-hosp');
                    expLi.removeAttribute('data-abroad-dest');
                    expLi.title = '';
                }
                this.hospNodes.splice(i, 1);
                i--;
                // Throttled refresh: ask CAT server for updated statuses (player may be re-hospitalized)
                if (this.pollingManager?.isActive() && (!this._lastExpiredRefresh || Date.now() - this._lastExpiredRefresh > 5000)) {
                    this._lastExpiredRefresh = Date.now();
                    this.pollingManager.requestCalls();
                }
                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')}`;
            node.textContent = timeStr;
            // Mark node so DOM scanner reads 'Hospital' instead of the countdown text
            node.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 so PDA Android script reloads don't lose timers
        StorageUtil.set('cat_hosp_times', this.hospTime);
    }
    function updateTravelingStatus() {
        if (Object.keys(this.travelData).length === 0)
            return;
        if (this._travelNodesValid && this._cachedTravelNodes) {
            const before = this._cachedTravelNodes.length;
            this._cachedTravelNodes = this._cachedTravelNodes.filter(n => n.isConnected);
            // If any nodes were disconnected (Torn re-rendered rows), invalidate cache
            // so we re-query and pick up the new DOM elements
            if (this._cachedTravelNodes.length !== before)
                this._travelNodesValid = false;
        }
        let statusElements;
        if (this._travelNodesValid && this._cachedTravelNodes) {
            statusElements = this._cachedTravelNodes;
        }
        else {
            statusElements = Array.from(document.querySelectorAll('.desc-wrap [class*="status___"], .desc-wrap .status.left'));
            this._cachedTravelNodes = statusElements;
            this._travelNodesValid = true;
        }
        statusElements.forEach(statusEl => {
            const text = (statusEl.textContent || '').trim();
            // Recognize: Torn classes, Torn text, OR our own previous modification
            const isTravelOrAbroad = statusEl.classList.contains('traveling') ||
                statusEl.classList.contains('abroad') ||
                text === 'Traveling' || text === 'Abroad' ||
                statusEl.dataset.originalStatus !== undefined;
            if (!isTravelOrAbroad) {
                return;
            }
            const memberRow = statusEl.closest('li');
            if (!memberRow)
                return;
            let userId = memberRow.dataset?.catUid || null;
            if (!userId) {
                const profileLink = memberRow.querySelector('a[href*="profiles.php?XID="]');
                if (profileLink) {
                    const m = profileLink.href.match(/XID=(\d+)/);
                    if (m)
                        userId = m[1];
                }
                if (!userId) {
                    const attackLink = memberRow.querySelector('a[href*="user2ID"]');
                    if (attackLink) {
                        const m = attackLink.href.match(/user2ID=(\d+)/);
                        if (m)
                            userId = m[1];
                    }
                }
                if (userId)
                    memberRow.dataset.catUid = userId;
            }
            if (!userId)
                return;
            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');
                }
                return;
            }
            // 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');
            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 = getTravelMode();
                    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 = StorageUtil.get('cat_eta_color', null) || (document.body.classList.contains('dark-mode') ? CONFIG.colors.travelEta : '#E65100');
                            }
                        }
                    }
                    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.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.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;
                }
            }
        });
    }

    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 doTimerWork = () => {
            const t0 = performance.now();
            this.scanHospitalizedMembers();
            this.updateHospTimers();
            this.updateTravelingStatus();
            this.injectLevelIndicators();
            pdaMetrics.recordDomWork(performance.now() - t0);
        };
        this.refreshInterval = setInterval(() => {
            if (isPDA) {
                doTimerWork();
            }
            else {
                if (document.hidden)
                    return;
                requestAnimationFrame(doTimerWork);
            }
        }, 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 visualDebounce = 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);
            };
            const applyVisuals = (isStructural) => {
                if (visualDebounce)
                    clearTimeout(visualDebounce);
                visualDebounce = setTimeout(() => {
                    isApplyingVisuals = true;
                    const t0 = performance.now();
                    // Invalidate caches only when DOM structure changed (nodes added/removed)
                    // Attribute-only changes (class toggles) don't change which nodes exist
                    if (isStructural) {
                        this._travelNodesValid = false;
                        this._memberRowsValid = false;
                    }
                    this._hospScanNeeded = true;
                    this.updateTravelingStatus();
                    this.injectLevelIndicators();
                    this.scanHospitalizedMembers();
                    this.updateHospTimers();
                    this.updateTacticalMarkers();
                    this.applyScoreStyles();
                    pdaMetrics.recordDomWork(performance.now() - t0);
                    // RAF fires after current microtask queue (including MutationObserver callbacks)
                    requestAnimationFrame(() => { isApplyingVisuals = false; });
                }, 50);
            };
            this.domObserver = new MutationObserver((mutations) => {
                if (isApplyingVisuals)
                    return;
                let hasRelevantChange = false;
                let hasStructuralChange = false;
                for (const mutation of mutations) {
                    const target = mutation.target;
                    // Skip mutations on our own elements
                    if (target.classList?.contains('cat-level-indicator') ||
                        target.closest?.('.cat-level-indicator') ||
                        target.classList?.contains('cat-travel-eta') ||
                        target.classList?.contains('cat-travel-text') ||
                        target.closest?.('.cat-travel-eta') ||
                        target.closest?.('.cat-travel-text') ||
                        target.classList?.contains('cat-info-panel') ||
                        target.classList?.contains('cat-tactical-marker') ||
                        target.closest?.('.cat-tactical-marker')) {
                        continue;
                    }
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        hasRelevantChange = true;
                        hasStructuralChange = true;
                        break; // No need to check more
                    }
                    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                        hasRelevantChange = true;
                        // Fast path: if a status element just got 'hospital' class, update timer immediately
                        // without waiting for the 50ms debounce — avoids brief "Hospital" flash
                        if (target.className.includes('hospital') &&
                            (target.className.includes('status___') || target.classList.contains('status'))) {
                            const memberRow = target.closest('li');
                            const memberId = memberRow?.dataset?.catUid;
                            if (memberId && this.hospTime[memberId]) {
                                this.registerHospNode(memberId, target);
                                // Don't call updateHospTimers() here — the 50ms debounce
                                // (applyVisuals) will handle it. Calling twice causes oscillation.
                            }
                        }
                        // Don't break — keep checking for structural changes
                    }
                }
                if (hasRelevantChange) {
                    sendDomUpdate();
                    applyVisuals(hasStructuralChange);
                }
                // Forward mutations to EnhancementManager (single-observer coordination:
                // dom-observer is the sole subtree watcher on .desc-wrap)
                if (this.enhancementManager) {
                    this.enhancementManager.handleMutations(mutations);
                }
            });
            this.domObserver.observe(descWrap, {
                childList: true,
                subtree: true,
                attributes: true,
                attributeFilter: ['class']
            });
            // Initial application
            sendDomUpdate();
        };
        setTimeout(observeWarPage, 0);
    }

    // Module-level state (survives across calls, not per-instance)
    const _lastDomStatuses = new Map(); // memberId → last DOM status text
    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() {
        // Query all war members (own + enemy faction)
        const rows = document.querySelectorAll('.desc-wrap li[class*="member___"], .desc-wrap li.enemy, .desc-wrap li.your, .desc-wrap li[class*="your___"]');
        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
            const prevDomStatus = _lastDomStatuses.get(memberId);
            _lastDomStatuses.set(memberId, 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
            });
        }
    }

    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.error('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.error('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.warn('\u274C Failed to fetch predictions:', response.status);
                return null;
            }
            const data = await response.json();
            return data.success ? data : null;
        }
        catch (error) {
            console.error('\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: IntersectionObserver on war container — fires without user interaction
            const watchDescWrap = () => {
                const descWrap = document.querySelector('.desc-wrap');
                if (!descWrap) {
                    setTimeout(watchDescWrap, 1000);
                    return;
                }
                const io = new IntersectionObserver((entries) => {
                    if (entries[0]?.isIntersecting) {
                        pdaForceResume();
                    }
                }, { threshold: 0.1 });
                io.observe(descWrap);
            };
            watchDescWrap();
        }
        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"]');
                if (link) {
                    e.preventDefault();
                    window.open(link.href, '_blank', 'noopener');
                }
            }, true);
        }
        this.setupCallButtonTooltips();
        this.setupEtaTooltips();
        this.setupGenericTooltips();
    }
    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';
            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: nowrap;
            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) => {
                if (!target.isConnected || !target.contains(document.elementFromPoint(ev.clientX, ev.clientY))) {
                    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;
        this.scanHospitalizedMembers();
        this.updateHospTimers();
        this.updateTravelingStatus();
        this.injectLevelIndicators();
        this.updateTacticalMarkers();
        this.applyScoreStyles();
        if (this.pollingManager?.isActive()) {
            this.pollingManager.requestCalls();
            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', []) || [];
            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._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._currentEtaTooltip = null;
            this._cachedMemberRows = null;
            this._memberRowsValid = false;
            this._domScanInterval = null;
            this._chainBoxInterval = null;
            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]) {
                            const endTime = s.until;
                            const endMs = endTime > 9999999999 ? endTime : endTime * 1000;
                            if (endMs > Date.now()) {
                                this.hospTime[id] = endTime;
                            }
                        }
                        if (s.previousStatus === 'Abroad' && s.previousArea != null && s.previousArea != 1) {
                            this.previousStatus[id] = { status: 'Abroad', area: s.previousArea };
                        }
                    }
                    else if (s.status === 'Traveling' || s.status === 'Abroad') {
                        const fromArea = s.lastDestination ?? undefined;
                        if (!this.travelData[id] && s.previousArea != null) {
                            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.onRevivableDataUpdate = (data) => {
                for (const [playerId, isRevivable] of Object.entries(data)) {
                    this.revivableCache[playerId] = isRevivable;
                }
            };
            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/loader.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) => {
                this.ffStats = data;
                if (this.enhancementManager) {
                    this.enhancementManager.updateFFColumns();
                }
            };
        }
        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();
            }
            this.startDomStatusScanner();
            this.injectChainBoxPanel();
            this._injectSidebarBadge();
            this._chainBoxInterval = setInterval(() => {
                if (document.querySelector('.chain-box') && !document.querySelector('.cat-info-panel')) {
                    this.injectChainBoxPanel();
                }
                // Re-try bonus claim UI if container doesn't exist yet
                if (!document.getElementById('cat-bonus-claim-row') && document.querySelector('.chain-box')) {
                    this.updateChainBonusClaimUI();
                }
            }, 2000);
            // 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)
            if (this.pollingManager) {
                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(userFactionId, enemyInfo, 'opposite faction');
                    }
                }
                catch (e) {
                    console.warn('[MEMBERS] Auto-sync error:', e);
                    this.apiManager.reportError('autoSyncMembers', e);
                }
                this.injectChainBoxPanel();
                this.updateChainBonusClaimUI();
                await this.checkActivationStatus();
                // 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.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.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.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;
    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;
            // ── Chain timer relay to server ─────────────────────────────────
            this._lastRelayedChainEnd = 0;
        }
        init() {
            // Install fetch intercept only once (survives SPA re-evals)
            if (!window.__CAT_CHAIN_INIT) {
                window.__CAT_CHAIN_INIT = true;
                this.installFetchIntercept();
            }
            // Always (re)inject UI — handles SPA navigation and dynamic DOM
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', () => {
                    this.injectUI();
                    this.loadCurrentMarker();
                });
            }
            else {
                this.injectUI();
                this.loadCurrentMarker();
            }
        }
        // ── 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) => {
                        const db = json?.DB;
                        const attacker = db?.attackerUser;
                        const chain = attacker?.chain ?? 0;
                        self.onChainUpdate(chain);
                        self.checkAttackResult(json, db);
                        // 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;
            };
        }
        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;
            GM_xmlhttpRequest({
                method: 'POST',
                url: `${serverUrl}/api/chain-timer`,
                headers: {
                    'Content-Type': 'application/json',
                    'Authorization': `Bearer ${authToken}`
                },
                data: JSON.stringify({ factionId, chainEnd, chainCount }),
                anonymous: true
            });
        }
        // ── Chain update handler ───────────────────────────────────────
        onChainUpdate(chain) {
            this.chain = chain;
            if (chain > 0) {
                const next = this.getNextBonus(chain);
                const left = next ? next - chain : null;
                if (next && left !== null) {
                    console.log(`%c[CAT Chain] %c${chain}/${next} %c· ${left} hits to bonus %c(attack page)`, 'color:#4FC3F7;font-weight:bold', 'color:#E2E8F0', 'color:#718096', 'color:#4FC3F7;font-style:italic');
                }
                else {
                    console.log(`%c[CAT Chain] %c${chain} hits %c(attack page)`, 'color:#4FC3F7;font-weight:bold', 'color:#E2E8F0', 'color:#4FC3F7;font-style:italic');
                }
            }
            else {
                console.log(`%c[CAT Chain] %cNo active chain %c(attack page)`, 'color:#4FC3F7;font-weight:bold', 'color:#718096', 'color:#4FC3F7;font-style:italic');
            }
            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.warn('[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';
            GM_xmlhttpRequest({
                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.warn('[CAT] Failed to send attack result:', response.status);
                    }
                },
                onerror: (err) => {
                    console.error('[CAT] Error sending attack result:', err);
                }
            });
        }
        // ── 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';
            // 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);
                    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-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;
            GM_xmlhttpRequest({
                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()));
            GM_xmlhttpRequest({
                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
                GM_xmlhttpRequest({
                    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);
        }
    }

    // ── Attack page: chain warning only ──
    const isAttackPage = window.location.pathname === '/loader.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');
                }
            }
        }
        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 (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 */ }
            try {
                installInterceptors();
                state.enhancer = new FactionWarEnhancer();
                exposeGlobalAPI(state.enhancer);
            }
            catch (e) {
                console.error('[CAT] Initialization error:', e);
            }
        }
    }

})();