TreeDibsMapper

Dibs, Faction-wide notes, and war management systems for Torn (PC AND TornPDA Support)

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          TreeDibsMapper
// @namespace     http://tampermonkey.net/
// @version       3.11.8
// @description   Dibs, Faction-wide notes, and war management systems for Torn (PC AND TornPDA Support)
// @author        TreeMapper [3573576]
// @match         https://www.torn.com/loader.php?sid=attack&user2ID=*
// @match         https://www.torn.com/factions.php*
// @grant         GM_xmlhttpRequest
// @grant         GM_registerMenuCommand
// @connect       api.torn.com
// @connect       us-central1-tornuserstracker.cloudfunctions.net
// @connect       apiget-codod64xdq-uc.a.run.app
// @connect       apipost-codod64xdq-uc.a.run.app
// @connect       issueauthtoken-codod64xdq-uc.a.run.app
// @connect       identitytoolkit.googleapis.com
// @connect       securetoken.googleapis.com
// @connect       storage.googleapis.com
// @connect       ffscouter.com
// @connect       update.greasyfork.org
// @connect       greasyfork.org
// ==/UserScript==
/*
    Documentation: Torn Faction Dibs and War Management Userscript
    For full user's guide: visit - https://www.torn.com/forums.php#/p=threads&f=67&t=16515676&b=0&a=0

    This userscript provides a comprehensive dibs, war management, and user notes system
    for the game Torn. Authentication uses the Torn API Key
    provided by the user for server-side verification.
    If you have any issues, send me a console log on discord or in-game.

    PDA:
    - This script relies on PDA's emulation of standard `GM_` functions and its `###PDA-APIKEY###` placeholder.
*/

(function() {
    'use strict';

    // ----- Global singleton / multi-injection guard (moved beneath metadata to not break TM/GF parsing) -----
    if (window.__TDM_SINGLETON__) {
        window.__TDM_SINGLETON__.reloads = (window.__TDM_SINGLETON__.reloads || 0) + 1;
        console.info('[TDM] Secondary script load detected; reusing existing UI artifacts. Reload count=', window.__TDM_SINGLETON__.reloads);
        return; // abort duplicate initialization
    }

    (function initTdmSingleton(){
        try {
            // IndexedDB helpers for storing heavy attack arrays (avoid localStorage quota issues)
            const idb = {
                dbName: 'tdmRankedWarAttacks',
                storeName: 'attacks',
                openDB: function() {
                    return new Promise((resolve, reject) => {
                        try {
                            // Prefer an active dibs-record name early to avoid flicker showing 'ID <id>' on load
                            try {
                                const dibEntry = Array.isArray(state.dibsData) ? state.dibsData.find(d => d && String(d.opponentId) === String(oppId) && (d.opponentname || d.opponentName)) : null;
                                if (dibEntry) oppNameFull = String(dibEntry.opponentname || dibEntry.opponentName || '');
                            } catch(_) {}
                            const req = indexedDB.open(this.dbName, 1);
                            req.onupgradeneeded = (e) => {
                                const db = e.target.result;
                                if (!db.objectStoreNames.contains(this.storeName)) {
                                    db.createObjectStore(this.storeName, { keyPath: 'warId' });
                                }
                            };
                            req.onsuccess = (e) => resolve(e.target.result);
                            req.onerror = (e) => reject(e.target.error || new Error('IndexedDB open failed'));
                        } catch (err) { reject(err); }
                    });
                },
                getAttacks: async function(warId) {
                    if (!warId) return null;
                    try {
                        const db = await this.openDB();
                        const res = await new Promise((resolve, reject) => {
                            try {
                                const tx = db.transaction([this.storeName], 'readonly');
                                const store = tx.objectStore(this.storeName);
                                const req = store.get(String(warId));
                                req.onsuccess = (e) => { const v = e.target.result; if (!v) return resolve({ attacks: null, updatedAt: null, meta: null }); resolve({ attacks: v.attacks || [], updatedAt: v.updatedAt || null, meta: v.meta || null }); };
                                req.onerror = () => resolve(null);
                            } catch (err) { resolve(null); }
                        });
                        try { db.close(); } catch(_) {}
                        return res;
                    } catch (err) { return null; }
                },
                saveAttacks: async function(warId, attacks, meta = {}) {
                    if (!warId) return;
                    try {
                        const db = await this.openDB();
                        await new Promise((resolve, reject) => {
                            try {
                                const tx = db.transaction([this.storeName], 'readwrite');
                                const store = tx.objectStore(this.storeName);
                                store.put({ warId: String(warId), attacks: Array.isArray(attacks) ? attacks : [], updatedAt: Date.now(), meta: meta });
                                tx.oncomplete = () => resolve();
                                tx.onerror = () => resolve();
                            } catch (_) { resolve(); }
                        });
                        try { db.close(); } catch(_) {}
                        if (state.debug?.idbLogs) tdmlogger('info', '[idb] saved attacks', { warId, count: Array.isArray(attacks) ? attacks.length : 0 });
                    } catch(_) { /* noop */ }
                },
                delAttacks: async function(warId) {
                    try {
                        const db = await this.openDB();
                        await new Promise((resolve) => {
                            try {
                                const tx = db.transaction([this.storeName], 'readwrite');
                                const store = tx.objectStore(this.storeName);
                                store.delete(String(warId));
                                tx.oncomplete = () => resolve();
                                tx.onerror = () => resolve();
                            } catch (_) { resolve(); }
                        });
                        if (state.debug?.idbLogs) tdmlogger('info', '[idb] deleted attacks', { warId });
                    } catch(_) { /* noop */ }
                }
            };
            window.__TDM_SINGLETON__ = { firstLoadAt: Date.now() };

            // Polyfill helpers: some host environments (PDA/SVG) expose
            // `element.className` as an SVGAnimatedString object which
            // doesn't have String methods like `includes`. That causes
            // runtime errors when other code calls `el.className.includes(...)`.
            // Safely augment the SVGAnimatedString prototype with lightweight
            // delegating methods so existing code continues to work.
            try {
                if (typeof SVGAnimatedString !== 'undefined' && SVGAnimatedString.prototype) {
                    const sad = SVGAnimatedString.prototype;
                    if (typeof sad.toString !== 'function') {
                        sad.toString = function() { try { return String(this && this.baseVal != null ? this.baseVal : ''); } catch(_) { return ''; } };
                    }
                    if (typeof sad.valueOf !== 'function') {
                        sad.valueOf = sad.toString;
                    }
                    if (typeof sad.includes !== 'function') {
                        sad.includes = function(sub, pos) { try { return String(this.baseVal || '').includes(sub, pos); } catch(_) { return false; } };
                    }
                    if (typeof sad.indexOf !== 'function') {
                        sad.indexOf = function(sub, pos) { try { return String(this.baseVal || '').indexOf(sub, pos); } catch(_) { return -1; } };
                    }
                    if (typeof sad.startsWith !== 'function') {
                        sad.startsWith = function(sub, pos) { try { return String(this.baseVal || '').startsWith(sub, pos); } catch(_) { return false; } };
                    }
                }
            } catch (e) { /* ignore */}
        } catch(_) { /* ignore */ }
    })();

    // Central configuration
    const config = {
        VERSION: '3.11.8',
        API_GET_URL: 'https://apiget-codod64xdq-uc.a.run.app',
        API_POST_URL: 'https://apipost-codod64xdq-uc.a.run.app',
        API_HTTP_GET_URL: 'https://us-central1-tornuserstracker.cloudfunctions.net/apiHttpGet',
        API_HTTP_POST_URL: 'https://us-central1-tornuserstracker.cloudfunctions.net/apiHttpPost',
        FIREBASE: {
            projectId: 'tornuserstracker',
            apiKey: 'AIzaSyBEWTXThhAF4-gZyax_8pZ_15xn8FhLZaE',
            customTokenUrl: 'https://issueauthtoken-codod64xdq-uc.a.run.app'
        },
        // PDA placeholder: replaced by PDA host with actual key at runtime; stays starting with '#' when not replaced
        PDA_API_KEY_PLACEHOLDER: '###PDA-APIKEY###',
        REFRESH_INTERVAL_ACTIVE_MS: 10000,
        REFRESH_INTERVAL_INACTIVE_MS: 60000,
        DEFAULT_FACTION_BUNDLE_REFRESH_MS: 30000,
        MIN_GLOBAL_FETCH_INTERVAL_MS: 2000,
        // Global cross-tab limiter for getWarStorageUrls to avoid spike-level bursts across pages
        GET_WAR_STORAGE_URLS_GLOBAL_MIN_INTERVAL_MS: 30000,
        // Client-side cooldown to avoid refetching manifest URLs that backend
        // reported as missing recently. Matches server-side cooldown but is
        // independent per client instance.
        CLIENT_MANIFEST_MISSING_COOLDOWN_MS: 60000,
        WAR_STATUS_AND_MANIFEST_MIN_INTERVAL_MS: 60000,
        WAR_STATUS_MIN_INTERVAL_MS: 30000,
        MANIFEST_BOOTSTRAP_MIN_INTERVAL_MS: 10 * 60 * 1000,
        IP_BLOCK_COOLDOWN_MS: 5 * 60 * 1000,
        IP_BLOCK_COOLDOWN_JITTER_MS: 15000,
        IP_BLOCK_LOG_INTERVAL_MS: 30000,
        ACTIVITY_TIMEOUT_MS: 30000,
        MIN_FACTION_CACHE_FRESH_MS: 30000,
        MIN_DIBS_STATUS_FRESH_MS: 10000,
        // Storage V2 flag & adaptive polling flag removed (manifest path now default; legacy polling unified)
        GREASYFORK: {
            scriptId: '540873',
            pageUrl: 'https://greasyfork.org/en/scripts/540873-treedibsmapper',
            downloadUrl: 'https://update.greasyfork.org/scripts/540873/TreeDibsMapper.user.js',
            updateMetaUrl: 'https://update.greasyfork.org/scripts/540873/TreeDibsMapper.meta.js'
        },
        customKeyUrl: 'https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=TreeDibsMapper&user=basic,profile,faction,job&faction=rankedwars,members,attacks,attacksfull,basic,chain,chains,positions,warfare,wars&torn=rankedwars,rankedwarreport',
        LANDED_TTL_MS: 600000,
        TRAVEL_PROMOTE_MS: 60000,
        DEFAULT_COLUMN_VISIBILITY: {
            rankedWar: { lvl: true, factionIcon: false, members: true, points: true, status: true, attack: true },
            membersList: { lvl: true, member: true, memberIcons: true, position: true, days: false, factionIcon: false, status: true, memberIndex: true, dibsDeals: true, notes: true }
        },
        // Default column widths (percent). These are per-table maps of column-key->percentage.
        // Values are used to set width / flex-basis for header and cell elements.
        DEFAULT_COLUMN_WIDTHS: {
            // Ranked War table
            rankedWar: { lvl: 12, members: 40, points: 12, status: 16, attack: 6, factionIcon: 6 },
            // Members list table
            membersList: { lvl: 6, member: 30, memberIcons: 16, memberIndex: 3, position: 10, days: 8, status: 9, factionIcon: 6, dibsDeals: 10, notes: 10 }
        },
        // PDA defaults: designed for narrow screens / mobile. Keep visibility similar to PC but with tighter widths.
        DEFAULT_COLUMN_VISIBILITY_PDA: {
            rankedWar: { lvl: true, factionIcon: false, members: true, points: true, status: true, attack: true },
            membersList: { lvl: true, member: true, memberIcons: false, position: false, days: false, factionIcon: false, status: true, memberIndex: true, dibsDeals: true, notes: true }
        },
        DEFAULT_COLUMN_WIDTHS_PDA: {
            rankedWar: { lvl: 6, members: 36, points: 12, status: 15, attack: 9, factionIcon: 4 },
            membersList: { lvl: 6, member: 40, memberIcons: 16, memberIndex: 2, position: 12, days: 12, status: 20, factionIcon: 4, dibsDeals: 14, notes: 16 }
        },
        DEFAULT_SETTINGS: {
            showAllRetaliations: false,
            chainTimerEnabled: true,
            inactivityTimerEnabled: false,
            opponentStatusTimerEnabled: true,
            apiUsageCounterEnabled: false,
            dibsDealsBadgeEnabled: true,
            attackModeBadgeEnabled: true,
            chainWatcherBadgeEnabled: true,
            activityTrackingEnabled: false,
            activityCadenceSeconds: 15,
            debugOverlay: false
        },
        CSS: {
            colors: {
                success: '#4CAF50',
                error: '#f44336',
                warning: '#ff9800',
                info: '#2196F3',
                dibsSuccess: '#ac241bff',
                dibsSuccessHover: '#7a1e1a',
                dibsOther: '#0f882bff',
                dibsOtherHover: '#0a7028ff',
                dibsInactive: '#4b4232ff',
                dibsInactiveHover: '#362f22ff',
                noteInactive: '#4a4f58',
                noteInactiveHover: '#56606b',
                noteActive: '#ffa200ff',
                noteActiveHover: '#d17a00ff',
                medDealInactive: '#1a2b1eff',
                medDealInactiveHover: '#36543e',
                medDealSet: '#b600ad',
                medDealSetHover: '#9C27B0',
                medDealMine: '#9001b7ff',
                medDealMineHover: '#370053ff',
                assistButton: '#40004bff',
                assistButtonHover: '#35003aff',
                modalBg: '#1a1a1a',
                modalBorder: '#333',
                buttonBg: '#2c2c2c',
                mainColor: '#344556'
            }
        }
    };

    // Safety guard: ensure initial cache shape is always an object to avoid runtime errors
    // when other code reads/modifies the cache. This protects the script from
    // earlier broken states and future regressions where the initializer might be mutated.
    // try { if (!state.rankedWarAttacksCache || typeof state.rankedWarAttacksCache !== 'object') state.rankedWarAttacksCache = {}; } catch(_) {}

    // set log levels
    // persisted log level is stored via storage key 'logLevel' (default: 'warn')
    const logLevels = ['debug', 'info', 'warn', 'error', 'log'];
    const ADMIN_ROLE_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
    
    //======================================================================
    // Terms of Service display / acknowledgment version
    const TDM_TOS_VERSION = 1;
    const TDM_TOS_ACK_KEY = `tdm_tos_ack_v${TDM_TOS_VERSION}`;
    //======================================================================
    // Selections must be in the fetch custom API key URL 
    const REQUIRED_API_KEY_SCOPES = Object.freeze([
        'faction.basic',
        'faction.members',
        'faction.rankedwars',
        'faction.warfare',
        'faction.wars',
        'faction.attacks',
        'faction.attacksfull',
        'faction.chain',
        'faction.chains',
        'faction.positions',
        'user.basic',
        'user.profile',
        'user.faction',
        'user.job',
        'torn.rankedwars',
        'torn.rankedwarreport'
    ]);

    const validateApiKeyScopes = (keyInfo) => {
        const access = keyInfo?.info?.access || {};
        const level = (typeof access.level === 'number') ? access.level : null;
        const scopes = [];
        const pushScope = (value) => {
            if (!value && value !== 0) return;
            const normalized = String(value).toLowerCase();
            if (!scopes.includes(normalized)) scopes.push(normalized);
        };

        const scopeList = Array.isArray(access.scopes) ? access.scopes : (Array.isArray(access.scope) ? access.scope : []);
        scopeList.forEach(pushScope);

        // Support multiple key info shapes.
        // Torn sometimes returns top-level `info.selections` while other times
        // selections are nested under `info.access.selections`. Accept either
        // and process any selection maps we find.
        const selectionSources = [];
        if (access.selections && typeof access.selections === 'object') selectionSources.push(access.selections);
        if (keyInfo?.info?.selections && typeof keyInfo.info.selections === 'object') selectionSources.push(keyInfo.info.selections);
        selectionSources.forEach((selMap) => {
            Object.entries(selMap).forEach(([root, entries]) => {
                if (!root) return;
                const base = String(root).toLowerCase();
                pushScope(base);
                if (Array.isArray(entries)) {
                    entries.forEach(sel => pushScope(`${base}.${String(sel).toLowerCase()}`));
                }
            });
        });

        const hasScope = (target) => {
            const normalized = String(target).toLowerCase();
            if (scopes.includes(normalized)) return true;
            const root = normalized.split('.')[0];
            return scopes.includes(root);
        };

        const missing = REQUIRED_API_KEY_SCOPES.filter(req => !hasScope(req));
        const isLimited = typeof level === 'number' && level >= 3;
        const ok = missing.length === 0 || isLimited;

        return { ok, missing, level, scopes, isLimited };
    };

    //======================================================================
    // 2. STORAGE & UTILITIES
    //======================================================================
    // Key mapping: legacy keys replaced with new namespaced keys
    const _keyMap = Object.freeze({
        columnVisibility: 'columnVisibility',
        adminFunctionality: 'adminFunctionality',
        dataTimestamps: 'dataTimestamps',
        'rankedWar.attacksCache': 'rankedWar.attacksCache',
        'rankedWar.lastAttacksSource': 'rankedWar.lastAttacksSource',
        'rankedWar.lastAttacksMeta': 'rankedWar.lastAttacksMeta',
        'rankedWar.summaryCache': 'rankedWar.summaryCache',
        'rankedWar.lastSummarySource': 'rankedWar.lastSummarySource',
        'rankedWar.lastSummaryMeta': 'rankedWar.lastSummaryMeta',
        'cache.factionData': 'cache.factionData',
        'fs.medDeals': 'fs.medDeals',
        'fs.dibs': 'fs.dibs',
        'fs.seenNotificationIds': 'fs.seenNotificationIds'
    });
    // Consistent prefix for all storage keys
    const _namespacedPrefix = 'tdm.';
    // Storage helper: always applies prefix, uses new key names
    const storage = {
        get(key, def) {
            const mappedKey = _keyMap[key] || key;
            let raw = localStorage.getItem(_namespacedPrefix + mappedKey);
            if (raw == null) return def;
            if (raw === 'undefined') { try { localStorage.removeItem(_namespacedPrefix + mappedKey); } catch(_) {}; return def; }
            if (raw === 'null') return null;
            try { return JSON.parse(raw); } catch(_) { return raw; }
        },
        set(key, value) {
            const mappedKey = _keyMap[key] || key;
            if (value === undefined) {
                try { localStorage.removeItem(_namespacedPrefix + mappedKey); } catch(_) {}
                return;
            }
            let serialized;
            let stringifyDuration = 0;
            let totalDuration = 0;
            const nowFn = (typeof performance !== 'undefined' && typeof performance.now === 'function')
                ? () => performance.now()
                : () => Date.now();
            // Storage write — no UI/CSS mutations here (avoid leaking variables into storage/set scope)
            try {
                if (typeof value === 'string') {
                    serialized = value;
                } else {
                    const stringifyStart = nowFn();
                    serialized = JSON.stringify(value);
                    stringifyDuration = Math.max(0, nowFn() - stringifyStart);
                }
                const storeStart = nowFn();
                localStorage.setItem(_namespacedPrefix + mappedKey, serialized);
                totalDuration = Math.max(0, nowFn() - storeStart) + stringifyDuration;
            } finally {
                if (typeof serialized === 'string') {
                    try {
                        const metricsRoot = state.metrics || (state.metrics = {});
                        const storageStats = metricsRoot.storageWrites || (metricsRoot.storageWrites = {});
                        const entry = storageStats[mappedKey] || (storageStats[mappedKey] = { count: 0, totalMs: 0, maxMs: 0, lastMs: 0, lastBytes: 0, totalStringifyMs: 0, lastStringifyMs: 0 });
                        entry.count += 1;
                        entry.lastMs = totalDuration;
                        entry.totalMs += totalDuration;
                        if (totalDuration > entry.maxMs) entry.maxMs = totalDuration;
                        entry.lastBytes = serialized.length;
                        entry.totalStringifyMs += stringifyDuration;
                        entry.lastStringifyMs = stringifyDuration;
                    } catch(_) { /* metrics are best-effort */ }
                }
            }
        },
        remove(key) {
            const mappedKey = _keyMap[key] || key;
            localStorage.removeItem(_namespacedPrefix + mappedKey);
        },
        updateStateAndStorage(key, value) { state[key] = value; try { this.set(key, value); } catch(_) {} },
        cleanupLegacyKeys() {
            // Remove known legacy localStorage keys/prefixes that predate namespacing
            try {
                const toRemove = [];
                for (let i = 0; i < localStorage.length; i++) {
                    try {
                        const k = localStorage.key(i);
                        if (!k) continue;
                        // Legacy timeline / sampler keys
                        if (k.startsWith('tdmTimeline') || k.startsWith('tdmTimeline.')) toRemove.push(k);
                        // Old per-flag live track toggles
                        if (k.startsWith('liveTrackFlag_')) toRemove.push(k);
                        // Very old baseline / polling keys
                        if (k === 'tdmBaselineV1' || k === 'forceActivePolling') toRemove.push(k);
                        // legacy underscore-prefixed keys
                        if (k.startsWith('tdm_')) toRemove.push(k);
                    } catch(_) { /* ignore per-key errors */ }
                }
                toRemove.forEach(k => { try { localStorage.removeItem(k); } catch(_) {} });
            } catch(_) { /* ignore */ }
        }
    };
    const FEATURE_FLAG_STORAGE_KEY = 'featureFlags';
    const FEATURE_FLAG_DEFAULTS = Object.freeze({
        rankWarEnhancements: {
            enabled: true,
            adapters: true,
            overlay: true,
            sorter: true,
            favorites: true,
            favoritesRail: false,
            hospital: false
        }
    });
    const _cloneFeatureFlagDefaults = () => JSON.parse(JSON.stringify(FEATURE_FLAG_DEFAULTS));
    const _hydrateFeatureFlags = (target, defaults, stored) => {
        if (!defaults || typeof defaults !== 'object') return;
        Object.keys(defaults).forEach((key) => {
            const defaultValue = defaults[key];
            const storedValue = stored && typeof stored === 'object' ? stored[key] : undefined;
            if (defaultValue && typeof defaultValue === 'object' && !Array.isArray(defaultValue)) {
                if (!target[key] || typeof target[key] !== 'object') target[key] = {};
                _hydrateFeatureFlags(target[key], defaultValue, storedValue);
            } else {
                target[key] = typeof storedValue === 'boolean' ? storedValue : defaultValue;
            }
        });
    };
    const featureFlagController = (() => {
        const flags = _cloneFeatureFlagDefaults();
        _hydrateFeatureFlags(flags, FEATURE_FLAG_DEFAULTS, storage.get(FEATURE_FLAG_STORAGE_KEY, {}));
        const persist = () => { try { storage.set(FEATURE_FLAG_STORAGE_KEY, flags); } catch(_) {} };
        const readPath = (path) => {
            if (!path) return undefined;
            return path.split('.').reduce((acc, segment) => {
                if (acc && Object.prototype.hasOwnProperty.call(acc, segment)) {
                    return acc[segment];
                }
                return undefined;
            }, flags);
        };
        const set = (path, value, opts = {}) => {
            if (!path) return false;
            const segments = path.split('.');
            let cursor = flags;
            for (let i = 0; i < segments.length; i++) {
                const segment = segments[i];
                if (i === segments.length - 1) {
                    cursor[segment] = !!value;
                } else {
                    if (!cursor[segment] || typeof cursor[segment] !== 'object') cursor[segment] = {};
                    cursor = cursor[segment];
                }
            }
            if (opts.persist !== false) persist();
            if (!opts.silent) {
                try { window.dispatchEvent(new CustomEvent('tdm:featureFlagUpdated', { detail: { path, value: !!value } })); } catch(_) {}
            }
            return true;
        };
        const isEnabled = (path) => {
            if (!path) return false;
            if (path === 'rankWarEnhancements') {
                return !!flags.rankWarEnhancements?.enabled;
            }
            if (path.startsWith('rankWarEnhancements.') && !flags.rankWarEnhancements?.enabled) {
                return false;
            }
            const value = readPath(path);
            return typeof value === 'boolean' ? value : false;
        };
        return { flags, set, isEnabled, persist };
    })();
    const ADAPTER_MEMO_TTL_MS = 1000;
    const adapterMemoController = (() => {
        const buckets = new Map();
        let lastReset = 0;
        return {
            get(name) {
                const now = Date.now();
                if (!lastReset || (now - lastReset) > ADAPTER_MEMO_TTL_MS) {
                    buckets.forEach(map => map.clear());
                    lastReset = now;
                }
                if (!buckets.has(name)) buckets.set(name, new Map());
                return buckets.get(name);
            },
            clear() {
                buckets.forEach(map => map.clear());
                lastReset = Date.now();
            }
        };
    })();
    const normalizeTimestampMs = (value) => {
        try {
            if (value == null) return null;
            if (typeof value === 'number' && Number.isFinite(value)) {
                if (value >= 1e12) return Math.floor(value);
                if (value >= 1e9) return Math.floor(value * 1000);
                if (value > 1e5) return Math.floor(value * 1000);
                return null;
            }
            if (typeof value === 'string') {
                const trimmed = value.trim();
                if (!trimmed) return null;
                if (/^\d+$/.test(trimmed)) return normalizeTimestampMs(Number(trimmed));
                const parsed = Date.parse(trimmed);
                return Number.isNaN(parsed) ? null : parsed;
            }
            if (typeof value === 'object') {
                if (value instanceof Date) return value.getTime();
                if (typeof value._seconds === 'number') {
                    const baseMs = value._seconds * 1000;
                    const extra = typeof value._nanoseconds === 'number' ? Math.floor(value._nanoseconds / 1e6) : 0;
                    return baseMs + extra;
                }
                if (typeof value.seconds === 'number') {
                    const baseMs = value.seconds * 1000;
                    const extra = typeof value.nanoseconds === 'number' ? Math.floor(value.nanoseconds / 1e6) : 0;
                    return baseMs + extra;
                }
            }
            return null;
        } catch(_) {
            return null;
        }
    };
    const coerceNumber = (input) => {
        const num = Number(input);
        return Number.isFinite(num) ? num : null;
    };
    const sanitizeString = (input) => {
        if (typeof input !== 'string') return null;
        const trimmed = input.trim();
        return trimmed ? trimmed : null;
    };
    const readLocalStorageRaw = (key) => {
        try { return localStorage.getItem(key); } catch(_) { return null; }
    };
    const parseJsonSafe = (raw) => {
        if (!raw) return null;
        try { return JSON.parse(raw); } catch(_) { return null; }
    };
    const FFSCOUTER_STORAGE_PREFIX = 'ffscouter.player_';
    const BSP_ENABLED_STORAGE_KEY = 'tdup.battleStatsPredictor.IsBSPEnabledOnPage_Faction';
    const LEVEL_DISPLAY_MODES = ['level','ff','ff-bs','bsp'];
    const DEFAULT_LEVEL_DISPLAY_MODE = 'level';
    const LEVEL_CELL_LONGPRESS_MS = 500;
    const formatBattleStatsValue = (rawValue) => {
        try {
            const number = Number(rawValue);
            if (!Number.isFinite(number)) return '';
            const absOriginal = Math.abs(number);
            const localized = absOriginal.toLocaleString('en-US');
            const parts = localized.split(',');
            if (!parts.length) return String(rawValue ?? '');
            if (absOriginal < 1000) {
                const prefix = number < 0 ? '-' : '';
                return `${prefix}${parts[0]}`;
            }
            let head = parts[0];
            const absHead = head.replace('-', '');
            const leadingInt = parseInt(absHead, 10);
            if (!Number.isNaN(leadingInt) && leadingInt < 10 && parts[1]) {
                const decimalsOnly = parts[1].replace(/[^0-9]/g, '');
                if (decimalsOnly && decimalsOnly[0] && decimalsOnly[0] !== '0') {
                    head += `.${decimalsOnly[0]}`;
                }
            }
            const suffixMap = { 2: 'k', 3: 'm', 4: 'b', 5: 't', 6: 'q' };
            const suffix = suffixMap[parts.length] || '';
            const sign = number < 0 ? '-' : '';
            return `${sign}${head}${suffix}`;
        } catch(_) {
            return String(rawValue ?? '');
        }
    };
    const formatFairFightValue = (rawValue) => {
        try {
            const num = Number(rawValue);
            if (!Number.isFinite(num)) return '';
            const abs = Math.abs(num);
            if (abs >= 10) return num.toFixed(1).replace(/\.0$/, '');
            if (abs >= 1) return num.toFixed(2).replace(/0$/, '').replace(/\.0$/, '');
            return num.toFixed(3).replace(/0+$/, '').replace(/\.$/, '');
        } catch(_) {
            return '';
        }
    };
    const normalizeFfRecord = (raw, playerId) => {
        if (!raw || typeof raw !== 'object') return null;
        const ffValue = coerceNumber(raw.ffValue ?? raw.fairFight ?? raw.fair_fight ?? raw.value ?? raw.ff);
        const bsEstimate = coerceNumber(raw.bsEstimate ?? raw.bs_estimate ?? raw.bs_est);
        const bsHuman = sanitizeString(raw.bsHuman ?? raw.bs_estimate_human ?? raw.bs_estimate_display) || (bsEstimate != null ? formatBattleStatsValue(bsEstimate) : null);
        const lastUpdatedMs = normalizeTimestampMs(raw.lastUpdated ?? raw.last_updated ?? raw.updatedAt ?? raw.updated ?? raw.timestamp ?? null);
        if (ffValue == null && bsEstimate == null && !bsHuman) return null;
        return {
            playerId: String(playerId),
            ffValue: ffValue != null ? ffValue : null,
            bsEstimate: bsEstimate != null ? bsEstimate : null,
            bsHuman: bsHuman || null,
            lastUpdatedMs: lastUpdatedMs || null,
            source: raw.source || raw._source || 'tdm.ffscouter',
            raw
        };
    };
    const normalizeBspRecord = (raw, playerId) => {
        if (!raw || typeof raw !== 'object') return null;
        const tbs = coerceNumber(raw.TBS ?? raw.TBS_Raw ?? raw.Score ?? raw.Result);
        const tbsBalanced = coerceNumber(raw.TBS_Balanced ?? raw.TBSBalanced);
        const score = coerceNumber(raw.Score);
        if (tbs == null && tbsBalanced == null && score == null) return null;
        const timestampMs = normalizeTimestampMs(raw.PredictionDate ?? raw.DateFetched ?? raw.UpdatedAt ?? null);
        const subscriptionEndMs = normalizeTimestampMs(raw.SubscriptionEnd ?? null);
        return {
            playerId: String(playerId),
            tbs: tbs != null ? tbs : null,
            tbsBalanced: tbsBalanced != null ? tbsBalanced : null,
            score: score != null ? score : null,
            formattedTbs: tbs != null ? formatBattleStatsValue(tbs) : null,
            timestampMs: timestampMs || null,
            subscriptionEndMs: subscriptionEndMs || null,
            reason: sanitizeString(raw.Reason) || null,
            source: 'tdup.battleStatsPredictor',
            raw
        };
    };
    // Canonical storage keys for API-key state with legacy fallbacks for migration
    const STORAGE_KEY = Object.freeze({
        CUSTOM_API_KEY: 'user.customApiKey',
        LIMITED_KEY_NOTICE: 'user.flags.limitedKeyNoticeShown',
        LEGACY_CUSTOM_API_KEY: 'torn_api_key',
        LEGACY_LIMITED_KEY_NOTICE: 'tdmLimitedKeyNoticeShown'
    });
    const getStoredCustomApiKey = () => {
        try {
            let raw = storage.get(STORAGE_KEY.CUSTOM_API_KEY, null);
            if (typeof raw === 'string') raw = raw.trim();
            if (raw) return raw;
        } catch (_) { /* noop */ }
        try {
            // Try the namespaced legacy key first, then fall back to an un-prefixed legacy key
            let legacy = storage.get(STORAGE_KEY.LEGACY_CUSTOM_API_KEY, null);
            if (!legacy) {
                try { legacy = localStorage.getItem(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch(_) { legacy = null; }
            }
            if (typeof legacy === 'string') legacy = legacy.trim();
            else if (legacy != null) legacy = String(legacy).trim();
            if (legacy) {
                storage.set(STORAGE_KEY.CUSTOM_API_KEY, legacy);
                try { tdmlogger('info','[migrateApiKey] migrated legacy torn_api_key -> user.customApiKey'); } catch(_) {}
                // Remove both namespaced and bare legacy keys so future lookups are consistent
                try { storage.remove(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch(_) {}
                try { localStorage.removeItem(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch(_) {}
                return legacy;
            }
            // If there was nothing useful, also ensure bare legacy key removed for hygiene
            try { localStorage.removeItem(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch(_) {}
        } catch (_) { /* noop */ }
        return null;
    };
    const setStoredCustomApiKey = (value) => {
        const trimmed = (typeof value === 'string') ? value.trim() : '';
        if (!trimmed) {
            try { storage.remove(STORAGE_KEY.CUSTOM_API_KEY); } catch (_) { /* noop */ }
            try { storage.remove(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch (_) { /* noop */ }
            return null;
        }
        storage.set(STORAGE_KEY.CUSTOM_API_KEY, trimmed);
        // Clear both namespaced and bare legacy keys if present
        try { storage.remove(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch (_) { /* noop */ }
        try { localStorage.removeItem(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch (_) { /* noop */ }
        return trimmed;
    };
    const clearStoredCustomApiKey = () => {
        try { storage.remove(STORAGE_KEY.CUSTOM_API_KEY); } catch (_) { /* noop */ }
        // Remove both the namespaced and legacy bare entries
        try { storage.remove(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch (_) { /* noop */ }
        try { localStorage.removeItem(STORAGE_KEY.LEGACY_CUSTOM_API_KEY); } catch (_) { /* noop */ }
    };
    // Post-reset verification hook (Phase 1 acceptance criteria #1 & #2)
    (function postResetVerify(){
        try {
            if (!sessionStorage.getItem('post_reset_check')) return;
            sessionStorage.removeItem('post_reset_check');
            const idbDeleted = sessionStorage.getItem('post_reset_idb_deleted') === '1';
            sessionStorage.removeItem('post_reset_idb_deleted');
            // Scan localStorage for residual legacy keys
            const residual = Object.keys(localStorage).filter(k => (
                k.startsWith('tdmTimeline') || k.startsWith('liveTrackFlag_') || k === 'tdmBaselineV1' || k === 'forceActivePolling'
            ));
            const trackingEnabled = !!localStorage.getItem(_namespacedPrefix + 'tdmActivityTrackingEnabled');
            const summary = { idbDeleted, residualLegacyKeys: residual, trackingEnabledAfterReset: trackingEnabled };
            if (residual.length === 0 && !trackingEnabled) {
                try { tdmlogger('info', '[PostReset]', 'Verification PASS', summary); } catch(_) {}
            } else {
                try { tdmlogger('warn', '[PostReset]', 'Verification WARN', summary); } catch(_) {}
            }
        } catch(_) { /* silent */ }
    })();

    const utils = {
            // Shared small helpers (consolidate duplicated local helpers)
            rawTrim: (s) => (typeof s === 'string' ? s.trim() : ''),
            rawHasValue: (s) => { const t = (typeof s === 'string' ? s.trim() : ''); if (!t) return false; if (t === '0') return false; return true; },
            fallbackNumIsPositive: (v) => (v != null && Number(v) > 0),
            pad2: (n) => String(n).padStart(2, '0'),
            formatTimeHMS: (totalSeconds) => {
                const hrs = Math.floor(totalSeconds / 3600);
                const mins = Math.floor((totalSeconds % 3600) / 60);
                const secs = Math.floor(totalSeconds % 60);
                if (hrs > 0) return `${hrs}:${utils.pad2(mins)}:${utils.pad2(secs)}`;
                return `${mins}:${utils.pad2(secs)}`;
            },
            coerceStorageString: (value, fallback = '') => {
                if (value == null) return fallback;
                if (typeof value === 'string') return value;
                if (Array.isArray(value)) {
                    return value
                        .map(v => (v == null ? '' : String(v).trim()))
                        .filter(Boolean)
                        .join(',');
                }
                if (typeof value === 'number' || typeof value === 'boolean') return String(value);
                return fallback;
            },
            // ---- Unified Status & Travel Maps (consolidated) START ----
            _statusMap: (() => {
                // Canonical keys: Okay, Hospital, HospitalAbroad, Travel, Returning, Abroad, Jail
                // Each entry: { aliases: [regex|string], events: [eventType strings], priority }
                // Priority: higher number wins when multiple match heuristics (HospitalAbroad > Hospital > Returning > Travel > Abroad > Jail > Okay)
                const make = (priority, aliases, events=[]) => ({ priority, aliases, events });
                return {
                    // Abroad hospital if description begins with article 'In a' or 'In an'
                    HospitalAbroad: make(90, [/^\s*in\s+a[n]?\s+/i], ['status:hospitalAbroad']),
                    // Domestic (Torn City) hospital strings: 'In hospital for ...' or plain 'Hospital'
                    Hospital: make(80, [/^\s*in\s+hospital\b/i, /^hospital/i], ['status:hospital']),
                    Returning: make(70, [/^returning to torn from /i, / returning$/i], ['travel:returning']),
                    Travel: make(60, [/^travell?ing to /i, /^travel(ing)?$/i, / flight to /i], ['travel:depart']),
                    Abroad: make(50, [/^in\s+[^.]+$/i, / abroad$/i], ['travel:abroad']),
                    Jail: make(40, [/^in jail/i, /^jail$/i], ['status:jail']),
                    Okay: make(10, [/^okay$/i, /^active$/i, /^idle$/i], ['status:okay'])
                };
            })(),
            // Travel destination map: canonicalName => { aliases:[regex|string], minutes (deprecated avg), planes:{light_aircraft, airliner, airliner_business, private_jet} }
            _travelMap: (() => {
                const mk = (base, aliases, planes, adjective, abbr) => ({ minutes: base, aliases, planes, adjective, abbr });
                return {
                    'Mexico': mk(15, [/^mex(?:ico)?$/i], { light_aircraft:18, airliner:26, airliner_business:8, private_jet:13 }, 'Mexican', 'MEX'),
                    'Cayman Islands': mk(60, [/^cayman$/i, /cayman islands?/i, /\bci\b/i], { light_aircraft:25, airliner:35, airliner_business:11, private_jet:18 }, 'Caymanian', 'CI'),
                    'Canada': mk(45, [/^can(ad?a)?$/i], { light_aircraft:29, airliner:41, airliner_business:12, private_jet:20 }, 'Canadian', 'CAN'),
                    'Hawaii': mk(120, [/^hawai/i], { light_aircraft:94, airliner:134, airliner_business:40, private_jet:67 }, 'Hawaiian', 'HI'),
                    'United Kingdom': mk(300, [/^(uk|united kingdom|london)$/i], { light_aircraft:111, airliner:159, airliner_business:48, private_jet:80 }, 'British', 'UK'),
                    'Argentina': mk(240, [/^arg(?:entina)?$/i], { light_aircraft:117, airliner:167, airliner_business:50, private_jet:83 }, 'Argentinian', 'ARG'),
                    'Switzerland': mk(360, [/^swiss|switzerland$/i], { light_aircraft:123, airliner:175, airliner_business:53, private_jet:88 }, 'Swiss', 'SWITZ'),
                    'Japan': mk(420, [/^jap(?:an)?$/i], { light_aircraft:158, airliner:225, airliner_business:68, private_jet:113 }, 'Japanese', 'JAP'),
                    'China': mk(420, [/^china$/i, /^chi$/i], { light_aircraft:169, airliner:242, airliner_business:72, private_jet:121 }, 'Chinese', 'CN'),
                    // UAE: broaden alias patterns to match descriptions like 'In UAE', 'in the UAE', or embedded tokens
                    'United Arab Emirates': mk(480, [/\buae\b/i, /united arab emir/i, /\bdubai\b/i], { light_aircraft:190, airliner:271, airliner_business:81, private_jet:135 }, 'Emirati', 'UAE'),
                    'South Africa': mk(540, [/^south africa$/i, /^sa$/i], { light_aircraft:208, airliner:297, airliner_business:89, private_jet:149 }, 'African', 'SA')
                };
            })(),
            // Legacy shims (kept for any residual calls) – prefer buildUnifiedStatusV2 outputs
            // Legacy status shims removed. Use buildUnifiedStatusV2 for canonical status records.
            parseUnifiedDestination: function(str) {
                let raw = (str||'').toString().trim();
                if (!raw) return null;
                // Strip leading 'In ' or 'In the ' forms which appear in Abroad descriptions
                raw = raw.replace(/^in\s+(the\s+)?/i,'').trim();
                for (const [canon, meta] of Object.entries(utils._travelMap)) {
                    for (const ali of meta.aliases) {
                        if (typeof ali === 'string') { if (raw.toLowerCase().includes(ali.toLowerCase())) return canon; }
                        else if (ali instanceof RegExp) { if (ali.test(raw)) return canon; }
                    }
                }
                return null;
            },
            // Ensure we upgrade to business class timing if user is eligible but still marked plain airliner.
            // Supports both legacy travel record shape (mins/ct0) and unified status v2 (durationMins/startedMs/arrivalMs).
            // If eligibility not yet known it will trigger an async check (non-blocking) once per cooldown window.
            ensureBusinessUpgrade: function(rec, id, opts={}){
                try {
                    if (!rec || !rec.dest) return; // nothing to do
                    const allowAsync = opts.async !== false; // default true
                    // Normalize plane fields across legacy + v2 records.
                    // If the current record does not include a plane (API omitted it), but the previous
                    // record indicates an outbound 'airliner', allow the eligibility check to proceed
                    // using the previous plane type as a hint. Do not overwrite rec.plane here; only
                    // set rec.plane when applying the business upgrade.
                    const curPlane = rec.plane || null;
                    const hintedPlane = (!curPlane && opts && opts.prevRec && opts.prevRec.plane) ? opts.prevRec.plane : null;
                    const effectivePlane = curPlane || hintedPlane || null;
                    if (!effectivePlane || effectivePlane !== 'airliner') return; // only upgrade airliner base

                    // Avoid repeated work: if we've already applied a business upgrade for this user
                    // and cached that fact, skip further checks. This prevents repeated KV/API checks
                    // and repeated application logs when the UI re-renders frequently.
                    state._businessApplied = state._businessApplied || {};
                    const sid = String(id || '');
                    // Quick synchronous cross-tab check via localStorage first
                    try {
                        const lsKey = 'tdm.business.applied.id_' + sid;
                        const raw = localStorage.getItem(lsKey);
                        if (raw) {
                            try { state._businessApplied[sid] = true; } catch(_) {}
                            return; // already applied (persisted)
                        }
                    } catch(_) {}
                    // If in-memory already marked, short-circuit
                    if (state._businessApplied[sid]) return;
                    // Also attempt to hydrate from async KV in background (non-blocking)
                    try {
                        if (typeof ui !== 'undefined' && ui && ui._kv && typeof ui._kv.getItem === 'function') {
                            ui._kv.getItem('tdm.business.applied.id_' + sid).then(v => {
                                if (v) {
                                    try { state._businessApplied[sid] = true; } catch(_) {}
                                }
                            }).catch(()=>{});
                        }
                    } catch(_) {}
                    const eligible = utils?.business?.isEligibleSync?.(id);
                    const applyUpgrade = () => {
                        try {
                            
                            rec.plane = 'airliner_business';
                            const newMins = utils.getTravelMinutes(rec.dest, 'airliner_business');
                            // Determine current duration
                            const legacyMins = rec.mins != null ? rec.mins : null;
                            const v2Mins = rec.durationMins != null ? rec.durationMins : null;
                            const currentMins = v2Mins != null ? v2Mins : legacyMins;
                            if (newMins && (!currentMins || newMins !== currentMins)) {
                                if (rec.durationMins != null) rec.durationMins = newMins; // unified record
                                else rec.mins = newMins; // legacy record
                                // Recompute arrival/eta only if we have a known start timestamp
                                const startMs = rec.startedMs || rec.ct0 || null;
                                if (startMs) {
                                    const newArrival = startMs + newMins*60000;
                                    if (rec.arrivalMs && Math.abs(newArrival - rec.arrivalMs) > 15000) {
                                        rec.arrivalMs = newArrival;
                                    } else if (!rec.arrivalMs) {
                                        rec.arrivalMs = newArrival;
                                    }
                                }
                                // High confidence ETA recompute (legacy path)
                                if (rec.ct0 && rec.confidence === 'HIGH') {
                                    if (!rec.etams) rec.etams = utils.travel.computeEtaMs(rec.ct0, newMins);
                                    rec.inferredmaxetams = 0;
                                }
                                try { tdmlogger('info', '[Travel]', 'Business upgrade applied', id, rec.dest, currentMins,'->',newMins); } catch(_) {}
                                // Mark as applied so we don't repeatedly attempt to re-apply on re-renders
                                try { state._businessApplied = state._businessApplied || {}; state._businessApplied[sid] = true; } catch(_) {}
                                // Persist the applied marker for cross-tab/reload suppression
                                try {
                                    const key = 'tdm.business.applied.id_' + sid;
                                    const payload = { appliedAt: Date.now() };
                                    // Try async KV first (preferred), fall back to localStorage
                                    if (typeof ui !== 'undefined' && ui && ui._kv && typeof ui._kv.setItem === 'function') {
                                        ui._kv.setItem(key, payload).catch(()=>{
                                            try { localStorage.setItem(key, JSON.stringify(payload)); } catch(_){}
                                        });
                                    } else {
                                        try { localStorage.setItem(key, JSON.stringify(payload)); } catch(_){}
                                    }
                                } catch(_) {}
                                // Persist upgrade into shared unified status store if present
                                try { if (state.unifiedStatus && state.unifiedStatus[id] && state.unifiedStatus[id] !== rec) state.unifiedStatus[id] = rec; } catch(_) {}
                                // Fire a lightweight update event so tooltips/overlay can refresh
                                try { window.dispatchEvent(new CustomEvent('tdm:unifiedStatusUpdated', { detail: { id, upgradedBusiness: true } })); } catch(_) {}
                            }
                        } catch(e) { /* swallow */ }
                    };
                    if (eligible) {
                        applyUpgrade();
                    } else if (allowAsync && utils?.business?.ensureAsync) {
                        // Kick off async eligibility determination; upgrade when resolved true.
                        // When calling back, ensure we still only upgrade if the record effectively
                        // represents an airliner outbound (i.e., either API plane was 'airliner' or
                        // previous hint indicated 'airliner'). We re-check rec.plane and hinted prevRec.
                        utils.business.ensureAsync(id).then(ok => {
                            try {
                                const stillEffective = (rec.plane === 'airliner') || (opts && opts.prevRec && opts.prevRec.plane === 'airliner');
                                if (ok && stillEffective) applyUpgrade();
                            } catch(_) { /* ignore */ }
                        }).catch(()=>{});
                    }
                } catch(_){ /* noop */ }
            },
            travelMinutesFor: function(destCanon) {
                return utils._travelMap[destCanon]?.minutes || 0;
            },
            travelMinutesForPlane: function(destCanon, planeType='light_aircraft') {
                const meta = utils._travelMap[destCanon];
                if (!meta) return 0;
                const p = (meta.planes && meta.planes[planeType]) || null;
                return (Number(p)||0) || meta.minutes || 0;
            },
            // Normalize a variety of legacy/new callers into the member-shaped payload that buildUnifiedStatusV2 expects.
            normalizeStatusInputForV2: function(candidate, context = {}) {
                if (!candidate && !context?.status) return null;
                const ctxId = context.id ?? context.player_id ?? context.user_id ?? context.userID ?? null;
                const ctxLast = context.last_action || context.lastAction || null;
                const ctxStatus = context.status || null;

                let statusObj = null;
                let lastAction = ctxLast;
                let id = ctxId;

                if (candidate && typeof candidate === 'object') {
                    if (candidate.status || candidate.last_action || candidate.lastAction || candidate.id || candidate.player_id || candidate.user_id || candidate.userID) {
                        statusObj = candidate.status || ctxStatus;
                        lastAction = candidate.last_action || candidate.lastAction || lastAction || null;
                        id = candidate.id ?? candidate.player_id ?? candidate.user_id ?? candidate.userID ?? id ?? null;
                    }

                    if (!statusObj && (candidate.state != null || candidate.description != null || candidate.until != null)) {
                        statusObj = candidate;
                    }
                }

                if (!statusObj) {
                    if (typeof candidate === 'string') {
                        statusObj = { state: candidate, description: candidate };
                    } else if (candidate && typeof candidate === 'object') {
                        statusObj = { ...candidate };
                    } else if (ctxStatus) {
                        statusObj = { ...ctxStatus };
                    }
                } else {
                    statusObj = { ...statusObj };
                }

                if (!statusObj) return null;

                const inferredState = utils.inferStatusStateFromText(statusObj.state) || utils.inferStatusStateFromText(statusObj.description);
                if (inferredState) {
                    statusObj.state = inferredState;
                } else if (statusObj.state) {
                    const canonicalMap = {
                        traveling: 'Traveling',
                        travel: 'Traveling',
                        return: 'Traveling',
                        returning: 'Traveling',
                        abroad: 'Abroad',
                        hospital: 'Hospital',
                        hospitalabroad: 'Hospital',
                        jail: 'Jail',
                        okay: 'Okay',
                        idle: 'Okay',
                        active: 'Okay'
                    };
                    const lower = String(statusObj.state || '').toLowerCase();
                    if (canonicalMap[lower]) statusObj.state = canonicalMap[lower];
                }

                if (!statusObj.description && typeof statusObj.state === 'string') {
                    statusObj.description = statusObj.state;
                }

                return {
                    id,
                    status: statusObj,
                    last_action: lastAction
                };
            },
            buildCurrentUnifiedActivityState: () => {
                const source = state.unifiedStatus || {};
                const out = {};
                for (const [id, rec] of Object.entries(source)) {
                    out[id] = {
                        id,
                        dest: rec?.dest,
                        arrivalMs: rec?.arrivalMs,
                        startMs: rec?.startedMs || rec?.startMs,
                        canonical: rec?.canonical || null,
                        confidence: rec?.confidence,
                        witness: rec?.witness,
                        previousPhase: rec?.previousPhase || null,
                        transient: !!rec?.transient
                    };
                }
                return out;
            },
            isActivityKeepActiveEnabled: () => {
                try {
                    return !!(storage.get('tdmActivityTrackingEnabled', false) && storage.get('tdmActivityTrackWhileIdle', false));
                } catch(_) {
                    return false;
                }
            },
            inferStatusStateFromText: function(text) {
                const str = String(text || '').trim().toLowerCase();
                if (!str) return '';
                if (/\bjail\b/.test(str)) return 'Jail';
                if (/returning to torn/.test(str)) return 'Traveling';
                if (/travell?ing|flight|depart|en route|en-route|plane/.test(str)) return 'Traveling';
                if (/\bhospital\b/.test(str)) return 'Hospital';
                if (/\babroad\b/.test(str) || /^\s*in\s+(?!torn\b)[a-z]/.test(str)) return 'Abroad';
                if (/okay|active|idle|home|city/.test(str)) return 'Okay';
                return '';
            },
            // Ensure a compact note button (icon or label) exists under a parent row/subrow.
            // options: { disabled?:bool, withLabel?:bool }
            ensureNoteButton: function(parent, options={}) {
                try {
                    if (!parent) return null;
                    let btn = parent.querySelector('.note-button');
                    if (btn) {
                        if (options.disabled) btn.disabled = true; else btn.disabled = false;
                        return btn;
                    }
                    const hasNote = false; // initial unknown state → inactive style
                    const cls = 'btn note-button ' + (hasNote ? 'active-note-button' : 'inactive-note-button');
                    btn = document.createElement('button');
                    btn.type = 'button';
                    btn.className = cls;
                    btn.style.minWidth = 'auto';
                    btn.style.padding = '0';
                    // If withLabel, show small text; else show icon-only (SVG) to match existing CSS expectations
                    if (options.withLabel) {
                        btn.textContent = 'Note';
                    } else {
                        btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h13a3 3 0 0 1 3 3v13"/><path d="M14 2v4"/><path d="M6 2v4"/><path d="M4 10h16"/><path d="M8 14h2"/><path d="M8 18h4"/></svg>';
                    }
                    btn.title = '';
                    btn.setAttribute('aria-label','');
                    if (options.disabled) btn.disabled = true;
                    parent.appendChild(btn);
                    return btn;
                } catch(_) { return null; }
            },
            // Update note button active/inactive styling & accessible labels
            updateNoteButtonState: function(btn, noteText) {
                try {
                    if (!btn) return;
                    const txt = (noteText||'').trim();
                    const has = txt.length > 0;
                    const want = 'btn note-button ' + (has ? 'active-note-button' : 'inactive-note-button');
                    if (btn.className !== want) btn.className = want;

                    // If this is a text-label button (no SVG icon), update text content to show the note
                    if (!btn.querySelector('svg')) {
                        if (has) {
                            if (btn.textContent !== txt) btn.textContent = txt;
                            // Style for multiline + truncation
                            btn.style.whiteSpace = 'pre'; 
                            btn.style.overflow = 'hidden';
                            btn.style.textOverflow = 'ellipsis';
                            btn.style.textAlign = 'left';
                            // Flexbox for top-left alignment
                            btn.style.display = 'flex';
                            btn.style.alignItems = 'flex-start';
                            btn.style.justifyContent = 'flex-start';
                            btn.style.padding = '2px'; // Slight padding for readability
                            
                            btn.style.width = '100%';
                            btn.style.maxWidth = '100%';
                        } else {
                            if (btn.textContent !== 'Note') btn.textContent = 'Note';
                            // Reset styles
                            btn.style.whiteSpace = '';
                            btn.style.overflow = '';
                            btn.style.textOverflow = '';
                            btn.style.textAlign = '';
                            btn.style.display = '';
                            btn.style.alignItems = '';
                            btn.style.justifyContent = '';
                            btn.style.padding = '0';
                            btn.style.width = '';
                            btn.style.maxWidth = '';
                        }
                    } else if (!btn.textContent) {
                        // ensure some visible affordance exists (fallback icon)
                        btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4h13a3 3 0 0 1 3 3v13"/><path d="M14 2v4"/><path d="M6 2v4"/><path d="M4 10h16"/><path d="M8 14h2"/><path d="M8 18h4"/></svg>';
                    }
                    if (btn.title !== txt) btn.title = txt;
                    if (btn.getAttribute('aria-label') !== txt) btn.setAttribute('aria-label', txt);
                } catch(_) { /* silent */ }
            },
            // --- Persistence (optional enhancement) ---
            saveUnifiedStatusSnapshot: function(){
                try {
                    // Keep unifiedStatus.v1 as a lightweight UI cache only (no embedded phaseHistory)
                    const snap = { ts: Date.now(), records: state.unifiedStatus };
                    storage.set('unifiedStatus.v1', snap);
                } catch(err){ /* ignore */ }
            },
            loadUnifiedStatusSnapshot: function(maxAgeMinutes=30){
                try {
                    const snap = storage.get('unifiedStatus.v1');
                    if (!snap || !snap.records || !snap.ts) return;
                    const ageMin = (Date.now() - snap.ts)/60000;
                    if (ageMin > maxAgeMinutes) return; // stale
                    // Filter out any records whose arrival already passed long ago (>15m)
                    const filtered = {};
                    const cutoff = Date.now() - (15*60*1000);
                    for (const [pid, rec] of Object.entries(snap.records)) {
                        if (rec.arrivalMs && rec.arrivalMs < cutoff && rec.canonical === 'Travel') continue; // old travel
                        filtered[pid] = rec;
                    }
                    state.unifiedStatus = filtered;
                    // NOTE: phase history is now persisted in IndexedDB per-player (tdm.phaseHistory.id_<id>).
                    // Restoration of phaseHistory is handled in ui._restorePersistedTravelMeta which runs on init.
                } catch(err){ /* ignore */ }
            },
            // Attach a single tooltip refresh listener once (idempotent)
            ensureUnifiedStatusUiListener: function(){
                if (window._tdmUnifiedUiListenerBound) return;
                window._tdmUnifiedUiListenerBound = true;
                window.addEventListener('tdm:unifiedStatusUpdated', (ev) => {
                    try {
                        const detail = ev.detail || {}; const id = detail.id;
                        if (!id) return;
                        const sel = `.tdm-travel-eta[data-opp-id="${id}"]`;
                        const el = document.querySelector(sel);
                        if (!el) return;
                        // Pull unified record and rebuild tooltip if in travel phase
                        const rec = state.unifiedStatus ? state.unifiedStatus[id] : null;
                        if (!rec) return;
                        const canon = (rec && (rec.canonical || rec.phase)) || '';
                        const travelPhases = canon === 'Travel' || canon === 'Returning' || canon === 'Abroad';
                        if (!travelPhases) return;
                        const dest = rec.dest || null;
                        const planeForMins = rec.plane || 'light_aircraft';
                        const mins = rec.mins || (dest ? utils.getTravelMinutes(dest, planeForMins) : 0) || 0;
                        if (rec.confidence === 'HIGH' && mins > 0 && rec.etams && el) {
                            const arrow = rec.isreturn ? '\u2190' : '\u2192';
                            const destAbbr = dest ? (utils.abbrevDest(dest) || dest.split(/[\s,]/)[0]) : '';
                            let etaMs = rec.etams;
                            const remMs = Math.max(0, etaMs - Date.now());
                            const remMin = Math.ceil(remMs/60000);
                            const leftLocal = new Date(rec.ct0).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', hour12:false});
                            const observedMin = Math.max(0, Math.floor((Date.now() - rec.ct0)/60000));
                            const remainingPart = ` * ETA ${remMin}m`;
                            const tooltip = `${arrow} ${dest||destAbbr} * ${planeForMins} * Observed ${observedMin}m of ${mins}m${remainingPart} * Since ${leftLocal}`.trim();
                            if (el.title !== tooltip) el.title = tooltip;
                            if (/dur\. \?/.test(el.textContent || '') || /dur\.\s+\d/.test(el.textContent||'')) {
                                let remStr = `${remMin}m`; if (remMin > 60) { const rh=Math.floor(remMin/60); const rm=remMin%60; remStr = rh+ 'h' + (rm? ' ' + rm + 'm':''); }
                                el.textContent = `${arrow} ${destAbbr} LAND~${remStr}`;
                                el.classList.remove('tdm-travel-lowconf');
                                el.classList.add('tdm-travel-conf');
                            }
                        }
                    } catch(err){ /* silent */ }
                });
                // Removed delegated handler: rely on per-element binding logic which is now maintained
            },
            // --- Pre-arrival Alert Hooks ---
            _arrivalAlerts: [], // { id, playerId|null, minutesBefore, fired:boolean, fn }
            registerArrivalAlert: function({ playerId=null, minutesBefore=5, fn }) {
                if (typeof fn !== 'function') return null;
                const id = 'alrt_'+Math.random().toString(36).slice(2,9);
                utils._arrivalAlerts.push({ id, playerId, minutesBefore: Number(minutesBefore)||0, fn, fired:false });
                return id;
            },
            unregisterArrivalAlert: function(id){
                utils._arrivalAlerts = utils._arrivalAlerts.filter(a => a.id !== id);
            },
            _evaluateArrivalAlerts: function(){
                if (!utils._arrivalAlerts.length) return;
                const now = Date.now();
                for (const alert of utils._arrivalAlerts) {
                    if (alert.fired) continue;
                    // Scan relevant records (all or specific player)
                    const recs = Object.values(state.unifiedStatus || {});
                    for (const r of recs) {
                        if (alert.playerId && String(r.playerId) !== String(alert.playerId)) continue;
                        if (r.canonical !== 'Travel' && r.canonical !== 'Returning') continue;
                        if (!r.arrivalMs) continue;
                        const msBefore = alert.minutesBefore * 60000;
                        if (r.arrivalMs - now <= msBefore && r.arrivalMs > now) {
                            try { alert.fn({ record: r, minutesBefore: alert.minutesBefore, now }); } catch(err){ tdmlogger('warn', '[ArrivalAlert]', 'error', err); }
                            alert.fired = true;
                            break;
                        }
                    }
                }
                // Optional cleanup of fired alerts (keep for inspection for now)
            },
            // Trim `state.tornFactionData` to the N most recently fetched entries (default 20)
            trimTornFactionData: function(limit = 5) {
                try {
                    if (!state || !state.tornFactionData || typeof state.tornFactionData !== 'object') return;
                    const cache = state.tornFactionData;
                    // Collect entries with fetchedAtMs (default to 0)
                    const arr = Object.entries(cache).map(([id, entry]) => ({ id, fetchedAt: Number(entry?.fetchedAtMs || 0) || 0 }));
                    arr.sort((a, b) => b.fetchedAt - a.fetchedAt);

                    // Also trim unified status periodically (piggyback here)
                    try { utils.trimUnifiedStatus(60); } catch(_) {}
                    // Also trim ranked war attacks cache (piggyback here)
                    try { utils.trimRankedWarAttacksCache(3); } catch(_) {}

                    if (arr.length <= limit) {
                        utils.schedulePersistTornFactionData();
                        return cache;
                    }
                    const keep = new Set(arr.slice(0, limit).map(x => String(x.id)));
                    for (const k of Object.keys(cache)) {
                        if (!keep.has(String(k))) delete cache[k];
                    }
                    utils.schedulePersistTornFactionData();
                    if (state?.debug?.statusWatch) tdmlogger('info', '[TornFactionData] trimmed to', limit, 'entries');
                    return cache;
                } catch (e) { /* best-effort, non-fatal */ return state.tornFactionData || {}; }
            },
            // Prune old ranked war attacks from memory (keep only N most recent)
            trimRankedWarAttacksCache: function(limit = 3) {
                try {
                    if (!state || !state.rankedWarAttacksCache) return;
                    const cache = state.rankedWarAttacksCache;
                    const arr = Object.entries(cache).map(([id, entry]) => ({ id, updatedAt: Number(entry?.updatedAt || 0) || 0 }));
                    arr.sort((a, b) => b.updatedAt - a.updatedAt);
                    
                    if (arr.length <= limit) return;
                    
                    const keep = new Set(arr.slice(0, limit).map(x => String(x.id)));
                    let removed = 0;
                    for (const k of Object.keys(cache)) {
                        if (!keep.has(String(k))) {
                            delete cache[k];
                            removed++;
                        }
                    }
                    if (removed > 0) {
                        persistRankedWarAttacksCache(cache); // Update storage to reflect memory trim
                        if (state?.debug?.apiLogs) tdmlogger('info', '[RankedWarAttacks] Trimmed', removed, 'old wars from memory');
                    }
                } catch(_) {}
            },
            // Prune old unified status records to prevent unbounded memory growth
            trimUnifiedStatus: function(maxAgeMinutes = 60) {
                try {
                    if (!state.unifiedStatus) return;
                    const now = Date.now();
                    const cutoff = now - (maxAgeMinutes * 60 * 1000);
                    let removed = 0;
                    for (const [id, rec] of Object.entries(state.unifiedStatus)) {
                        // Keep if updated recently OR if it's a member of a currently cached faction
                        if ((rec.updated || 0) < cutoff) {
                            // Check if user is in any currently cached faction before deleting
                            // TODO:(Optimization: this check might be expensive, so maybe just rely on timestamp?
                            //  If they are in a cached faction, they would have been updated recently when that faction was fetched.)
                            delete state.unifiedStatus[id];
                            removed++;
                        }
                    }
                    if (removed > 0 && state?.debug?.statusWatch) tdmlogger('info', '[UnifiedStatus] Pruned', removed, 'old records');
                } catch(_) {}
            },
            // Emergency memory safety valve: checks for runaway array/object growth and hard-trims if necessary
            enforceMemoryLimits: function() {
                try {
                    // 1. Unified Status: Hard cap at 5000 records (approx 1-2MB)
                    if (state.unifiedStatus) {
                        const keys = Object.keys(state.unifiedStatus);
                        if (keys.length > 5000) {
                            tdmlogger('warn', '[Memory] UnifiedStatus exceeded 5000 records. Hard trimming...');
                            utils.trimUnifiedStatus(10); // Trim to 10 minutes
                            // If still too big, random slash
                            if (Object.keys(state.unifiedStatus).length > 5000) {
                                state.unifiedStatus = {}; // Nuclear option
                                tdmlogger('error', '[Memory] UnifiedStatus cleared completely due to overflow.');
                            }
                        }
                    }
                    // 2. Dibs Data: Hard cap at 2000 entries (unlikely to be reached legitimately)
                    if (Array.isArray(state.dibsData) && state.dibsData.length > 2000) {
                        tdmlogger('warn', '[Memory] DibsData exceeded 2000 entries. Truncating...');
                        state.dibsData = state.dibsData.slice(0, 2000);
                    }
                    // 3. Med Deals: Hard cap at 2000 entries
                    if (state.medDeals && Object.keys(state.medDeals).length > 2000) {
                        tdmlogger('warn', '[Memory] MedDeals exceeded 2000 entries. Clearing...');
                        state.medDeals = {};
                    }
                    // 4. Metrics: Clear if too large
                    if (state.metrics && JSON.stringify(state.metrics).length > 50000) {
                        state.metrics = {};
                    }
                } catch(_) {}
            },
            schedulePersistTornFactionData: (() => {
                let timer = null;
                return () => {
                    if (timer) clearTimeout(timer);
                    timer = setTimeout(() => {
                        timer = null;
                        try { storage.set('tornFactionData', state.tornFactionData || {}); } catch(_) {}
                    }, 2000);
                };
            })(),
            // Ranked-war meta / snapshot persistence helpers
            // Debounced + cross-tab aware: coalesces frequent updates and avoids duplicate writes across tabs
            _rwMetaDebounceMs: Number(config.RW_META_DEBOUNCE_MS) || 5000,
            _rwMetaTimers: {},
            _rwMetaLastHash: {},
            _rwMetaLastWriteTs: {},
            // Internal cross-tab signal key (uses native localStorage to trigger storage events)
            _rwMetaSignalKey: 'tdm.rw_meta_signal',

            // compute a stable JSON string for hashing (simple canonicalization)
            _rwMetaStringify: function(obj) {
                try { return JSON.stringify(obj); } catch (_) { return String(obj || ''); }
            },

            // Persist ranked war meta for warKey after debounce; avoids writing if unchanged
            schedulePersistRankedWarMeta: function(warKey, opts = {}) {
                try {
                    if (!warKey) return;
                    const key = `tdm.rw_meta_${warKey}`;
                    const payload = { v: state.rankedWarChangeMeta || {}, ts: Date.now(), warId: state.lastRankWar?.id || null };
                    const payloadStr = utils._rwMetaStringify(payload.v);
                    const lastHash = utils._rwMetaLastHash[key] || null;
                    // If nothing changed and it's been written recently, skip
                    const now = Date.now();
                    if (lastHash && lastHash === payloadStr && (now - (utils._rwMetaLastWriteTs[key] || 0) < (opts.forceWriteMs || 15000))) return;

                    // Clear existing timer
                    try { if (utils._rwMetaTimers[key]) clearTimeout(utils._rwMetaTimers[key]); } catch(_) {}

                    // Schedule write with some jitter so concurrent tabs reduce collisions
                    const delay = utils._rwMetaDebounceMs + (Math.floor(Math.random() * 200) - 100);
                    utils._rwMetaTimers[key] = setTimeout(() => {
                        try {
                            // Compare one more time before writing
                            const now2 = Date.now();
                            const current = { v: state.rankedWarChangeMeta || {}, ts: now2, warId: state.lastRankWar?.id || null };
                            const curStr = utils._rwMetaStringify(current.v);
                            const last = utils._rwMetaLastHash[key] || null;
                            if (last && last === curStr && (now2 - (utils._rwMetaLastWriteTs[key]||0) < (opts.forceWriteMs || 15000))) {
                                // nothing to do
                                try { delete utils._rwMetaTimers[key]; } catch(_) {}
                                return;
                            }
                            // Write via storage wrapper if available; also emit a native localStorage signal for other tabs
                            try { storage.set(key, current); } catch(_) { try { localStorage.setItem(key, JSON.stringify(current)); } catch(_) {} }
                            try { localStorage.setItem(utils._rwMetaSignalKey, JSON.stringify({ key, ts: Date.now(), hash: curStr })); } catch(_) {}
                            utils._rwMetaLastHash[key] = curStr;
                            utils._rwMetaLastWriteTs[key] = Date.now();
                            try { delete utils._rwMetaTimers[key]; } catch(_) {}
                        } catch(e) { try { delete utils._rwMetaTimers[key]; } catch(_) {} }
                    }, delay);
                } catch(_) {}
            },

            // Persist ranked war snapshot (debounced, simpler: uses same debounce window)
            schedulePersistRankedWarSnapshot: function(warKey, opts = {}) {
                try {
                    if (!warKey) return;
                    const key = `tdm.rw_snap_${warKey}`;
                    const payload = state.rankedWarTableSnapshot || {};
                    const payloadStr = utils._rwMetaStringify(payload);
                    const lastHash = utils._rwMetaLastHash[key] || null;
                    const now = Date.now();
                    if (lastHash && lastHash === payloadStr && (now - (utils._rwMetaLastWriteTs[key] || 0) < (opts.forceWriteMs || 15000))) return;

                    try { if (utils._rwMetaTimers[key]) clearTimeout(utils._rwMetaTimers[key]); } catch(_) {}
                    const delay = utils._rwMetaDebounceMs + (Math.floor(Math.random() * 200) - 100);
                    utils._rwMetaTimers[key] = setTimeout(() => {
                        try {
                            const now2 = Date.now();
                            const current = payload;
                            const curStr = utils._rwMetaStringify(current);
                            const last = utils._rwMetaLastHash[key] || null;
                            if (last && last === curStr && (now2 - (utils._rwMetaLastWriteTs[key]||0) < (opts.forceWriteMs || 15000))) {
                                try { delete utils._rwMetaTimers[key]; } catch(_) {}
                                return;
                            }
                            try { storage.set(key, current, { warId: state.lastRankWar?.id }); } catch(_) { try { localStorage.setItem(key, JSON.stringify(current)); } catch(_) {} }
                            try { localStorage.setItem(utils._rwMetaSignalKey, JSON.stringify({ key, ts: Date.now(), hash: curStr })); } catch(_) {}
                            utils._rwMetaLastHash[key] = curStr;
                            utils._rwMetaLastWriteTs[key] = Date.now();
                            try { delete utils._rwMetaTimers[key]; } catch(_) {}
                        } catch(e) { try { delete utils._rwMetaTimers[key]; } catch(_) {} }
                    }, delay);
                } catch(_) {}
            },

            // On storage event notify: when another tab writes to a meta or snap key, update our lastHash bookkeeping
            _initRwMetaSignalListener: function() {
                try {
                    if (utils._rwMetaSignalListenerBound) return;
                    utils._rwMetaSignalListenerBound = true;
                    window.addEventListener('storage', (ev) => {
                        try {
                            if (!ev || !ev.key) return;
                            const sigKey = utils._rwMetaSignalKey;
                            if (ev.key === sigKey) {
                                const detail = ev.newValue ? JSON.parse(ev.newValue) : null;
                                if (!detail || !detail.key) return;
                                // Update lastWriteTs/hash for the signaled key so we don't re-write unnecessarily
                                try { utils._rwMetaLastHash[detail.key] = detail.hash || (localStorage.getItem(detail.key) ? JSON.stringify(JSON.parse(localStorage.getItem(detail.key)).v || JSON.parse(localStorage.getItem(detail.key))) : null); } catch(_) {}
                                try { utils._rwMetaLastWriteTs[detail.key] = Number(detail.ts || Date.now()); } catch(_) {}
                                // Clear local timers for this key — another tab already wrote
                                try { if (utils._rwMetaTimers[detail.key]) { clearTimeout(utils._rwMetaTimers[detail.key]); delete utils._rwMetaTimers[detail.key]; } } catch(_) {}
                            }
                        } catch(_) {}
                    });
                } catch(_) {}
            },
            //
            // Return a sorted list of cached faction bundle metadata for quick inspection
            listCachedFactionBundles: function() {
                try {
                    const cache = state.tornFactionData || {};
                    const out = Object.entries(cache).map(([id, entry]) => ({
                        factionId: String(id),
                        fetchedAtMs: Number(entry?.fetchedAtMs || 0) || 0,
                        selections: Array.isArray(entry?.selections) ? entry.selections.slice() : (entry?.selections ? [entry.selections] : []),
                        memberCount: (entry?.data && (entry.data.members || entry.data.member)) ? (Array.isArray(entry.data.members || entry.data.member) ? (entry.data.members || entry.data.member).length : Object.keys(entry.data.members || entry.data.member || {}).length) : 0
                    }));
                    out.sort((a,b)=> b.fetchedAtMs - a.fetchedAtMs);
                    return out;
                } catch (_) { return []; }
            },
            // List cached ranked war meta keys (for inspection)
            listCachedRankedWarMeta: function(prefix = 'tdm.rw_meta_') {
                try {
                    const out = [];
                    for (const k of Object.keys(localStorage || {})) {
                        if (!String(k || '').startsWith(String(prefix))) continue;
                        try {
                            const raw = localStorage.getItem(k);
                            const obj = raw ? JSON.parse(raw) : null;
                            out.push({ key: k, ts: Number(obj?.ts || 0) || 0, warId: obj?.warId || null, count: obj?.v && typeof obj.v === 'object' ? Object.keys(obj.v).length : 0 });
                        } catch(_) { out.push({ key: k, ts: 0 }); }
                    }
                    out.sort((a,b)=> b.ts - a.ts);
                    return out;
                } catch(_) { return []; }
            },
            // API-driven unified status builder (v2) – ignores Awoken, Dormant, Fallen, Federal for canonical events
            /*
            * Unified Status Record (buildUnifiedStatusV2 output)
            * ------------------------------------------------------------------
            * Fields:
            *   playerId?          (attached externally when stored)
            *   rawState           Original API status.state
            *   rawDescription     Original API status.description
            *   canonical          Canonical status: Okay | Travel | Returning | Abroad | Hospital | HospitalAbroad | Jail | SelfHosp | LostAttack | LostAttackAbroad
            *   activity           last_action.status or null
            *   dest               Canonical destination (key of _travelMap) when traveling
            *   plane              plane_image_type (e.g. light_aircraft, airliner_business, private_jet) or null
            *   startedMs          Inferred start time (ms) of travel (arrivalMs - duration or reused from previous record)
            *   arrivalMs          Expected arrival (ms) – from API until (sec) or inferred
            *   durationMins       Inferred duration from travel map and plane-specific matrix
            *   landedTornRecent   Within grace window just after landing in Torn
            *   landedAbroadRecent Within grace window after landing abroad
            *   landedGrace        Generic landed grace boolean
            *   issues             Validation anomalies array (see validateTravelRecord)
            *   generatedAt        Construction timestamp (ms)
            *
            * Events (emitStatusChangeV2):
            *   travel:start | travel:complete | travel:eta | travel:destination | travel:plane | travel:duration
            *   status:hospital | status:jail | activity:change
            *
            * Diagnostics:
            *   validateTravelRecord: no-destination | no-duration | duration-mismatch | unknown-destination
            *   Drift detection logs >90s deviation between stored vs recomputed arrival.
            */
            buildUnifiedStatusV2: function(member, prev) {
                try {
                    const normalized = utils.normalizeStatusInputForV2(member, { id: prev?.id });
                    const rawStatus = normalized?.status || {};
                    const rawState = rawStatus.state || '';
                    const rawDesc = rawStatus.description || '';
                    const rawUntil = rawStatus.until != null ? Number(rawStatus.until) || null : null; // epoch seconds
                    const plane = rawState === 'Traveling' ? (rawStatus.plane_image_type || null) : null;
                    const now = Date.now();
                    const prevRec = prev || null;
                    // Ignore states we don't track as canonical (treat as Okay baseline unless in travel grace)
                    // Do not ignore Fallen - it is a valid canonical state we want to display
                    const ignoreSet = new Set(['Awoken','Dormant','Federal']);
                    let canonical = 'Okay';
                    let dest = null;
                    let isreturn = false;
                    let startedMs = null;
                    let arrivalMs = null;
                    let durationMins = null;
                    // Legacy etaConfidence removed in favor of unified confidence ladder
                    let landedGrace = false;
                    const graceMs = 5*60*1000;
                    // Parse destination from description for traveling/abroad forms (simple regex then alias map)
                    const parseDestFromDesc = (desc) => {
                        if (!desc) return null;
                        // Return: 'Returning to Torn from X'
                        let m = desc.match(/^returning to torn from\s+([A-Za-z][A-Za-z \-]{1,40})/i);
                        if (m && m[1]) {
                            // Returning flight: dest is Torn, from is m[1]
                            return { from: m[1].trim(), to: 'Torn' };
                        }
                        // Outbound: 'Traveling to X', 'In X', etc.
                        m = /(travell?ing to|in)\s+([A-Za-z][A-Za-z \-]{1,40})/i.exec(desc);
                        if (m && m[2]) {
                            const raw = m[2].trim();
                            const mapped = utils.parseUnifiedDestination(raw) || null;
                            // from is Torn, to is mapped or raw
                            return { from: 'Torn', to: mapped || raw}
                        }
                        return null;
                    };
                    if (rawState === 'Traveling') {
                        // Determine outbound vs inbound (returning) by description. Inbound flights are now
                        // classified as canonical 'Returning' (previously always 'Travel'), so that the overlay
                        // and activity metrics reflect real-time returning counts instead of only recent landings.
                        let parsed = parseDestFromDesc(rawDesc);
                        const inbound = !!(parsed && parsed.to === 'Torn');
                        if (inbound) {
                            canonical = 'Returning';
                            // dest represents the origin we are coming FROM (parsed.from)
                            dest = utils.parseUnifiedDestination(parsed.from) || parsed.from || (prevRec && prevRec.dest);
                            isreturn = true;
                        } else {
                            canonical = 'Travel';
                            if (parsed && parsed.to && parsed.to !== 'Torn') {
                                dest = utils.parseUnifiedDestination(parsed.to) || parsed.to;
                            } else {
                                // Reuse previous destination if we remain in a traveling phase
                                dest = (prevRec && (prevRec.canonical === 'Travel' || prevRec.canonical === 'Returning') ? prevRec.dest : null);
                            }
                        }
                        if (dest) {
                            durationMins = utils.travelMinutesForPlane(dest, plane || 'light_aircraft') || utils.travelMinutesFor(dest) || null;
                            if (durationMins) {
                                // Reuse prior startedMs if still same dest/plane and previously traveling or returning
                                if (prevRec && (prevRec.canonical === 'Travel' || prevRec.canonical === 'Returning') && prevRec.dest === dest && prevRec.plane === plane && prevRec.startedMs) {
                                    startedMs = prevRec.startedMs;
                                } else {
                                    if (rawUntil && rawUntil > 0) {
                                        arrivalMs = rawUntil * 1000;
                                        startedMs = arrivalMs - (durationMins*60*1000);
                                    } else {
                                        startedMs = now;
                                    }
                                }
                                if (!arrivalMs) arrivalMs = startedMs + (durationMins*60*1000);
                            }
                        }
                    } else if (rawState === 'Abroad') {
                        canonical = 'Abroad';
                        const parsed = parseDestFromDesc(rawDesc);
                        dest = (parsed && parsed.to) || (prevRec && prevRec.dest ? prevRec.dest : null);
                        // If we recently landed (prev was Travel) adjust returning vs landed flags later
                        // Extra inference: simple 'In UAE' or 'In <alias>' forms sometimes miss due to short token
                        if (!dest && /^\s*in\s+([A-Za-z]{2,10})/i.test(rawDesc)) {
                            const token = rawDesc.replace(/^\s*in\s+/i,'').trim().replace(/^the\s+/i,'').split(/\s+/)[0];
                            const maybe = utils.parseUnifiedDestination(token);
                            if (maybe) dest = maybe;
                        }
                    } else if (rawState === 'Hospital') {
                        canonical = 'Hospital';
                        // Examine description + details (some sources embed attacker outcome text)
                        const descLower = (rawDesc || '').toLowerCase();
                        const detailsLower = (normalized?.status?.details || normalized?.status?.detail || member?.status?.details || '').toLowerCase();
                        // Self-inflicted hospitalization (using a medical item): starts with "Suffering from"
                        if (/^suffering\s+from\b/i.test(rawDesc || '') && !/^in\s+an?\s+/i.test(rawDesc || '')) {
                            canonical = 'SelfHosp';
                        } else if (/^lost\s+to\s+/i.test(detailsLower) && /^in\s+an?\s+/i.test(rawDesc || '')) {
                            // Lost a fight and is abroad (description begins with In a|an ... )
                            canonical = 'LostAttackAbroad';
                        } else if (/^lost\s+to\s+/i.test(detailsLower) && /^in\s+hospital\b/i.test(descLower)) {
                            // Lost a fight, standard Torn hospital phrasing
                            canonical = 'LostAttack';
                        } else {
                            // Legacy abroad detection only if previous record indicates non-Torn destination
                            if (prevRec && prevRec.dest && prevRec.dest !== 'Torn City') {
                                canonical = 'HospitalAbroad';
                                dest = prevRec.dest;
                            }
                        }
                    } else { canonical = rawState; }
                    // Returning / landed recent logic
                    // Landed subtype convenience flags (separate from canonical; consumer can inspect)
                    let landedTornRecent = false;
                    let landedAbroadRecent = false;
                    if (prevRec && (prevRec.canonical === 'Travel' || prevRec.canonical === 'Returning') && (canonical !== 'Travel' && canonical !== 'Returning')) {
                        const withinGrace = !prevRec.arrivalMs || (now - prevRec.arrivalMs) <= graceMs;
                        if (withinGrace) {
                            landedGrace = true;
                            if (!dest && prevRec.dest) dest = prevRec.dest;
                            const inbound = !!prevRec.isreturn || (prevRec.dest && /^torn\b/i.test(prevRec.dest));
                            if (inbound) {
                                landedTornRecent = true;
                                if (canonical === 'Okay' || canonical === 'Returning') {
                                    canonical = 'Returning';
                                }
                            } else {
                                landedAbroadRecent = true;
                            }
                        }
                    }
                    // Reconcile legacy landed Returning (post-arrival) without affecting in-flight returning.
                    // Only run if canonical Returning due to landing (isreturn false) not an active inbound flight.
                    if (!landedTornRecent && !landedAbroadRecent && canonical === 'Returning' && prevRec && !isreturn) {
                        if (prevRec.isreturn || (prevRec.dest && /^torn\b/i.test(prevRec.dest))) landedTornRecent = true;
                        else landedAbroadRecent = true;
                    }
                    const rec = {
                        id: normalized?.id ?? member?.id ?? member?.player_id ?? null,
                        rawState,
                        rawDescription: rawDesc || null,
                        rawUntil: rawUntil,
                        // Use API-provided plane when present. If API did not provide plane (null/undefined)
                        // and we have a previous record that contained an outbound plane (airliner/light_aircraft/etc),
                        // copy it forward. This preserves outbound plane info across brief transitions where Torn
                        // sometimes omits the plane on intermediate states (e.g., Returning/Abroad).
                        // Respect explicit API clears: only copy when API plane is null/undefined and prevRec has a non-empty plane.
                        plane: (plane != null ? plane : (prevRec && prevRec.plane ? prevRec.plane : null)),
                        canonical,
                        dest,
                        isreturn,
                        startedMs,
                        arrivalMs,
                        durationMins,
                        // etaConfidence removed
                        landedGrace,
                        landedTornRecent,
                        landedAbroadRecent,
                        activity: (normalized?.last_action?.status || normalized?.lastAction?.status || member?.last_action?.status || member?.lastAction?.status || '').trim(),
                        // Include member display name to make unified records more useful for UI consumers
                        name: (normalized?.name || member?.name || member?.username || member?.playername || null) || null,
                        updated: now,
                        sourceVersion: 2
                    };
                    // Maintain legacy aliases so downstream callers relying on phase continue to function,
                    // while canonical remains the single source of truth for status classification.
                    rec.phase = canonical;
                    rec.statusCanon = canonical;
                    const prevConf = prevRec?.confidence;
                    const rank = { LOW:1, MED:2, HIGH:3 };
                    if (prevConf && rank[prevConf]) {
                        rec.confidence = prevConf;
                    } else {
                        rec.confidence = 'LOW';
                    }
                    return rec;
                } catch(err) {
                    tdmlogger('error', '[StatusV2]', 'build error', err);
                    return null;
                }
            },
            emitStatusChangeV2: function(prev, next) {
                try {
                    if (!next || !next.id) return;
                    const tracked = new Set(['Travel','Returning','Abroad','Hospital','HospitalAbroad','Jail','Okay','SelfHosp','LostAttack','LostAttackAbroad']);
                    if (prev && !tracked.has(prev.canonical) && !tracked.has(next.canonical)) return; // both untracked
                    const changes = {};
                    if (!prev || prev.canonical !== next.canonical) changes.canonical = true;
                    if (!prev || prev.dest !== next.dest) changes.dest = true;
                    if (!prev || prev.plane !== next.plane) changes.plane = true;
                    if (!prev || prev.arrivalMs !== next.arrivalMs) changes.arrivalMs = true;
                    if (!prev || prev.durationMins !== next.durationMins) changes.durationMins = true;
                    if (!prev || prev.activity !== next.activity) changes.activity = true;
                    if (Object.keys(changes).length === 0) return; // no material delta
                    const types = [];
                    if (changes.canonical && next.canonical === 'Travel') types.push('travel:start');
                    if (prev && prev.canonical === 'Travel' && next.canonical !== 'Travel') types.push('travel:complete');
                    if (changes.dest) types.push('travel:destination');
                    if (changes.plane) types.push('travel:plane');
                    if (changes.arrivalMs) types.push('travel:eta');
                    if (changes.durationMins) types.push('travel:duration');
                    if (changes.canonical && /Hospital/.test(next.canonical)) types.push('status:hospital');
                    if (changes.canonical && next.canonical === 'Jail') types.push('status:jail');
                    if (changes.activity) types.push('activity:change');
                    const evt = { id: next.id, ts: Date.now(), prev, next, changes, types };
                    if (state?.debug?.statusFlow) tdmlogger('info', '[StatusV2][Change]', evt);
                    state._recentStatusEvents = state._recentStatusEvents || [];
                    state._recentStatusEvents.push(evt);
                    if (state._recentStatusEvents.length > 200) state._recentStatusEvents.shift();
                    // Diagnostics: arrival mismatch (inferred vs recomputed) – simple tolerance check
                    if (next.canonical === 'Travel' && next.startedMs && next.arrivalMs && next.durationMins) {
                        const expectedArrival = next.startedMs + next.durationMins*60*1000;
                        const driftMs = Math.abs(expectedArrival - next.arrivalMs);
                        if (driftMs > 90000) {
                            tdmlogger('warn', '[Travel][Drift]', { id: next.id, driftMs, expectedArrival, arrivalMs: next.arrivalMs, durationMins: next.durationMins });
                        }
                    }
                    // Evaluate pre-arrival alerts when travel state/time changes
                    if (types.some(t => t.startsWith('travel:'))) {
                        try { utils._evaluateArrivalAlerts(); } catch(_) { /* ignore */ }
                    }
                } catch(err) { /* swallow */ }
            },
            validateTravelRecord: function(rec) {
                try {
                    if (!rec || rec.canonical !== 'Travel') return null;
                    const issues = [];
                    if (!rec.dest) issues.push('no-destination');
                    if (!rec.durationMins) issues.push('no-duration');
                    if (rec.startedMs && rec.arrivalMs) {
                        const calcDur = (rec.arrivalMs - rec.startedMs) / 60000;
                        if (rec.durationMins && Math.abs(calcDur - rec.durationMins) > 3) issues.push('duration-mismatch');
                    }
                    if (rec.dest && !utils._travelMap[rec.dest]) issues.push('unknown-destination');
                    if (issues.length) {
                        tdmlogger('debug', '[Travel][Validate]', { id: rec.id, issues, rec });
                    }
                    return issues;
                } catch(e) { return null; }
            },
            // ---- Unified Status & Travel Maps (consolidated) END ----
        // Short relative time from unix seconds; stable across devices given same timestamp
        formatAgoShort: (unixSeconds) => {
            try {
                const ts = Number(unixSeconds || 0);
                if (!Number.isFinite(ts) || ts <= 0) return '';
                const diff = Math.max(0, Math.floor(Date.now() / 1000) - ts);
                if (diff < 60) return 'now';
                if (diff < 3600) return `${Math.floor(diff / 60)}m`;
                if (diff < 86400) return `${Math.floor(diff / 3600)}h`;
                return `${Math.floor(diff / 86400)}d`;
            } catch(_) { return ''; }
        },
        // Full relative time with days/hours/minutes/seconds from unix seconds
        formatAgoFull: (unixSeconds) => {
            try {
                const ts = Number(unixSeconds || 0);
                if (!Number.isFinite(ts) || ts <= 0) return '';
                let diff = Math.max(0, Math.floor(Date.now() / 1000) - ts);
                const d = Math.floor(diff / 86400); diff -= d * 86400;
                const h = Math.floor(diff / 3600); diff -= h * 3600;
                const m = Math.floor(diff / 60); diff -= m * 60;
                const s = diff;
                const parts = [];
                if (d > 0) parts.push(`${d} ${d === 1 ? 'day' : 'days'}`);
                if (h > 0 || d > 0) parts.push(`${h} ${h === 1 ? 'hour' : 'hours'}`);
                if (m > 0 || h > 0 || d > 0) parts.push(`${m} ${m === 1 ? 'minute' : 'minutes'}`);
                parts.push(`${s} ${s === 1 ? 'second' : 'seconds'}`);
                return parts.join(' ') + ' ago';
            } catch(_) { return ''; }
        },
        // Central Torn user normalization (legacy or new schema)
        normalizeTornUser: (raw) => {
            try {
                if (!raw || typeof raw !== 'object') return null;
                const profile = raw.profile || {};
                const faction = raw.faction || {};
                const id = raw.player_id || profile.id || profile.player_id || raw.id || null;
                if (!id) return null;
                return {
                    id: String(id),
                    name: raw.name || profile.name || '',
                    factionId: faction.faction_id || faction.id || profile.faction_id || null,
                    position: faction.position || null,
                    status: profile.status || raw.status || null,
                    last_action: profile.last_action || raw.last_action || null,
                    faction: faction.id ? { id: faction.faction_id || faction.id, name: faction.name, tag: faction.tag } : null
                };
            } catch(_) { return null; }
        },
        debounce: (func, delay) => {
            let timeout;
            return function(...args) {
                const context = this;
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(context, args), delay);
            };
        },
        // --- Runtime resource registry helpers (timers, observers, listeners) ---
        registerInterval: (id) => {
            try {
                if (!id) return id;
                state._resources.intervals.add(id);
            } catch(_) {}
            return id;
        },
        unregisterInterval: (id) => {
            try {
                if (!id) return;
                try { clearInterval(id); } catch(_) {}
                state._resources.intervals.delete(id);
            } catch(_) {}
        },
        registerTimeout: (id) => {
            try {
                if (!id) return id;
                state._resources.timeouts.add(id);
            } catch(_) {}
            return id;
        },
        unregisterTimeout: (id) => {
            try {
                if (!id) return;
                try { clearTimeout(id); } catch(_) {}
                state._resources.timeouts.delete(id);
            } catch(_) {}
        },
        registerObserver: (obs) => {
            try { if (!obs) return obs; state._resources.observers.add(obs); } catch(_) {}
            return obs;
        },
        unregisterObserver: (obs) => {
            try { if (!obs) return; try { obs.disconnect(); } catch(_) {} state._resources.observers.delete(obs); } catch(_) {}
        },
        registerWindowListener: (type, handler, opts) => {
            try {
                if (!type || typeof handler !== 'function') return null;
                window.addEventListener(type, handler, opts);
                state._resources.windowListeners.push({ type, handler, opts });
                return handler;
            } catch (_) { return null; }
        },
        unregisterAllWindowListeners: () => {
            try {
                for (const l of (state._resources.windowListeners || [])) {
                    try { window.removeEventListener(l.type, l.handler, l.opts); } catch(_) {}
                }
                state._resources.windowListeners = [];
            } catch(_) {}
        },
        unregisterWindowListener: (type, handler, opts) => {
            try {
                if (!type || typeof handler !== 'function') return;
                window.removeEventListener(type, handler, opts);
                const arr = state._resources.windowListeners || [];
                state._resources.windowListeners = arr.filter(l => !(l.type === type && l.handler === handler));
            } catch(_) {}
        },
        // Attach handler on an element while recording it on the element to avoid building
        // a strong global registry that would keep DOM nodes alive. `cleanupElementHandlers`
        // will remove handlers when the element is taken down.
        addElementHandler: (el, event, handler, opts) => {
            try {
                if (!el || typeof handler !== 'function') return;
                el.addEventListener(event, handler, opts);
                try { if (!el._tdmHandlers) el._tdmHandlers = []; el._tdmHandlers.push({ event, handler, opts }); el.dataset && (el.dataset.tdmHandled = '1'); } catch(_) {}
            } catch(_) {}
        },
        cleanupElementHandlers: (el) => {
            try {
                if (!el || !el._tdmHandlers) return;
                for (const h of el._tdmHandlers) {
                    try { el.removeEventListener(h.event, h.handler, h.opts); } catch(_) {}
                }
                try { el._tdmHandlers = []; } catch(_) {}
            } catch(_) {}
        },
        // Walk known resource collections and clean everything up. This is a best-effort
        // attempt to release long-lived references on page unload / hard reset.
        cleanupAllResources: () => {
            try {
                // intervals
                for (const id of Array.from(state._resources.intervals || [])) {
                    try { clearInterval(id); } catch(_) {}
                }
                state._resources.intervals.clear();
                // timeouts
                for (const id of Array.from(state._resources.timeouts || [])) {
                    try { clearTimeout(id); } catch(_) {}
                }
                state._resources.timeouts.clear();
                // observers
                for (const obs of Array.from(state._resources.observers || [])) {
                    try { obs.disconnect(); } catch(_) {}
                }
                state._resources.observers.clear();
                // window listeners
                try { utils.unregisterAllWindowListeners(); } catch(_) {}
                // best-effort: clear per-element handlers where possible (search for common containers)
                try {
                    const els = document.querySelectorAll('[data-tdm-handled], #tdm-settings-popup, #tdm-attack-container');
                    els && els.forEach(el => { try { utils.cleanupElementHandlers(el); } catch(_) {} });
                } catch(_) {}
            } catch(e) { try { tdmlogger('warn', '[cleanupAllResources] error', e); } catch(_) {} }
        },
        // TODO Review this debugger
        // Runtime diagnostics: lightweight snapshot of tracked resources and cache sizes
        debugResources: (opts = {}) => {
            try {
                const out = {
                    intervals: state._resources.intervals ? state._resources.intervals.size : 0,
                    timeouts: state._resources.timeouts ? state._resources.timeouts.size : 0,
                    observers: state._resources.observers ? state._resources.observers.size : 0,
                    windowListeners: state._resources.windowListeners ? state._resources.windowListeners.length : 0,
                    ui: {
                        apiCadenceInfoIntervalId: !!state.ui?.apiCadenceInfoIntervalId,
                        noteSnapshotInterval: !!state.ui?._noteSnapshotInterval,
                    },
                    script: {
                        mainRefreshIntervalId: !!state.script?.mainRefreshIntervalId,
                        fetchWatchdogIntervalId: !!state.script?.fetchWatchdogIntervalId,
                        factionBundleRefreshIntervalId: !!state.script?.factionBundleRefreshIntervalId,
                        activityTimeoutId: !!state.script?.activityTimeoutId,
                    },
                    caches: {
                        scoreBumpTimers: (state._scoreBumpTimers && typeof state._scoreBumpTimers === 'object') ? Object.keys(state._scoreBumpTimers).length : 0,
                        phaseHistoryWriteTimers: (handlers && handlers._phaseHistoryWriteTimers && typeof handlers._phaseHistoryWriteTimers === 'object') ? Object.keys(handlers._phaseHistoryWriteTimers).length : 0,
                        pendingSets: (ui && ui._kv && ui._kv._pendingSets) ? ui._kv._pendingSets.size : (typeof state._pendingSets === 'object' && state._pendingSets?.size ? state._pendingSets.size : 0)
                    }
                };
                if (opts.log !== false) {
                    try { console.info('TDM resources:', out); } catch(_) {}
                }
                return out;
            } catch(_) { return null; }
        },
        exposeDebugToWindow: () => {
            try { window.__TDM_DEBUG = window.__TDM_DEBUG || {}; window.__TDM_DEBUG.resources = utils.debugResources; } catch(_) {}
        },
        // Fingerprint helpers (canonical) for dibs & medDeals content gating
        computeDibsFingerprint: (arr) => {
            try {
                const cur = Array.isArray(arr) ? arr : [];
                const stable = cur.map(d => ({ o:d.opponentId, u:d.userId, a:!!d.dibsActive, t:d.updatedAt||d.updated||0 }))
                    .sort((a,b)=> (a.o - b.o) || String(a.u).localeCompare(String(b.u)));
                const json = JSON.stringify(stable);
                // FNV-1a 32-bit
                let h = 0x811c9dc5;
                for (let i=0;i<json.length;i++){ h^=json.charCodeAt(i); h = (h>>>0)*0x01000193; }
                return 'd:' + (h>>>0).toString(36);
            } catch(_) { return null; }
        },
        computeMedDealsFingerprint: (map) => {
            try {
                const cur = (map && typeof map === 'object') ? map : {};
                const stableEntries = Object.entries(cur).map(([id,v]) => [id, !!v?.isMedDeal, v?.medDealForUserId||v?.forUserId||null, (v?.updatedAt?._seconds||v?.updatedAt||0)]);
                stableEntries.sort((a,b)=> String(a[0]).localeCompare(String(b[0])));
                const json = JSON.stringify(stableEntries);
                let h = 0x811c9dc5;
                for (let i=0;i<json.length;i++){ h^=json.charCodeAt(i); h = (h>>>0)*0x01000193; }
                return 'm:' + (h>>>0).toString(36);
            } catch(_) { return null; }
        },
        // Apply status color classes to an element based on a member.last_action.status
        // Returns true if status applied, false if no status info present or on error.
        addLastActionStatusColor: (el, member) => {
            try {
                if (!el || !member || !member.last_action || !member.last_action.status) {
                    // remove any previous status classes to avoid stale coloring
                    try { el.classList && el.classList.remove && el.classList.remove('tdm-la-online','tdm-la-idle','tdm-la-offline'); } catch(_) {}
                    return false;
                }
                const statusText = String(member.last_action.status || '').toLowerCase();
                // normalize classes
                try { el.classList.remove('tdm-la-online','tdm-la-idle','tdm-la-offline'); } catch(_) {}
                if (statusText === 'online') el.classList.add('tdm-la-online');
                else if (statusText === 'idle') el.classList.add('tdm-la-idle');
                else el.classList.add('tdm-la-offline');
                return true;
            } catch(_) { return false; }
        },
        // Score formatting & anti-flicker helpers
        formatScore: (value, scoreType) => {
            if (value == null || isNaN(value)) return '0';
            if (String(scoreType||'').toLowerCase().includes('r')) {
                // r for respect, r, rnc, rnb
                const fixed = Number(value).toFixed(2);
                return fixed.replace(/\.00$/, '').replace(/(\.\d)0$/, '$1');
            }
            return String(value);
        },
        formatBattleStats: (rawValue) => formatBattleStatsValue(rawValue),
        normalizeTimestampMs: (value) => normalizeTimestampMs(value),
        isFeatureEnabled: (path) => {
            try { return featureFlagController.isEnabled(path); } catch(_) { return false; }
        },
        setFeatureFlag: (path, value, opts) => {
            try { return featureFlagController.set(path, value, opts); } catch(_) { return false; }
        },
        readFFScouter: (playerId, opts = {}) => {
            try {
                if (!playerId) return null;
                // Allow if flag is enabled OR if a key is present (implicit enable)
                const hasKey = !!storage.get('ffscouterApiKey', null);
                if (opts.ignoreFeatureFlag !== true && !featureFlagController.isEnabled('rankWarEnhancements.adapters') && !hasKey) return null;
                const sid = String(playerId).trim();
                if (!sid) return null;
                const memo = adapterMemoController.get('ff');
                if (memo.has(sid)) return memo.get(sid);
                let record = null;
                if (state.ffscouterCache && typeof state.ffscouterCache === 'object' && state.ffscouterCache[sid]) {
                    record = state.ffscouterCache[sid];
                }
                if (!record) {
                    try {
                        const raw = localStorage.getItem('ffscouterv2-' + sid);
                        if (raw) {
                            record = JSON.parse(raw);
                            if (record) {
                                state.ffscouterCache = state.ffscouterCache || {};
                                state.ffscouterCache[sid] = record;
                            }
                        }
                    } catch(_) {}
                }
                if (!record) {
                    memo.set(sid, null);
                    recordAdapterMetric('ff', 'miss');
                    return null;
                }
                const normalized = normalizeFfRecord(record, sid);
                memo.set(sid, normalized);
                recordAdapterMetric('ff', normalized ? 'hit' : 'miss');
                return normalized;
            } catch (err) {
                recordAdapterMetric('ff', 'error', err?.message || 'ffscouter-read-failed');
                return null;
            }
        },
        readBSP: (playerId, opts = {}) => {
            try {
                if (!playerId) return null;
                if (opts.ignoreFeatureFlag !== true && !featureFlagController.isEnabled('rankWarEnhancements.adapters')) return null;
                const sid = String(playerId).trim();
                if (!sid) return null;
                if (opts.skipPageFlag !== true) {
                    const flagRaw = (readLocalStorageRaw(BSP_ENABLED_STORAGE_KEY) || '').toString().toLowerCase();
                    if (flagRaw !== 'true') return null;
                }
                const memo = adapterMemoController.get('bsp');
                if (memo.has(sid)) return memo.get(sid);
                const rawString = readLocalStorageRaw(`tdup.battleStatsPredictor.cache.prediction.${sid}`);
                if (!rawString) {
                    memo.set(sid, null);
                    recordAdapterMetric('bsp', 'miss');
                    return null;
                }
                const parsed = parseJsonSafe(rawString);
                if (!parsed) {
                    memo.set(sid, null);
                    recordAdapterMetric('bsp', 'error', 'json-parse');
                    return null;
                }
                const normalized = normalizeBspRecord(parsed, sid);
                memo.set(sid, normalized);
                recordAdapterMetric('bsp', normalized ? 'hit' : 'miss');
                return normalized;
            } catch (err) {
                recordAdapterMetric('bsp', 'error', err?.message || 'bsp-read-failed');
                return null;
            }
        },
        // Fetch ranked war summary rows (snapshot of per-attacker aggregates) with a tiny in-memory TTL cache
        getSummaryRowsCached: async (warId, factionId) => {
            try {
                if (!warId) return [];
                const ttlMs = 1500; // prevent double network within a burst of UI refreshes
                const cache = state._summaryCache || (state._summaryCache = {});
                const entry = cache[warId];
                const now = Date.now();
                if (entry && (now - entry.fetchedAt) < ttlMs && Array.isArray(entry.rows)) {
                    return entry.rows;
                }
                // Prefer local/storage -> server chain (single helper already implements that)
                let rows = [];
                try {
                    rows = await api.getRankedWarSummaryPreferLocal(warId, factionId);
                } catch(_) { /* ignore */ }
                if (!Array.isArray(rows)) rows = [];
                cache[warId] = { rows, fetchedAt: now };
                return rows;
            } catch(_) { return []; }
        },
        computeScoreFromRow: (row, scoreType) => {
            if (!row) return 0;
            if (scoreType === 'Respect') return Number(row.totalRespectGain || 0);
            if (scoreType === 'Respect (no chain)') return Number(row.totalRespectGainNoChain || 0);
            if (scoreType === 'Respect (no bonus)') return Number(row.totalRespectGainNoBonus || 0);
            // For 'Attacks' score-type we now count only "successful" attacks (see totalAttacksSuccessful)
            return Number(row.totalAttacksSuccessful ?? row.totalAttacks ?? 0);
        },
        scores: {
            shouldUpdate(prevRaw, nextRaw) {
                if (prevRaw == null) return true;
                const diff = Math.abs(Number(prevRaw) - Number(nextRaw));
                return diff >= 0.005; // ignore micro jitter under half a hundredth
            }
        },
        // TODO Review all calls and verify accuracy
        incrementApiCalls: (n = 1) => {
            try {
                state.session.apiCalls = (state.session.apiCalls || 0) + (Number(n) || 0);
                tdmlogger('debug', '[API calls]', [state.session.apiCalls, state.session.apiCallsClient, n]);
                try { sessionStorage.setItem('tdm.api_calls', String(state.session.apiCalls)); } catch(_) {}
                if (handlers?.debouncedUpdateApiUsageBadge) {
                    handlers.debouncedUpdateApiUsageBadge();
                } else if (ui && typeof ui.updateApiUsageBadge === 'function') {
                    ui.updateApiUsageBadge();
                }
            } catch (_) { /* noop */ }
        },
        incrementClientApiCalls: (n = 1) => {
            try {
                const add = Number(n) || 0;
                state.session.apiCallsClient = (state.session.apiCallsClient || 0) + add;
                try { sessionStorage.setItem('tdm.api_calls_client', String(state.session.apiCallsClient)); } catch(_) {}
                utils.incrementApiCalls(add);
            } catch(_) { /* noop */ }
        },
        incrementBackendApiCalls: (n = 1) => {
            try {
                const add = Number(n) || 0;
                state.session.apiCallsBackend = (state.session.apiCallsBackend || 0) + add;
                try { sessionStorage.setItem('tdm.api_calls_backend', String(state.session.apiCallsBackend)); } catch(_) {}
                utils.incrementApiCalls(add);
            } catch(_) { /* noop */ }
        },
        // Semantic version comparison: returns -1 if a<b, 1 if a>b, 0 equal.
        // Accepts versions like "1.2.3", "1.2", "1.2.3-beta" (suffix ignored for ordering among base numbers).
        compareVersions: (a, b) => {
            try {
                if (a === b) return 0;
                const sanitize = (v) => String(v || '0')
                    .trim()
                    .replace(/[^0-9.]/g, '') // drop non-numeric qualifiers
                    .replace(/\.\.+/g, '.');
                const pa = sanitize(a).split('.').map(x => parseInt(x, 10) || 0);
                const pb = sanitize(b).split('.').map(x => parseInt(x, 10) || 0);
                const len = Math.max(pa.length, pb.length);
                for (let i = 0; i < len; i++) {
                    const na = pa[i] ?? 0;
                    const nb = pb[i] ?? 0;
                    if (na > nb) return 1;
                    if (na < nb) return -1;
                }
                return 0;
            } catch(_) { return 0; }
        },
        createElement: (tag, attributes = {}, children = []) => {
            const element = document.createElement(tag);
            Object.entries(attributes).forEach(([key, value]) => {
                if (key === 'style' && typeof value === 'object') Object.assign(element.style, value);
                else if (key === 'className' || key === 'class') element.className = value;
                else if (key === 'innerHTML') element.innerHTML = value;
                else if (key === 'textContent') element.textContent = value;
                else if (key === 'onclick') utils.addElementHandler(element, 'click', value);
                else if (key.startsWith('on') && typeof value === 'function') utils.addElementHandler(element, key.substring(2).toLowerCase(), value);
                else if (key === 'dataset' && typeof value === 'object') Object.entries(value).forEach(([dataKey, dataValue]) => element.dataset[dataKey] = dataValue);
                else element.setAttribute(key, value);
            });
            children.forEach(child => {
                if (typeof child === 'string') element.appendChild(document.createTextNode(child));
                else if (child instanceof Node) element.appendChild(child);
            });
            return element;
        },
        buildProfileLink: (id, name, extra = {}) => {
            try {
                const a = document.createElement('a');
                a.href = `/profiles.php?XID=${id}`;
                const display = utils.sanitizePlayerName(name, id);
                a.textContent = display || String(id);
                const cls = ['t-blue'];
                if (extra.className) cls.push(extra.className);
                a.className = cls.join(' ');
                if (extra.title) a.title = extra.title;
                return a;
            } catch(_) {
                const fallback = document.createElement('span');
                fallback.textContent = utils.sanitizePlayerName(name, id) || String(id);
                return fallback;
            }
        },
        // Normalize names read from the page or external sources.
        // - trims whitespace
        // - strips trailing separator hyphens like ' -' or '-'
        // - treats purely-empty or dash-only names as missing and returns a sensible fallback
        // If opponentId is provided we'll return `Opponent ID (<id>)` for missing names.
        // TODO is this whats causing issue with BSP names in links?
        sanitizePlayerName: (rawName, opponentId = null, { fallbackPrefix = 'Opponent ID' } = {}) => {
            try {
                let n = String(rawName ?? '').trim();
                // If name is empty or a single separator, return fallback
                if (!n || /^-+$/i.test(n)) {
                    return opponentId ? `${fallbackPrefix} (${opponentId})` : '';
                }
                // Remove a trailing hyphen separator(s) plus whitespace, e.g. 'Alice -' or 'Alice - '
                n = n.replace(/[\s\-]+$/g, '').trim();
                // If removing left us empty, fallback
                if (!n) return opponentId ? `${fallbackPrefix} (${opponentId})` : '';
                return n;
            } catch (_) { return opponentId ? `${fallbackPrefix} (${opponentId})` : (rawName || ''); }
        },
        getWarById: (warId) => {
            try {
                const id = String(warId);
                const arr = Array.isArray(state.rankWars) ? state.rankWars : (Array.isArray(state.rankWars?.rankedwars) ? state.rankWars.rankedwars : []);
                return Array.isArray(arr) ? arr.find(w => String(w?.id) === id) : null;
            } catch(_) { return null; }
        },
        isWarActive: (warId) => {
            try {
                const w = utils.getWarById(warId) || state.lastRankWar;
                const now = Math.floor(Date.now() / 1000);
                const start = Number(w?.start || w?.startTime || 0);
                const end = Number(w?.end || w?.endTime || 0);
                let active = false;
                if (!w) {
                    tdmlogger('debug', '[isWarActive] No war found for warId', warId);
                } else if (start && !end) {
                    active = now >= start;
                } else if (start && end) {
                    active = now >= start && now <= end;
                }
                return active;
            } catch(e) {
                tdmlogger('warn', '[isWarActive] Exception', e);
                return false;
            }
        },
        // Active or within grace-hours after end
        isWarInActiveOrGrace: (warId, graceHours = 6) => {
            try {
                const w = utils.getWarById(warId) || state.lastRankWar;
                if (!w) return false;
                const now = Math.floor(Date.now() / 1000);
                const start = Number(w.start || w.startTime || 0);
                const end = Number(w.end || w.endTime || 0);
                if (!start) return false; // no start -> not active
                if (now < start) return false; // scheduled in future
                if (!end) return true;    // active (no end yet)
                return now <= end + (graceHours * 3600); // within grace window
            } catch(_) { return false; }
        },
        // Returns status + text for diagnostics and fallback logic
        httpGetDetailed: (url) => {
            return new Promise((resolve) => {
                try {
                    if (state?.gm?.rD_xmlhttpRequest) {
                        const ret = state.gm.rD_xmlhttpRequest({
                            method: 'GET', url,
                            onload: r => resolve({ status: Number(r.status || 0), text: typeof r.responseText === 'string' ? r.responseText : '' }),
                            onerror: (e) => resolve({ status: 0, text: '' })
                        });
                        if (ret && typeof ret.catch === 'function') ret.catch(() => {});
                        return;
                    }
                } catch (_) { /* ignore and try fetch */ }
                // Fallback to fetch
                fetch(url).then(async (res) => {
                    const text = await res.text().catch(() => '');
                    resolve({ status: Number(res.status || 0), text });
                }).catch(() => resolve({ status: 0, text: '' }));
            });
        },
        perf: {
            timers: {},
            last: {},
            thresholdMs: 1000,
            start: function(name) {
                this.timers[name] = performance.now();
            },
            stop: function(name) {
                if (this.timers[name]) {
                    const end = performance.now();
                    const start = this.timers[name];
                    delete this.timers[name];
                    const elapsed = end - start;
                    this.last[name] = elapsed;
                    const threshold = typeof this.thresholdMs === 'number' ? this.thresholdMs : 200;
                    if (elapsed >= threshold) {
                        tdmlogger('info', '[TDM Perf]', `${name} took ${elapsed.toFixed(2)} ms`);
                    }
                }
            },
            getLast: function(name) {
                return this.last && typeof this.last[name] === 'number' ? this.last[name] : 0;
            }
        },
        isCollectionChanged: (clientTimestamps, masterTimestamps, collectionKey) => {
            // Fast skip if backend no longer exposes this collection
            const masterTs = masterTimestamps?.[collectionKey];
            if (!masterTs) return false;
            const clientTs = clientTimestamps?.[collectionKey];
            if (!clientTs) return true;
            // Compare Firestore Timestamp objects
            const masterMillis = masterTs._seconds ? masterTs._seconds * 1000 : (masterTs.toMillis ? masterTs.toMillis() : 0);
            const clientMillis = clientTs._seconds ? clientTs._seconds * 1000 : (clientTs.toMillis ? clientTs.toMillis() : 0);
            const isChanged = masterMillis > clientMillis;
            if (isChanged) {
                tdmlogger('info', '[Collection]', {changed: isChanged, master: masterMillis, client: clientMillis});
            }
            return isChanged;
        },
        getVisibleOpponentIds: () => {
            const ids = new Set();
            try {
                // Ranked war tables
                document.querySelectorAll('.tab-menu-cont .members-list > li a[href*="profiles.php?XID="], .tab-menu-cont .members-cont > li a[href*="profiles.php?XID="]').forEach(a => {
                    const m = a.href.match(/XID=(\d+)/);
                    if (m) ids.add(m[1]);
                });
                // Faction page list
                document.querySelectorAll('.f-war-list .table-body a[href*="profiles.php?XID="]').forEach(a => {
                    const m = a.href.match(/XID=(\d+)/);
                    if (m) ids.add(m[1]);
                });
                // Attack page current opponent
                const attackId = new URLSearchParams(window.location.search).get('user2ID');
                if (attackId) ids.add(String(attackId));
            } catch (_) { /* ignore */ }
            return Array.from(ids);
        },
        getClientNoteTimestamps: () => {
            const map = {};
            try {
                for (const [id, note] of Object.entries(state.userNotes || {})) {
                    const ts = note?.lastEdited?._seconds ? note.lastEdited._seconds * 1000 : (note?.lastEdited?.toMillis ? note.lastEdited.toMillis() : (note?.lastEdited ? new Date(note.lastEdited).getTime() : 0));
                    map[id] = ts || 0;
                }
            } catch (_) { /* ignore */ }
            return map;
        },
        // canonicalizeStatus removed. Use buildUnifiedStatusV2 for canonical status records.
        getDibsStyleOptions: () => {
            const fs = (state.script && state.script.factionSettings) || {};
            const dibs = (fs.options && fs.options.dibsStyle) || {};
            const defaultStatuses = { Okay: true, Hospital: true, Travel: false, Abroad: false, Jail: false };
            const defaultLastAction = { Online: true, Idle: true, Offline: true };
                return {
                keepTillInactive: dibs.keepTillInactive !== false,
                mustRedibAfterSuccess: !!dibs.mustRedibAfterSuccess,
                inactivityTimeoutSeconds: parseInt(dibs.inactivityTimeoutSeconds || 300),
                // New: if > 0, only allow dibbing Hospital opponents when release time < N minutes
                maxHospitalReleaseMinutes: Number.isFinite(Number(dibs.maxHospitalReleaseMinutes)) ? Number(dibs.maxHospitalReleaseMinutes) : 0,
                // Opponent status allowance
                allowStatuses: { ...defaultStatuses, ...(dibs.allowStatuses || {}) },
                // Opponent activity allowance (last_action.status)
                allowLastActionStatuses: { ...defaultLastAction, ...(dibs.allowLastActionStatuses || {}) },
                // User status allowance
                allowedUserStatuses: { ...defaultStatuses, ...(dibs.allowedUserStatuses || {}) },
                // Opponent travel removal
                removeOnFly: !!dibs.removeOnFly,
                // User travel removal
                removeWhenUserTravels: !!dibs.removeWhenUserTravels,
                    // Admin option: bypass dibs style enforcement (prevents automated cleanup/enforcement)
                    bypassDibStyle: !!dibs.bypassDibStyle,
            };
        },
        /**
         * Centralized dibs button state updater.
         * Handles suppression, active dibs (yours vs others), policy gating, and async opponent status policy check (optional).
         * @param {HTMLButtonElement} btn
         * @param {string|number} opponentId
         * @param {string} opponentName
         * @param {Object} opts
         * @param {boolean} [opts.opponentPolicyCheck=false] If true, performs async opponent status allowance check (attack page usage)
         */
        updateDibsButton: (btn, opponentId, opponentName, opts = {}) => {
            try {
                btn.setAttribute('data-opponent-id', opponentId);
                if (opponentName) btn.setAttribute('data-opponent-name', opponentName);
            } catch(_) {}
            const suppress = state.needsSuppression && state.needsSuppression[opponentId];
            const activeDibs = Array.isArray(state.dibsData) ? state.dibsData.find(d => d && String(d.opponentId) === String(opponentId) && d.dibsActive) : null;
            const canAdmin = state.script?.canAdministerMedDeals && storage.get('adminFunctionality', true) === true;

            // Suppression overrides everything (still clickable to show warning)
            if (suppress) {
                const warn = `Don't dib - Suppress now!`;
                const cls = 'btn dibs-btn btn-dibs-disabled';
                if (btn.textContent !== warn) btn.textContent = warn;
                if (btn.className !== cls) btn.className = cls;
                if (btn.disabled !== false) btn.disabled = false;
                btn.onclick = () => ui.showMessageBox(warn, 'warning', 4000);
                return;
            }

            if (activeDibs) {
                const mine = activeDibs.userId === state.user.tornId;
                const txt = mine ? 'YOU Dibbed' : activeDibs.username;
                const cls = 'btn dibs-btn ' + (mine ? 'btn-dibs-success-you' : 'btn-dibs-success-other');
                if (btn.textContent !== txt) btn.textContent = txt;
                if (btn.className !== cls) btn.className = cls;
                const dis = !(mine || canAdmin);
                if (btn.disabled !== dis) btn.disabled = dis;
                // Use a safe resolver in case debounced handlers haven't been initialized yet
                const removeHandler = (typeof handlers.debouncedRemoveDibsForTarget === 'function')
                    ? handlers.debouncedRemoveDibsForTarget
                    : (typeof handlers.removeDibsForTarget === 'function')
                        ? handlers.removeDibsForTarget
                        : null;
                if (removeHandler) btn.onclick = (e) => removeHandler(opponentId, e.currentTarget);
                else btn.onclick = (e) => { ui.showMessageBox('Handler unavailable. Please reload the page.', 'warning', 3000); };
                return;
            }

            // Inactive baseline
            let cls = 'btn dibs-btn btn-dibs-inactive';
            let disabled = false;
            const styleOpts = utils.getDibsStyleOptions();
            const myCanon = utils.getMyCanonicalStatus();
            const policyUserDisabled = styleOpts?.allowedUserStatuses && styleOpts.allowedUserStatuses[myCanon] === false;
            if (policyUserDisabled) {
                cls = 'btn dibs-btn btn-dibs-disabled';
                disabled = false; // remain interactive for message
                const msg = `Disabled by policy: Your status (${myCanon})`;
                btn.title = msg;
                btn.onclick = () => ui.showMessageBox(msg, 'warning', 4000);
            } else {
                // Resolve the effective dibs handler safely — debounced variants may be initialized later
                const effectiveDibsHandler = (typeof handlers.debouncedDibsTarget === 'function')
                    ? handlers.debouncedDibsTarget
                    : (typeof handlers.dibsTarget === 'function')
                        ? handlers.dibsTarget
                        : null;
                if (canAdmin) {
                    btn.onclick = (e) => ui.openDibsSetterModal(opponentId, opponentName, e.currentTarget);
                } else if (effectiveDibsHandler) {
                    btn.onclick = (e) => effectiveDibsHandler(opponentId, opponentName, e.currentTarget);
                } else {
                    btn.onclick = (e) => { ui.showMessageBox('Handler unavailable. Please reload the page.', 'warning', 3000); };
                }
            }

            if (btn.textContent !== 'Dibs') btn.textContent = 'Dibs';
            if (btn.className !== cls) btn.className = cls;
            if (btn.disabled !== disabled) btn.disabled = disabled;

            // Optional opponent policy gating (async) - only when initial baseline active (not suppressed/active dibs)
            if (opts.opponentPolicyCheck) {
                (async () => {
                    try {
                        const style = utils.getDibsStyleOptions();
                        // Fast path: if no allowStatuses overrides, skip
                        if (!style || !style.allowStatuses) return;
                        let canonOpp = null;
                        // Prefer cached faction data
                        const tf = state.tornFactionData || {};
                        const oppFactionId = state?.lastOpponentFactionId || state?.warData?.opponentId;
                        const cachedOpp = oppFactionId ? tf[oppFactionId]?.data : null;
                        const members = cachedOpp?.members ? (Array.isArray(cachedOpp.members) ? cachedOpp.members : Object.values(cachedOpp.members)) : null;
                        if (members) {
                            const m = members.find(m => String(m.id) === String(opponentId));
                            if (m?.status) canonOpp = utils.buildUnifiedStatusV2(m).canonical;
                        }
                        if (!canonOpp) {
                            const s = await utils.getUserStatus(opponentId);
                            canonOpp = s?.canonical;
                        }
                        if (canonOpp && style.allowStatuses[canonOpp] === false) {
                            const msg = `Disabled by policy: Opponent status (${canonOpp})`;
                            if (!btn.classList.contains('btn-dibs-disabled')) {
                                btn.className = 'btn dibs-btn btn-dibs-disabled';
                                btn.title = msg;
                                btn.onclick = () => ui.showMessageBox(msg, 'warning', 2000);
                            }
                        }
                    } catch(_) { /* silent */ }
                })();
            }
        },
        /**
         * Centralized Med Deal button updater.
         * Mirrors dibs helper style; handles inactive, mine, other states with admin gating.
         * @param {HTMLButtonElement} btn
         * @param {string|number} opponentId
         * @param {string} opponentName
         */
        updateMedDealButton: (btn, opponentId, opponentName) => {
            const warType = state.warData?.warType;
            // If the current warData explicitly disables med deals for this war, hide med deal button
            if (state.warData?.disableMedDeals === true) {
                btn.style.display = 'none';
                return;
            }
            if (warType !== 'Termed War') {
                btn.style.display = 'none';
                return;
            }
            btn.style.display = 'inline-flex';
            const medDealStatus = state.medDeals?.[opponentId];
            const isActive = !!medDealStatus?.isMedDeal;
            const mine = isActive && medDealStatus.medDealForUserId === state.user.tornId;
            const canAdmin = state.script?.canAdministerMedDeals && storage.get('adminFunctionality', true) === true;
            let html, cls, disabled;
            if (mine) {
                html = 'Remove Deal';
                cls = 'btn btn-med-deal-default btn-med-deal-mine';
                disabled = false;
                btn.onclick = (e) => handlers.debouncedHandleMedDealToggle(opponentId, opponentName, false, state.user.tornId, state.user.tornUsername, e.currentTarget);
            } else if (isActive) {
                html = `${medDealStatus.medDealForUsername || 'Someone'}`;
                cls = 'btn btn-med-deal-default btn-med-deal-set';
                disabled = !canAdmin;
                btn.onclick = (e) => handlers.debouncedHandleMedDealToggle(opponentId, opponentName, false, medDealStatus.medDealForUserId || opponentId, medDealStatus.medDealForUsername || opponentName, e.currentTarget);
            } else {
                html = 'Set Deal';
                cls = 'btn btn-med-deal-default btn-med-deal-inactive';
                disabled = false;
                btn.onclick = canAdmin
                    ? (e) => ui.openMedDealSetterModal(opponentId, opponentName, e.currentTarget)
                    : (e) => handlers.debouncedHandleMedDealToggle(opponentId, opponentName, true, state.user.tornId, state.user.tornUsername, e.currentTarget);
            }
            if (btn.innerHTML !== html) btn.innerHTML = html;
            if (btn.className !== cls) btn.className = cls;
            if (btn.disabled !== disabled) btn.disabled = disabled;
        },
        getUserStatus: async (userId /* string|number|null/undefined = self */) => {
            // Cached Torn user status fetcher with small TTL
            const id = userId ? String(userId) : String(state.user.tornId || 'self');
            const now = Date.now();
            const cache = state.session.userStatusCache || (state.session.userStatusCache = {});
            const cached = cache[id];
            if (cached && (now - cached.fetchedAtMs < 10000)) {
                try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][cacheHit]', { id, cached }); } catch(_) {}
                return cached; // 10s TTL
            }
            // Single-flight: avoid multiple concurrent Torn API calls for the same user
            try {
                const pmap = state.session._userStatusPromises || (state.session._userStatusPromises = {});
                if (pmap[id]) {
                    try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][waitingOnInFlight]', { id }); } catch(_) {}
                    return await pmap[id];
                }
                // create promise placeholder so other callers reuse it
                const promise = (async () => {
                    // Check persistent cache in IDB (legacy v1 gate coerced)
                    try {
                        const kv = ui && ui._kv;
                        if (kv && useV1) {
                            const key = `tdm.status.id_${id}`;
                            const raw = await kv.getItem(key);
                            if (raw) {
                                const obj = (typeof raw === 'string') ? JSON.parse(raw) : raw;
                                if (obj && typeof obj.fetchedAtMs === 'number' && (Date.now() - obj.fetchedAtMs) < 10000) {
                                    cache[id] = obj;
                                    try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][kvHit][v1]', { id, obj }); } catch(_) {}
                                    return obj;
                                }
                            }
                        }
                    } catch(_) { /* ignore */ }

                    // Prefer cached faction members (own or opponent) if available
                    const tryMemberFromCache = () => {
                        try {
                            const tf = state.tornFactionData || {};
                            const own = tf[state?.user?.factionId]?.data;
                            const oppId = state?.lastOpponentFactionId || (state?.warData?.opponentId);
                            const opp = oppId ? tf[oppId]?.data : null;
                            const getArr = (data) => {
                                const members = data?.members || data?.member || data?.faction?.members || null;
                                if (!members) return null;
                                return Array.isArray(members) ? members : Object.values(members);
                            };
                            const inOwn = getArr(own)?.find(m => String(m.id) === String(id));
                            const inOpp = inOwn ? null : (getArr(opp)?.find(m => String(m.id) === String(id)) || null);
                            const m = inOwn || inOpp;
                            if (!m) return null;
                            const canon = utils.buildUnifiedStatusV2(m).canonical;
                            const until = Number(m?.status?.until || 0);
                            const activity = String(m?.last_action?.status || m?.lastAction?.status || '').trim();
                            const lastActionTs = Number(m?.last_action?.timestamp || m?.lastAction?.timestamp || 0) || undefined;
                            const factionId = (m.factionId || m.faction_id || m.faction?.faction_id || m.faction?.id || (inOwn ? state.user.factionId : (inOpp ? (state.lastOpponentFactionId || state.warData?.opponentFactionId) : null))) || null;
                            return { raw: m?.status || {}, canonical: canon, until, activity, lastActionTs, factionId: factionId ? String(factionId) : null, fetchedAtMs: Date.now() };
                        } catch(_) { return null; }
                    };
                    const fromCache = tryMemberFromCache();
                    if (fromCache) {
                        cache[id] = fromCache;
                        return fromCache;
                    }

                    // Fallback to Torn user API
                    try {
                        const user = await api.getTornUser(state.user.actualTornApiKey, userId ? id : null);
                        const canon = utils.buildUnifiedStatusV2(user).canonical;
                        const until = Number(user?.status?.until || 0);
                        const activity = String(user?.last_action?.status || '').trim();
                        const factionId = (user?.faction?.faction_id || user?.faction?.id || user?.faction_id || user?.factionId) || null;
                        const lastActionTs = Number(user?.last_action?.timestamp || 0) || undefined;
                        const packed = { raw: user?.status || {}, canonical: canon, until, activity, lastActionTs, factionId: factionId ? String(factionId) : null, fetchedAtMs: Date.now() };
                        cache[id] = packed;
                        try {
                            const kv = ui && ui._kv;
                            if (kv && storage.get('tdmActivityTrackingEnabled', false)) {
                                try { await kv.setItem(`tdm.status.v2.id_${id}`, packed); } catch(_) {}
                            }
                        } catch(_) {}
                        try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][api]', { id, packed }); } catch(_) {}
                        return packed;
                    } catch (e) {
                        let canon = 'Okay', until = 0;
                        if (!userId) {
                            const selfMember = (state.factionMembers || []).find(m => String(m.id) === String(state.user.tornId));
                            if (selfMember?.status) {
                                canon = utils.buildUnifiedStatusV2(selfMember).canonical;
                            }
                        }
                        const fallbackFactionId = state.user?.factionId || null;
                        const packed = { raw: {}, canonical: canon, until, activity: undefined, factionId: fallbackFactionId ? String(fallbackFactionId) : null, fetchedAtMs: Date.now() };
                        cache[id] = packed;
                        try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][fallback]', { id, packed, err: e && e.message }); } catch(_) {}
                        return packed;
                    }
                })();
                pmap[id] = promise;
                try {
                    const result = await promise;
                    return result;
                } finally {
                    try { delete pmap[id]; } catch(_) {}
                }
            } catch (err) {
                try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][outerError]', { id, err: err && err.message }); } catch(_) {}
            }
            // Check persistent cache in IDB (legacy v1 gate coerced)
            try {
                const kv = ui && ui._kv;
                if (kv && useV1) {
                    const key = `tdm.status.id_${id}`;
                    const raw = await kv.getItem(key);
                    if (raw) {
                        const obj = (typeof raw === 'string') ? JSON.parse(raw) : raw;
                        if (obj && typeof obj.fetchedAtMs === 'number' && (now - obj.fetchedAtMs) < 10000) {
                            cache[id] = obj;
                            try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][kvHit][v1]', { id, obj }); } catch(_) {}
                            return obj;
                        }
                    }
                }
            } catch(_) { /* ignore */ }

            // Prefer cached faction members (own or opponent) if available
            const tryMemberFromCache = () => {
                try {
                    const tf = state.tornFactionData || {};
                    const own = tf[state?.user?.factionId]?.data;
                    const oppId = state?.lastOpponentFactionId || (state?.warData?.opponentId);
                    const opp = oppId ? tf[oppId]?.data : null;
                    const getArr = (data) => {
                        const members = data?.members || data?.member || data?.faction?.members || null;
                        if (!members) return null;
                        return Array.isArray(members) ? members : Object.values(members);
                    };
                    const inOwn = getArr(own)?.find(m => String(m.id) === String(id));
                    const inOpp = inOwn ? null : (getArr(opp)?.find(m => String(m.id) === String(id)) || null);
                    const m = inOwn || inOpp;
                    if (!m) return null;
                    const canon = utils.buildUnifiedStatusV2(m).canonical;
                    const until = Number(m?.status?.until || 0);
                    const activity = String(m?.last_action?.status || m?.lastAction?.status || '').trim();
                    const lastActionTs = Number(m?.last_action?.timestamp || m?.lastAction?.timestamp || 0) || undefined;
                    // Include factionId (supports both nested faction obj or direct factionId field patterns)
                    const factionId = (m.factionId || m.faction_id || m.faction?.faction_id || m.faction?.id || (inOwn ? state.user.factionId : (inOpp ? (state.lastOpponentFactionId || state.warData?.opponentFactionId) : null))) || null;
                    return { raw: m?.status || {}, canonical: canon, until, activity, lastActionTs, factionId: factionId ? String(factionId) : null, fetchedAtMs: now };
                } catch(_) { return null; }
            };
            const fromCache = tryMemberFromCache();
            if (fromCache) {
                cache[id] = fromCache;
                return fromCache;
            }

            // Fallback to Torn user API
            try {
                const user = await api.getTornUser(state.user.actualTornApiKey, userId ? id : null);
                const canon = utils.buildUnifiedStatusV2(user).canonical;
                const until = Number(user?.status?.until || 0);
                const activity = String(user?.last_action?.status || '').trim(); // 'Online'|'Idle'|'Offline'
                const factionId = (user?.faction?.faction_id || user?.faction?.id || user?.faction_id || user?.factionId) || null;
                const lastActionTs = Number(user?.last_action?.timestamp || 0) || undefined;
                const packed = { raw: user?.status || {}, canonical: canon, until, activity, lastActionTs, factionId: factionId ? String(factionId) : null, fetchedAtMs: now };
                cache[id] = packed;
                // Optional: persist latest API-derived status to KV for cross-tab/window durability
                    try {
                        const kv = ui && ui._kv;
                        // Persist v2 status to KV only when activity tracking is enabled (single authoritative toggle)
                        if (kv && storage.get('tdmActivityTrackingEnabled', false)) {
                            try { await kv.setItem(`tdm.status.v2.id_${id}`, packed); } catch(_) {}
                        }
                    } catch(_) {}
                try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][api]', { id, packed }); } catch(_) {}
                return packed;
            } catch (e) {
                // Fallback to last known factionMembers/self data if any
                let canon = 'Okay', until = 0;
                if (!userId) {
                    const selfMember = (state.factionMembers || []).find(m => String(m.id) === String(state.user.tornId));
                    if (selfMember?.status) {
                        canon = utils.buildUnifiedStatusV2(selfMember).canonical;
                    }
                }
                const fallbackFactionId = state.user?.factionId || null;
                const packed = { raw: {}, canonical: canon, until, activity: undefined, factionId: fallbackFactionId ? String(fallbackFactionId) : null, fetchedAtMs: now };
                cache[id] = packed;
                // Removed legacy v1 status persistence logic.
                try { if (storage.get('debugDibsEnforce', false)) tdmlogger('debug', '[getUserStatus][fallback]', { id, packed, err: e && e.message }); } catch(_) {}
                return packed;
            }
        },
        // Fast, synchronous canonical status for the current user (no async calls)
        // Order of preference:
        // 1) session userStatusCache (freshest cached canonical)
        // 2) factionMembers cache entry for self
        // 3) fallback 'Okay'
        getMyCanonicalStatus: () => {
            try {
                const selfId = String(state?.user?.tornId || '');
                if (selfId) {
                    const now = Date.now();
                    // Prefer fresh cached status if available
                    const cache = state.session?.userStatusCache || null;
                    const cached = cache && cache[selfId];
                    const isFresh = cached && (now - (cached.fetchedAtMs || 0) < 10000);

                    if (isFresh && typeof cached.canonical === 'string' && cached.canonical) {
                        return cached.canonical;
                    }

                    // If cache is stale or missing, trigger a background refresh
                    (async () => { try { await utils.getUserStatus(selfId); } catch(_) {} })();

                    // Try factionMembers (own member row) as immediate fallback
                    try {
                        const selfMember = (state.factionMembers || []).find(m => String(m.id) === selfId);
                        if (selfMember?.status) {
                            return utils.buildUnifiedStatusV2(selfMember).canonical;
                        }
                    } catch(_) { /* ignore */ }

                    // Return stale cache if available
                    if (cached && typeof cached.canonical === 'string' && cached.canonical) {
                        return cached.canonical;
                    }
                }
            } catch(_) { /* ignore */ }
            return 'Okay';
        },
        getActivityStatusFromRow: (row) => {
            try {
                if (!row) return null;
                // 1) Text markers within typical activity/status containers
                const txt = (row.querySelector('.userStatusWrap, .last_action, .lastAction, .activity, .status')?.textContent || '').toLowerCase();
                if (txt.includes('online')) return 'Online';
                if (txt.includes('idle')) return 'Idle';
                if (txt.includes('offline')) return 'Offline';
                // 2) Classname hints on the row or descendants
                const cls = (row.className || '').toString();
                if (/svg_status_online|status[_-]?online|\bonline\b/i.test(cls)) return 'Online';
                if (/svg_status_idle|status[_-]?idle|\bidle\b/i.test(cls)) return 'Idle';
                if (/svg_status_offline|status[_-]?offline|\boffline\b/i.test(cls)) return 'Offline';
                // 3) Check common <use> hrefs that point to status icons
                const use = row.querySelector('use[href*="svg_status_"], use[xlink\\:href*="svg_status_"]');
                if (use) {
                    const href = use.getAttribute('href') || use.getAttribute('xlink:href') || '';
                    if (/svg_status_online/i.test(href)) return 'Online';
                    if (/svg_status_idle/i.test(href)) return 'Idle';
                    if (/svg_status_offline/i.test(href)) return 'Offline';
                }
                // 4) Last resort: inspect outerHTML of first svg
                const anySvg = row.querySelector('svg');
                if (anySvg) {
                    const html = anySvg.outerHTML || '';
                    if (html.includes('svg_status_online')) return 'Online';
                    if (html.includes('svg_status_idle')) return 'Idle';
                    if (html.includes('svg_status_offline')) return 'Offline';
                }
                // 5) Computed style/class hints on a limited set of nodes
                const styleNodes = row.querySelectorAll('svg, svg *, use, [class*="status"], [class*="Status"]');
                const maxCheck = Math.min(styleNodes.length, 12);
                for (let i = 0; i < maxCheck; i++) {
                    const n = styleNodes[i];
                    try {
                        const cs = window.getComputedStyle(n);
                        const f = (cs && (cs.fill || cs.getPropertyValue('fill'))) || '';
                        const s = (n.getAttribute && (n.getAttribute('href') || n.getAttribute('xlink:href') || n.getAttribute('class') || '')) || '';
                        const hay = (f + ' ' + s).toLowerCase();
                        if (hay.includes('svg_status_online')) return 'Online';
                        if (hay.includes('svg_status_idle')) return 'Idle';
                        if (hay.includes('svg_status_offline')) return 'Offline';
                    } catch(_) { /* noop */ }
                }
            } catch(_) { /* noop */ }
            return null;
        },
        getStatusTextFromRow: (row) => {
            // Status column may be manipulated; prefer preserved original text if present
            // Try common selectors, then broader matches, but avoid the activity wrapper
            let statusCell = row.querySelector('.status, .status___XXAGt, .statusCol');
            if (!statusCell) {
                statusCell = row.querySelector('[class*="customStatus"], [class*="statusCol"]');
            }
            // Avoid picking the activity wrapper
            if (statusCell && /userStatusWrap/i.test(statusCell.className)) {
                statusCell = null;
            }
            if (!statusCell) return '';
            const orig = statusCell.querySelector('.ffscouter-original');
            if (orig && orig.textContent) return orig.textContent.trim();
            return (statusCell.textContent || '').trim();
        },
        // Unified travel equivalence map (single source of truth)
        getTravelEquivalence: () => {
            try { if (utils._travelEquiv) return utils._travelEquiv; } catch(_) {}
            const items = Object.entries(utils._travelMap).map(([name, meta]) => {
                const aliasStrings = (meta.aliases||[]).map(a=> (typeof a === 'string') ? a.replace(/\^|\$/g,'') : null).filter(Boolean);
                if (meta.adjective) aliasStrings.push(meta.adjective);
                if (meta.abbr) aliasStrings.push(meta.abbr);
                return {
                    name,
                    light_aircraft: meta.planes?.light_aircraft || meta.minutes,
                    airliner: meta.planes?.airliner || meta.minutes,
                    airliner_business: meta.planes?.airliner_business || meta.minutes,
                    private_jet: meta.planes?.private_jet || meta.minutes,
                    displayAbbr: meta.abbr || (name.match(/\b([A-Z])[A-Za-z]+/g)? name.split(/\s+/).map(w=>w[0]).join('').slice(0,3) : name.slice(0,3)),
                    adjective: meta.adjective || null,
                    abbr: meta.abbr || null,
                    aliases: aliasStrings
                };
            });
            const byName = {}; const namesLower = []; const aliasToNameLower = {}; const aliasToNameUpper = {}; const abbrsLowerSet = new Set();
            for (const it of items) {
                byName[it.name] = it; namesLower.push(it.name.toLowerCase());
                if (it.abbr) abbrsLowerSet.add(it.abbr.toLowerCase());
                for (const al of (it.aliases||[])) {
                    const low = al.toLowerCase();
                    aliasToNameLower[low] = it.name;
                    aliasToNameUpper[al] = it.name;
                }
            }
            const equiv = { list: items, byName, namesLower, abbrsLower: Array.from(abbrsLowerSet), aliasToNameLower, aliasToNameUpper };
            try { utils._travelEquiv = equiv; } catch(_) {}
            return equiv;
        },
        // Detect hospital-abroad ONLY for strict phrasing:
        //   "In a <adjective> hospital for <duration>" or "In an <adjective> hospital for <duration>"
        // Where <adjective> must exactly match a known travel destination adjective (e.g. Swiss, Caymanian, Japanese).
        // This avoids misclassifying flavor text like "Shot in the back by the club boss".
        parseHospitalAbroadDestination: (statusStr) => {
            try {
                if (!statusStr) return null;
                const txt = String(statusStr || '').trim();
                // Fast reject: must begin with In a / In an (case-insensitive)
                if (!/^In\s+an?\s+/i.test(txt)) return null;
                // Strict pattern: start -> In a(n) <word> hospital for <number> <timeunit>
                // Capture adjective between the article and the word 'hospital'
                const m = txt.match(/^In\s+an?\s+([A-Za-z][A-Za-z\-']{1,40})\s+hospital\s+for\s+\d+\s+(?:second|seconds|minute|minutes|hour|hours)\b/i);
                if (!m) return null;
                const adjective = m[1].toLowerCase();
                if (!adjective) return null;
                const eq = utils.getTravelEquivalence();
                for (const it of eq.list) {
                    if (!it || !it.adjective) continue;
                    if (it.adjective.toLowerCase() === adjective) return it.name;
                }
                return null; // adjective not recognized as a mapped travel destination
            } catch (_) { return null; }
        },
        // Resolve minutes with plane type if available; defaults to light_aircraft for compatibility
        getTravelMinutes: (dest, planeType = 'light_aircraft') => {
            const eq = utils.getTravelEquivalence();
            const it = eq.byName[String(dest || '')];
            if (!it) return 0;
            // Map unknowns to light_aircraft
            const key = (function(pt) {
                if (!pt) return 'light_aircraft';
                const k = String(pt).toLowerCase();
                if (k === 'airliner' || k === 'airliner_business' || k === 'private_jet' || k === 'light_aircraft') return k;
                return 'light_aircraft';
            })(planeType);
            return Number(it[key] || 0) || 0;
        },
        // Try to resolve a player's base plane type from cached status (does not apply business-class on its own)
        getKnownPlaneTypeForId: (id) => {
            try {
                const sid = String(id || '');
                if (!sid) return null;
                const cached = state.session?.userStatusCache?.[sid];
                const pt = cached?.raw?.plane_image_type || null; // 'airliner' | 'light_aircraft' | 'private_jet'
                return pt; // business-class application is contextual (outbound only) and handled elsewhere
            } catch(_) { return null; }
        },
        // Business-class detection & caching (best-effort):
        // - isEligibleSync: fast in-memory check only (no IO)
        // - ensureAsync: try KV, then Torn API (user job) once per cooldown
        business: {
            isEligibleSync(id) {
                try {
                    const sid = String(id||'');
                    if (!sid) return false;
                    return !!(state.session && state.session.businessClassById && state.session.businessClassById[sid]);
                } catch(_) { return false; }
            },
            async ensureAsync(id) {
                try {
                    const sid = String(id||'');
                    if (!sid) return false;
                    state.session = state.session || {};
                    state.session.businessClassById = state.session.businessClassById || {};
                    state._businessCheckTs = state._businessCheckTs || {};
                    const now = Date.now();
                    const last = state._businessCheckTs[sid] || 0;
                    // Cooldown 60 minutes between network checks per user
                    if (now - last < 60*60*1000 && (sid in state.session.businessClassById)) return state.session.businessClassById[sid];
                    state._businessCheckTs[sid] = now;
                    // 1) Try KV cache
                    const kv = ui && ui._kv;
                    let cached = null;
                    try { cached = await kv?.getItem(`tdm.business.id_${sid}`); } catch(_) { cached = null; }
                    if (cached && typeof cached === 'object') {
                        const b = !!cached.b;
                        state.session.businessClassById[sid] = b;
                        return b;
                    }
                    tdmlogger('debug', '[BusinessDetect] KV cache miss, making API call for user id:', sid);
                    // 2) Try Torn API: fetch user job info (best-effort)
                    const key = state?.user?.actualTornApiKey;
                    if (!key || !api?.getTornUser) return false;
                    let data = null;
                    try { data = await api.getTornUser(key, sid, 'job'); } catch(_) { data = null; }
                    let eligible = false;
                    try {
                        const job = data?.job;
                        if (job) {
                            if (job?.type_id === 32 && job?.rating === 10) {
                                tdmlogger('info', '[BusinessDetect] User has Business Class job:', { id: sid, job });
                                eligible = true;
                            } else {
                                tdmlogger('info', '[BusinessDetect] User job does not qualify for Business Class:', { id: sid, job });
                                eligible = false;
                            }
                        }
                    } catch(_) { eligible = false; }
                    try { await kv?.setItem(`tdm.business.id_${sid}`, { b: eligible, ts: now }); } catch(_) {}
                    state.session.businessClassById[sid] = eligible;
                    if (storage.get('debugBusinessDetect', false)) {
                        tdmlogger('info', '[BusinessDetect]', { id: sid, eligible: eligible });
                    }
                    return eligible;
                } catch(_) { return false; }
            }
        },
        // Unified travel helpers module
        travel: {
            _unknownLogged: new Set(),
            detectType(stateStr, descStr) {
                try {
                    const state = String(stateStr || '');
                    const desc = String(descStr || '');
                    const canon = utils.buildUnifiedStatusV2({ state, description: desc }).canonical;
                    // Returning first
                    if (/^returning to torn from\s+/i.test(desc)) {
                        // DEPRECATED legacy returning parser path (superseded by buildUnifiedStatusV2)
                        return { type: 'returning', dest: utils.parseUnifiedDestination(desc) || null };
                    }
                    // Hospital abroad
                    if (/hospital/i.test(canon || state) && /hospital/i.test(desc)) {
                        const d = utils.parseHospitalAbroadDestination(desc) || null;
                        if (d) return { type: 'hosp_abroad', dest: d };
                    }
                    // Abroad
                    if (/^in\s+/i.test(desc) || /abroad/i.test(canon || '')) {
                        // DEPRECATED legacy abroad parser path
                        return { type: 'abroad', dest: utils.parseUnifiedDestination(desc) || null };
                    }
                    // Traveling
                    if (/travel/i.test(canon || state) || /^travell?ing to\s+/i.test(desc)) {
                        // DEPRECATED legacy traveling parser path (spelling unified to 'traveling')
                        return { type: 'traveling', dest: utils.parseUnifiedDestination(desc) || null };
                    }
                } catch(_) {}
                return { type: null, dest: null };
            },
            getMinutes(dest) { return utils.getTravelMinutes(dest); },
            logUnknownDestination(rawDesc) {
                try {
                    const key = String(rawDesc || '').trim().toLowerCase();
                    if (!key) return;
                    if (this._unknownLogged.has(key)) return;
                    this._unknownLogged.add(key);
                    tdmlogger('warn', '[Travel] Unknown destination in status:', rawDesc);
                } catch(_) { /* noop */ }
            },
            computeEtaMs(leftMs, minutes) {
                const mins = Number(minutes) || 0;
                if (!Number.isFinite(leftMs) || !Number.isFinite(mins) || mins <= 0) return 0;
                return leftMs + mins * 60000;
            },
            formatTravelLine(leftMs, minutes, statusDescription) {
                try {
                    // [left time] ETA [localtime] -status.description
                    const leftLocal = new Date(leftMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
                    const etaMs = this.computeEtaMs(leftMs, minutes);
                    const etaLocal = etaMs ? new Date(etaMs).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
                    const desc = String(statusDescription || '').trim();
                    const line = etaLocal ? `[${leftLocal}] ETA ${etaLocal} -${desc}` : (desc || '');
                    try { tdmlogger('info', '[Travel][formatTravelLine]', { leftMs, minutes, statusDescription: desc, etaMs, line }); } catch(_) { /* noop */ }
                    return line;
                } catch(_) { return String(statusDescription || '').trim(); }
            }
        },
        // Destination and status/activity abbreviations for compact UI labels
        abbrevDest: (dest) => {
            const eq = utils.getTravelEquivalence();
            const meta = eq.byName[String(dest || '')];
            if (!meta) return '';
            return meta.abbr || meta.displayAbbr || '';
        },
        abbrevStatus: (canon) => {
            const c = String(canon || '').toLowerCase();
            if (c === 'okay') return 'Ok';
            if (c === 'hospitalabroad') return 'Hosp*';
            if (c === 'hospital') return 'Hosp';
            if (c === 'travel') return 'Trav';
            if (c === 'abroad') return 'Abrd';
            if (c === 'jail') return 'Jail';
            // Fallback: title-case then trim to 6 chars max to avoid overflow
            const t = (canon || '').trim();
            return t.length > 6 ? t.slice(0, 6) : t;
        },
        abbrevActivity: (activity) => {
            const a = String(activity || '').toLowerCase();
            if (a === 'online') return 'On';
            if (a === 'offline') return 'Off';
            if (a === 'idle') return 'Idle';
            const t = (activity || '').trim();
            return t.length > 6 ? t.slice(0, 6) : t;
        },
        // Extract the per-row points for an opponent in the ranked war table
        // Stores decimals precisely (if present) but callers can truncate for display.
        // Heuristics:
        //  - Prefer specific points class (.points___* or .points)
        //  - Support decimal numbers (e.g. 12.5)
        //  - If multiple numbers are present and a '/' exists (e.g. "12 / 34"), take the first.
        //    Otherwise take the last numeric token (common for labels like "Pts: 12.5 (curr)").
        getPointsFromRow: (row) => {
            try {
                if (!row) return null;
                let el = row.querySelector('.points___TQbnu, .points');
                if (!el) return null;
                const raw = (el.textContent || '').trim();
                if (!raw) return null;
                const matches = raw.match(/-?\d+(?:\.\d+)?/g);
                if (!matches || !matches.length) return null;
                let chosen;
                if (matches.length === 1) {
                    chosen = matches[0];
                } else {
                    chosen = raw.includes('/') ? matches[0] : matches[matches.length - 1];
                }
                const val = parseFloat(chosen);
                if (!Number.isFinite(val)) return null;
                if (val > 1e7) return null; // sanity guard against concatenation artifacts
                if (state?.debug?.rowLogs) {
                    // PointsParse verbose logging gated separately to avoid flooding row logs.
                    try { tdmlogger('debug', '[PointsParse]', { raw, matches, chosen, val }); } catch(_) { /* noop */ }
                }
                return val;
            } catch(_) { return null; }
        },
        // --- Ranked war scoreboard helpers ---
        // Build a stable key for the currently shown war based on the two faction names in the rank box
        // This avoids relying on URL (Torn does not expose the war id in hash) and prevents cross-war state bleed
        getCurrentWarPageKey: () => {
            try {
                const rankBox = state.dom.rankBox || document.querySelector('.rankBox___OzP3D');
                if (!rankBox) return null;
                const oppName = rankBox.querySelector('.nameWp___EX6gT .opponentFactionName___vhESM')?.textContent?.trim() || '';
                const curName = rankBox.querySelector('.nameWp___EX6gT .currentFactionName___eq7n8')?.textContent?.trim() || '';
                if (!oppName && !curName) return null;
                const norm = (s) => s.toLowerCase().replace(/\s+/g,' ').trim();
                // Sort for stability regardless of left/right placement
                const a = norm(oppName), b = norm(curName);
                const [k1, k2] = a < b ? [a,b] : [b,a];
                return `${k1}__vs__${k2}`;
            } catch(_) { return null; }
        },
        // Extract faction id from a factions.php profile href
        parseFactionIdFromHref: (href) => {
            try {
                if (!href) return null;
                const m = String(href).match(/[?&]ID=(\d+)/i);
                return m ? m[1] : null;
            } catch(_) { return null; }
        },
        // Parse a string or array into a sanitized, de-duplicated list of faction ids (strings).
        parseFactionIdList: (raw, meta) => {
            const result = new Set();
            const recordInvalid = (token) => {
                if (!meta) return;
                meta.invalidTokens = meta.invalidTokens || [];
                meta.invalidTokens.push(String(token || '').trim());
            };
            const recordValid = () => {
                if (!meta) return;
                meta.validTokensSeen = (meta.validTokensSeen || 0) + 1;
            };
            const add = (value) => {
                const v = String(value ?? '').trim();
                if (!v) return;
                if (!/^[0-9]+$/.test(v)) {
                    recordInvalid(value);
                    return;
                }
                recordValid();
                result.add(v);
            };
            if (Array.isArray(raw)) {
                raw.forEach(add);
            } else if (typeof raw === 'string') {
                raw.split(/[\s,;]+/).forEach(add);
            }
            if (meta) {
                meta.hadInvalid = Array.isArray(meta.invalidTokens) && meta.invalidTokens.length > 0;
                meta.duplicateCount = (meta.validTokensSeen || 0) - result.size;
                if (meta.duplicateCount < 0) meta.duplicateCount = 0;
            }
            return Array.from(result);
        },
        // Read both visible faction IDs from the ranked war rank box (left/right)
        getVisibleRankedWarFactionIds: () => {
            try {
                const box = state.dom.rankBox || document.querySelector('.rankBox___OzP3D');
                if (!box) return { leftId: null, rightId: null, ids: [] };
                const leftNode = box.querySelector('.nameWp___EX6gT .opponentFactionName___vhESM');
                const rightNode = box.querySelector('.nameWp___EX6gT .currentFactionName___eq7n8');
                const leftA = (leftNode && (leftNode.closest('a') || leftNode.querySelector('a'))) || leftNode;
                const rightA = (rightNode && (rightNode.closest('a') || rightNode.querySelector('a'))) || rightNode;
                const leftId = utils.parseFactionIdFromHref(leftA && leftA.href);
                const rightId = utils.parseFactionIdFromHref(rightA && rightA.href);
                const ids = [leftId, rightId].filter(Boolean);
                return { leftId: leftId || null, rightId: rightId || null, ids: Array.from(new Set(ids)) };
            } catch(_) {
                return { leftId: null, rightId: null, ids: [] };
            }
        },
        pageWarIncludesOurFaction: () => {
            try {
                const key = utils.getCurrentWarPageKey?.();
                if (!key) return false;
                const ourName = (state.factionPull?.name || '').toLowerCase().trim();
                return ourName && key.includes(ourName.toLowerCase());
            } catch(_) { return false; }
        },
        readRankBoxScores: () => {
            try {
                const root = state.dom.rankBox || document.querySelector('.rankBox___OzP3D');
                let opp = 0, our = 0;
                if (root) {
                    const left = root.querySelector('.statsBox___zH9Ai .scoreBlock___Pr3xV .left.scoreText___uVRQm');
                    const right = root.querySelector('.statsBox___zH9Ai .scoreBlock___Pr3xV .right.scoreText___uVRQm');
                    const oppNode = root.querySelector('.statsBox___zH9Ai .scoreBlock___Pr3xV .scoreText___uVRQm.opponentFaction___HmQpL') || left;
                    const ourNode = root.querySelector('.statsBox___zH9Ai .scoreBlock___Pr3xV .scoreText___uVRQm.currentFaction___Omz6o') || right;
                    opp = parseInt((oppNode?.textContent || '0').replace(/[\s,]/g,''), 10) || 0;
                    our = parseInt((ourNode?.textContent || '0').replace(/[\s,]/g,''), 10) || 0;
                }
                // Additional fallback: read first row points from left/right tables
                if (!opp || !our) {
                    try {
                        const leftTable = (state.dom.rankwarContainer || document).querySelector('.tab-menu-cont.left .members-list, .tab-menu-cont.left .members-cont');
                        const rightTable = (state.dom.rankwarContainer || document).querySelector('.tab-menu-cont.right .members-list, .tab-menu-cont.right .members-cont');
                        const leftFirst = leftTable?.querySelector(':scope > li .points___TQbnu, :scope > .table-body > li .points___TQbnu');
                        const rightFirst = rightTable?.querySelector(':scope > li .points___TQbnu, :scope > .table-body > li .points___TQbnu');
                        const lVal = parseInt((leftFirst?.textContent || '').replace(/[\s,]/g,''), 10);
                        const rVal = parseInt((rightFirst?.textContent || '').replace(/[\s,]/g,''), 10);
                        // Heuristic: the larger side likely equals the current faction or opponent depending on labels; we keep them as opp/our only if both parsed
                        if (Number.isFinite(lVal) && Number.isFinite(rVal)) {
                            // Map to opp/our by role if rankBox labels exist; else leave as-is (will still detect deltas)
                            if (!opp) opp = lVal;
                            if (!our) our = rVal;
                        }
                    } catch(_) { /* ignore */ }
                }
                return { opp, our };
            } catch(_) { return null; }
        }
    };
    //======================================================================
    // 3. STATE MANAGEMENT
    //======================================================================
    // Initialize state keys from localStorage or use default values
    const bootstrapNowMs = Date.now();
    const cachedUserPosition = storage.get('LastUserPosition', null);
    const cachedAdminFlagRaw = storage.get('CanAdministerMedDeals', false);
    const cachedAdminFlag = cachedAdminFlagRaw === true;
    const cachedAdminTsRaw = Number(storage.get('CanAdministerMedDealsTs', 0)) || 0;
    const cachedAdminFresh = cachedAdminFlag && cachedAdminTsRaw > 0 && (bootstrapNowMs - cachedAdminTsRaw) < ADMIN_ROLE_CACHE_TTL_MS;
    if (cachedAdminFlag && !cachedAdminFresh && cachedAdminTsRaw > 0) {
        try { storage.set('CanAdministerMedDeals', false); storage.remove('CanAdministerMedDealsTs'); } catch (_) { /* cleanup best-effort */ }
    }

    const state = {
        featureFlags: featureFlagController.flags,
        featureFlagController,
        dibsData: storage.get('dibsData', []),
        userScore: storage.get('userScore', null),
        warData: storage.get('warData', { warType: 'War Type Not Set' }),
        rankWars: storage.get('rankWars', []),
        lastRankWar: storage.get('lastRankWar', { id: 1, start: 0, end: 0, target: 42069, winner: null, factions: [{ id: 41419, name: "Neon Cartel", score: 7620, chain: 666 }] }),
        lastOpponentFactionId: storage.get('lastOpponentFactionId', 0),
        lastOpponentFactionName: storage.get('lastOpponentFactionName', 'Not Pulled'),
        medDeals: storage.get('medDeals', {}),
        userNotes: storage.get('userNotes', {}),
        factionMembers: storage.get('factionMembers', []),
        factionPull: storage.get('factionPull', {id: 0, name: 'nofactionpulled', members: 0 }),
        dibsNotifications: storage.get('dibsNotifications', []),
        unauthorizedAttacks: storage.get('unauthorizedAttacks', []),
        retaliationOpportunities: storage.get('retaliationOpportunities', {}),
        unifiedStatus: {},
        // Snapshot and change tracking for ranked war table alerts
        rankedWarTableSnapshot: {},
        rankedWarChangeMeta: {}, // { [opponentId]: { statusChangedAtMs?: number, lastStatus?: string, lastActivity?: string } }
        // Global ranked war score tracking (opponent vs current faction)
        rankedWarScoreSnapshot: { opp: 0, our: 0 },
        // Opponents that currently need immediate suppression (retal, score spike, or online)
        needsSuppression: {}, // { [opponentId]: { reason: 'retal'|'score'|'online', ts: number } }
        _autoUndibLastTs: {},
        dataTimestamps: storage.get('dataTimestamps', {}), // --- Timestamp-based polling ---
        ffscouterCache: storage.get('ffscouterCache', {}),
        user: (() => {
            const defaults = {
                tornId: null,
                tornUsername: '',
                tornUserObject: null,
                actualTornApiKey: null,
                actualTornApiKeyAccess: 0,
                hasReachedScoreCap: false,
                factionId: null,
                apiKeySource: 'none',
                keyValidation: null,
                keyInfoCache: null,
                keyValidatedAt: null,
                apiKeyUiMessage: null,
                factionAPIAccess: false
            };
            const stored = storage.get('user', defaults);
            if (!stored || typeof stored !== 'object') return { ...defaults };
            return { ...defaults, ...stored };
        })(),
        auth: storage.get('firebaseAuthSession', null),
        page: { url: new URL(window.location.href), isFactionProfilePage: false, isMyFactionPrivatePage: false, isMyFactionProfilePage: false, isMyFactionYourInfoTab: false, isRankedWarPage: false, isFactionPage: false, isMyFactionPage: false, isAttackPage: false },
        dom: { factionListContainer: null, customControlsContainer: null, rankwarContainer: null, rankwarmembersWrap: null, rankwarfactionTables: null, rankBox: null },
        script: { currentUserPosition: cachedUserPosition, canAdministerMedDeals: cachedAdminFresh, lastActivityTime: Date.now(), isWindowActive: true, currentRefreshInterval: config.REFRESH_INTERVAL_ACTIVE_MS, mainRefreshIntervalId: null, activityTimeoutId: null, mutationObserver: null, hasProcessedRankedWarTables: false, hasProcessedFactionList: false, factionBundleRefreshIntervalId: null, factionBundleRefreshMs: null, lightPingIntervalId: null, fetchWatchdogIntervalId: null, idleTrackingOverride: false, apiTransport: 'unknown', useHttpEndpoints: false },
        ui: { retalNotificationActive: false, retalNotificationElement: null, retalTimerIntervals: [], noteModal: null, noteTextarea: null, currentNoteTornID: null, currentNoteTornUsername: null, currentNoteButtonElement: null, setterModal: null, setterList: null, setterSearchInput: null, currentOpponentId: null, currentOpponentName: null, currentButtonElement: null, currentSetterType: null, unauthorizedAttacksModal: null, currentWarAttacksModal: null, chainTimerEl: null, chainTimerValueEl: null, chainTimerIntervalId: null, chainFallback: { lastFetch: 0, timeoutEpoch: 0 }, inactivityTimerEl: null, inactivityTimerValueEl: null, inactivityTimerIntervalId: null, opponentStatusEl: null, opponentStatusValueEl: null, opponentStatusIntervalId: null, opponentStatusCache: { lastFetch: 0, untilEpoch: 0, text: '', opponentId: null }, apiUsageEl: null, apiUsageValueEl: null, apiUsageDetailEl: null, attackModeEl: null, attackModeValueEl: null, activityTickerIntervalId: null, badgeDockEl: null, badgeDockToggleEl: null, badgeDockItemsEl: null, badgeDockActionsEl: null, badgeDockCollapsedKey: 'tdmBadgeDockCollapsed_v1', debugOverlayMinimizedKey: 'liveTrackDebugOverlayMinimized', levelDisplayModeKey: 'ui.levelDisplayMode', levelDisplayMode: null, levelCellLongPressMs: LEVEL_CELL_LONGPRESS_MS, debugOverlayToggleEl: null, debugOverlayMinimizeEl: null, debugOverlayMinimized: storage.get('liveTrackDebugOverlayMinimized', false), userScoreBadgeEl: null, factionScoreBadgeEl: null, dibsDealsBadgeEl: null , chainWatcherIntervalId: null },
        gm: { rD_xmlhttpRequest: null, rD_registerMenuCommand: null },
        session: { apiCalls: 0, apiCallsClient: 0, apiCallsBackend: 0, userStatusCache: {}, lastEnforcementMs: 0, nonActiveWarFetchedOnce: {}, factionApi: { hasFactionAccess: null, allowAttacksSelection: null }, selectionsPerFaction: {} },
        // Debug/logging & observer throttling
        debug: { 
            rowLogs: storage.get('debugRowLogs', false),
            statusWatch: storage.get('debugStatusWatch', false),
            pointsParseLogs: storage.get('debugPointsParseLogs', false),
            adoptionInfo: storage.get('debugAdoptionInfo', false),
            statusCanon: storage.get('debugStatusCanon', false),
            // New: cadence/api logs to help diagnose focus/visibility gated refresh behavior
            cadence: storage.get('debugCadence', false),
            apiLogs: storage.get('debugApiLogs', false),
            // New: IndexedDB perf logs toggle (can also be overridden by elapsed threshold)
            idbLogs: storage.get('debugIdbLogs', false)
        },
        // Client setting: if true, while the user has any active dibs, we always send a fresh heartbeat timestamp
        passiveActivityHeartbeatEnabled: storage.get('passiveActivityHeartbeatEnabled', true),
        _statusWatchLastLogs: {},
        // Cached ranked war summaries keyed by warId: { [warId]: { fingerprint, updatedAt, summary, source?, etag?, lastModified?, summaryUrl? } }
        rankedWarSummaryCache: storage.get('rankedWarSummaryCache', {}),
        // Cached ranked war attacks keyed by warId: { [warId]: { manifestUrl, attacksUrl, lastSeq, etags: { manifest?: string, [seq]: string }, lastModified?: string, attacks: Array } }
        // NOTE: attack arrays are kept in-memory only to avoid exceeding localStorage quotas.
        // persistedRankedWarAttacksCache contains a minimized form saved into storage (no heavy 'attacks' arrays).
        // NOTE: we must not reference `state` here because `state` is being created
        // in this object literal — referencing it would trigger a TDZ ReferenceError.
        // Use the persisted form from storage for initial load; in-memory caches
        // will be used/updated at runtime.
        rankedWarAttacksCache: storage.get('rankedWarAttacksCache', {}),
        // Last summary source used when presenting the Ranked War Summary modal: 'local' | 'storage200' | 'storage304' | 'server' | null
        rankedWarLastSummarySource: storage.get('rankedWarLastSummarySource', null),
        // Lightweight meta about the last summary, for debugging/verification (etag, lastModified, counts)
        rankedWarLastSummaryMeta: storage.get('rankedWarLastSummaryMeta', {}),
        // Attacks provenance tracking for the war summary modal badge/logs
        rankedWarLastAttacksSource: storage.get('rankedWarLastAttacksSource', null),
        rankedWarLastAttacksMeta: storage.get('rankedWarLastAttacksMeta', {}),
        // Cached Torn faction bundles by factionId
        tornFactionData: storage.get('tornFactionData', {}), // { [factionId]: { data, fetchedAtMs, selections: string[] } }
        // Per-war single-flight guard for ranked war fetches
        _warFetchInFlight: {},
        // Persisted content fingerprints (loaded once; kept minimal)
        _fingerprints: storage.get('fingerprints', { dibs: null, medDeals: null })
        ,
        // Central resource registry for timers/observers/listeners added at runtime.
        // Stored here to keep existing patterns of putting app-global runtime state on `state`.
        _resources: {
            intervals: new Set(),   // holds interval ids
            timeouts: new Set(),    // holds timeout ids
            observers: new Set(),   // MutationObserver instances
            windowListeners: [],    // { type, handler, opts }
            // intentionally avoid a strong global map of DOM elements -> handlers to prevent retaining
            // elements; instead, add handlers using `utils.addElementHandler(el, event, handler)`
            // which records handlers directly on the element under a small `_tdmHandlers` array so
            // they can be cleared when the element is removed.
        }
    };

    const sanitizeLevelDisplayMode = (value) => (LEVEL_DISPLAY_MODES.includes(value) ? value : DEFAULT_LEVEL_DISPLAY_MODE);
    try {
        const persistedLevelMode = storage.get(state.ui.levelDisplayModeKey, DEFAULT_LEVEL_DISPLAY_MODE);
        state.ui.levelDisplayMode = sanitizeLevelDisplayMode(persistedLevelMode);
    } catch(_) {
        state.ui.levelDisplayMode = DEFAULT_LEVEL_DISPLAY_MODE;
    }

    const recordAdapterMetric = (source, status, detail) => {
        try {
            const metricsRoot = state.metrics || (state.metrics = {});
            const adapters = metricsRoot.rankWarAdapters || (metricsRoot.rankWarAdapters = {});
            const bucket = adapters[source] || (adapters[source] = { hit: 0, miss: 0, error: 0 });
            bucket[status] = (bucket[status] || 0) + 1;
            bucket.lastStatus = status;
            bucket.lastDetail = detail || null;
            bucket.updatedAt = Date.now();
        } catch(_) {
            /* metrics collection is best-effort */
        }
    };

    // Persist a minimized attacks cache to storage to avoid localStorage quota issues.
    // Debounced to prevent rapid synchronous writes during heavy attack polling.
    const schedulePersistRankedWarAttacksCache = utils.debounce((cache) => {
        try {
            const persisted = {};
            for (const [wid, entry] of Object.entries(cache || {})) {
                // Shallow copy to avoid mutating in-memory array
                const c = { ...entry };
                // Never persist full attacks array to localStorage
                if (Array.isArray(c.attacks)) {
                    // Keep small fingerprint/length for diagnostics but remove heavy payload
                    c.attacksCount = c.attacks.length;
                    delete c.attacks;
                }
                persisted[wid] = c;
            }
            storage.set('rankedWarAttacksCache', persisted);
        } catch (e) {
            // Best-effort only; avoid throwing
        }
    }, 2000);

    function persistRankedWarAttacksCache(cache) {
        schedulePersistRankedWarAttacksCache(cache);
    }

    // Debug: log persisted fingerprints at startup for verification (only if debugging enabled & any value present)
    try {
        if ((state.debug?.apiLogs || state.debug?.cadence) && state._fingerprints && (state._fingerprints.dibs || state._fingerprints.medDeals)) {
            tdmlogger('debug', '[Startup] Loaded persisted fingerprints', { dibs: state._fingerprints.dibs, medDeals: state._fingerprints.medDeals });
        }
    } catch(_) { /* noop */ }

    // Startup recompute & consistency check (silently heals drift)
    (function fingerprintStartupConsistency(){
        try {
            const recomputedD = utils.computeDibsFingerprint(state.dibsData);
            const recomputedM = utils.computeMedDealsFingerprint(state.medDeals);
            let healed = false; let warnings = [];
            if (recomputedD && state._fingerprints?.dibs && state._fingerprints.dibs !== recomputedD) {
                warnings.push({ type:'dibs', persisted: state._fingerprints.dibs, recomputed: recomputedD });
                state._fingerprints.dibs = recomputedD; healed = true;
            } else if (!state._fingerprints?.dibs && recomputedD) {
                state._fingerprints.dibs = recomputedD; healed = true;
            }
            if (recomputedM && state._fingerprints?.medDeals && state._fingerprints.medDeals !== recomputedM) {
                warnings.push({ type:'medDeals', persisted: state._fingerprints.medDeals, recomputed: recomputedM });
                state._fingerprints.medDeals = recomputedM; healed = true;
            } else if (!state._fingerprints?.medDeals && recomputedM) {
                state._fingerprints.medDeals = recomputedM; healed = true;
            }
            if (healed) { try { storage.set('fingerprints', state._fingerprints); } catch(_) {} }
            if (warnings.length && (state.debug?.apiLogs || state.debug?.cadence)) {
                tdmlogger('warn', '[Startup] Fingerprint drift healed', warnings);
            }
        } catch(_) { /* ignore */ }
    })();

    // ================================================================
    // Event Bus (lightweight) for decoupled UI & data updates
    // ================================================================
    const events = (function(){
        const listeners = new Map(); // evt -> Set
        return {
            on(evt, fn) { if (!listeners.has(evt)) listeners.set(evt, new Set()); listeners.get(evt).add(fn); return () => listeners.get(evt)?.delete(fn); },
            once(evt, fn) { const off = this.on(evt, (...a)=>{ try { fn(...a); } finally { off(); } }); return off; },
            off(evt, fn) { listeners.get(evt)?.delete(fn); },
            emit(evt, payload) { const set = listeners.get(evt); if (!set || !set.size) return; [...set].forEach(fn => { try { fn(payload); } catch(e){ /* swallow */ } }); }
        };
    })();
    state.events = events;

    const applyAdminCapability = ({ position, computedFlag, source = 'backend', refreshTimestamp } = {}) => {
        const canonicalPosition = (() => {
            if (typeof position === 'string' && position.trim().length > 0) return position.trim();
            if (typeof state.script.currentUserPosition === 'string' && state.script.currentUserPosition.trim().length > 0) return state.script.currentUserPosition.trim();
            return position || state.script.currentUserPosition || null;
        })();
        const prevFlag = !!state.script.canAdministerMedDeals;
        const prevPosition = state.script.currentUserPosition || null;
        const cachedTs = Number(storage.get('CanAdministerMedDealsTs', 0)) || 0;
        const cachedFlag = storage.get('CanAdministerMedDeals', false) === true;
        const nowMs = Date.now();
        let nextFlag = !!computedFlag;
        let nextTs = cachedTs > 0 ? cachedTs : 0;
        const shouldRefreshTs = typeof refreshTimestamp === 'boolean' ? refreshTimestamp : !!computedFlag;

        if (nextFlag) {
            if (shouldRefreshTs || cachedTs <= 0) {
                nextTs = nowMs;
            }
        } else if ((prevFlag || cachedFlag) && cachedTs > 0 && (nowMs - cachedTs) < ADMIN_ROLE_CACHE_TTL_MS) {
            nextFlag = true;
            nextTs = cachedTs;
        } else {
            nextTs = 0;
        }

        const normalizedPosition = (typeof canonicalPosition === 'string' && canonicalPosition.trim().length) ? canonicalPosition.trim() : null;
        const positionChanged = normalizedPosition !== prevPosition;

        state.script.currentUserPosition = normalizedPosition;
        state.script.canAdministerMedDeals = nextFlag;

        try { storage.set('LastUserPosition', normalizedPosition); } catch (_) {}
        try { storage.set('CanAdministerMedDeals', nextFlag); } catch (_) {}
        if (nextFlag && nextTs > 0) {
            try { storage.set('CanAdministerMedDealsTs', nextTs); } catch (_) {}
        } else {
            try { storage.remove('CanAdministerMedDealsTs'); } catch (_) {}
        }

        if (nextFlag !== prevFlag || positionChanged) {
            try { state.events.emit('script:admin-permissions-updated', { canAdmin: nextFlag, position: normalizedPosition, source, grantedAt: nextTs }); } catch (_) {}
        }
    };

    const firebaseAuth = (() => {
        const SESSION_KEY = 'firebaseAuthSession';
        const firebaseCfg = config.FIREBASE || {};
        const API_KEY = firebaseCfg.apiKey || '';
        const CUSTOM_TOKEN_URL = firebaseCfg.customTokenUrl || '';
        const SIGN_IN_ENDPOINT = API_KEY ? `https://identitytoolkit.googleapis.com/v1/accounts:signInWithCustomToken?key=${API_KEY}` : null;
        const REFRESH_ENDPOINT = API_KEY ? `https://securetoken.googleapis.com/v1/token?key=${API_KEY}` : null;

        const initialSession = (state.auth && typeof state.auth === 'object') ? { ...state.auth } : null;
        let session = (initialSession && initialSession.idToken && initialSession.refreshToken) ? initialSession : null;
        if (!session) {
            state.auth = null;
            try { storage.remove(SESSION_KEY); } catch(_) {}
        }
        const now = () => Date.now();
        const keySnippet = (key) => {
            if (!key || typeof key !== 'string') return null;
            return key.length <= 8 ? key : `${key.slice(0, 4)}:${key.slice(-4)}`;
        };
        const setSession = (next) => {
            session = next ? { ...next } : null;
            state.auth = session;
            if (session) storage.set(SESSION_KEY, session);
            else storage.remove(SESSION_KEY);
            try { state.events.emit('auth:updated', session); } catch(_) {}
        };
        const clearSession = (reason) => {
            if (reason) {
                try { tdmlogger('warn', `[Auth] session cleared: ${reason}`); } catch(_) {}
            }
            setSession(null);
        };
        const computeExpiresAt = (expiresInSeconds) => {
            const secs = Number(expiresInSeconds) || 0;
            const buffer = 120; // refresh a bit early
            return now() + Math.max(0, secs - buffer) * 1000;
        };
        const ensureRequestClient = () => state?.gm?.rD_xmlhttpRequest || null;
        const requestRaw = ({ url, method = 'POST', headers = {}, body = '', expectJson = true }) => {
            const client = ensureRequestClient();
            return new Promise((resolve, reject) => {
                const handleSuccess = async (status, text) => {
                    if (status >= 400) {
                        return reject(new Error(`HTTP ${status}: ${String(text || '').slice(0, 180)}`));
                    }
                    if (!expectJson) return resolve(text);
                    if (!text) return resolve({});
                    try { return resolve(JSON.parse(text)); }
                    catch (err) { return reject(new Error(`Invalid JSON from ${url}: ${err.message}`)); }
                };
                if (!client) {
                    const opts = { method, headers, body };
                    fetch(url, opts).then(async (res) => {
                        const text = await res.text().catch(() => '');
                        handleSuccess(Number(res.status || 0), text);
                    }).catch(err => reject(new Error(err?.message || 'Network error')));
                    return;
                }
                client({
                    method,
                    url,
                    headers,
                    data: body,
                    onload: (response) => {
                        const status = Number(response?.status || 0);
                        const text = typeof response?.responseText === 'string' ? response.responseText : '';
                        handleSuccess(status, text);
                    },
                    onerror: (error) => reject(new Error(error?.status ? `Request failed: ${error.status}` : 'Request failed'))
                });
            });
        };

        const postJson = (url, data) => {
            const headers = { 'Content-Type': 'application/json' };
            return requestRaw({ url, headers, body: JSON.stringify(data || {}), expectJson: true });
        };

        const postForm = (url, data) => {
            const params = new URLSearchParams();
            Object.entries(data || {}).forEach(([k, v]) => {
                if (typeof v !== 'undefined' && v !== null) params.append(k, v);
            });
            const headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
            return requestRaw({ url, headers, body: params.toString(), expectJson: true });
        };

        const mintCustomToken = async ({ tornApiKey, tornId, factionId, version }) => {
            if (!CUSTOM_TOKEN_URL) throw new Error('Custom auth endpoint not configured.');
            const response = await postJson(CUSTOM_TOKEN_URL, { tornApiKey, tornId, factionId, version });
            if (!response || !response.customToken) throw new Error('Custom token missing in response.');
            return response;
        };

        const exchangeCustomToken = async (customToken) => {
            if (!SIGN_IN_ENDPOINT) throw new Error('Firebase API key missing.');
            const response = await postJson(SIGN_IN_ENDPOINT, { token: customToken, returnSecureToken: true });
            if (!response || !response.idToken || !response.refreshToken) throw new Error('Failed to exchange Firebase custom token.');
            return response;
        };

        const refreshIdToken = async (refreshToken) => {
            if (!REFRESH_ENDPOINT) throw new Error('Firebase refresh endpoint unavailable.');
            const response = await postForm(REFRESH_ENDPOINT, { grant_type: 'refresh_token', refresh_token: refreshToken });
            if (!response || !response.id_token || !response.refresh_token) throw new Error('Failed to refresh Firebase token.');
            const refreshed = {
                idToken: response.id_token,
                refreshToken: response.refresh_token,
                expiresAt: computeExpiresAt(response.expires_in),
                keySnippet: session?.keySnippet || null,
                keyHash: session?.keyHash || null,
                tornId: response.user_id || session?.tornId || null,
                factionId: session?.factionId || null
            };
            setSession(refreshed);
            return refreshed;
        };

        const signIn = async ({ tornApiKey, tornId, factionId, version } = {}) => {
            if (!tornApiKey || !tornId) throw new Error('Missing Torn API details for sign-in.');
            const snippet = keySnippet(tornApiKey);
            if (session && session.keySnippet === snippet) {
                try {
                    const idToken = await ensureIdToken({ allowAutoSignIn: false });
                    if (idToken) return session;
                } catch (_) { /* proceed to full sign-in */ }
            }
            const minted = await mintCustomToken({ tornApiKey, tornId, factionId, version });
            const exchanged = await exchangeCustomToken(minted.customToken);
            const nextSession = {
                idToken: exchanged.idToken,
                refreshToken: exchanged.refreshToken,
                expiresAt: computeExpiresAt(exchanged.expiresIn),
                keySnippet: snippet,
                keyHash: minted.keyHash || null,
                tornId: minted?.user?.tornId || tornId,
                factionId: minted?.user?.factionId || factionId || null
            };
            setSession(nextSession);
            return nextSession;
        };

        const ensureIdToken = async ({ allowAutoSignIn = false } = {}) => {
            if (session && session.idToken && session.expiresAt && (now() + 60000) < session.expiresAt) {
                return session.idToken;
            }
            if (session && session.refreshToken) {
                try {
                    const refreshed = await refreshIdToken(session.refreshToken);
                    return refreshed.idToken;
                } catch (error) {
                    if (api.isVersionUpdateError?.(error)) {
                        // Version toast already surfaced via API helper; stop initialization here.
                        return false;
                    }
                    ui.showMessageBox(`API Key Error: ${error.message}. Please check your key.`, "error");
                }
            }
            if (allowAutoSignIn && state.user?.actualTornApiKey && state.user?.tornId) {
                const signedIn = await signIn({
                    tornApiKey: state.user.actualTornApiKey,
                    tornId: state.user.tornId,
                    factionId: state.user.factionId,
                    version: config.VERSION
                });
                return signedIn.idToken;
            }
            return null;
        };

        return { signIn, ensureIdToken, clearSession };
    })();

    // ================================================================
    // Reactive wrappers for dibsData and medDeals to automatically emit
    // ================================================================

    // Debounced storage helpers
    const schedulePersistDibsData = utils.debounce((data) => {
        try { storage.set('dibsData', data); } catch(_) {}
    }, 1000);

    const schedulePersistMedDeals = utils.debounce((data) => {
        try { storage.set('medDeals', data); } catch(_) {}
    }, 1000);

    function setDibsData(next, meta={}) {
        if (!Array.isArray(next)) next = [];
        const prevFp = state._fingerprints?.dibs || null;
        state.dibsData = next;
        schedulePersistDibsData(next);
        // If the mutation is authoritative (local create/update/remove) and a new computed fingerprint supplied, persist it
        if (meta && meta.fingerprint) {
            try {
                state._fingerprints = state._fingerprints || {};
                state._fingerprints.dibs = meta.fingerprint;
                storage.set('fingerprints', state._fingerprints);
            } catch(_) {}
        }
        if (meta?.fingerprint && meta.fingerprint !== prevFp) {
            try {
                state._fingerprintsMeta = state._fingerprintsMeta || {}; state._fingerprintsMeta.dibsChangedAt = Date.now();
                document.dispatchEvent(new CustomEvent('tdm:dibsFingerprintChanged', { detail: { fingerprint: meta.fingerprint, previous: prevFp } }));
            } catch(_) {}
        }
        events.emit('dibs:update', { data: next, meta });
        // Opportunistic badge refresh (debounced via orchestrator if heavy churn later)
        try { ui.updateDibsDealsBadge?.(); } catch(_) {}
        try { ui.updateDebugOverlayFingerprints?.(); } catch(_) {}
    }
    function patchDibs(updater, meta={}) {
        try {
            const cur = Array.isArray(state.dibsData) ? state.dibsData.slice() : [];
            const result = updater(cur) || cur;
            // Auto-compute fingerprint if not provided
            if (!meta.fingerprint) {
                try { meta.fingerprint = utils.computeDibsFingerprint(result); } catch(_) {}
            }
            setDibsData(result, meta);
        } catch(e) { /* noop */ }
    }
    function setMedDeals(next, meta={}) {
        if (!next || typeof next !== 'object') next = {};
        const prevFp = state._fingerprints?.medDeals || null;
        
        // Debug logging for setMedDeals
        if (state.debug?.apiLogs) {
            const prevCount = Object.keys(state.medDeals || {}).length;
            const nextCount = Object.keys(next || {}).length;
            
            tdmlogger('debug', '[setMedDeals] Called with', {
                dataCount: nextCount,
                fingerprint: meta?.fingerprint,
                prevFp
            });
            
            // Debug for critical transitions
            if (prevCount === 0 && nextCount > 0) {
                tdmlogger('debug', '[setMedDeals] Empty to non-empty transition', {
                    prevCount,
                    nextCount,
                    fingerprint: meta.fingerprint,
                    source: meta.source || 'backend'
                });
            }
        }
        
        state.medDeals = next;
        
        schedulePersistMedDeals(next);
        if (meta && typeof meta.fingerprint !== 'undefined') {
            try {
                state._fingerprints = state._fingerprints || {};
                state._fingerprints.medDeals = meta.fingerprint;
                storage.set('fingerprints', state._fingerprints);
            } catch(_) {}
        }
        if (meta?.fingerprint && meta.fingerprint !== prevFp) {
            try {
                state._fingerprintsMeta = state._fingerprintsMeta || {}; state._fingerprintsMeta.medDealsChangedAt = Date.now();
                document.dispatchEvent(new CustomEvent('tdm:medDealsFingerprintChanged', { detail: { fingerprint: meta.fingerprint, previous: prevFp } }));
            } catch(_) {}
        }
        events.emit('medDeals:update', { data: next, meta });
        try { ui.updateDibsDealsBadge?.(); } catch(_) {}
        try { ui.updateDebugOverlayFingerprints?.(); } catch(_) {}
    }
    function patchMedDeals(updater, meta={}) {
        try {
            const cur = (state.medDeals && typeof state.medDeals === 'object') ? { ...state.medDeals } : {};
            const result = updater(cur) || cur;
            if (!meta.fingerprint) {
                try { meta.fingerprint = utils.computeMedDealsFingerprint(result); } catch(_) {}
            }
            setMedDeals(result, meta);
        } catch(e) { /* noop */ }
    }
    // Expose mutation helpers (non-breaking; future code can migrate to use these)
    state._mutate = Object.assign(state._mutate || {}, { setDibsData, patchDibs, setMedDeals, patchMedDeals });

    // Example consumer: prewire badge & overlay updates when listeners first attach
    events.on('dibs:update', (p)=>{ if (state.debug?.apiLogs) console.debug('[TDM events] dibs:update', p?.meta); });
    events.on('medDeals:update', (p)=>{ if (state.debug?.apiLogs) console.debug('[TDM events] medDeals:update', p?.meta); });

    const scheduleUnifiedStatusSnapshotSave = utils.debounce(() => {
        try {utils.saveUnifiedStatusSnapshot();} catch (_) { /* ignored */ }
    }, 500);

    //======================================================================
    // 4. API MODULE
    //======================================================================

    // Ranked War Storage V2 Notes:
    //   - Fast bootstrap via recent window (window JSON) or full (snapshot+delta) from backend callable getRankedWarBundle.
    //   - api.fetchRankedWarAttacksV2Enhanced caches attacks and marks entry.v2; old chunk aggregation paths will be phased out.
    //   - config.ENABLE_STORAGE_V2 must be true to attempt bootstrap; fallback logic remains for legacy mode.
    //   - To rollback: set ENABLE_STORAGE_V2=false and remove fetchRankedWarAttacksV2Enhanced usage in getRankedWarAttacksSmart.

    const rateLimitMeta = { lastLogMs: 0, lastSkipLogMs: 0 };

    const api = {
        _markIpRateLimited(context = {}) {
            const now = Date.now();
            const baseMs = Number(config.IP_BLOCK_COOLDOWN_MS) || 300000;
            const jitterMax = Number(config.IP_BLOCK_COOLDOWN_JITTER_MS) || 0;
            const jitter = jitterMax > 0 ? Math.floor(Math.random() * Math.max(1, jitterMax)) : 0;
            const until = now + baseMs + jitter;
            try {
                state.script = state.script || {};
                const prev = Number(state.script.ipRateLimitUntilMs) || 0;
                if (!prev || until > prev) {
                    state.script.ipRateLimitUntilMs = until;
                }
                state.script.ipRateLimitLastReason = context.reason || context.code || context.message || null;
                state.script.ipRateLimitLastAction = context.action || null;
                state.script.ipRateLimitLastSeenAt = now;
                state.script.lastFactionRefreshSkipReason = 'ip-blocked';
            } catch(_) { /* ignore state issues */ }
            const logInterval = Number(config.IP_BLOCK_LOG_INTERVAL_MS) || 30000;
            if (!Number.isFinite(rateLimitMeta.lastLogMs) || (now - rateLimitMeta.lastLogMs) >= logInterval) {
                rateLimitMeta.lastLogMs = now;
                const remainingSec = Math.max(0, Math.round((until - now) / 1000));
                try {
                    tdmlogger('warn', `[Cadence] Backend IP rate limit detected; pausing API calls for ~${remainingSec}s${context.action ? ` (action=${context.action})` : ''}.`);
                } catch(_) { /* logging best-effort */ }
            }
            try { ui.updateApiCadenceInfo?.(); } catch(_) { /* UI update best-effort */ }
        },
        isIpRateLimited() {
            try {
                state.script = state.script || {};
                const until = Number(state.script.ipRateLimitUntilMs) || 0;
                if (!until) return false;
                if (Date.now() >= until) {
                    state.script.ipRateLimitUntilMs = 0;
                    state.script.ipRateLimitLastReason = null;
                    state.script.ipRateLimitLastAction = null;
                    return false;
                }
                return true;
            } catch(_) {
                return false;
            }
        },
        getIpRateLimitRemainingMs() {
            try {
                state.script = state.script || {};
                const until = Number(state.script.ipRateLimitUntilMs) || 0;
                if (!until) return 0;
                return Math.max(0, until - Date.now());
            } catch(_) {
                return 0;
            }
        },
        verifyFFScouterKey: (key) => {
            return new Promise((resolve) => {
                if (!key) return resolve({ ok: false, message: 'No key provided' });
                if (!state.gm.rD_xmlhttpRequest) return resolve({ ok: false, message: 'Script not fully initialized' });
                const url = `https://ffscouter.com/api/v1/check-key?key=${encodeURIComponent(key)}`;
                tdmlogger('info', `[FFScouter Verify] Verifying key at ${url}`);
                try {
                    state.gm.rD_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        timeout: 10000,
                        onload: (resp) => {
                            if (resp.status === 429) {
                                resolve({ ok: false, message: 'Rate limited (429) - Please wait' });
                                return;
                            }
                            try {
                                const json = JSON.parse(resp.responseText);
                                if (json && json.is_registered === true) {
                                    resolve({ ok: true, message: 'Key verified' });
                                } else {
                                    try { tdmlogger('warn', '[FFScouter Verify] Key rejected', json); } catch(_) {}
                                    resolve({ ok: false, message: 'Key invalid or not registered' });
                                }
                            } catch (e) {
                                try { tdmlogger('error', '[FFScouter Verify] Invalid response', { error: e.message, responseText: resp.responseText, status: resp.status }); } catch(_) {}
                                resolve({ ok: false, message: 'Invalid response from FFScouter' });
                            }
                        },
                        onerror: (err) => {
                            try { console.error('[TDM] FFScouter verify error:', err); } catch(_) {}
                            resolve({ ok: false, message: 'Network error checking key' });
                        },
                        ontimeout: () => {
                            resolve({ ok: false, message: 'Request timed out' });
                        }
                    });
                } catch (e) {
                    resolve({ ok: false, message: 'Request failed: ' + e.message });
                }
            });
        },
        fetchFFScouterStats: (playerIds) => {
            // tdmlogger('info', `[FFScouter Fetch] Requested stats for ${Array.isArray(playerIds) ? playerIds.length : '1'} players`);
            return new Promise((resolve) => {
                const key = storage.get('ffscouterApiKey', null);
                if (!key) return resolve({ ok: false, message: 'No API key' });
                if (!playerIds || !playerIds.length) return resolve({ ok: true, data: {} });

                // Initialize pending and attempted sets
                state.ffscouterPending = state.ffscouterPending || new Set();
                state.ffscouterAttempted = state.ffscouterAttempted || new Set();

                // Filter out IDs we've fetched recently (e.g. last 5 minutes) to avoid spamming
                const now = Date.now();
                const idsToFetch = [];
                const ids = Array.isArray(playerIds) ? playerIds : [playerIds];
                
                ids.forEach(id => {
                    const sid = String(id);
                    
                    // Skip if currently pending or already attempted this session
                    if (state.ffscouterPending.has(sid) || state.ffscouterAttempted.has(sid)) return;

                    let cached = state.ffscouterCache?.[sid];

                    // If not in memory, check storage (using new shared key format)
                    if (!cached) {
                        try {
                            const raw = localStorage.getItem('ffscouterv2-' + sid);
                            if (raw) {
                                cached = JSON.parse(raw);
                                if (cached) {
                                    state.ffscouterCache = state.ffscouterCache || {};
                                    state.ffscouterCache[sid] = cached;
                                }
                            }
                        } catch(_) {}
                    }

                    // Fetch if not cached or expired
                    // Check 'expiry' (new format) or fallback to 5 min TTL for old format
                    const isExpired = cached ? (cached.expiry ? (now > cached.expiry) : ((now - (cached.lastUpdated || 0)) > 300000)) : true;

                    if (!cached || isExpired) {
                        idsToFetch.push(sid);
                        state.ffscouterPending.add(sid); // Mark as pending immediately
                        state.ffscouterAttempted.add(sid); // Mark as attempted to prevent retry loops
                    }
                });

                if (!idsToFetch.length) return resolve({ ok: true, count: 0, message: 'All cached, pending, or attempted' });

                // Chunk into batches of 200 to avoid URL length issues or 400s
                const chunks = [];
                while (idsToFetch.length) {
                    chunks.push(idsToFetch.splice(0, 200));
                }

                let totalProcessed = 0;
                
                // Process chunks sequentially
                const processChunk = async (chunk) => {
                    return new Promise(resolveChunk => {
                        const cleanup = () => {
                            chunk.forEach(id => state.ffscouterPending.delete(String(id)));
                            resolveChunk();
                        };

                        try {
                            // Don't encode the commas, some APIs prefer literal commas for lists
                            const url = `https://ffscouter.com/api/v1/get-stats?key=${encodeURIComponent(key)}&targets=${chunk.join(',')}`;
                            tdmlogger('info', `[FFScouter Fetch] Fetching stats for ${chunk.length} players`);
                            state.gm.rD_xmlhttpRequest({
                                method: 'GET',
                                url: url,
                                timeout: 10000,
                                onload: (resp) => {
                                    try {
                                        const json = JSON.parse(resp.responseText);
                                        const nowMs = Date.now();
                                        
                                        // Handle array response (current API behavior)
                                        if (Array.isArray(json)) {
                                            state.ffscouterCache = state.ffscouterCache || {};
                                            
                                            json.forEach(data => {
                                                const pid = String(data.player_id);
                                                const record = {
                                                    value: data.fair_fight,
                                                    last_updated: Math.floor(nowMs / 1000),
                                                    expiry: nowMs + 3600000, // 1 hour expiry
                                                    bs_estimate: data.bs_estimate,
                                                    bs_estimate_human: data.bs_estimate_human
                                                };
                                                state.ffscouterCache[pid] = record;
                                                try {
                                                    localStorage.setItem('ffscouterv2-' + pid, JSON.stringify(record));
                                                } catch(_) {}
                                            });
                                            totalProcessed += json.length;
                                            tdmlogger('info', `[FFScouter Fetch] Successfully cached ${json.length} players`);
                                        } 
                                        // Handle legacy/alternative object response
                                        else if (json && json.success) {
                                            const results = json.data || {};
                                            state.ffscouterCache = state.ffscouterCache || {};
                                            
                                            Object.entries(results).forEach(([pid, data]) => {
                                                const record = {
                                                    value: data.ff || data.fair_fight,
                                                    last_updated: Math.floor(nowMs / 1000),
                                                    expiry: nowMs + 3600000, // 1 hour expiry
                                                    bs_estimate: data.estimate || data.bs_estimate,
                                                    bs_estimate_human: data.estimate_text || data.bs_estimate_human
                                                };
                                                state.ffscouterCache[pid] = record;
                                                try {
                                                    localStorage.setItem('ffscouterv2-' + pid, JSON.stringify(record));
                                                } catch(_) {}
                                            });
                                            totalProcessed += Object.keys(results).length;
                                        } else {
                                            tdmlogger('warn', `[FFScouter Fetch] API Error: ${json?.message || 'Unknown error'}`, json);
                                        }
                                    } catch (e) { 
                                        tdmlogger('error', `[FFScouter Fetch] Parse error: ${e.message}`, { response: resp.responseText });
                                    }
                                    cleanup();
                                },
                                onerror: () => {
                                    tdmlogger('warn', '[FFScouter Fetch] Network error');
                                    cleanup();
                                },
                                ontimeout: () => {
                                    tdmlogger('warn', '[FFScouter Fetch] Timeout');
                                    cleanup();
                                }
                            });
                        } catch (e) { cleanup(); }
                    });
                };

                // Execute all chunks
                (async () => {
                    for (const chunk of chunks) {
                        await processChunk(chunk);
                    }
                    // Trigger UI refresh if we got new data
                    if (totalProcessed > 0) {
                        try { ui.queueLevelOverlayRefresh({ reason: 'ffscouter-update' }); } catch(_) {}
                    }
                    resolve({ ok: true, count: totalProcessed });
                })();
            });
        },
        _shouldBailDueToIpRateLimit(contextLabel) {
            if (!this.isIpRateLimited()) return false;
            const now = Date.now();
            const logInterval = Number(config.IP_BLOCK_LOG_INTERVAL_MS) || 30000;
            if (!Number.isFinite(rateLimitMeta.lastSkipLogMs) || (now - rateLimitMeta.lastSkipLogMs) >= logInterval) {
                rateLimitMeta.lastSkipLogMs = now;
                const remainingSec = Math.max(0, Math.round(this.getIpRateLimitRemainingMs() / 1000));
                try {
                    tdmlogger('info', `[Cadence] Skip ${contextLabel || 'API call'}; backend IP block cool-down ~${remainingSec}s remaining.`);
                } catch(_) { /* logging best-effort */ }
            }
            return true;
        },
        _maybeHandleRateLimitError(error, context = {}) {
            if (!error) return false;
            const status = Number(error.status || error.statusCode || error.httpStatus || 0);
            const codeRaw = error.code || error.status;
            const code = typeof codeRaw === 'string' ? codeRaw.toLowerCase() : '';
            const msg = typeof error.message === 'string' ? error.message.toLowerCase() : '';
            const isResourceExhausted = code === 'resource-exhausted' || code === 'resource_exhausted';
            const isHttp429 = status === 429;
            const mentionsIpLimit = msg.includes('too many requests') && msg.includes('ip');
            if (isResourceExhausted || isHttp429 || mentionsIpLimit) {
                this._markIpRateLimited({ ...context, code: codeRaw || status || null, message: error.message || null });
                error.isIpRateLimited = true;
                return true;
            }
            return false;
        },
        isVersionUpdateError: (err) => {
            if (!err) return false;
            const safeLower = (value) => (typeof value === 'string' ? value.toLowerCase() : '');
            const code = safeLower(err.code || err.status || '');
            const reasonCandidates = [
                err.reason,
                err.errorReason,
                err.error_reason,
                err.details?.reason,
                err.details?.errorReason
            ];
            const reason = safeLower(reasonCandidates.find((r) => typeof r === 'string' && r) || '');
            const message = typeof err.message === 'string' ? err.message : '';
            const messageHasVersionHint = /userscript is outdated|update required|client version|new version/i.test(message);
            const reasonHasVersionHint = /outdated|version|update/.test(reason);
            const hasExplicitVersion = !!(err.minVersion || err.requiredVersion || err.minimumVersion);
            if (hasExplicitVersion || reason === 'client_version_outdated') return true;
            if (messageHasVersionHint || reasonHasVersionHint) return true;
            if ((code === 'failed-precondition' || code === 'failed_precondition')) {
                // Only treat failed-precondition as version-related when paired with explicit hints.
                const updateFlag = err.updateRequired === true || err.clientVersionOutdated === true;
                const detailsFlag = err.details && (err.details.updateRequired === true || err.details.clientVersionOutdated === true);
                return Boolean(updateFlag || detailsFlag);
            }
            return false;
        },
        _extractVersionFromMessage: (message) => {
            if (!message) return null;
            const text = String(message);
            const strict = text.match(/version\s+([0-9]+(?:\.[0-9]+){1,3})/i);
            if (strict && strict[1]) return strict[1].trim();
            const loose = text.match(/([0-9]+\.[0-9]+\.[0-9]+)/);
            if (loose && loose[1]) return loose[1].trim();
            return null;
        },
        _markUpdateAvailable: (candidateVersion) => {
            if (!candidateVersion) return;
            try {
                if (!state.script) state.script = {};
                const prev = state.script.updateAvailableLatestVersion || null;
                if (!prev || utils.compareVersions(prev, candidateVersion) < 0) {
                    state.script.updateAvailableLatestVersion = candidateVersion;
                    try { storage.set('lastKnownLatestVersion', candidateVersion); } catch(_) { /* noop */ }
                }
            } catch(_) {
                try { state.script.updateAvailableLatestVersion = candidateVersion; } catch(__) { /* noop */ }
            }
            try { ui.updateSettingsButtonUpdateState(); } catch(_) { /* noop */ }
        },
        _handleVersionOutdatedError: (err, context = {}) => {
            if (!api.isVersionUpdateError(err)) return false;
            try { state.script = state.script || {}; } catch(_) { /* noop */ }
            const minVersion = err.minVersion || err.requiredVersion || err.minimumVersion || api._extractVersionFromMessage(err.message);
            if (minVersion) {
                api._markUpdateAvailable(minVersion);
            } else if (!state.script.updateAvailableLatestVersion) {
                // Ensure the settings button still reflects update-needed state even without explicit version detail
                try {
                    const fallbackVersion = `${config.VERSION || '0.0.0'}.999`;
                    state.script.updateAvailableLatestVersion = fallbackVersion;
                    ui.updateSettingsButtonUpdateState();
                } catch(_) { /* noop */ }
            }
            const updateUrl = err.updatePageUrl || err.updateUrl || config.GREASYFORK?.pageUrl || config.GREASYFORK?.downloadUrl;
            if (updateUrl) {
                try { state.script.updateAvailableLatestVersionUrl = updateUrl; } catch(_) { /* noop */ }
            }
            if (!state.script.versionBlockShown) {
                state.script.versionBlockShown = true;
                const minLabel = minVersion || 'latest';
                const baseMessage = err.message && err.message.length < 160 ? err.message : `Your TreeDibsMapper userscript is out of date.`;
                const prompt = `${baseMessage}\nUpdate required: v${minLabel} or newer.${updateUrl ? `\nClick to open update: ${updateUrl}` : ''}`;
                try {
                    ui.showMessageBox(prompt, 'error', 15000, updateUrl ? () => {
                        try { window.open(updateUrl, '_blank', 'noopener'); } catch(_) { /* noop */ }
                    } : null);
                } catch(_) { /* noop */ }
            }
            return true;
        },
        // Build a safe selection list for Torn faction endpoint (always exclude 'attacks' on client)
        buildSafeFactionSelections: (wantList, forFactionId) => {
            try {
                const desired = (wantList || []).map(s => String(s).trim()).filter(Boolean);
                const filtered = desired.filter(s => s !== '');
                // Unconditionally exclude 'attacks' to avoid permission errors and reduce churn
                const pruned = filtered.filter(s => s.toLowerCase() !== 'attacks');
                // Track per-faction last requested selections
                try {
                    state.session.selectionsPerFaction = state.session.selectionsPerFaction || {};
                    state.session.selectionsPerFaction[String(forFactionId||'self')] = pruned.slice();
                } catch(_) {}
                return pruned;
            } catch(_) {
                return (wantList || []).filter(Boolean);
            }
        },
        _call: async (method, url, action, params = {}) => {
            if (!state.user.tornId) {
                throw new Error(`User context missing for Firebase ${method}.`);
            }
            const defaultFaction = state?.user?.factionId ? { factionId: state.user.factionId } : {};
            let idToken = null;
            try {
                idToken = await firebaseAuth.ensureIdToken({ allowAutoSignIn: true });
            } catch (authError) {
                try { tdmlogger('warn', `[Auth] ensureIdToken failed: ${authError.message}`); } catch(_) {}
            }
            if (!idToken) {
                throw new Error('Unable to obtain Firebase ID token. Please reauthenticate.');
            }
            const payload = { action, tornId: state.user.tornId, version: config.VERSION, clientTimestamps: state.dataTimestamps, ...defaultFaction, ...params };
            const requestBody = { data: payload };
            const headers = { 'Content-Type': 'application/json' };
            headers.Authorization = `Bearer ${idToken}`;

            return await new Promise((resolve, reject) => {
                const rejectWith = (error) => {
                    try { api._maybeHandleRateLimitError(error, { action, params, method, url }); } catch(_) { /* noop */ }
                    try { api._handleVersionOutdatedError?.(error, { action, params, method, url }); } catch(_) { /* noop */ }
                    reject(error);
                };

                // Perform request with retries on transient errors (401 handled by refreshing idToken).
                const MAX_ATTEMPTS = 4;
                let attempt = 0;

                const transientStatusCodes = new Set([408, 429, 500, 502, 503, 504]);

                const doRequest = async () => {
                    attempt += 1;
                    // Ensure Authorization header is current
                    headers.Authorization = `Bearer ${idToken}`;
                    const ret = state.gm.rD_xmlhttpRequest({
                        method: 'POST',
                        url,
                        headers,
                        data: JSON.stringify(requestBody),
                        onload: async function(response) {
                            const raw = typeof response?.responseText === 'string' ? response.responseText : '';
                            const statusCode = Number(response?.status || 0);
                            // If we received HTTP 401 and we haven't retried yet, attempt to refresh the token and retry.
                            if (statusCode === 401 && attempt < MAX_ATTEMPTS) {
                                try {
                                    const newToken = await firebaseAuth.ensureIdToken({ allowAutoSignIn: true });
                                    if (newToken && newToken !== idToken) {
                                        idToken = newToken;
                                        // try again immediately
                                        return doRequest();
                                    }
                                } catch (e) {
                                    // fallthrough to parsing/error handling below
                                }
                            }

                            // If we received a transient HTTP error (timeout/rate-limit/server error), retry with backoff
                            if (transientStatusCodes.has(statusCode) && attempt < MAX_ATTEMPTS) {
                                try {
                                    const baseMs = 250;
                                    const backoff = Math.pow(2, attempt - 1) * baseMs;
                                    const jitter = Math.floor(Math.random() * 200);
                                    await new Promise(r => setTimeout(r, backoff + jitter));
                                    return doRequest();
                                } catch (e) {
                                    // fallthrough to parse/reject below
                                }
                            }

                            // If there is no body, treat as error
                            const isHttpError = statusCode >= 400;
                            if (!raw || raw.trim() === '') {
                                return rejectWith(new Error(`Empty or invalid response from server (${method}).`));
                            }
                            try {
                                const payload = JSON.parse(raw);
                                try {
                                    const _metaCalls = payload?.result?.meta?.userKeyApiCalls ?? payload?.meta?.userKeyApiCalls;
                                    const add = Number(_metaCalls) || 0;
                                    if (add > 0) utils.incrementBackendApiCalls(add);
                                } catch (_) { /* ignore */ }
                                if (payload?.result?.status === 'success' && 'data' in payload.result) return resolve(payload.result.data);
                                if (payload?.status === 'success' && 'data' in payload) return resolve(payload.data);
                                const fbError = payload?.error || payload?.result?.error || payload?.data?.error;
                                if (fbError) {
                                    const err = new Error(fbError.message || 'Firebase error');
                                    if (fbError.status) err.code = fbError.status;
                                    if (typeof fbError.httpStatus === 'number') err.status = fbError.httpStatus;
                                    if (typeof fbError.statusCode === 'number' && typeof err.status === 'undefined') err.status = fbError.statusCode;
                                    try {
                                        if (fbError.details && typeof fbError.details === 'object') {
                                            Object.assign(err, fbError.details);
                                            if (fbError.details.tornError) err.tornError = fbError.details.tornError;
                                            if (typeof fbError.details.tornErrorCode !== 'undefined') err.tornErrorCode = fbError.details.tornErrorCode;
                                            if (typeof fbError.details.tornErrorMessage !== 'undefined') err.tornErrorMessage = fbError.details.tornErrorMessage;
                                            if (!err.tornErrorMessage && fbError.details.tornError && fbError.details.tornError.error) {
                                                err.tornErrorMessage = fbError.details.tornError.error;
                                            }
                                        }
                                    } catch(_) { /* noop */ }
                                    if (typeof err.tornErrorCode !== 'undefined' && err.tornErrorMessage) err.message = `Torn API error ${err.tornErrorCode}: ${err.tornErrorMessage}`;
                                    else if (err.tornErrorMessage) err.message = `Torn API error: ${err.tornErrorMessage}`;
                                    return rejectWith(err);
                                }
                                if (payload?.result && 'data' in payload.result) return resolve(payload.result.data);
                                if (isHttpError) {
                                    const err = new Error(`Request failed (${statusCode}): ${raw.slice(0, 200)}`);
                                    if (typeof statusCode === 'number') { err.status = statusCode; err.httpStatus = statusCode; }
                                    return rejectWith(err);
                                }
                                return resolve(payload);
                            } catch (e) {
                                return rejectWith(new Error(`Failed to parse Firebase API response: ${e.message}`));
                            }
                        },
                        onerror: async (error) => {
                            // Treat network errors as transient and retry with backoff when possible
                            if (attempt < MAX_ATTEMPTS) {
                                try {
                                    const baseMs = 250;
                                    const backoff = Math.pow(2, attempt - 1) * baseMs;
                                    const jitter = Math.floor(Math.random() * 200);
                                    await new Promise(r => setTimeout(r, backoff + jitter));
                                    return doRequest();
                                } catch (_) { /* fallthrough to reject below */ }
                            }
                            const err = new Error(`Firebase API request failed: Status ${error?.status || 'Unknown'}`);
                            if (error && typeof error.status !== 'undefined') err.status = error.status;
                            rejectWith(err);
                        }
                    });
                    if (ret && typeof ret.catch === 'function') ret.catch(() => {});
                };

                // Kick off first request
                try { doRequest(); } catch (e) { rejectWith(e); }
            });
        },
        // Cross-tab GET coalescing: use BroadcastChannel when available, fallback to localStorage notify/cache.
        // This reduces duplicate identical GET requests from multiple open pages/tabs.
        _crossTabGetHelper: (function(){
            const bcSupported = typeof BroadcastChannel === 'function';
            const bc = bcSupported ? new BroadcastChannel('tdm:crossget') : null;
            const TAB_ID = Math.random().toString(36).slice(2);
            const pending = new Map();
            const CACHE_PREFIX = 'tdm:crossget:cache:';
            const WAIT_MS = 120; // wait briefly for another tab to respond
            const CACHE_TTL_MS = Number((window.__TDM_CROSSGET_TTL_MS__) || 5000);

            function makeKey(action, params) {
                try {
                    // Normalize param keys order for stable key
                    const p = params && typeof params === 'object' && !Array.isArray(params) ? params : {};
                    const ordered = {};
                    Object.keys(p).sort().forEach(k => { ordered[k] = p[k]; });
                    return `${action}|${JSON.stringify(ordered)}`;
                } catch (e) { return `${action}|${String(params)}`; }
            }

            function invalidateKey(action, params) {
                try {
                    const exact = makeKey(action, params || {});
                    const prefix = action + '|';
                    // remove exact and prefix matches from localStorage
                    try {
                        for (let i = localStorage.length - 1; i >= 0; i--) {
                            const k = localStorage.key(i);
                            if (!k) continue;
                            if (k.indexOf(CACHE_PREFIX) !== 0) continue;
                            const bare = k.slice(CACHE_PREFIX.length);
                            if (bare === exact || bare.indexOf(prefix) === 0) {
                                try { localStorage.removeItem(k); } catch(_){}
                            }
                        }
                    } catch(_){}
                    // broadcast invalidation
                    if (bc) {
                        try { bc.postMessage({ type: 'tdm:GET:invalidate', action, key: exact, sender: TAB_ID }); } catch(_) {}
                    } else {
                        try { localStorage.setItem(CACHE_PREFIX + 'invalidate:' + Date.now(), JSON.stringify({ action, key: exact, ts: Date.now() })); } catch(_) {}
                    }
                } catch (_) {}
            }

            // Global invalidation listener: clear local cache entries when peers broadcast invalidation
            if (bc) {
                try {
                    bc.addEventListener('message', (ev) => {
                        try {
                            const msg = ev?.data;
                            if (!msg) return;
                            if (msg.type === 'tdm:GET:invalidate') {
                                const action = msg.action;
                                const key = msg.key;
                                try {
                                    for (let i = localStorage.length - 1; i >= 0; i--) {
                                        const k = localStorage.key(i);
                                        if (!k) continue;
                                        if (k.indexOf(CACHE_PREFIX) !== 0) continue;
                                        const bare = k.slice(CACHE_PREFIX.length);
                                        if (bare === key || (action && bare.indexOf(action + '|') === 0)) {
                                            try { localStorage.removeItem(k); } catch(_){}
                                        }
                                    }
                                } catch(_){}
                            }
                        } catch(_){}
                    });
                } catch(_){}
            }
            // Listen for localStorage-based invalidation fallback
            try {
                window.addEventListener('storage', (ev) => {
                    try {
                        if (!ev || !ev.key) return;
                        if (ev.key.indexOf(CACHE_PREFIX + 'invalidate:') !== 0) return;
                        const raw = ev.newValue;
                        if (!raw) return;
                        let msg = null;
                        try { msg = JSON.parse(raw); } catch(_) { msg = null; }
                        const action = msg?.action || null;
                        const key = msg?.key || null;
                        try {
                            for (let i = localStorage.length - 1; i >= 0; i--) {
                                const k = localStorage.key(i);
                                if (!k) continue;
                                if (k.indexOf(CACHE_PREFIX) !== 0) continue;
                                const bare = k.slice(CACHE_PREFIX.length);
                                if (key && bare === key) { try { localStorage.removeItem(k); } catch(_){} }
                                else if (action && bare.indexOf(action + '|') === 0) { try { localStorage.removeItem(k); } catch(_){} }
                            }
                        } catch(_){}
                    } catch(_){}
                });
            } catch(_){}

            const crossTabGet = async function crossTabGet(action, params, performFn) {
                const key = makeKey(action, params || {});
                if (pending.has(key)) return pending.get(key);

                // Check local cache first
                try {
                    const raw = localStorage.getItem(CACHE_PREFIX + key);
                    if (raw) {
                        const obj = JSON.parse(raw);
                        if (obj && (Date.now() - (obj.ts || 0) < CACHE_TTL_MS)) {
                            return Promise.resolve(obj.data);
                        }
                    }
                } catch (_) {}

                const p = new Promise((resolve, reject) => {
                    let resolved = false;

                    const onMessage = (ev) => {
                        try {
                            const msg = ev?.data;
                            if (!msg || msg.type !== 'tdm:GET:response' || msg.key !== key) return;
                            if (msg.sender === TAB_ID) return; // ignore our own
                            resolved = true;
                            resolve(msg.data);
                            cleanup();
                        } catch (e) { /* ignore */ }
                    };

                    const onStorage = (ev) => {
                        try {
                            if (ev.key !== (CACHE_PREFIX + key) || !ev.newValue) return;
                            const obj = JSON.parse(ev.newValue);
                            if (obj && (Date.now() - (obj.ts || 0) < CACHE_TTL_MS)) {
                                resolved = true;
                                resolve(obj.data);
                                cleanup();
                            }
                        } catch (_) {}
                    };

                    function cleanup() {
                        try { if (bc) bc.removeEventListener('message', onMessage); } catch(_){}
                        try { window.removeEventListener('storage', onStorage); } catch(_){}
                        pending.delete(key);
                    }

                    if (bc) bc.addEventListener('message', onMessage);
                    window.addEventListener('storage', onStorage);

                    // After a short wait, if no other tab answered, perform the request ourselves
                    const timer = setTimeout(async () => {
                        if (resolved) return;
                        try {
                            const result = await performFn();
                            try {
                                const cacheObj = { ts: Date.now(), data: result };
                                try { localStorage.setItem(CACHE_PREFIX + key, JSON.stringify(cacheObj)); } catch(_){}
                                if (bc) {
                                    try { bc.postMessage({ type: 'tdm:GET:response', key, data: result, sender: TAB_ID }); } catch(_){}
                                }
                            } catch(_){}
                            if (!resolved) {
                                resolved = true;
                                resolve(result);
                                cleanup();
                            }
                        } catch (err) {
                            if (!resolved) {
                                resolved = true;
                                reject(err);
                                cleanup();
                            }
                        }
                    }, WAIT_MS);
                });

                // expose invalidate helper on the returned function
                try { p.invalidate = invalidateKey; } catch(_){}
                pending.set(key, p);
                return p;
            };

            try { crossTabGet.invalidate = invalidateKey; } catch(_){}
            return crossTabGet;
        })(),
        get: function(action, params = {}) { return this._crossTabGetHelper(action, params, () => this._call('GET', config.API_GET_URL, action, params)); },
        post: async function(action, data = {}) {
            const res = await this._call('POST', config.API_POST_URL, action, data);
            try {
                // best-effort invalidate related GET cache entries across tabs
                try {
                    if (this._crossTabGetHelper && typeof this._crossTabGetHelper === 'function') {
                        // prefer explicit invalidate function if present
                        if (typeof this._crossTabGetHelper.invalidate === 'function') {
                            try { this._crossTabGetHelper.invalidate(action, data); } catch(_) {}
                        } else if (typeof this._crossTabGetHelper === 'function' && typeof this._crossTabGetHelper.invalidateKey === 'function') {
                            try { this._crossTabGetHelper.invalidateKey(action, data); } catch(_) {}
                        }
                    }
                } catch(_) {}
            } catch(_) {}
            return res;
        },
        // remove soon?
        getGlobalData: async (params = {}) => {
            if (!state.user.tornId) {
                throw new Error('User context missing for getGlobalData.');
            }
            let idToken = null;
            try {
                idToken = await firebaseAuth.ensureIdToken({ allowAutoSignIn: true });
            } catch (authError) {
                try { tdmlogger('warn', `[Auth] ensureIdToken failed: ${authError.message}`); } catch(_) {}
            }
            if (!idToken) {
                throw new Error('Unable to obtain Firebase ID token. Please reauthenticate.');
            }
            // --- Differential polling: send client timestamps ---
            const payload = {
                tornId: state.user.tornId,
                clientTimestamps: state.dataTimestamps,
                ...params
            };
            const headers = { 'Content-Type': 'application/json' };
            headers.Authorization = `Bearer ${idToken}`;
            return await new Promise((resolve, reject) => {
                const ret = state.gm.rD_xmlhttpRequest({
                    method: 'POST',
                    url: 'https://getglobaldata-codod64xdq-uc.a.run.app',
                    headers,
                    data: JSON.stringify({ data: payload }),
                    onload: function(response) {
                        if (!response || typeof response.responseText !== 'string' || response.responseText.trim() === '') {
                            return reject(new Error('Empty or invalid response from getGlobalData endpoint.'));
                        }
                        try {
                            const jsonResponse = JSON.parse(response.responseText);
                            if (jsonResponse.error) {
                                const error = new Error(jsonResponse.error.message || 'Unknown getGlobalData error');
                                if (jsonResponse.error.details) Object.assign(error, jsonResponse.error.details);
                                reject(error);
                            } else if (jsonResponse.result) {
                                // Session API usage counter (backend-reported, user-key only)
                                try {
                                    const add = Number(jsonResponse?.result?.meta?.userKeyApiCalls || 0);
                                    if (add > 0) utils.incrementBackendApiCalls(add);
                                } catch (_) { /* ignore */ }
                                // --- Persist masterTimestamps after each fetch ---
                                if (jsonResponse.result.masterTimestamps) {
                                    state.dataTimestamps = jsonResponse.result.masterTimestamps;
                                    storage.set('dataTimestamps', state.dataTimestamps);
                                }
                                // --- Update only changed collections in state ---
                                // --- Support full backend response structure ---
                                const firebaseCollections = [
                                    'dibsData',
                                    'userNotes',
                                    'medDeals',
                                    'dibsNotifications',
                                    'unauthorizedAttacks'
                                ];
                                const tornApiCollections = [
                                    'rankWars',
                                    'warData',
                                    'retaliationOpportunities'
                                ];
                                const actionsCollections = [
                                    'attackerLastAction',
                                    'unauthorizedAttacks'
                                ];
                                // Update firebase collections
                                if (jsonResponse.result.firebase && typeof jsonResponse.result.firebase === 'object') {
                                    for (const key of firebaseCollections) {
                                        if (jsonResponse.result.firebase.hasOwnProperty(key) && jsonResponse.result.firebase[key] !== null && jsonResponse.result.firebase[key] !== undefined) {
                                            storage.updateStateAndStorage(key, jsonResponse.result.firebase[key]);
                                        }
                                    }
                                }
                                // Update tornApi collections
                                if (jsonResponse.result.tornApi && typeof jsonResponse.result.tornApi === 'object') {
                                    for (const key of tornApiCollections) {
                                        if (Object.prototype.hasOwnProperty.call(jsonResponse.result.tornApi, key) && jsonResponse.result.tornApi[key] !== null && jsonResponse.result.tornApi[key] !== undefined) {
                                            storage.updateStateAndStorage(key, jsonResponse.result.tornApi[key]);
                                        }
                                    }
                                }
                                // Update actions collections
                                if (jsonResponse.result.actions && typeof jsonResponse.result.actions === 'object') {
                                    for (const key of actionsCollections) {
                                        if (jsonResponse.result.actions.hasOwnProperty(key) && jsonResponse.result.actions[key] !== null && jsonResponse.result.actions[key] !== undefined) {
                                            storage.updateStateAndStorage(key, jsonResponse.result.actions[key]);
                                        }
                                    }
                                }
                                resolve(jsonResponse.result);
                            } else {
                                resolve(jsonResponse);
                            }
                        } catch (e) {
                            reject(new Error('Failed to parse getGlobalData response: ' + e.message));
                        }
                    },
                    onerror: (error) => reject(new Error('getGlobalData request failed: Status ' + (error.status || 'Unknown')))
                });
                if (ret && typeof ret.catch === 'function') ret.catch(() => {});
            });
        },
        // Smart fetch for ranked war summary using Cloud Storage JSON with ETag; falls back to Firebase meta+reads
        getRankedWarSummarySmart: async (rankedWarId, factionId) => {
            utils.perf.start('getRankedWarSummarySmart');
            try {
                const warId = String(rankedWarId);
                const cacheKey = warId; // per-war cache
                const clientEntry = state.rankedWarSummaryCache?.[cacheKey] || {};
                // Ensure we have a storage URL
                let summaryUrl = clientEntry.summaryUrl;
                if (!summaryUrl) {
                    // Attempt lazy materialization first time we notice absence
                    // Prefer cheap GET; only force ensure when explicitly needed
                    await api.ensureWarArtifactsSafe(warId, factionId).catch(() => null);
                    const urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: clientEntry.etag || null });
                    summaryUrl = urls?.summaryUrl || null;
                    if (summaryUrl) {
                        clientEntry.summaryUrl = summaryUrl;
                        state.rankedWarSummaryCache[cacheKey] = { ...clientEntry };
                        storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                    }
                }
                // Try Cloud Storage first when URL is known
                if (summaryUrl) {
                    let { status, json, etag, lastModified } = await api.fetchStorageJson(summaryUrl, { etag: clientEntry.etag, ifModifiedSince: clientEntry.lastModified });
                    if (status === 200 && Array.isArray(json)) {
                        const next = { ...clientEntry, summaryUrl, etag: etag || null, lastModified: lastModified || null, updatedAt: Date.now(), summary: json, source: 'storage200' };
                        // summary persisted unconditionally (metrics pruned)
                        state.rankedWarSummaryCache[cacheKey] = next;
                        storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                        try {
                            state.rankedWarLastSummarySource = 'storage200';
                            state.rankedWarLastSummaryMeta = { source: 'storage', etag: etag || null, lastModified: lastModified || null, count: Array.isArray(json) ? json.length : 0, url: summaryUrl };
                            storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                            storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                        } catch(_) { /* noop */ }
                        tdmlogger('debug', `getRankedWarSummarySmart: 200 OK for war ${warId}, fetched fresh summary (${Array.isArray(json) ? json.length : 0} entries).`);
                        utils.perf.stop('getRankedWarSummarySmart');
                        return next.summary;
                    }
                    if (status === 304 && Array.isArray(clientEntry.summary)) {
                        try {
                            state.rankedWarLastSummarySource = 'storage304';
                            state.rankedWarLastSummaryMeta = { source: 'storage', etag: clientEntry.etag || null, lastModified: clientEntry.lastModified || null, count: Array.isArray(clientEntry.summary) ? clientEntry.summary.length : 0, url: summaryUrl };
                            storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                            storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                        } catch(_) { /* noop */ }
                        tdmlogger('debug', `getRankedWarSummarySmart: 304 Not Modified for war ${warId}, using cached summary (${Array.isArray(clientEntry.summary) ? clientEntry.summary.length : 0} entries).`);
                        utils.perf.stop('getRankedWarSummarySmart');
                        return clientEntry.summary;
                    }
                    // If 304 but we don't have a cached summary yet, force a one-time bootstrap without conditional headers
                    if (status === 304 && !Array.isArray(clientEntry.summary)) {
                        const forced = await api.fetchStorageJson(summaryUrl, {});
                        if (forced.status === 200 && Array.isArray(forced.json)) {
                            const next = { ...clientEntry, summaryUrl, etag: forced.etag || null, lastModified: forced.lastModified || null, updatedAt: Date.now(), summary: forced.json, source: 'storage200' };
                            // forced summary persisted (metrics pruned)
                            state.rankedWarSummaryCache[cacheKey] = next;
                            storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                            try { state.rankedWarLastSummarySource = 'storage200'; state.rankedWarLastSummaryMeta = { source: 'storage', etag: forced.etag || null, lastModified: forced.lastModified || null, count: next.summary.length, url: summaryUrl }; storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource); storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta); } catch(_) {}
                            tdmlogger('debug', `getRankedWarSummarySmart: Forced 200 OK for war ${warId}, fetched fresh summary (${Array.isArray(forced.json) ? forced.json.length : 0} entries).`);
                            utils.perf.stop('getRankedWarSummarySmart');
                            return next.summary;
                        }
                    }
                    if (status === 404) {
                        // Trigger a forced ensure so missing historical summaries get materialized server-side
                        await api.ensureWarArtifactsSafe(warId, factionId, { force: true }).catch(() => null);
                        try { await new Promise(r => setTimeout(r, 400)); } catch(_) {}
                        try {
                            const urls2 = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: clientEntry.etag || null }).catch(() => null);
                            if (urls2?.summaryUrl) {
                                summaryUrl = urls2.summaryUrl;
                                clientEntry.summaryUrl = summaryUrl;
                                state.rankedWarSummaryCache[cacheKey] = { ...clientEntry };
                                storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                                const again = await api.fetchStorageJson(summaryUrl, { etag: clientEntry.etag, ifModifiedSince: clientEntry.lastModified });
                                if (again.status === 200 && Array.isArray(again.json)) {
                                    const next = { ...clientEntry, summaryUrl, etag: again.etag || null, lastModified: again.lastModified || null, updatedAt: Date.now(), summary: again.json, source: 'storage200' };
                                    // retry summary persisted (metrics pruned)
                                    state.rankedWarSummaryCache[cacheKey] = next;
                                    storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                                    try { state.rankedWarLastSummarySource = 'storage200'; state.rankedWarLastSummaryMeta = { source: 'storage', etag: again.etag || null, lastModified: again.lastModified || null, count: next.summary.length, url: summaryUrl }; storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource); storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta); } catch(_) {}
                                    tdmlogger('debug', `getRankedWarSummarySmart: Retry 200 OK for war ${warId}, fetched fresh summary (${Array.isArray(again.json) ? again.json.length : 0} entries).`);
                                    utils.perf.stop('getRankedWarSummarySmart');
                                    return next.summary;
                                }
                            }
                            tdmlogger('info', `getRankedWarSummarySmart: 404 Not Found for war ${warId}, attempting lazy materialization and retry.`);
                        
                        } catch(_) { /* ignore retry errors */ }
                    }
                    // If 404 or other error, fall through to Firebase path
                }
                // No Firestore fallback: return cached summary if present else empty list
                if (Array.isArray(clientEntry.summary)) { 
                    tdmlogger('debug', `getRankedWarSummarySmart: Falling back to cached summary for war ${warId} (${clientEntry.summary.length} entries).`);
                    utils.perf.stop('getRankedWarSummarySmart');
                    return clientEntry.summary;
                }
                tdmlogger('info', `getRankedWarSummarySmart: No summary URL available for war ${warId}, no cached summary present.`);
                utils.perf.stop('getRankedWarSummarySmart');
                return [];
            } catch (e) {
                // Fallback to last cached if available
                const warId = String(rankedWarId);
                const clientEntry = state.rankedWarSummaryCache?.[warId];
                if (clientEntry && Array.isArray(clientEntry.summary)) {
                    tdmlogger('debug', `getRankedWarSummarySmart: Falling back to cached summary for war ${warId} (${clientEntry.summary.length} entries).`, e);
                    utils.perf.stop('getRankedWarSummarySmart');
                    return clientEntry.summary;
                }
                utils.perf.stop('getRankedWarSummarySmart');
                throw e;
            }
        },
        // Smart fetch for ranked war attacks using Cloud Storage manifest+chunked JSON. Returns aggregated attacks array.
        // options: { onDemand?: boolean } — when not in active war, only fetch if onDemand or not yet fetched once this page load
        getRankedWarAttacksSmart: async (rankedWarId, factionId, options = {}) => {
            // If V2 manifest path available, prefer that
                try {
                    // Short-circuit manifest fetch when a fresh finalized final JSON is cached locally to avoid repeated conditional GETs
                    try {
                        const warId = String(rankedWarId);
                        const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
                        const entry = cache[warId] || {};
                        const now = Date.now();
                        const isNonActive = !((utils.isWarActive && utils.isWarActive(warId)) || false);
                        // Use cached attacks if present and not forced to refresh and the client-side backoff allows it
                        if (!options.forceManifest && Array.isArray(entry.attacks) && entry.attacks.length > 0 && isNonActive) {
                            const freshMs = (entry.updatedAt && (now - entry.updatedAt) < 60_000); // 1 minute freshness
                            const backoffOk = entry.nextAllowedFetchMs ? (now < entry.nextAllowedFetchMs) : false;
                            if (freshMs || backoffOk || !!entry.finalized) {
                                try { tdmlogger('debug', `getRankedWarAttacksSmart: using cached final attacks and skipping manifest fetch for war ${warId}`, { count: entry.attacks.length }); } catch(_) {}
                                return entry.attacks;
                            }
                        }
                    } catch(_) { /* swallow cache-inspection errors and fall through */ }
                    const manifestRes = await api.fetchWarManifestV2(rankedWarId, factionId, { force: options.forceManifest });
                    if (manifestRes && manifestRes.status !== 'error' && manifestRes.status !== 'missing') {
                        // If this war is not active, prefer the final export (if available) to avoid assembling empty snapshot/delta parts
                        try {
                            const warId = String(rankedWarId);
                            const activeNow = utils.isWarActive ? utils.isWarActive(warId) : false;
                            if (!activeNow) {
                                const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
                                let entry = cache[warId] || {};
                                if (!entry.attacksUrl) {
                                    // Try to populate urls quickly
                                    try {
                                        const candidateIfNone = (entry && typeof entry.lastSeq === 'number') ? String(entry.lastSeq) : (entry && (entry.manifestEtag || entry.storageSummaryEtag || entry.storageAttacksEtag)) || null;
                                        const urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: candidateIfNone }).catch(()=>null);
                                        if (urls) { entry = cache[warId] = { ...(entry||{}), manifestUrl: urls.manifestUrl||entry.manifestUrl, attacksUrl: urls.attacksUrl||entry.attacksUrl, lastSeq: Number(urls.latestChunkSeq||entry.lastSeq||0) }; persistRankedWarAttacksCache(cache); }
                                    } catch(_) {}
                                }
                                if (entry.attacksUrl) {
                                    try {
                                        const { status: fStatus, json: fJson } = await api.fetchStorageJson(entry.attacksUrl, {});
                                        if (fStatus === 200 && Array.isArray(fJson) && fJson.length > 0) {
                                            entry.attacks = fJson; entry.finalized = true; entry.updatedAt = Date.now(); cache[warId] = entry;
                                            try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                                            persistRankedWarAttacksCache(cache);
                                            try { state.rankedWarLastAttacksSource = 'storage-final-200'; state.rankedWarLastAttacksMeta = { source: 'storage-final', count: fJson.length, url: entry.attacksUrl }; storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource); storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta); tdmlogger('info', `getRankedWarAttacksSmart: loaded final attacks for war ${warId} (fast-path)`, { count: fJson.length, url: entry.attacksUrl }); } catch(_) {}
                                            return entry.attacks;
                                        }
                                    } catch(_) { /* ignore final fetch failures and fall through to assembly */ }
                                }
                            }
                        } catch(_) {}
                        const assembled = await api.assembleAttacksFromV2(rankedWarId, factionId, options);
                        if (Array.isArray(assembled) && assembled.length) return assembled;
                    }
                } catch(_) { /* fall back to legacy path below */ }
            const warId = String(rankedWarId);
            const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
            let entry = cache[warId] || {};
            try { tdmlogger('debug', `getRankedWarAttacksSmart: start war=${warId} active=${utils.isWarActive(warId)} lastSeq=${entry.lastSeq||0} finalized=${!!entry.finalized}`); } catch(_) {}

            // Non-active war gating: only one fetch per page load unless onDemand=true
            const active = utils.isWarActive(warId);
            const onceMap = state.session.nonActiveWarFetchedOnce || (state.session.nonActiveWarFetchedOnce = {});
            // If this war is finalized (we've confirmed final file) and it's not active, return cached attacks only if non-empty.
            // This prevents returning an empty array when the manifest signals finalization but the final file hasn't been fetched yet.
            if (!active && entry.finalized && Array.isArray(entry.attacks) && entry.attacks.length > 0) {
                return entry.attacks;
            }
            // If finalized marker present but we have no cached attacks, try one immediate final-file fetch
            if (!active && entry.finalized && Array.isArray(entry.attacks) && entry.attacks.length === 0) {
                try {
                    tdmlogger('warn', `[WarAttacks] finalized marker present but no cached attacks; attempting direct final fetch`, { warId, manifestUrl: entry.manifestUrl, attacksUrl: entry.attacksUrl });
                } catch(_) {}
                if (entry.attacksUrl) {
                    try {
                        const { status: fStatus, json: fJson } = await api.fetchStorageJson(entry.attacksUrl, {});
                        if (fStatus === 200 && Array.isArray(fJson) && fJson.length > 0) {
                            entry.attacks = fJson;
                            try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                            entry.finalized = true;
                            entry.updatedAt = Date.now();
                            cache[warId] = entry; persistRankedWarAttacksCache(cache);
                            try {
                                state.rankedWarLastAttacksSource = 'storage-final-200';
                                state.rankedWarLastAttacksMeta = { source: 'storage-final', count: fJson.length, url: entry.attacksUrl };
                                storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                                storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                                tdmlogger('info', `getRankedWarAttacksSmart: loaded final attacks for war ${warId} during finalized-recovery`, { count: fJson.length, url: entry.attacksUrl });
                            } catch(_) {}
                            return entry.attacks;
                        } else {
                            try { tdmlogger('warn', '[WarAttacks] final fetch during recovery returned no data', { warId, status: fStatus }); } catch(_) {}
                        }
                    } catch (e) {
                        try { tdmlogger('warn', '[WarAttacks] final fetch during recovery failed', { warId, err: e && e.message }); } catch(_) {}
                    }
                }
            }
            if (!active && onceMap[warId] && !options.onDemand) {
                return entry.attacks || [];
            }

            // Single-flight guard to avoid duplicate concurrent calls/logs
            if (state._warFetchInFlight[warId]) {
                try { await state._warFetchInFlight[warId]; } catch(_) {}
                return (state.rankedWarAttacksCache?.[warId]?.attacks) || entry.attacks || [];
            }
            let resolveSf; const sfPromise = new Promise(res => resolveSf = res);
            state._warFetchInFlight[warId] = sfPromise;
            try {
            // Ensure we have storage URLs
            if (!entry.manifestUrl || !entry.attacksUrl) {
                let urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: (entry && typeof entry.lastSeq === 'number') ? String(entry.lastSeq) : (entry && (entry.manifestEtag || entry.storageSummaryEtag || entry.storageAttacksEtag)) || null }).catch(() => null);
                if (!urls) {
                    // Attempt lazy materialization then retry lookup once
                    await api.ensureWarArtifactsSafe(warId, factionId).catch(() => null);
                    urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: (entry && typeof entry.lastSeq === 'number') ? String(entry.lastSeq) : (entry && (entry.manifestEtag || entry.storageSummaryEtag || entry.storageAttacksEtag)) || null }).catch(() => null);
                }
                if (urls) {
                    entry.manifestUrl = urls.manifestUrl || entry.manifestUrl || null;
                    entry.attacksUrl = urls.attacksUrl || entry.attacksUrl || null; // final export (used when war ends)
                    entry.lastSeq = typeof entry.lastSeq === 'number' ? entry.lastSeq : Number(urls.latestChunkSeq || 0);
                    cache[warId] = entry; persistRankedWarAttacksCache(cache);
                    try { tdmlogger('info', `getRankedWarAttacksSmart: obtained storage urls for war ${warId}`, { manifestUrl: entry.manifestUrl, attacksUrl: entry.attacksUrl, lastSeq: entry.lastSeq }); } catch(_) {}
                    // Reset attacks provenance when URLs refresh
                    try {
                        state.rankedWarLastAttacksSource = null;
                        state.rankedWarLastAttacksMeta = {};
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    } catch(_) { /* noop */ }
                } else {
                    try { tdmlogger('debug', '[TDM] War attacks: failed to get storage URLs from backend', { warId, factionId }); } catch(_) { /* noop */ }
                }
            }
            // If war is not active and we have a final attacksUrl, prefer it before attempting any manifest/segments to avoid 404 noise
            if (!active && entry.attacksUrl && !entry.finalized) {
                const { status, json } = await api.fetchStorageJson(entry.attacksUrl, {});
                if (status === 200 && Array.isArray(json)) {
                    entry.attacks = json;
                    try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                    entry.lastSeq = Number.isFinite(entry.lastSeq) ? entry.lastSeq : 0;
                    entry.finalized = true;
                    // Long backoff (12h jittered) because final won't change
                    try { entry.nextAllowedFetchMs = Date.now() + Math.floor(12 * 60 * 60 * 1000 * (1 + (Math.random() * 2 - 1) * 0.2)); } catch(_) {}
                    cache[warId] = entry; persistRankedWarAttacksCache(cache);
                    try {
                        state.rankedWarLastAttacksSource = 'storage-final-200';
                        state.rankedWarLastAttacksMeta = { source: 'storage-final', count: json.length, url: entry.attacksUrl };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                        tdmlogger('debug', '[War attacks source: storage final (200)]', state.rankedWarLastAttacksMeta);
                    } catch(_) { /* noop */ }
                    try { tdmlogger('info', `getRankedWarAttacksSmart: loaded final attacks for war ${warId}`, { count: Array.isArray(json)?json.length:0, url: entry.attacksUrl }); } catch(_) {}
                    return entry.attacks;
                }
            }
            // If no manifestUrl, as a fallback try final attacksUrl (only valid after war end)
        if (!entry.manifestUrl && entry.attacksUrl) {
                const { status, json } = await api.fetchStorageJson(entry.attacksUrl, {});
                if (status === 200 && Array.isArray(json)) {
                    entry.attacks = json;
                    try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                    entry.lastSeq = Number.isFinite(entry.lastSeq) ? entry.lastSeq : 0;
            // Mark as finalized to avoid future manifest attempts
            entry.finalized = true;
            // Long backoff since final file won't change; 12h jittered
            try { entry.nextAllowedFetchMs = Date.now() + Math.floor(12 * 60 * 60 * 1000 * (1 + (Math.random() * 2 - 1) * 0.2)); } catch(_) {}
                    cache[warId] = entry; persistRankedWarAttacksCache(cache);
                    try {
                        state.rankedWarLastAttacksSource = 'storage-final-200';
                        state.rankedWarLastAttacksMeta = { source: 'storage-final', count: json.length, url: entry.attacksUrl };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                        tdmlogger('debug', '[War attacks source: storage final (200)]', state.rankedWarLastAttacksMeta);
                    } catch(_) { /* noop */ }
                    return entry.attacks;
                } else {
                    try { tdmlogger('warn', 'War attacks: final file fetch failed', { url: entry.attacksUrl, status }); } catch(_) { /* noop */ }
                }
            }
            if (!entry.manifestUrl) {
                try { tdmlogger('warn', 'War attacks: no manifest URL available; returning cached attacks', { warId, hasCached: Array.isArray(entry.attacks) }); } catch(_) { /* noop */ }
                return entry.attacks || [];
            }
        // Jittered throttle to avoid herd effects across many clients
            try {
                const nowMs = Date.now();
                if (entry.nextAllowedFetchMs && nowMs < entry.nextAllowedFetchMs) {
            return entry.attacks || [];
                }
            } catch(_) { /* ignore throttle read errors */ }
            // Fetch manifest with conditional headers
            // Decide if we should fetch manifest now; if not, fast return cached attacks
            const now0 = Date.now();
            if (!api.shouldFetchManifest(entry, options?.reason || null, active, now0)) {
                return entry.attacks || [];
            }
            try { tdmlogger('debug', `getRankedWarAttacksSmart: fetching manifest for war ${warId}`, { manifestUrl: entry.manifestUrl, etag: entry.etags?.manifest, lastModified: entry.lastModified }); } catch(_) {}
            let { status: mStatus, json: mJson, etag: mEtag, lastModified: mLm } = await api.fetchStorageJson(entry.manifestUrl, { etag: entry.etags?.manifest, ifModifiedSince: entry.lastModified });
            let didUpdate = false;
            if (mStatus === 200 && mJson && typeof mJson === 'object') {
                entry.etags = entry.etags || {};
                entry.etags.manifest = mEtag || entry.etags.manifest || null;
                entry.lastModified = mLm || entry.lastModified || null;
                entry.lastManifestFetchMs = Date.now();
                // Capture enriched fields
                if (typeof mJson.warStart !== 'undefined') entry.warStart = mJson.warStart;
                if (typeof mJson.warEnd !== 'undefined') entry.warEnd = mJson.warEnd;
                if (typeof mJson.storageAttacksComplete !== 'undefined') entry.storageAttacksComplete = !!mJson.storageAttacksComplete;
                if (typeof mJson.storageAttacksThrough !== 'undefined') entry.storageAttacksThrough = mJson.storageAttacksThrough;
                if (typeof mJson.storageSummaryLastWriteAt !== 'undefined') entry.storageSummaryLastWriteAt = mJson.storageSummaryLastWriteAt;
                if (typeof mJson.storageAttacksLastWriteAt !== 'undefined') entry.storageAttacksLastWriteAt = mJson.storageAttacksLastWriteAt;
                try {
                    state.rankedWarLastAttacksSource = 'manifest-200';
                    state.rankedWarLastAttacksMeta = { source: 'manifest', etag: mEtag || null, lastModified: mLm || null, attacksSeq: Number((mJson.latestSeq != null ? mJson.latestSeq : mJson.attacksSeq) || 0), summaryEtag: mJson.summaryEtag || null, manifestUrl: entry.manifestUrl };
                    storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                    storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    tdmlogger('info', 'War attacks source: manifest (200)', state.rankedWarLastAttacksMeta);
                } catch(_) { /* noop */ }
                const latestSeq = Number((mJson.latestSeq != null ? mJson.latestSeq : mJson.attacksSeq) || 0);
                const fromSeq = Number(entry.lastSeq || 0);
                // Build new chunks list
                const want = [];
                for (let seq = fromSeq + 1; seq <= latestSeq; seq++) want.push(seq);
                // Fetch each wanted chunk (immutable) and append
                if (want.length > 0 && entry.attacksUrl) {
                    const base = entry.attacksUrl.replace(/\.json$/i, '');
                    const loaded = [];
                    for (const seq of want) {
                        const url = `${base}_${seq}.json`;
                        const { status, json, etag } = await api.fetchStorageJson(url, {});
                        if (status === 200 && Array.isArray(json)) {
                            // Append and record
                            if (!entry.attacks) entry.attacks = [];
                            // Basic de-dupe by attack id if present
                            const had = new Set(entry.attacks.map(a => a?.attackId || a?.id || a?.timestamp));
                            for (const a of json) { const key = a?.attackId || a?.id || a?.timestamp; if (!had.has(key)) entry.attacks.push(a); }
                            entry.etags[String(seq)] = etag || null;
                            loaded.push(seq);
                            try {
                                state.rankedWarLastAttacksSource = 'chunk-200';
                                state.rankedWarLastAttacksMeta = { source: 'chunk', seq, count: json.length, url };
                                storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                                storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                                tdmlogger('info', 'War attacks source: chunk (200)', state.rankedWarLastAttacksMeta);
                            } catch(_) { /* noop */ }
                        } else {
                            try { tdmlogger('warn', 'War attacks: chunk fetch failed', { seq, url, status }); } catch(_) { /* noop */ }
                        }
                    }
                    // Trim to last 4000 attacks to bound memory
                    if (entry.attacks && entry.attacks.length > 4000) entry.attacks = entry.attacks.slice(-4000);
                    if (loaded.length > 0) entry.lastSeq = Math.max(...loaded, fromSeq);
                    cache[warId] = entry; persistRankedWarAttacksCache(cache);
                    if (loaded.length > 0) didUpdate = true;
                }
                // Optionally refresh summary if manifest indicates change
                if (mJson.summaryEtag && state.rankedWarSummaryCache?.[warId]?.etag !== mJson.summaryEtag) {
                    try { await api.getRankedWarSummarySmart(warId, factionId); } catch(_) { /* ignore */ }
                }
            }
            if (mStatus === 304) {
                try {
                    state.rankedWarLastAttacksSource = 'manifest-304';
                    state.rankedWarLastAttacksMeta = { source: 'manifest', etag: entry.etags?.manifest || null, lastModified: entry.lastModified || null, manifestUrl: entry.manifestUrl };
                    storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                    storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    tdmlogger('debug', 'War attacks source: manifest (304/not modified)', state.rankedWarLastAttacksMeta);
                } catch(_) { /* noop */ }
                entry.lastManifestFetchMs = Date.now();
                // Build a lightweight manifest object from cached entry fields so callers can operate
                // without requiring a subsequent full manifest body fetch. This helps when fetch returns 304.
                try {
                    const pseudo = {};
                    pseudo.manifestVersion = entry.v2Enabled ? 2 : 1;
                    pseudo.latestSeq = Number(entry.lastSeq || 0);
                    pseudo.attacksSeq = Number(entry.lastSeq || 0);
                    if (typeof entry.warStart !== 'undefined') pseudo.warStart = entry.warStart;
                    if (typeof entry.warEnd !== 'undefined') pseudo.warEnd = entry.warEnd;
                    if (entry.windowUrl) pseudo.window = { url: entry.windowUrl, fromSeq: Number(entry.windowFromSeq||0), toSeq: Number(entry.windowToSeq||0), count: Number(entry.windowCount||0) };
                    if (entry.snapshotUrl) pseudo.snapshot = { url: entry.snapshotUrl, seq: Number(entry.lastSnapshotSeq||0) };
                    if (entry.deltaUrl) pseudo.delta = { url: entry.deltaUrl };
                    if (entry.attacksUrl) pseudo.final = { url: entry.attacksUrl };
                    pseudo.finalized = !!entry.finalized;
                    return { status: 'not-modified', manifest: pseudo };
                } catch(_) { /* ignore pseudo-build errors and fall through */ }
            }
            if (mStatus === 404) {
                // Attempt ensure then retry manifest once
                await api.ensureWarArtifactsSafe(warId, factionId).catch(() => null);
                try { await new Promise(r => setTimeout(r, 400)); } catch(_) {}
                try {
                    const urls2 = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: (entry && typeof entry.lastSeq === 'number') ? String(entry.lastSeq) : (entry && (entry.manifestEtag || entry.storageSummaryEtag || entry.storageAttacksEtag)) || null }).catch(() => null);
                    if (urls2?.manifestUrl && urls2.manifestUrl !== entry.manifestUrl) {
                        entry.manifestUrl = urls2.manifestUrl; cache[warId] = entry; persistRankedWarAttacksCache(cache);
                        ({ status: mStatus, json: mJson, etag: mEtag, lastModified: mLm } = await api.fetchStorageJson(entry.manifestUrl, { etag: entry.etags?.manifest, ifModifiedSince: entry.lastModified }));
                        if (mStatus === 200 && mJson && typeof mJson === 'object') {
                            entry.etags = entry.etags || {}; entry.etags.manifest = mEtag || entry.etags.manifest || null; entry.lastModified = mLm || entry.lastModified || null;
                        }
                    }
                } catch(_) { /* ignore retry errors */ }
            }
            if (mStatus !== 200 && mStatus !== 304) {
                try { tdmlogger('warn', 'War attacks: manifest fetch failed', { url: entry.manifestUrl, status: mStatus }); } catch(_) { /* noop */ }
                // Fallback: if manifest is missing (e.g., only final export exists), try the final attacks JSON
                if (entry.attacksUrl && (mStatus === 404 || (mStatus >= 400 && mStatus < 600))) {
                    try {
                        const { status: fStatus, json: fJson } = await api.fetchStorageJson(entry.attacksUrl, {});
                        if (fStatus === 200 && Array.isArray(fJson)) {
                            entry.attacks = fJson;
                            try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                            entry.lastSeq = Number.isFinite(entry.lastSeq) ? entry.lastSeq : 0;
                            // Mark as finalized and avoid manifest re-tries
                            entry.finalized = true;
                            entry.manifestUrl = null;
                            // Long backoff since final file won't change; 12h jittered
                            try { entry.nextAllowedFetchMs = Date.now() + Math.floor(12 * 60 * 60 * 1000 * (1 + (Math.random() * 2 - 1) * 0.2)); } catch(_) {}
                            cache[warId] = entry; persistRankedWarAttacksCache(cache);
                            try {
                                state.rankedWarLastAttacksSource = 'storage-final-200';
                                state.rankedWarLastAttacksMeta = { source: 'storage-final', count: fJson.length, url: entry.attacksUrl };
                                storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                                storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                                tdmlogger('info', 'War attacks source: storage final (200)', state.rankedWarLastAttacksMeta);
                            } catch(_) { /* noop */ }
                            return entry.attacks;
                        } else {
                            try { tdmlogger('warn', 'War attacks: final fallback fetch failed', { url: entry.attacksUrl, status: fStatus }); } catch(_) { /* noop */ }
                        }
                    } catch(_) { /* ignore */ }
                }
            }
            // Set next allowed fetch with jitter/backoff: shorter after updates, longer when idle
            try {
                const jitter = (ms, pct = 0.2) => Math.floor(ms * (1 + (Math.random() * 2 - 1) * pct));
                // If war not active, lengthen idle backoff to reduce background traffic
                const phaseActive = active;
                const finalized = !!entry.storageAttacksComplete && !!entry.warEnd && entry.warEnd > 0;
                let baseMs;
                if (phaseActive) baseMs = didUpdate ? 7000 : 25000;
                else if (finalized) baseMs = 300000; // 5m when finalized
                else baseMs = 180000; // inactive pre-start
                entry.nextAllowedFetchMs = Date.now() + jitter(baseMs, 0.2);
                if (didUpdate) entry.updatedAt = Date.now();
                cache[warId] = entry; persistRankedWarAttacksCache(cache);
            } catch(_) { /* ignore throttle write errors */ }
            // Release single-flight
            resolveSf(); delete state._warFetchInFlight[warId];
            // If 304, no changes; just return current
            return entry.attacks || [];
            } finally {
                try { if (!utils.isWarActive(warId)) { const map = state.session.nonActiveWarFetchedOnce || (state.session.nonActiveWarFetchedOnce = {}); map[warId] = true; } } catch(_) {}
                try { resolveSf(); } catch(_) {}
                delete state._warFetchInFlight[warId];
            }
        },
        // Choose the freshest source between local aggregation and server summary
        getRankedWarSummaryFreshest: async (rankedWarId, factionId) => {
            const warId = String(rankedWarId);

            // PATCH: Prefer cheap server summary first to avoid expensive local attack aggregation
            // This trades potential <2min latency for massive bandwidth/CPU savings.
            try {
                const smartSummary = await api.getRankedWarSummarySmart(warId, factionId);
                if (Array.isArray(smartSummary) && smartSummary.length > 0) {
                     return smartSummary;
                }
            } catch(_) {/* swallow and fall through to full freshness check */ }

            // Only poll attacks/manifest automatically during active wars; for inactive wars rely on cached/finalized data unless UI triggers onDemand
            try { if (utils.isWarActive(warId)) { await api.getRankedWarAttacksSmart(warId, factionId, { onDemand: false }); } } catch(_) {}
            const attacksEntry = state.rankedWarAttacksCache?.[warId] || {};
            const summaryEntry = state.rankedWarSummaryCache?.[warId] || {};
            const parseLm = (lm) => {
                if (!lm) return 0;
                if (typeof lm === 'number') return lm;
                const t = Date.parse(lm); return Number.isFinite(t) ? t : 0;
            };
            const localTs = Math.max(parseLm(attacksEntry.lastModified), Number(attacksEntry.updatedAt || 0));
            const serverTs = Math.max(parseLm(summaryEntry.lastModified), Number(summaryEntry.updatedAt || 0));
            if (localTs && localTs > serverTs) {
                try {
                    const local = await api.getRankedWarSummaryLocal(warId, factionId);
                    if (Array.isArray(local) && local.length > 0) {
                        // Stamp provenance when local is chosen
                        try {
                            state.rankedWarLastSummarySource = 'local';
                            state.rankedWarLastSummaryMeta = {
                                source: 'local',
                                attacksCount: Array.isArray(attacksEntry.attacks) ? attacksEntry.attacks.length : 0,
                                lastSeq: Number(attacksEntry.lastSeq || 0),
                                lastModified: attacksEntry.lastModified || null,
                            };
                            storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                            storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                            // console.info('[TDM] War summary source: local-attacks', state.rankedWarLastSummaryMeta); // silenced to reduce noise
                        } catch(_) { /* noop */ }
                        return local;
                    }
                } catch(_) {}
            }
            return api.getRankedWarSummarySmart(warId, factionId);
        },
        // Unified user fetch that normalizes both legacy (player_id etc.) and new v2 (profile,faction) shapes
        getTornUser: (apiKey, id = null, selections = 'faction,profile') => {
            if (!apiKey) return Promise.reject(new Error("No API key provided"));
            const apiUrl = `https://api.torn.com/v2/user/${id ? id + '/' : ''}?selections=${selections}&key=${apiKey}&comment=TDM_FEgTU&timestamp=${Math.floor(Date.now()/1000)}`;
            return new Promise((resolve, reject) => {
                const ret = state.gm.rD_xmlhttpRequest({
                    method: "GET",
                    url: apiUrl,
                    onload: res => {
                        try {
                            const data = JSON.parse(res.responseText);
                            utils.incrementClientApiCalls(1);
                            if (data.error) {
                                const err = new Error(data.error?.error || 'Torn API error');
                                err.tornError = data.error;
                                err.tornErrorCode = data.error?.code;
                                err.tornErrorMessage = data.error?.error;
                                if (typeof err.tornErrorCode !== 'undefined' && err.tornErrorMessage) {
                                    err.message = `Torn API error ${err.tornErrorCode}: ${err.tornErrorMessage}`;
                                } else if (err.tornErrorMessage) {
                                    err.message = `Torn API error: ${err.tornErrorMessage}`;
                                }
                                return reject(err);
                            }
                            // Normalize shape
                            let normalized = data;
                            try {
                                const hasProfile = data && data.profile && typeof data.profile === 'object';
                                const hasFaction = data && data.faction && typeof data.faction === 'object';
                                if (hasProfile) {
                                    const p = data.profile;
                                    normalized = {
                                        profile: {
                                            id: p.id || p.player_id || p.playerId,
                                            name: p.name,
                                            level: p.level,
                                            rank: p.rank,
                                            title: p.title,
                                            age: p.age,
                                            signed_up: p.signed_up || p.signup || p.signedup,
                                            faction_id: p.faction_id || p.factionId || (data.faction?.id),
                                            honor_id: p.honor_id || p.honorId || p.honor,
                                            property: p.property || { id: p.property_id, name: p.property },
                                            image: p.image || p.profile_image,
                                            gender: p.gender,
                                            revivable: typeof p.revivable === 'boolean' ? p.revivable : (p.revivable === 1),
                                            role: p.role,
                                            status: p.status || data.status || {},
                                            spouse: p.spouse || p.married || null,
                                            awards: p.awards,
                                            friends: p.friends,
                                            enemies: p.enemies,
                                            forum_posts: p.forum_posts || p.forumposts,
                                            karma: p.karma,
                                            last_action: p.last_action || data.last_action || {},
                                            life: p.life || data.life || {},
                                        },
                                        faction: hasFaction ? {
                                            id: data.faction.id || data.faction.faction_id,
                                            name: data.faction.name || data.faction.faction_name,
                                            tag: data.faction.tag || data.faction.faction_tag,
                                            tag_image: data.faction.tag_image || data.faction.faction_tag_image,
                                            position: data.faction.position,
                                            days_in_faction: data.faction.days_in_faction
                                        } : (p.faction_id ? { id: p.faction_id } : null)
                                    };
                                } else if (data.player_id) {
                                    // Legacy flat shape -> wrap into profile
                                    normalized = {
                                        profile: {
                                            id: data.player_id,
                                            name: data.name,
                                            level: data.level,
                                            rank: data.rank,
                                            title: data.title,
                                            age: data.age,
                                            signed_up: data.signed_up || data.signup,
                                            faction_id: data.faction?.faction_id,
                                            honor_id: data.honor_id || data.honor,
                                            property: data.property ? { id: data.property_id, name: data.property } : undefined,
                                            image: data.profile_image,
                                            gender: data.gender,
                                            revivable: data.revivable === 1 || data.revivable === true,
                                            role: data.role,
                                            status: data.status || {},
                                            spouse: data.spouse || data.married || null,
                                            awards: data.awards,
                                            friends: data.friends,
                                            enemies: data.enemies,
                                            forum_posts: data.forum_posts,
                                            karma: data.karma,
                                            last_action: data.last_action || {},
                                            life: data.life || {},
                                        },
                                        faction: data.faction ? {
                                            id: data.faction.faction_id,
                                            name: data.faction.faction_name,
                                            tag: data.faction.faction_tag,
                                            tag_image: data.faction.faction_tag_image,
                                            position: data.faction.position,
                                            days_in_faction: data.faction.days_in_faction
                                        } : null
                                    };
                                }
                            } catch(_) { /* swallow normalization issues */ }
                            resolve(normalized);
                        } catch (e) { reject(new Error("Invalid JSON from Torn API")); }
                    },
                    onerror: () => reject(new Error("Torn API request failed"))
                });
                if (ret && typeof ret.catch === 'function') ret.catch(() => {});
            });
        },
        // Centralized: update all unified status records from tornFactionData using V2 model
        updateAllUnifiedStatusRecordsFromFactionData: function() {
            try {
                const allFactions = state.tornFactionData || {};
                state.unifiedStatus = state.unifiedStatus || {};
                for (const [factionId, entry] of Object.entries(allFactions)) {
                    const membersObj = entry?.data?.members || entry?.data?.faction?.members || null;
                    const members = membersObj ? (Array.isArray(membersObj) ? membersObj : Object.values(membersObj)) : [];
                    for (const m of members) {
                        try {
                            const id = String(m?.id || m?.player_id || m?.user_id || m?.userID || '');
                            if (!id) continue;
                            const prevRec = state.unifiedStatus[id] || null;
                            const nextRec = utils.buildUnifiedStatusV2(m, prevRec);
                            if (nextRec) {
                                state.unifiedStatus[id] = nextRec;
                                utils.emitStatusChangeV2(prevRec, nextRec);
                            }
                        } catch(_) { /* per-member swallow */ }
                    }
                }
                scheduleUnifiedStatusSnapshotSave();
            } catch(e) { if (state?.debug?.statusWatch) tdmlogger('warn', '[StatusV2] updateAllUnifiedStatusRecordsFromFactionData error', e); }
        },

        getTornFaction: async function(apiKey, selections = '', factionIdParam = null, options = {}) {
            try {
                const factionId = factionIdParam || state?.user?.factionId;
                if (!factionId) return null;
                const want = api.buildSafeFactionSelections(String(selections || '')
                    .split(',')
                    .map(s => s.trim())
                    .filter(Boolean), factionId);

                // Merge with existing cached selections to avoid re-calling for already-fetched fields
                const cache = state.tornFactionData || (state.tornFactionData = {});
                const entry = cache[factionId];
                const now = Date.now();
                const isFresh = entry && (now - (entry.fetchedAtMs || 0) < config.MIN_FACTION_CACHE_FRESH_MS);

                // If data is fresh and includes all requested selections, return it
                if (isFresh && entry?.data) {
                    const have = new Set(entry.selections || []);
                    const allIncluded = want.every(s => have.has(s));
                    if (allIncluded) {
                        try {
                            state.script.lastFactionRefreshSkipReason = 'fresh-cache';
                            // tdmlogger('debug', '[getTornFaction] Skip fetch: fresh-cache (<=10s) for selections', selections, 'factionId=', factionId||'default');
                            ui.updateApiCadenceInfo?.();
                        } catch(_) {}
                        return entry.data;
                    }
                }

                // Determine selections to request: union of want and already cached
                const merged = new Set([...(entry?.selections || []), ...want]);
                const mergedList = Array.from(merged);

                // If fresh but missing some fields, we still wait until freshness expires unless we never had data
                if (isFresh && entry?.data) {
                    // Return current data immediately; schedule a background refresh for missing fields
                    (async () => {
                        try {
                            const urlBg = `https://api.torn.com/v2/faction/${factionId}?selections=${mergedList.join(',')}&key=${apiKey}&sort=DESC&comment=TDM_BEgTF&timestamp=${Math.floor(Date.now()/1000)}`;
                            const resBg = await fetch(urlBg);
                            const jsonBg = await resBg.json();
                            utils.incrementClientApiCalls(1);
                            if (!jsonBg.error) {
                                cache[factionId] = { data: jsonBg, fetchedAtMs: Date.now(), selections: mergedList };
                                // Trim and persist cached faction bundles to keep memory/storage bounded
                                try { utils.trimTornFactionData?.(20); } catch(_) { utils.schedulePersistTornFactionData(); }
                                // Update chain fallback timing from background chain data if present
                                try {
                                    const chain = jsonBg?.chain;
                                    // Only update chainFallback if this fetch corresponds to OUR faction (avoid opponent flicker)
                                    if (chain && typeof chain === 'object' && String(factionId) === String(state?.user?.factionId)) {
                                        // Chain timeout in Torn v2 may be a remaining-seconds counter (small number) not an epoch.
                                        (function(){
                                            try {
                                                const nowSec = Math.floor(Date.now()/1000);
                                                const current = Number(chain.current)||0;
                                                const raw = Number(chain.timeout)||0; // could be seconds remaining OR epoch
                                                let timeoutEpoch = 0;
                                                if (raw > 0) {
                                                    // Heuristic: treat as epoch if it already looks like a unix timestamp (> 1B).
                                                    timeoutEpoch = raw > 1_000_000_000 ? raw : (nowSec + raw);
                                                }
                                                state.ui.chainFallback.current = current;
                                                state.ui.chainFallback.timeoutEpoch = timeoutEpoch;
                                                // Preserve chain end epoch if provided for fallback display
                                                const endEpoch = Number(chain.end)||0;
                                                if (endEpoch > 0) state.ui.chainFallback.endEpoch = endEpoch;
                                            } catch(_) { /* ignore */ }
                                        })();
                                    }
                                } catch(_) {}
                                if (!options.skipUnifiedUpdate) {
                                    try { api.updateAllUnifiedStatusRecordsFromFactionData(); } catch(_) {}
                                }
                                try {
                                    state.script.lastFactionRefreshBackgroundMs = Date.now();
                                    tdmlogger('debug', '[getTornFaction] Background update applied for faction', factionId, 'selections=', mergedList);
                                    ui.updateApiCadenceInfo?.();
                                } catch(_) {}
                            } else {
                                // Log background errors to help debug "Traveling" issues
                                try { tdmlogger('warn', '[getTornFaction] Background fetch API error', jsonBg.error); } catch(_) {}
                            }
                        } catch (e) {
                            // Catch all background errors to prevent unhandled rejections
                            try { tdmlogger('warn', '[getTornFaction] Background fetch exception', e); } catch(_) {}
                        }
                    })();
                    try {
                        state.script.lastFactionRefreshSkipReason = 'fresh-partial-bg';
                        tdmlogger('debug', '[getTornFaction] Partial fresh detected; background refresh scheduled for missing selections');
                        ui.updateApiCadenceInfo?.();
                    } catch(_) {}
                    return entry.data;
                }

                // Fetch merged bundle now
                const url = `https://api.torn.com/v2/faction/${factionId}?selections=${mergedList.join(',')}&key=${apiKey}&sort=DESC&comment=TDM_BEgTF2&timestamp=${Math.floor(Date.now()/1000)}`;
                let response = await fetch(url);
                let data = await response.json();
                utils.incrementClientApiCalls(1);
                if (data.error) {
                    const err = new Error(data.error?.error || 'Torn API error');
                    err.tornError = data.error;
                    err.tornErrorCode = data.error?.code;
                    err.tornErrorMessage = data.error?.error;
                    if (typeof err.tornErrorCode !== 'undefined' && err.tornErrorMessage) {
                        err.message = `Torn API error ${err.tornErrorCode}: ${err.tornErrorMessage}`;
                    } else if (err.tornErrorMessage) {
                        err.message = `Torn API error: ${err.tornErrorMessage}`;
                    }
                    throw err;
                }
                
                cache[factionId] = { data, fetchedAtMs: now, selections: mergedList };
                // Trim and persist cached faction bundles to keep memory/storage bounded
                try { utils.trimTornFactionData?.(20); } catch(_) { utils.schedulePersistTornFactionData(); }
                // Update chain fallback timing from fresh fetch if chain data present
                try {
                    const chain = data?.chain;
                    // Only update chainFallback if this is OUR faction's data (prevent swapping with opponent faction chain)
                    if (chain && typeof chain === 'object' && String(factionId) === String(state?.user?.factionId)) {
                        (function(){
                            try {
                                const nowSec = Math.floor(Date.now()/1000);
                                const current = Number(chain.current)||0;
                                const raw = Number(chain.timeout)||0;
                                let timeoutEpoch = 0;
                                if (raw > 0) timeoutEpoch = raw > 1_000_000_000 ? raw : (nowSec + raw);
                                state.ui.chainFallback.current = current;
                                state.ui.chainFallback.timeoutEpoch = timeoutEpoch;
                                const endEpoch = Number(chain.end)||0;
                                if (endEpoch > 0) state.ui.chainFallback.endEpoch = endEpoch;
                            } catch(_) { /* noop */ }
                        })();
                    }
                } catch(_) {}
                // Centralized: update all unified status records from tornFactionData (V2)
                if (!options.skipUnifiedUpdate) {
                    try { api.updateAllUnifiedStatusRecordsFromFactionData(); } catch(_) {}
                }
                try {
                    state.script.lastFactionRefreshFetchMs = Date.now();
                    state.script.lastFactionRefreshSkipReason = null;
                    // tdmlogger('debug', '[getTornFaction] Fresh fetch completed for selections', selections, 'factionId=', factionId||'default');
                    ui.updateApiCadenceInfo?.();
                } catch(_) {}
                // (Removed: in-memory status cache update; now handled by unified status record update)
                return data;
            } catch (error) {
                tdmlogger('error', '[getTornFaction] Error fetching faction data:', error);
                return null;
            }
        },
        getKeyInfo:async function(apiKey) {
            try{
                const kv = ui && ui._kv;
                const cacheKey = 'torn_api_key';
                const now = Date.now();
                // Try IndexedDB cache first
                try {
                    const cached = await kv?.getItem(cacheKey);
                    if (cached && typeof cached === 'object') {
                        const { data: cachedData, ts } = cached;
                        if (cachedData && ts && (now - ts < 60 * 60 * 1000)) {
                            // Use cached result and update state flags
                            try {
                                const access = cachedData?.info?.access;
                                state.session.factionApi = state.session.factionApi || {};
                                state.user.factionAPIAccess = !!(access && access.faction);
                                if (typeof access?.level === 'number') state.user.actualTornApiKeyAccess = access.level;
                                storage.updateStateAndStorage('user', state.user);
                            } catch(_) {}
                            return cachedData;
                        }
                    }
                } catch(_) { /* ignore cache read issues */ }

                const url = `https://api.torn.com/v2/key/info?key=${apiKey}&comment=TDMKey`; // removed &timestamp=${Math.floor(Date.now()/1000)}
                const response = await fetch(url);
                const data = await response.json();
                utils.incrementClientApiCalls(1);
                if (data.error) {
                    const err = new Error(data.error?.error || 'Torn API error');
                    err.tornError = data.error;
                    err.tornErrorCode = data.error?.code;
                    err.tornErrorMessage = data.error?.error;
                    if (typeof err.tornErrorCode !== 'undefined' && err.tornErrorMessage) {
                        err.message = `Torn API error ${err.tornErrorCode}: ${err.tornErrorMessage}`;
                    } else if (err.tornErrorMessage) {
                        err.message = `Torn API error: ${err.tornErrorMessage}`;
                    }
                    throw err;
                }
                
                // Persist to IndexedDB with 1h TTL
                try { await kv?.setItem(cacheKey, { data, ts: now }); } catch(_) {}
                // Track access level and faction access scope and expose factionAPIAccess
                try {
                    state.session.factionApi = state.session.factionApi || {};
                    const access = data?.info?.access;
                    if (access && typeof access.level === 'number') {
                        state.user.actualTornApiKeyAccess = access.level;
                    }
                    state.user.factionAPIAccess = !!(access && access.faction);
                    storage.updateStateAndStorage('user', state.user);
                    // Maintain legacy flags for any consumers
                    const scopes = access?.scopes || access?.scope || [];
                    const scopesStr = Array.isArray(scopes) ? scopes.map(s=>String(s).toLowerCase()) : [];
                    if (scopesStr.length > 0) {
                        const hasFaction = scopesStr.some(s => s.startsWith('factions'));
                        const hasFactionAttacks = scopesStr.includes('factions.attacks');
                        state.session.factionApi.hasFactionAccess = !!hasFaction;
                        if (!hasFactionAttacks) state.session.factionApi.allowAttacksSelection = false;
                    }
                } catch(_) { /* non-fatal */ }
                return data;
            } catch (error) {
                tdmlogger('error', '[getKeyInfo] Error fetching key info:', error);
                return null;
            }
        },
        // Bundle refresh for both factions (ours and opponent) with union selections; respects freshness
        refreshFactionBundles: async function(options = {}) {
            if (options === true) {
                options = { force: true };
            } else if (!options || typeof options !== 'object') {
                options = {};
            }
            // Ensure throttle state exists immediately to prevent access errors
            state._factionBundleThrottle = state._factionBundleThrottle || { lastCall: 0, lastIds: [], skipped: 0, lastSkipLog: 0, lastSourceCall: {} };

            const key = state.user.actualTornApiKey;
            if (!key) return;
            const nowThrottle = Date.now();
            // Reuse existing cadence concepts: use MIN_GLOBAL_FETCH_INTERVAL_MS as a hard floor, and the
            // current factionBundleRefreshMs (user adjustable) as the primary pacing reference.
            const userCadence = state.script?.factionBundleRefreshMs || config.DEFAULT_FACTION_BUNDLE_REFRESH_MS;
            // Allow some mid-interval opportunistic calls (e.g. focus regain) but never closer than MIN_GLOBAL_FETCH_INTERVAL_MS.
            // We permit at most one extra call between scheduled ticks -> choose floor = MIN_GLOBAL_FETCH_INTERVAL_MS
            const minIntervalMs = Math.max(config.MIN_GLOBAL_FETCH_INTERVAL_MS || 2000, 0);
            state._factionBundleThrottle = state._factionBundleThrottle || { lastCall: 0, lastIds: [], skipped: 0, lastSkipLog: 0 };
            state._factionBundleThrottle.lastSourceCall = state._factionBundleThrottle.lastSourceCall || {};
            // Allow force bypass with options.force (used sparingly for manual user refresh actions)
            if (!options.force) {
                if (state._factionBundleThrottle.lastCall && (nowThrottle - state._factionBundleThrottle.lastCall) < minIntervalMs) {
                    state._factionBundleThrottle.skipped++;
                    try {
                        state.script.lastFactionRefreshSkipReason = `throttled:${nowThrottle - state._factionBundleThrottle.lastCall}ms<${minIntervalMs}`;
                        if ((nowThrottle - state._factionBundleThrottle.lastSkipLog) > 4000) {
                            state._factionBundleThrottle.lastSkipLog = nowThrottle;
                            tdmlogger('debug', '[FactionBundles] SKIP (throttled)', state.script.lastFactionRefreshSkipReason);
                        }
                        ui.updateApiCadenceInfo?.();
                    } catch(_) {}
                    return; // hard skip
                }
            }
            // Soft guard against overlapping concurrent executions
            if (state._factionBundleThrottle.inFlight) {
                tdmlogger('debug', '[FactionBundles] SKIP (inFlight)');
                state.script.lastFactionRefreshSkipReason = 'inflight';
                return;
            }
            // Exclude 'attacks' client-side; retaliation and attacks are provided by backend/storage
            const ourSel = options.ourSelections || 'basic,members,rankedwars,chain';
            const oppSel = options.oppSelections || 'basic,members,rankedwars,chain';
            const ourId = state.user.factionId || null;
            const oppId = state.lastOpponentFactionId || state?.warData?.opponentId || null;

            // If explicit page faction ids were passed in, use them; otherwise try to detect from rank box when not on our own war
            let idsToFetch = Array.isArray(options.pageFactionIds) ? options.pageFactionIds.filter(Boolean) : null;
            if (!idsToFetch || idsToFetch.length === 0) {
                try {
                    const vis = utils.getVisibleRankedWarFactionIds?.();
                    if (vis && Array.isArray(vis.ids) && vis.ids.length > 0) {
                        idsToFetch = vis.ids.slice();
                    }
                } catch(_) { /* noop */ }
            }
            // Fallback to our/opponent ids if we couldn't detect from page
            if (!idsToFetch || idsToFetch.length === 0) {
                idsToFetch = [];
            }

            const extraPollRaw = storage.get('tdmExtraFactionPolls', '');
            const extraPollIds = utils.parseFactionIdList(extraPollRaw);
            try { state.script.additionalFactionPolls = extraPollIds.slice(); } catch(_) { /* noop */ }

            const warId = state.lastRankWar?.id ? String(state.lastRankWar.id) : null;
            const warActive = warId ? !!(utils.isWarActive?.(warId)) : false;
            
            // Detect pre-war (announced but not started)
            const warPre = (() => {
                if (!warId) return false;
                const w = utils.getWarById?.(warId) || state.lastRankWar;
                if (!w) return false;
                const now = Math.floor(Date.now() / 1000);
                const start = Number(w.start || w.startTime || 0);
                // Pre-war if start is in future
                return start && now < start;
            })();

            const oppIdStr = oppId ? String(oppId) : null;
            const shouldPollOpponent = !!(oppIdStr && (warActive || warPre || extraPollIds.includes(oppIdStr)));
            try {
                state.script.lastOpponentPollActive = shouldPollOpponent;
                state.script.lastOpponentPollWarActive = warActive;
                
                let reason = 'none';
                if (shouldPollOpponent) {
                    if (warActive) reason = 'war-active';
                    else if (warPre) reason = 'war-pre';
                    else reason = 'forced';
                } else if (oppIdStr) {
                    reason = 'paused';
                }
                state.script.lastOpponentPollReason = reason;

                if (!oppIdStr) state.script.lastOpponentPollAt = null;
            } catch(_) { /* noop */ }

            const candidateIds = new Set();
            if (Array.isArray(idsToFetch)) {
                idsToFetch.forEach(id => {
                    const val = String(id || '').trim();
                    if (!val) return;
                    // Always poll explicitly requested IDs (e.g. visible on page), ignoring opponent poll restrictions
                    candidateIds.add(val);
                });
            }
            if (ourId) {
                candidateIds.add(String(ourId));
            }
            if (shouldPollOpponent && oppIdStr) {
                candidateIds.add(oppIdStr);
            }
            extraPollIds.forEach(id => candidateIds.add(id));

            if (candidateIds.size === 0) return;
            idsToFetch = Array.from(candidateIds);
            // Mark attempt timestamp and details for diagnostics
            try {
                state.script.lastFactionRefreshAttemptMs = Date.now();
                state.script.lastFactionRefreshAttemptIds = (idsToFetch || []).slice();
                state.script.lastFactionRefreshSkipReason = null; // clear until proven otherwise
                tdmlogger('debug', '[FactionBundles] Attempt refresh, ids=', idsToFetch);
                ui.updateApiCadenceInfo?.();
            } catch(_) { /* ignore */ }

            // Mark as inFlight for concurrency guard
            state._factionBundleThrottle.inFlight = true;
            let anyFetched = false;
            try {
                // Fetch bundles, using ourSel for our own faction and oppSel for others
                for (const id of idsToFetch) {
                    const sel = (ourId && String(id) === String(ourId)) ? ourSel : oppSel;
                    try {
                        // tdmlogger('debug', '[FactionBundles] getTornFaction begin id=', id, 'sel=', sel);
                        await api.getTornFaction(key, sel, id);
                        if (shouldPollOpponent && oppIdStr && String(id) === oppIdStr) {
                            try { state.script.lastOpponentPollAt = Date.now(); } catch(_) { /* noop */ }
                        }
                        anyFetched = true;
                        // tdmlogger('debug', '[FactionBundles] getTornFaction id=', id, 'sel=', sel);
                    } catch(e) {
                        tdmlogger('error', '[FactionBundles] fetch error id=', id, e?.message||e);
                    }
                }
            } finally {
                const throttleState = state._factionBundleThrottle || (state._factionBundleThrottle = { lastCall: 0, lastIds: [], skipped: 0, lastSkipLog: 0, lastSourceCall: {} });
                throttleState.inFlight = false;
                const stamp = Date.now();
                if (anyFetched) {
                    throttleState.lastCall = stamp;
                    throttleState.lastIds = idsToFetch.slice();
                }
                const sourceKey = options.source || 'default';
                throttleState.lastSourceCall[sourceKey] = stamp;
            }
        },
        // Get public URLs for storage-hosted ranked war JSON assets
        // Consolidated war status fetch (phase-driven cadence) – lightweight doc read via callable.
        getWarStatus: async (rankedWarId, factionId, opts = {}) => {
            if (api._shouldBailDueToIpRateLimit('getWarStatus')) return null;
            const warId = String(rankedWarId || state.lastRankWar?.id || '');
            if (!warId) return null;
            const throttleState = state._warStatusThrottle || (state._warStatusThrottle = { lastCall: 0, lastStatus: null, lastPromise: null, inFlight: false });
            const minInterval = Number.isFinite(opts.minIntervalMs) ? Number(opts.minIntervalMs) : (config.WAR_STATUS_MIN_INTERVAL_MS || 30000);
            const now = Date.now();
            const source = typeof opts.source === 'string' && opts.source ? opts.source : 'unspecified';
            if (!opts.force) {
                if (throttleState.inFlight && throttleState.lastPromise) {
                    return throttleState.lastPromise;
                }
                if (throttleState.lastCall && (now - throttleState.lastCall) < minInterval) {
                    if (state.debug.cadence) {
                        tdmlogger('debug', '[Cadence] getWarStatus throttled', { warId, source, ageMs: now - throttleState.lastCall, minInterval });
                    }
                    return throttleState.lastStatus;
                }
            }
            const fetchPromise = (async () => {
                throttleState.inFlight = true;
                try {
                    tdmlogger('debug', '[Cadence] getWarStatus fetching', { warId, source, ensureArtifacts: !!opts.ensureArtifacts });
                    const res = await api.get('getWarStatus', { rankedWarId: warId, factionId, ensureArtifacts: !!opts.ensureArtifacts, source });
                    const fetchedAt = Date.now();
                    const statusPayload = (res && typeof res.status === 'object') ? res.status : ((res && res.status) || null);
                    throttleState.lastCall = fetchedAt;
                    throttleState.lastStatus = statusPayload;
                    state.script.lastWarStatusFetchMs = fetchedAt;
                    if (statusPayload) {
                        state._warStatusCache = { data: statusPayload, fetchedAt };
                    }
                    return statusPayload;
                } catch(e) {
                    if (state.debug.cadence) tdmlogger('warn', '[Cadence] getWarStatus failed', e?.message||e);
                    return null;
                } finally {
                    throttleState.inFlight = false;
                    throttleState.lastPromise = null;
                }
            })();
            throttleState.lastPromise = fetchPromise;
            return fetchPromise;
        },
        // Combined status + manifest fetch to coalesce two reads when we are cold booting.
        getWarStatusAndManifest: async (rankedWarId, factionId, opts = {}) => {
            if (api._shouldBailDueToIpRateLimit('getWarStatusAndManifest')) return null;
            const warId = String(rankedWarId || state.lastRankWar?.id || '');
            if (!warId) return null;
            const throttleState = state._warStatusAndManifestThrottle || (state._warStatusAndManifestThrottle = { lastCall: 0, lastResult: null, lastPromise: null, inFlight: false });
            const minInterval = Number.isFinite(opts.minIntervalMs) ? Number(opts.minIntervalMs) : (config.WAR_STATUS_AND_MANIFEST_MIN_INTERVAL_MS || 60000);
            const now = Date.now();
            const source = typeof opts.source === 'string' && opts.source ? opts.source : 'unspecified';
            if (!opts.force) {
                if (throttleState.inFlight && throttleState.lastPromise) {
                    return throttleState.lastPromise;
                }
                if (throttleState.lastCall && (now - throttleState.lastCall) < minInterval) {
                    if (state.debug.cadence) {
                        tdmlogger('debug', '[Cadence] getWarStatusAndManifest throttled', { warId, source, ageMs: now - throttleState.lastCall, minInterval });
                    }
                    return throttleState.lastResult || null;
                }
            }
            const fetchPromise = (async () => {
                throttleState.inFlight = true;
                try {
                    tdmlogger('debug', '[Cadence] getWarStatusAndManifest fetching', { warId, source, ensureArtifacts: !!opts.ensureArtifacts });
                    const res = await api.get('getWarStatusAndManifest', { rankedWarId: warId, factionId, ensureArtifacts: !!opts.ensureArtifacts, source });
                    const fetchedAt = Date.now();
                    throttleState.lastCall = fetchedAt;
                    throttleState.lastResult = res;
                    state.script.lastWarStatusFetchMs = fetchedAt;
                    if (res && res.status) {
                        state._warStatusCache = { data: res.status, fetchedAt };
                    }
                    if (res && res.manifestPointer && typeof res.manifestPointer === 'object') {
                        try {
                            const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
                            const entry = cache[warId] || (cache[warId] = { warId });
                            const p = res.manifestPointer;
                            const urls = p.urls || {};
                            const seqs = p.seqs || {};
                            const etags = p.etags || {};
                            if (urls.window) entry.windowUrl = urls.window;
                            if (urls.snapshot) entry.snapshotUrl = urls.snapshot;
                            if (urls.delta) entry.deltaUrl = urls.delta;
                            if (urls.attacks) entry.attacksUrl = urls.attacks;
                            if (urls.final) entry.attacksUrl = urls.final;
                            if (Number.isFinite(seqs.latestSeq)) entry.lastSeq = seqs.latestSeq;
                            if (Number.isFinite(seqs.attacksSeq) && !entry.lastSeq) entry.lastSeq = seqs.attacksSeq;
                            if (!entry.lastSeq && Number.isFinite(etags.attacksSeq)) entry.lastSeq = etags.attacksSeq;
                            if (typeof p.warStart !== 'undefined') entry.warStart = p.warStart;
                            if (typeof p.warEnd !== 'undefined') entry.warEnd = p.warEnd;
                            if (p.manifestFingerprint) entry._lastManifestFingerprint = p.manifestFingerprint;
                            if (typeof seqs.windowFromSeq !== 'undefined') entry.windowFromSeq = seqs.windowFromSeq;
                            if (typeof seqs.windowToSeq !== 'undefined') entry.windowToSeq = seqs.windowToSeq;
                            if (typeof seqs.lastSnapshotSeq !== 'undefined') entry.lastSnapshotSeq = seqs.lastSnapshotSeq;
                            if (typeof p.windowCount !== 'undefined') entry.windowCount = p.windowCount;
                            entry.v2Enabled = (p.storageMode === 'v2') || (p.manifestVersion === 2) || !!urls.window || !!urls.delta;
                            cache[warId] = entry; persistRankedWarAttacksCache(cache);
                        } catch(_) {}
                    }
                    if (res && res.manifest && typeof res.manifest === 'object') {
                        try {
                            const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
                            const entry = cache[warId] || (cache[warId] = { warId });
                            const m = res.manifest;
                            if (m.window && m.window.url) entry.windowUrl = m.window.url;
                            if (m.snapshot && m.snapshot.url) entry.snapshotUrl = m.snapshot.url;
                            if (m.delta && m.delta.url) entry.deltaUrl = m.delta.url;
                            if (m.final && m.final.url) entry.attacksUrl = m.final.url;
                            if (Number.isFinite(m.latestSeq)) entry.lastSeq = m.latestSeq;
                            if (Number.isFinite(m.attacksSeq) && !entry.lastSeq) entry.lastSeq = m.attacksSeq;
                            if (m.warStart) entry.warStart = m.warStart;
                            if (m.warEnd) entry.warEnd = m.warEnd;
                            if (m.manifestFingerprint) entry._lastManifestFingerprint = m.manifestFingerprint;
                            if (m.window) { entry.windowFromSeq = m.window.fromSeq; entry.windowToSeq = m.window.toSeq; entry.windowCount = m.window.count; }
                            if (m.snapshot) entry.lastSnapshotSeq = m.snapshot.seq;
                            entry.v2Enabled = m.storageMode === 'v2' || m.manifestVersion === 2 || !!m.window || !!m.delta;
                            cache[warId] = entry; persistRankedWarAttacksCache(cache);
                        } catch(_) {}
                    }
                    return res;
                } catch(e) {
                    if (state.debug.cadence) tdmlogger('warn', '[Cadence] getWarStatusAndManifest failed', e?.message||e);
                    return null;
                } finally {
                    throttleState.inFlight = false;
                    throttleState.lastPromise = null;
                }
            })();
            throttleState.lastPromise = fetchPromise;
            return fetchPromise;
        },
        getWarStorageUrls: async (rankedWarId, factionId, opts = {}) => {
            if (api._shouldBailDueToIpRateLimit('getWarStorageUrls')) return null;
            const warId = String(rankedWarId || '');
            if (!warId) return null;

            // Per-war throttle/dedupe state
            const throttleMap = state._warStorageUrlsThrottle || (state._warStorageUrlsThrottle = {});
            const throttleState = throttleMap[warId] || (throttleMap[warId] = { lastCall: 0, lastResult: null, lastPromise: null, inFlight: false });
            const minInterval = Number.isFinite(opts.minIntervalMs) ? Number(opts.minIntervalMs) : (config.GET_WAR_STORAGE_URLS_MIN_INTERVAL_MS || 30000);
            // Respect a lightweight global (cross-tab) burst limiter so multiple tabs won't spam the backend.
            const globalMinInterval = Number.isFinite(opts.globalMinIntervalMs) ? Number(opts.globalMinIntervalMs) : (config.GET_WAR_STORAGE_URLS_GLOBAL_MIN_INTERVAL_MS || 30000);
            const now = Date.now();

            // Fast-path: return in-flight promise or a recent cached result unless caller forces a fresh fetch
            if (!opts.force) {
                if (throttleState.inFlight && throttleState.lastPromise) return throttleState.lastPromise;
                if (throttleState.lastCall && (now - throttleState.lastCall) < minInterval) {
                    if (state.debug?.cadence) tdmlogger('debug', '[Cadence] getWarStorageUrls throttled', { warId, ageMs: now - throttleState.lastCall, minInterval });
                    return throttleState.lastResult;
                }
            }

            // Global cross-tab rate limit (1-per-interval) when not explicitly forced.
            try {
                const globalKey = 'getWarStorageUrlsGlobalLastCallMs';
                const lastGlobalMs = Number(storage.get(globalKey, 0) || 0);
                if (!opts.force && !opts.ensure && lastGlobalMs && (now - lastGlobalMs) < globalMinInterval) {
                    if (state.debug?.cadence) tdmlogger('debug', '[Cadence] getWarStorageUrls globally throttled', { warId, ageMs: now - lastGlobalMs, globalMinInterval });
                    // Prefer returning last in-memory result, otherwise return a synthesized entry if available.
                    if (throttleState.lastResult) return throttleState.lastResult;
                    const cachedShort = cache[warId] ? { summaryUrl: cache[warId].manifestUrl || cache[warId].summaryUrl || null, attacksUrl: cache[warId].attacksUrl || null, manifestUrl: cache[warId].manifestUrl || null, latestChunkSeq: cache[warId].lastSeq || 0 } : null;
                    return cachedShort;
                }
            } catch(_) { /* best-effort global throttle guard */ }

            // Allow short-circuit if we already have cached urls & same latest-seq (light fingerprint)
            const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
            const entry = cache[warId];
            if (!opts.force && entry && typeof entry.lastSeq === 'number' && throttleState.lastResult && typeof throttleState.lastResult.latestChunkSeq === 'number') {
                if (throttleState.lastResult.latestChunkSeq === entry.lastSeq) {
                    if (state.debug?.cadence) tdmlogger('debug', '[Cadence] getWarStorageUrls short-circuited via lastSeq fingerprint', { warId, lastSeq: entry.lastSeq });
                    return throttleState.lastResult;
                }
            }

            // Perform the fetch, with in-flight dedupe
            const fetchPromise = (async () => {
                throttleState.inFlight = true;
                try {
                    // Build a client-side conditional token when possible so server can return lightweight not-modified
                    // responses. Priority: explicit opts.ifNoneMatch -> entry.lastSeq -> entry.manifestEtag -> summary/attacks etags -> throttleState.lastResult.latestChunkSeq
                    const candidateIfNone = opts.ifNoneMatch || (entry && (typeof entry.lastSeq === 'number' ? String(entry.lastSeq) : (entry.manifestEtag || entry.storageSummaryEtag || entry.storageAttacksEtag || null))) || (throttleState.lastResult && typeof throttleState.lastResult.latestChunkSeq === 'number' ? String(throttleState.lastResult.latestChunkSeq) : null);
                    const queryParams = { rankedWarId: warId, factionId, ensure: opts.ensure };
                    if (candidateIfNone) queryParams.ifNoneMatch = candidateIfNone;
                    const res = await api.get('getWarStorageUrls', queryParams);
                    const fetchedAt = Date.now();
                    throttleState.lastCall = fetchedAt;
                    // Server may respond with { status: 'not-modified' } when our ifNoneMatch matched.
                    if (res && res.status === 'not-modified') {
                        // Prefer previous lastResult; if none, synthesize a small result from our cached entry
                        const synthesized = throttleState.lastResult || (entry ? ({ summaryUrl: entry.summaryUrl || entry.manifestUrl || null, attacksUrl: entry.attacksUrl || null, manifestUrl: entry.manifestUrl || null, latestChunkSeq: entry.lastSeq || 0, summaryEtag: entry.storageSummaryEtag || null, attacksEtag: entry.storageAttacksEtag || null, manifestEtag: entry.manifestEtag || null }) : null);
                        throttleState.lastResult = synthesized;
                        // lastCall already updated
                        return synthesized;
                    }
                    // Always record the server response (may indicate manifestMissing)
                    throttleState.lastResult = res || null;
                    // Update cross-tab last-call timestamp on successful fetch (even if server returned not-modified)
                    try {
                        const globalKey = 'getWarStorageUrlsGlobalLastCallMs';
                        storage.set(globalKey, Date.now());
                    } catch(_) { /* best-effort */ }

                    // Update local cache where appropriate for callers that rely on entry fields
                    try {
                            if (throttleState.lastResult && entry) {
                                if (typeof throttleState.lastResult.latestChunkSeq === 'number') entry.lastSeq = Number(throttleState.lastResult.latestChunkSeq || entry.lastSeq || 0);
                                if (!entry.manifestUrl && throttleState.lastResult.manifestUrl) entry.manifestUrl = throttleState.lastResult.manifestUrl;
                                if (!entry.attacksUrl && throttleState.lastResult.attacksUrl) entry.attacksUrl = throttleState.lastResult.attacksUrl;
                                // Persist any returned etags so future conditional checks can use them
                                if (typeof throttleState.lastResult.summaryEtag === 'string') entry.storageSummaryEtag = throttleState.lastResult.summaryEtag;
                                if (typeof throttleState.lastResult.attacksEtag === 'string') entry.storageAttacksEtag = throttleState.lastResult.attacksEtag;
                                if (typeof throttleState.lastResult.manifestEtag === 'string') entry.manifestEtag = throttleState.lastResult.manifestEtag;
                                // Honor server hint that manifest is missing and avoid retries for a cooldown
                                if (throttleState.lastResult.manifestMissing) {
                                    try {
                                        const until = Date.now() + (config.CLIENT_MANIFEST_MISSING_COOLDOWN_MS || 60000);
                                        entry.manifestMissingUntil = until;
                                        if (state.debug?.cadence) tdmlogger('debug', '[Cadence] server reported manifest missing; setting client cooldown', { warId, until });
                                    } catch(_) { /* swallow */ }
                                }
                            persistRankedWarAttacksCache(cache);
                        }
                    } catch (_) { /* non-fatal cache persistence */ }

                    return throttleState.lastResult;
                } catch (e) {
                    if (state.debug?.cadence) tdmlogger('warn', '[Cadence] getWarStorageUrls failed', e?.message || e);
                    return null;
                } finally {
                    throttleState.inFlight = false;
                    throttleState.lastPromise = null;
                }
            })();

            throttleState.lastPromise = fetchPromise;
            return fetchPromise;
        },
        // Raw Firestore attacks (full list) retained ONLY for deep debugging; not used in normal manifest flow.
        getRankedWarAttacksFromFirestore: async (rankedWarId, factionId) => {
            const warId = String(rankedWarId);
            try {
                const data = await api.get('getRankedWarAttacksFromFirestore', { rankedWarId: warId, factionId });
                if (data && Array.isArray(data)) return data;
                // Some server responses may wrap in { items: [] }
                if (data && Array.isArray(data.items)) return data.items;
                return [];
            } catch(e) {
                tdmlogger('warn', '[TDM] getRankedWarAttacksFromFirestore failed', e?.message||e);
                return [];
            }
        },
        // Fetch manifest (V2) with caching and ETag handling. Updates cache entry fields.
        fetchWarManifestV2: async (rankedWarId, factionId, opts = {}) => {
            const warId = String(rankedWarId);
            const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
            const entry = cache[warId] || (cache[warId] = { warId });
            // If we recently observed the server reporting the manifest as missing,
            // avoid repeated probes until the client-side cooldown expires.
            if (entry.manifestMissingUntil && Date.now() < entry.manifestMissingUntil) {
                if (state.debug?.cadence) tdmlogger('debug', `fetchWarManifestV2: manifest suppressed by client cooldown for war ${warId}`, { until: entry.manifestMissingUntil });
                return { status: 'missing', manifestMissing: true };
            }

            if (!entry.manifestUrl) {
                // Populate URLs if absent
                try {
                    let urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: (entry && entry.manifestEtag) ? entry.manifestEtag : ((entry && typeof entry.lastSeq === 'number') ? String(entry.lastSeq) : null) }).catch(() => null);
                    // If server explicitly reports manifestMissing, set a client cooldown and skip ensure attempts
                    if (urls && urls.manifestMissing) {
                        entry.manifestMissingUntil = Date.now() + (config.CLIENT_MANIFEST_MISSING_COOLDOWN_MS || 60000);
                        cache[warId] = entry; persistRankedWarAttacksCache(cache);
                        if (state.debug?.cadence) tdmlogger('debug', `fetchWarManifestV2: server reported manifestMissing for war ${warId}`, { cooldownMs: config.CLIENT_MANIFEST_MISSING_COOLDOWN_MS });
                        return { status: 'missing', manifestMissing: true };
                    }
                    if (!urls) { await api.ensureWarArtifactsSafe(warId, factionId, { force: true }).catch(()=>null); urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: (entry && entry.manifestEtag) ? entry.manifestEtag : ((entry && typeof entry.lastSeq === 'number') ? String(entry.lastSeq) : null) }).catch(()=>null); }
                    if (urls?.manifestUrl) entry.manifestUrl = urls.manifestUrl;
                    if (urls?.attacksUrl) entry.attacksUrl = urls.attacksUrl;
                    // Write early so that localStorage shows initial manifest/attacks URLs even before first 200 manifest apply
                    cache[warId] = entry; persistRankedWarAttacksCache(cache); // debug duplicate cache write removed
                } catch(_) { /* noop */ }
            }
            if (!entry.manifestUrl) return { status: 'missing' };
            // Use GM-based fetch (fetchStorageJson) to bypass site CSP. Maintain prior return semantics.
            const etag = entry.manifestEtag && !opts.force ? entry.manifestEtag : null;
            let result;
            try {
                result = await api.fetchStorageJson(entry.manifestUrl, { etag });
                try { tdmlogger('debug', `fetchWarManifestV2: fetched manifest for war ${warId}`, { url: entry.manifestUrl, status: result?.status, etag: result?.etag }); } catch(_) {}
            } catch(e) {
                return { status: 'error', error: e.message };
            }
            if (!result || typeof result.status === 'undefined') return { status: 'error', error: 'no-response' };
            if (result.status === 304) {
                // If we already have manifest-derived URLs, treat as not-modified; ensure v2 stays enabled.
                entry.v2Enabled = true; entry.v2 = true;
                // Edge case: If no window/snapshot/delta URLs recorded yet and no attacks cached, force a refetch without ETag once.
                const missingStructure = !entry.windowUrl && !entry.snapshotUrl && !entry.deltaUrl && !entry.finalized;
                const noAttacksYet = !Array.isArray(entry.attacks) || entry.attacks.length === 0;
                if (missingStructure && noAttacksYet && !opts._forcedOnce) {
                    try {
                        const forceRes = await api.fetchWarManifestV2(rankedWarId, factionId, { force: true, _forcedOnce: true });
                        return forceRes.status === 'ok' ? forceRes : { status: 'not-modified', manifest: null };
                    } catch(_) { /* swallow */ }
                }
                return { status: 'not-modified', manifest: null };
            }
            if (result.status === 404) return { status: 'missing' };
            if (result.status < 200 || result.status >= 300) return { status: 'error', code: result.status };
            let manifest = result.json;
            if (!manifest || typeof manifest !== 'object') return { status: 'error', error: 'bad-json' };
            // Fingerprint short-circuit (before heavy apply) if unchanged
            const mfp = manifest.manifestFingerprint || manifest.manifestFP || null;
            // Short-circuit if fingerprint unchanged AND we already populated core URL fields
            if (mfp && entry._lastManifestFingerprint === mfp) {
                return { status: 'not-modified', manifest: null };
            }
            if (result.etag) entry.manifestEtag = result.etag;
            // Persist important fields
            entry.v2Enabled = true; entry.v2 = true;
            if (manifest.window) {
                entry.windowFromSeq = manifest.window.fromSeq;
                entry.windowToSeq = manifest.window.toSeq;
                entry.windowCount = manifest.window.count;
                if (manifest.window.url) entry.windowUrl = manifest.window.url;
            }
            if (manifest.snapshot) {
                entry.lastSnapshotSeq = manifest.snapshot.seq;
                if (manifest.snapshot.url) entry.snapshotUrl = manifest.snapshot.url;
            }
            if (manifest.delta && manifest.delta.url) entry.deltaUrl = manifest.delta.url;
            if (manifest.final && manifest.final.url) entry.attacksUrl = manifest.final.url;
            // Persist latest known sequence number for gating window vs snapshot path
            if (typeof manifest.attacksSeq !== 'undefined') entry.lastSeq = manifest.attacksSeq;
            // Manifest v2 uses latestSeq; fall back to this when attacksSeq is absent
            if (typeof manifest.latestSeq !== 'undefined' && (!Number.isFinite(entry.lastSeq) || Number(entry.lastSeq) === 0)) {
                entry.lastSeq = manifest.latestSeq;
            }
            if (typeof manifest.warStart !== 'undefined') entry.warStart = manifest.warStart;
            if (typeof manifest.warEnd !== 'undefined') entry.warEnd = manifest.warEnd;
            if (manifest.finalized) entry.finalized = true;
            entry.updatedAt = Date.now();
            cache[warId] = entry; persistRankedWarAttacksCache(cache); // debug duplicate cache write removed
            // Track last manifest fingerprint locally (lightweight, no metrics object)
            if (mfp) entry._lastManifestFingerprint = mfp;
            return { status: 'ok', manifest };
        },
        // Decide whether to fetch window or snapshot+delta and assemble full attack list
        assembleAttacksFromV2: async (rankedWarId, factionId, opts = {}) => {
            const warId = String(rankedWarId);
            const cache = state.rankedWarAttacksCache || (state.rankedWarAttacksCache = {});
            let entry = cache[warId];
            if (!entry) {
                // Cache entry missing (possibly manual localStorage clear) – create minimal shell then force manifest fetch to populate URLs
                entry = cache[warId] = { warId, createdAt: Date.now() };
                try { await api.fetchWarManifestV2(warId, factionId, { force: true }); entry = cache[warId] || entry; } catch(_) { /* noop */ }
            }
            if (!entry.v2Enabled) {
                // Attempt one forced manifest refresh to enable V2 artifacts before bailing
                try { await api.fetchWarManifestV2(warId, factionId, { force: true }); entry = cache[warId] || entry; } catch(_) { /* noop */ }
                if (!entry.v2Enabled) return entry.attacks || [];
            }
            // Short-circuit: if finalized and we already have a local final attacks cache, return it immediately
            try {
                if (entry.finalized && Array.isArray(entry.attacks) && entry.attacks.length > 0) {
                    try { tdmlogger('debug', `assembleAttacksFromV2: final cache present, short-circuit for war=${warId}`, { count: entry.attacks.length, url: entry.attacksUrl }); } catch(_) {}
                    return entry.attacks;
                }
            } catch(_) {}
            try { tdmlogger('debug', `assembleAttacksFromV2: start war=${warId}`, { lastSeq: entry.lastSeq||0, windowCount: entry.windowCount||0, snapshotSeq: entry.lastSnapshotSeq||0, attacksLen: Array.isArray(entry.attacks)?entry.attacks.length:0, v2Enabled: !!entry.v2Enabled }); } catch(_) {}
            // Prefer window aggressively during active wars or when we have no cached attacks yet
            const lastSeq = Number(entry.lastSeq || 0);
            const windowCount = Number(entry.windowCount || 0);
            const active = (() => { try { return !!utils.isWarActive(warId); } catch(_) { return false; } })();
            const haveExisting = Array.isArray(entry.attacks) && entry.attacks.length > 0;
            const snapshotSeq = Number(entry.lastSnapshotSeq || 0);
            const windowFromSeq = Number(entry.windowFromSeq || 0);
            const windowToSeq = Number(entry.windowToSeq || 0);
            // Decide whether snapshot+delta should be preferred.
            // Heuristics:
            //  - snapshot covers almost entire history (snapshotSeq >= lastSeq - 5)
            //  - OR snapshot exists and history length (snapshotSeq) is much larger than windowCount (meaning we want full history)
            //  - OR we already have a partial (window-only) cached set whose length is clearly below latest sequence
            const historyMuchLargerThanWindow = snapshotSeq > 0 && windowCount > 0 && (snapshotSeq - windowCount) > 200;
            const snapshotNearTip = snapshotSeq > 0 && lastSeq > 0 && (snapshotSeq >= (lastSeq - 5));
            const cachedLikelyTruncated = haveExisting && lastSeq > 0 && entry.attacks.length + 50 < lastSeq; // truncated if attacks << seq
            // Additional heuristic: if windowCount equals cached attacks length and < lastSeq by a material margin, treat as truncated
            const windowClearlyTruncated = windowCount > 0 && lastSeq > 0 && windowCount < lastSeq && (!haveExisting || (haveExisting && entry.attacks.length === windowCount && windowCount + 25 < lastSeq));
            // Extra heuristics: snapshotSeq greater than lastSeq is suspicious (manifest inconsistency) — prefer snapshot
            const snapshotAheadOfLast = snapshotSeq > 0 && lastSeq > 0 && snapshotSeq > lastSeq;
            // Treat canonical small window sizes (e.g., 800) as potentially truncated when lastSeq is larger
            const suspiciousWindowSize = windowCount > 0 && (windowCount === 800 || windowCount < 1000) && lastSeq > windowCount + 25;
            let shouldPreferSnapshot = (snapshotNearTip || historyMuchLargerThanWindow || cachedLikelyTruncated || windowClearlyTruncated || snapshotAheadOfLast || suspiciousWindowSize);
            // Record explicit reasons for diagnostics
            const reasonFlags = { snapshotNearTip, historyMuchLargerThanWindow, cachedLikelyTruncated, windowClearlyTruncated, snapshotAheadOfLast, suspiciousWindowSize };
            // Record a decision object for diagnostics to help debug why window vs snapshot is chosen
            try {
                const decision = {
                    lastSeq, windowCount, snapshotSeq, windowFromSeq, windowToSeq,
                    active, haveExisting, entryAttacksLen: Array.isArray(entry.attacks) ? entry.attacks.length : 0,
                    reasonFlags
                };
                entry.fetchDecision = entry.fetchDecision || {};
                entry.fetchDecision.lastComputed = decision;
                cache[warId] = entry; persistRankedWarAttacksCache(cache);
                try { tdmlogger('info', '[TDM][ManifestDecision] computed', decision); } catch(_) {}
            } catch(_) { /* swallow diagnostics errors */ }
            // If caller explicitly forced window bootstrap, honor that one time
            if (opts.forceWindowBootstrap) shouldPreferSnapshot = false;
            // Base window decision (fast path for very early war)
            let useWindow = !!entry.windowUrl && windowCount > 0 && (active || !haveExisting);
            if (shouldPreferSnapshot && snapshotSeq > 0 && entry.snapshotUrl) {
                useWindow = false;
            }
            // Stamp planned fetch path for diagnostics / overlay
            try { entry.fetchPathPlanned = useWindow ? 'window' : 'snapshot-delta'; cache[warId] = entry; persistRankedWarAttacksCache(cache); } catch(_) {}
            if (!useWindow && opts.forceWindowBootstrap && entry.windowUrl) {
                // Caller explicitly requested a window bootstrap (e.g., summary recovery path)
                useWindow = true;
            }
            const etags = entry.etags || (entry.etags = {});
            // GM-based conditional JSON fetch honoring ETag
            const conditionalGet = async (url, key, force = false) => {
                if (!url) return { changed:false, data:null };
                try {
                    const { status, json, etag } = await api.fetchStorageJson(url, force ? {} : { etag: etags[key] });
                    if (status === 304) return { changed:false, data:null };
                    if (status >= 200 && status < 300 && Array.isArray(json)) {
                        if (etag) etags[key] = etag;
                        return { changed:true, data: json };
                    }
                    return { changed:false, data:null };
                } catch(_) { return { changed:false, data:null }; }
            };
            if (useWindow) {
                // If heuristics strongly recommend snapshot, skip window fetch and fall through
                if (shouldPreferSnapshot && entry.snapshotUrl) {
                    try { tdmlogger('info', '[TDM] Overriding window fetch due to strong snapshot preference', { warId, reasonFlags }); } catch(_) {}
                } else {
                    let { changed, data } = await conditionalGet(entry.windowUrl, 'window');
                    // 'forced' must be visible to later metadata stamping when we prefer to bootstrap
                    let forced;
                // If server says 304 but we don't have any attacks yet, force a one-time bootstrap fetch without ETag
                if (!changed && (!haveExisting || !Array.isArray(entry.attacks) || entry.attacks.length === 0)) {
                    forced = await conditionalGet(entry.windowUrl, 'window', true);
                    if (forced.changed && Array.isArray(forced.data)) { changed = true; data = forced.data; }
                }
                // If we got the window but it's clearly truncated (length far below lastSeq), immediately attempt snapshot+delta upgrade once
                if (changed && Array.isArray(data) && lastSeq > 0 && data.length + 25 < lastSeq && entry.snapshotUrl) {
                    try { tdmlogger('info', '[TDM] Window fetch appears truncated; attempting immediate snapshot+delta upgrade'); } catch(_) {}
                    // Store window first for reference
                    entry.attacks = data; entry.updatedAt = Date.now(); cache[warId] = entry; try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {} persistRankedWarAttacksCache(cache);
                    try { state.rankedWarLastAttacksSource = 'window-truncated'; state.rankedWarLastAttacksMeta = { source: 'window', count: data.length, windowCount, lastSeq, url: entry.windowUrl, truncated: true }; storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource); storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta); } catch(_) {}
                    // Recursively invoke without forcing window to allow snapshot path
                    return await api.assembleAttacksFromV2(warId, factionId, { forceWindowBootstrap: false, _recursedFromTruncated: true });
                }
                if (changed && Array.isArray(data)) {
                    // set merged attacks then add decision metadata and persist once
                    entry.attacks = data;
                    entry.updatedAt = Date.now();
                    try {
                        entry.fetchDecision = entry.fetchDecision || {};
                        entry.fetchDecision.snapshotFetch = entry.fetchDecision.snapshotFetch || {};
                        entry.fetchDecision.snapshotFetch.forcedBootstrap = true;
                        entry.fetchDecision.snapshotFetch.changed = !!forced.changed;
                        entry.fetchDecision.snapshotFetch.fetchedCount = Array.isArray(forced.data) ? forced.data.length : 0;
                        entry.fetchDecision.deltaFetch = { tried: true, forced: !!shouldPreferSnapshot, changed: !!changed, fetchedCount: Array.isArray(data) ? data.length : 0 };
                        entry.fetchDecision.deltaFetch.forcedBootstrap = true;
                        entry.fetchDecision.deltaFetch.changed = !!forced.changed;
                        entry.fetchDecision.deltaFetch.fetchedCount = Array.isArray(forced.data) ? forced.data.length : 0;
                    } catch (_){ /* noop - best-effort metadata */ }
                    cache[warId] = entry;
                    persistRankedWarAttacksCache(cache);
                                        // snapshotDirect diagnostic not applicable in window fetch branch (no 'resp' object)
                    // Stamp provenance for visibility
                    try {
                        state.rankedWarLastAttacksSource = 'window-200';
                        state.rankedWarLastAttacksMeta = { source: 'window', count: Array.isArray(entry.attacks) ? entry.attacks.length : 0, windowCount, lastSeq, url: entry.windowUrl };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    } catch(_) { /* noop */ }
                } else {
                    // No change; if we have cached, keep it and stamp meta
                    try {
                        state.rankedWarLastAttacksSource = 'window-304';
                        state.rankedWarLastAttacksMeta = { source: 'window', count: Array.isArray(entry.attacks) ? entry.attacks.length : 0, windowCount, lastSeq, url: entry.windowUrl };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    } catch(_) { /* noop */ }
                }
                    return entry.attacks || [];
                }
            }
            // snapshot + delta path
            const switchingFromWindow = entry.fetchPathPlanned === 'snapshot-delta' && Array.isArray(entry.attacks) && entry.attacks.length === windowCount && windowCount > 0 && snapshotSeq > 0;
            const existing = Array.isArray(entry.attacks) ? entry.attacks : [];
            const parts = [];
            if (entry.snapshotUrl) {
                // If heuristics prefer snapshot, force fetch the snapshot even when existing data present
                let { changed, data } = await conditionalGet(entry.snapshotUrl, 'snapshot', !!shouldPreferSnapshot);
                try {
                    entry.fetchDecision = entry.fetchDecision || {};
                    entry.fetchDecision.snapshotFetch = { tried: true, forced: !!shouldPreferSnapshot, changed: !!changed, fetchedCount: Array.isArray(data) ? data.length : (data ? (typeof data === 'object' ? Object.keys(data).length : 0) : 0) };
                    cache[warId] = entry; persistRankedWarAttacksCache(cache);
                } catch(_) {}
                if (!changed && existing.length === 0) {
                    // bootstrap if first contact returned 304
                    const forced = await conditionalGet(entry.snapshotUrl, 'snapshot', true);
                    if (forced.changed) { changed = true; data = forced.data; }
                    try {
                        entry.fetchDecision.snapshotFetch.forcedBootstrap = true;
                        entry.fetchDecision.snapshotFetch.changed = !!forced.changed;
                        entry.fetchDecision.snapshotFetch.fetchedCount = Array.isArray(forced.data) ? forced.data.length : 0;
                        cache[warId] = entry;
                        persistRankedWarAttacksCache(cache);
                    } catch (_) { }
                }
                if (changed && Array.isArray(data)) parts.push(...data);
                else if (!changed) parts.push(...existing.filter(a => Number(a.seq||0) <= Number(entry.lastSnapshotSeq||0)));
            }
            if (entry.deltaUrl) {
                // Force delta fetch when snapshot pref is present to ensure we get latest deltas
                let { changed, data } = await conditionalGet(entry.deltaUrl, 'delta', !!shouldPreferSnapshot);
                try { entry.fetchDecision = entry.fetchDecision || {}; entry.fetchDecision.deltaFetch = { tried:true, forced:!!shouldPreferSnapshot, changed:!!changed, fetchedCount: Array.isArray(data)?data.length:0 }; cache[warId]=entry; persistRankedWarAttacksCache(cache); } catch(_){}
                if (!changed && existing.length === 0) {
                    // bootstrap if first contact returned 304
                    const forced = await conditionalGet(entry.deltaUrl, 'delta', true);
                    if (forced.changed) { changed = true; data = forced.data; }
                    try { entry.fetchDecision.deltaFetch.forcedBootstrap = true; entry.fetchDecision.deltaFetch.changed = !!forced.changed; entry.fetchDecision.deltaFetch.fetchedCount = Array.isArray(forced.data)?forced.data.length:0; cache[warId]=entry; persistRankedWarAttacksCache(cache); } catch(_){}
                }
                if (changed && Array.isArray(data)) parts.push(...data);
                else if (!changed) parts.push(...existing.filter(a => Number(a.seq||0) > Number(entry.lastSnapshotSeq||0)));
            }
            // Deduplicate & sort
            const dedupeAndSort = (items) => {
                const seen = new Set();
                const out = [];
                for (const a of items) {
                    const s = Number(a.seq || a.attackSeq || a.attack_id || a.attackId || 0);
                    if (!s || seen.has(s)) continue; seen.add(s); out.push(a);
                }
                out.sort((a,b) => Number(a.seq||0)-Number(b.seq||0));
                return out;
            };

            let merged = dedupeAndSort(parts);

            // Tolerance: if dedupe produced nothing but we fetched a snapshot (parts populated),
            // try to assign provisional seq numbers (using lastSnapshotSeq when available) and
            // re-run dedupe/sort. This prevents falling back to the window when the snapshot
            // payload lacks explicit sequence fields but is otherwise valid.
            if ((!merged || merged.length === 0) && Array.isArray(parts) && parts.length > 0) {
                try {
                    const anySeq = parts.some(p => {
                        return Boolean(Number(p.seq || p.attackSeq || p.attack_id || p.attackId || 0));
                    });
                    if (!anySeq) {
                        // Determine an offset for provisional seq assignment
                        const snapBase = Number(entry.lastSnapshotSeq || snapshotSeq || 0) || 0;
                        // If snapshot seq looks valid and parts length plausible, compute offset
                        let offset = snapBase - parts.length;
                        if (!Number.isFinite(offset) || offset < 0) offset = 0;
                        // Sort parts by timestamp as a stable deterministic order
                        parts.sort((a,b) => {
                            const ta = Number(a.seq || a.ended || a.started || 0) || 0;
                            const tb = Number(b.seq || b.ended || b.started || 0) || 0;
                            return ta - tb;
                        });
                        for (let i = 0; i < parts.length; i++) {
                            const p = parts[i];
                            if (!Number(p.seq || p.attackSeq || p.attack_id || p.attackId || 0)) {
                                // assign provisional seq
                                try { p.seq = offset + i + 1; } catch(_) { p.seq = (offset + i + 1); }
                            }
                        }
                        // Re-run dedupe/sort with provisional seqs
                        merged = dedupeAndSort(parts);
                    }
                } catch(_) { /* best-effort; fall through to existing fallback */ }
            }
            // Normalize a boolean stealth flag for downstream use
            try {
                for (const a of merged) {
                    if (a && typeof a.isStealth === 'undefined' && typeof a.is_stealthed !== 'undefined') {
                        a.isStealth = !!a.is_stealthed;
                    }
                }
            } catch(_) { /* noop */ }
            // If merged is empty but window is available, try a forced window bootstrap once to avoid blank UI during active wars
            if ((!merged || merged.length === 0) && entry.windowUrl) {
                const forcedWin = await conditionalGet(entry.windowUrl, 'window', true);
                if (forcedWin.changed && Array.isArray(forcedWin.data) && forcedWin.data.length > 0) {
                    entry.attacks = forcedWin.data; entry.updatedAt = Date.now(); try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                    cache[warId] = entry; persistRankedWarAttacksCache(cache);
                    try {
                        state.rankedWarLastAttacksSource = 'window-200';
                        state.rankedWarLastAttacksMeta = { source: 'window', count: entry.attacks.length, windowCount, lastSeq, url: entry.windowUrl };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    } catch(_) { /* noop */ }
                    return entry.attacks;
                }
            }
            // If merged is empty, attempt explicit snapshot fetch as a last-resort diagnostic/fix
            if ((!merged || merged.length === 0) && entry.snapshotUrl) {
                try {
                    const resp = await api.fetchStorageJson(entry.snapshotUrl, {});
                    try { entry.fetchDecision = entry.fetchDecision || {}; entry.fetchDecision.snapshotDirect = { status: resp.status, isArray: Array.isArray(resp.json), count: Array.isArray(resp.json)?resp.json.length:0 }; cache[warId]=entry; persistRankedWarAttacksCache(cache); } catch(_){}
                    if (resp.status === 200 && Array.isArray(resp.json) && resp.json.length > 0) {
                        // persist attacks captured from the direct snapshot fetch as a single write
                        entry.attacks = resp.json;
                        try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                        entry.updatedAt = Date.now();
                        cache[warId] = entry;
                        persistRankedWarAttacksCache(cache);
                        try { state.rankedWarLastAttacksSource = 'snapshot-direct-200'; state.rankedWarLastAttacksMeta = { source: 'snapshot', count: entry.attacks.length, snapshotSeq: entry.lastSnapshotSeq, url: entry.snapshotUrl }; storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource); storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta); } catch(_){}
                        return entry.attacks;
                    }
                } catch(e) {
                    try { tdmlogger('warn', '[TDM] direct snapshot fetch failed', { warId, err: (e && e.message) || e }); } catch(_){}
                }
            }
            try { tdmlogger('info', `assembleAttacksFromV2: assembled merged attacks for war ${warId}`, { mergedLen: Array.isArray(merged)?merged.length:0, partsCount: Array.isArray(parts)?parts.length:0, fetchPathPlanned: entry.fetchPathPlanned }); } catch(_) {}
            // Only overwrite cached attacks when we actually produced a non-empty merged result.
            if (Array.isArray(merged) && merged.length > 0) {
                entry.attacks = merged; entry.updatedAt = Date.now(); try { await idb.saveAttacks(warId, entry.attacks); } catch(_) {}
                try {
                    if (switchingFromWindow) {
                        state.rankedWarLastAttacksSource = 'snapshot-delta-switch';
                        state.rankedWarLastAttacksMeta = { source: 'snapshot-delta', merged: merged.length, snapshotSeq, lastSeq, windowCount, windowFromSeq, windowToSeq };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    } else {
                        state.rankedWarLastAttacksSource = 'snapshot-delta';
                        state.rankedWarLastAttacksMeta = { source: 'snapshot-delta', merged: merged.length, snapshotSeq, lastSeq };
                        storage.set('rankedWarLastAttacksSource', state.rankedWarLastAttacksSource);
                        storage.set('rankedWarLastAttacksMeta', state.rankedWarLastAttacksMeta);
                    }
                } catch(_) { /* noop */ }
            } else {
                try { tdmlogger('debug', `assembleAttacksFromV2: merged result empty; preserving existing cache for war=${warId}`, { existingCount: Array.isArray(entry.attacks)?entry.attacks.length:0, lastSeq, snapshotSeq, windowCount }); } catch(_) {}
            }
            cache[warId] = entry; persistRankedWarAttacksCache(cache);
            return merged;
        },
        // Admin trigger: force backend to rebuild full summary (forceFull=true). Returns true on success.
        forceFullWarSummaryRebuild: async (rankedWarId, factionId) => {
            try {
                // New path via api.post action
                try {
                    const res = await api.post('forceRankedWarSummaryRebuild', { rankedWarId, factionId, forceFull: true });
                    tdmlogger('info', '[TDM] Forced full summary rebuild invoked (api.post)', res);
                    return true;
                } catch(primaryErr) {
                    tdmlogger('warn', '[TDM] api.post forceRankedWarSummaryRebuild failed, attempting legacy callable fallback', primaryErr);
                    if (state.firebase?.functions?.httpsCallable) {
                        try {
                            const fn = state.firebase.functions.httpsCallable('triggerRankedWarSummaryRebuild');
                            const legacy = await fn({ rankedWarId, factionId, forceFull: true });
                            tdmlogger('info', '[TDM] Forced full summary rebuild via legacy callable', legacy?.data || legacy);
                            return true;
                        } catch(fallbackErr) {
                            tdmlogger('error', '[TDM] forceFullWarSummaryRebuild callable fallback failed', fallbackErr);
                        }
                    }
                    throw primaryErr;
                }
            } catch(e){ tdmlogger('warn', '[TDM] forceFullWarSummaryRebuild failed', e); }
            return false;
        },
        // Dev: force backend to pull latest attacks (manifest refresh + attempt snapshot + delta) via callable (if implemented).
        forceBackendWarAttacksRefresh: async (rankedWarId, factionId) => {
            try {
                try {
                    const res = await api.post('forceWarAttacksRefresh', { rankedWarId, factionId });
                    tdmlogger('info', '[TDM] Forced backend war attacks refresh invoked (api.post)', res);
                    return true;
                } catch(primaryErr) {
                    tdmlogger('warn', '[TDM] api.post forceWarAttacksRefresh failed, attempting legacy callable fallback', primaryErr);
                    if (state.firebase?.functions?.httpsCallable) {
                        try {
                            const fn = state.firebase.functions.httpsCallable('triggerRankedWarAttacksRefresh');
                            const legacy = await fn({ rankedWarId, factionId, force: true });
                            tdmlogger('info', '[TDM] Forced backend war attacks refresh via legacy callable', legacy?.data || legacy);
                            return true;
                        } catch(fallbackErr) {
                            tdmlogger('error', '[TDM] forceBackendWarAttacksRefresh callable fallback failed', fallbackErr);
                        }
                    }
                    throw primaryErr;
                }
            } catch(e){ tdmlogger('warn', '[TDM] forceBackendWarAttacksRefresh failed', e); }
            return false;
        },
        // Integrity checker: evaluate local attack coverage vs manifest sequence metrics.
        verifyWarAttackIntegrity: (rankedWarId) => {
            const warId = String(rankedWarId);
            const entry = state.rankedWarAttacksCache?.[warId];
            if (!entry) return { ok:false, reason:'no_cache' };
            const attacks = Array.isArray(entry.attacks) ? entry.attacks : [];
            const seqs = attacks.map(a => Number(a.seq||a.attackSeq||0)).filter(n=>n>0).sort((a,b)=>a-b);
            const first = seqs[0] || null;
            const last = seqs.length ? seqs[seqs.length-1] : null;
            const gaps = [];
            for (let i=1;i<seqs.length;i++){ const prev=seqs[i-1]; const cur=seqs[i]; if (cur>prev+1) { gaps.push([prev+1, cur-1]); if (gaps.length>10) break; } }
            const latestSeq = Number(entry.lastSeq||0);
            const coveragePct = latestSeq>0? Number(((attacks.length/latestSeq)*100).toFixed(2)) : null;
            return { ok:true, attacks:attacks.length, first, last, latestSeq, coveragePct, gapCount:gaps.length, sampleGaps: gaps.slice(0,5), fetchPath: entry.fetchPathPlanned };
        },
        // Compute ranked war summary locally from cached attacks (no backend reads)
        // Returns array of rows. Fields align with server summary shape where possible:
        // [{ attackerId, attackerName, attackerFactionId, attackerFactionName, totalAttacks, wins, losses, stalemates, totalRespectGain, totalRespectGainNoChain, respect, lastTs }]
        getRankedWarSummaryLocal: async (rankedWarId, factionId) => {
            const warId = String(rankedWarId);
            // Try summary.json first
            let summaryRows = [];
            // Prefer summary cache ETag for conditional check when available; fallback to attacks cache lastSeq
            const summaryCache = state.rankedWarSummaryCache?.[warId];
            const fallbackSeq = state.rankedWarAttacksCache?.[warId]?.lastSeq;
            const summaryIfNone = summaryCache?.etag ? summaryCache.etag : (typeof fallbackSeq === 'number' ? String(fallbackSeq) : null);
            let urls = await api.getWarStorageUrls(warId, factionId, { ifNoneMatch: summaryIfNone }).catch(() => null);
            if (urls && urls.summaryUrl) {
                // FIX: Pass etag to fetchStorageJson to enable 304 Not Modified
                const { status, json, etag, lastModified } = await api.fetchStorageJson(urls.summaryUrl, { etag: summaryCache?.etag });

                // Handle 304 Not Modified: use cached summary if available
                if (status === 304 && summaryCache && Array.isArray(summaryCache.summary)) {
                    summaryRows = summaryCache.summary;
                    // Update timestamp on cache hit
                    summaryCache.updatedAt = Date.now();
                    state.rankedWarSummaryCache = state.rankedWarSummaryCache || {};
                    state.rankedWarSummaryCache[warId] = summaryCache;
                    storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                    
                    // Stamp provenance for 304
                    try {
                        state.rankedWarLastSummarySource = 'summary-json-304';
                        state.rankedWarLastSummaryMeta = { source: 'summary-json-304', count: summaryRows.length, url: urls.summaryUrl };
                        storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                        storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                    } catch(_) {}
                    return summaryRows;
                }

                if (status === 200 && (Array.isArray(json) || (json && Array.isArray(json.items)))) {
                        if (Array.isArray(json)) {
                            summaryRows = json;
                        } else if (json && Array.isArray(json.items)) {
                            summaryRows = json.items;
                            try { state.rankedWarLastSummaryMeta = state.rankedWarLastSummaryMeta || {}; state.rankedWarLastSummaryMeta.scoreBleed = json.scoreBleed || null; } catch(_) {}
                        } else {
                            summaryRows = [];
                        }

                    // Cache the result for future 304s
                    if (summaryRows.length > 0) {
                        const nextCache = {
                            warId,
                            summaryUrl: urls.summaryUrl,
                            etag: etag || null,
                            lastModified: lastModified || null,
                            updatedAt: Date.now(),
                            summary: summaryRows,
                            source: 'storage200'
                        };
                        state.rankedWarSummaryCache = state.rankedWarSummaryCache || {};
                        state.rankedWarSummaryCache[warId] = nextCache;
                        storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                    }

                    let provenanceStamped = false;
                    try {
                        state.rankedWarLastSummarySource = 'summary-json';
                        state.rankedWarLastSummaryMeta = { source: 'summary-json', count: summaryRows.length, url: urls.summaryUrl };
                        storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                        storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                        provenanceStamped = true;
                    } catch(_) { /* noop */ }
                    // Sanity fallback: if every attacker has zero totalRespectGain but we have (or can fetch) window attacks
                    try {
                        const allZero = summaryRows.length > 0 && summaryRows.every(r => !Number(r.totalRespectGain));
                        if (allZero) {
                            // Attempt forced window bootstrap to verify if local attacks show positive gains
                            try { await api.assembleAttacksFromV2(warId, factionId, { forceWindowBootstrap: true }); } catch(_) { /* ignore */ }
                            const cacheAttacks = state.rankedWarAttacksCache?.[warId]?.attacks || [];
                            const anyPositive = cacheAttacks.some(a => Number(a.respect_gain || a.respect || 0) > 0);
                            if (anyPositive) {
                                // We'll ignore summary.json and aggregate locally instead to surface real gains
                                summaryRows = [];
                            }
                        }
                    } catch(_) { /* ignore */ }
                    if (summaryRows.length > 0) {
                        // Keep summary (either had non-zero gains or no local positive evidence)
                        return summaryRows;
                    } else if (provenanceStamped) {
                        // Overriding summary due to zero-gain anomaly; update provenance note later when local aggregation completes
                        try {
                            state.rankedWarLastSummaryMeta.zeroGainAnomaly = true;
                        } catch(_) { /* noop */ }
                    }
                }
            }
            // Fallback: aggregate attacks
            const cache = state.rankedWarAttacksCache?.[warId];
            let attacks = Array.isArray(cache?.attacks) ? cache.attacks : [];
            // If in-memory cache lacked attacks (not persisted to localStorage by design), try IndexedDB
            if ((!Array.isArray(attacks) || attacks.length === 0)) {
                try {
                    const idbRes = await idb.getAttacks(warId);
                    if (idbRes && Array.isArray(idbRes.attacks) && idbRes.attacks.length > 0) {
                        attacks = idbRes.attacks;
                        // Rehydrate in-memory state for faster subsequent access and preserve IDB freshness
                        try {
                            state.rankedWarAttacksCache = state.rankedWarAttacksCache || {};
                            const current = state.rankedWarAttacksCache[warId] || {};
                            const next = { ...(current||{}), attacks };
                            if (!next.lastModified && idbRes.updatedAt) next.lastModified = idbRes.updatedAt;
                            if (!next.updatedAt && idbRes.updatedAt) next.updatedAt = idbRes.updatedAt;
                            state.rankedWarAttacksCache[warId] = next;
                        } catch(_) {}
                    }
                } catch(_) { /* noop */ }
            }
            // Auto-upgrade: if we are still on window path and clearly truncated vs latestSeq, attempt snapshot+delta assembly.
            try {
                if (cache && Number(cache.lastSeq||0) > 0 && attacks.length + 50 < Number(cache.lastSeq||0) && cache.fetchPathPlanned === 'window') {
                    tdmlogger('info', '[TDM] Local summary detected truncated window; triggering snapshot+delta fetch');
                    try { await api.assembleAttacksFromV2(warId, factionId, { forceWindowBootstrap: false }); } catch(_) {}
                    // Re-evaluate attacks after upgrade
                    const upgraded = state.rankedWarAttacksCache?.[warId]?.attacks || attacks;
                    if (upgraded !== attacks) {
                        attacks.length = 0; // mutate existing reference if used further down (safeguard)
                        for (const a of upgraded) attacks.push(a);
                    }
                }
            } catch(_) { /* noop */ }
            // Aggregate per attacker
            const per = new Map();
            for (const a of attacks) {
                if (!a) continue;
                // Handle stealth attacks in ranked wars: attacker may be null but war modifier indicates war context
                try {
                    const isRankedContext = (a && (a.is_ranked_war === true || Number(a?.modifiers?.war || 0) === 2));
                    const hasNoAttacker = !(a?.attackerId || a?.attacker_id || (a?.attacker && (a.attacker.id || a.attacker)));
                    const isStealth = !!(a?.is_stealthed || a?.isStealthed || a?.isStealth);
                    if (isRankedContext && hasNoAttacker && isStealth) {
                        const ourFactionId = String(state?.user?.factionId || '');
                        const oppFactionId = String(state?.warData?.opponentId || state?.lastOpponentFactionId || '');
                        const defFac = String(a?.defenderFactionId || a?.defender?.faction?.id || a?.defender_faction_id || '');
                        // Infer attacking faction: if defender is ours, then attacker is opponent; if defender is opponent, attacker is ours
                        let inferredAttackerFaction = '';
                        if (ourFactionId && defFac === ourFactionId) inferredAttackerFaction = oppFactionId || 'opponent';
                        else if (oppFactionId && defFac === oppFactionId) inferredAttackerFaction = ourFactionId || 'ours';
                        a.attackerId = `someone:${inferredAttackerFaction || 'unknown'}`;
                        a.attackerName = 'Someone (stealth)';
                        if (inferredAttackerFaction) a.attackerFactionId = inferredAttackerFaction;
                        if (typeof a.isStealth === 'undefined') a.isStealth = true;
                    }
                } catch(_) { /* noop */ }
                const attackerId = String(a.attackerId || a.attacker_id || a.attacker?.id || '');
                if (!attackerId) continue;
                let row = per.get(attackerId);
                if (!row) {
                    row = {
                        attackerId,
                        attackerName: a.attackerName || a.attacker.name || '',
                        attackerFactionId: a.attackerFactionId || a.attacker.faction?.id || null,
                        attackerFactionName: a.attackerFactionName || a.attacker.faction?.name || '',
                        totalAttacks: 0,
                        // Successful attack counter (counts only outcomes that should be considered "scoring" for 'Attacks' caps)
                        totalAttacksSuccessful: 0,
                        wins: 0,
                        losses: 0,
                        stalemates: 0,
                        // Keep original 'respect' for compatibility, but also expose server-like field names
                        respect: 0,
                        totalRespectGain: 0,
                        totalRespectGainNoChain: 0,
                        totalRespectLoss: 0,
                        lastTs: 0,
                        // Compatibility alias used in some consumers
                        factionId: null
                    };
                    per.set(attackerId, row);
                }
                row.totalAttacks += 1;
                // Count success-only attacks (Mugged, Attacked, Hospitalized, Arrested, Bounty)
                try {
                    const outcome = String(a.result || a.outcome || '').toLowerCase();
                    const successRE = /(?:mug|attack|hospital|arrest|bounty)/i;
                    if (successRE.test(outcome)) row.totalAttacksSuccessful += 1;
                } catch(_) {}
                const res = (a.result || a.outcome || '').toLowerCase();
                const r = Number(a.respect || a.respect_gain || 0);
                const hasPositiveGain = Number.isFinite(r) && r > 0;
                // New win classification: treat any positive respect gain as a win unless explicitly a loss keyword.
                if ((/hospital|mug|arrest|leave/.test(res)) || hasPositiveGain) {
                    row.wins += 1;
                } else if (/lost|escape/.test(res)) {
                    row.losses += 1;
                } else {
                    row.stalemates += 1; // includes timeouts/cancels/unknowns
                }
                if (Number.isFinite(r)) {
                    row.respect += r;
                    row.totalRespectGain += r;
                    row.totalRespectGainNoChain += r; // chain-agnostic mirror
                }
                // Respect lost accumulates only when our faction is the defender in this event
                try {
                    const ourFactionId = String(state?.user?.factionId || '');
                    const defFacId = String(a?.defenderFactionId || a?.defender?.faction?.id || a?.defender_faction_id || '');
                    if (ourFactionId && defFacId === ourFactionId) {
                        const loss = Number(a?.respect_loss || a?.respectLoss || 0);
                        if (Number.isFinite(loss) && loss > 0) {
                            row.totalRespectLoss = (row.totalRespectLoss || 0) + loss;
                        }
                    }
                } catch(_) { /* noop */ }
                const ts = Number(a.ended || a.timestamp || 0);
                if (ts > row.lastTs) row.lastTs = ts;
                // Keep aliases in sync
                row.factionId = row.attackerFactionId;
                row.attackerFaction = row.attackerFactionId;
            }
            const rows = Array.from(per.values());
            // Stable sort: highest respect then most recent
            rows.sort((x, y) => (y.respect - x.respect) || (y.lastTs - x.lastTs));
            // Provenance
            try {
                state.rankedWarLastSummarySource = 'local-attacks';
                state.rankedWarLastSummaryMeta = {
                    source: 'local-attacks',
                    attacksCount: attacks.length,
                    lastSeq: Number(cache?.lastSeq || 0),
                    lastModified: cache?.lastModified || null,
                };
                storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
            } catch(_) {}
            return rows;
        },
        // Prefer local aggregation during active wars; fallback to storage/server summary
        getRankedWarSummaryPreferLocal: async (rankedWarId, factionId) => {
            try {
                utils.perf.start('getRankedWarSummaryPreferLocal');
                // Recovery: if attacks cache was manually cleared (e.g., user deleted localStorage key) ensure we attempt a bootstrap
                try {
                    const warId = String(rankedWarId);
                    const cacheEntry = state.rankedWarAttacksCache?.[warId];
                    const needsBootstrap = !cacheEntry || !Array.isArray(cacheEntry.attacks) || cacheEntry.attacks.length === 0;
                    if (needsBootstrap) {
                        // Force manifest fetch (ignore 304) to repopulate URLs, then attempt window path if available
                        await api.fetchWarManifestV2(warId, factionId, { force: true }).catch(()=>null);
                        // Attempt a one-shot assemble; this will choose window if possible
                        try { await api.assembleAttacksFromV2(warId, factionId, { forceWindowBootstrap: true }); } catch(_) { /* ignore */ }
                    }
                } catch(_) { /* swallow bootstrap errors */ }
                const local = await api.getRankedWarSummaryLocal(rankedWarId, factionId);
                if (Array.isArray(local) && local.length > 0) {
                    // Stamp provenance when local is chosen
                    try {
                        const warId = String(rankedWarId);
                        const cache = state.rankedWarAttacksCache?.[warId] || {};
                        state.rankedWarLastSummarySource = 'local';
                        state.rankedWarLastSummaryMeta = {
                            source: 'local',
                            attacksCount: Array.isArray(cache.attacks) ? cache.attacks.length : 0,
                            lastSeq: Number(cache.lastSeq || 0),
                            lastModified: cache.lastModified || null,
                        };
                        storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                        storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                        // console.info('[TDM] War summary source: local-attacks', state.rankedWarLastSummaryMeta); // silenced to reduce noise
                    } catch(_) { /* noop */ }
                    utils.perf.stop('getRankedWarSummaryPreferLocal');
                    return local;
                }
            } catch(_) { /* ignore */ }
            utils.perf.stop('getRankedWarSummaryPreferLocal');
            return api.getRankedWarSummarySmart(rankedWarId, factionId);
        },
        // Fetch JSON from a public URL with optional ETag conditional; returns { status, json, etag, lastModified }
        fetchStorageJson: (url, opts = {}) => {
            const headers = {};
            if (opts && opts.etag) headers['If-None-Match'] = opts.etag;
            if (opts && opts.ifModifiedSince) headers['If-Modified-Since'] = opts.ifModifiedSince;
            return new Promise((resolve) => {
                try {
                    const ret = state.gm.rD_xmlhttpRequest({
                        method: 'GET', url, headers,
                        onload: (resp) => {
                            try {
                                const status = Number(resp.status || 0);
                                const rawHeaders = String(resp.responseHeaders || '');
                                const hdrs = {};
                                rawHeaders.split(/\r?\n/).forEach(line => {
                                    const idx = line.indexOf(':');
                                    if (idx > 0) {
                                        const k = line.slice(0, idx).trim().toLowerCase();
                                        const v = line.slice(idx + 1).trim();
                                        hdrs[k] = v;
                                    }
                                });
                                const etag = hdrs['etag'] || null;
                                const lastModified = hdrs['last-modified'] || null;
                                if (status === 304) {
                                    resolve({ status, json: null, etag, lastModified });
                                    return;
                                }
                                if (status >= 200 && status < 300) {
                                    let json = [];
                                    try { json = JSON.parse(resp.responseText || '[]'); } catch(_) { json = []; }
                                    resolve({ status, json, etag, lastModified });
                                    return;
                                }
                                resolve({ status, json: null, etag, lastModified });
                            } catch(_) { resolve({ status: 0, json: null, etag: null, lastModified: null }); }
                        },
                        onerror: () => resolve({ status: 0, json: null, etag: null, lastModified: null })
                    });
                    if (ret && typeof ret.catch === 'function') ret.catch(() => {});
                } catch(_) { resolve({ status: 0, json: null, etag: null, lastModified: null }); }
            });
        },
        // Decide if we should attempt a manifest fetch based on phase / last activity.
        shouldFetchManifest: (entry, reason, active, nowMs) => {
            try {
                // Always fetch if reason explicitly score-change
                if (reason === 'score-change') return true;
                // Respect nextAllowedFetchMs throttle if set
                if (entry.nextAllowedManifestMs && nowMs < entry.nextAllowedManifestMs) return false;
                const lastSeq = Number(entry.lastSeq || 0);
                const haveSummary = !!(state.rankedWarSummaryCache?.[entry.warId]?.etag);
                const warStart = Number(entry.warStart || 0);
                const warEnd = Number(entry.warEnd || 0);
                // PRE phase (warStart in future or 0 && !active)
                if (!active && (!warStart || warStart > (Date.now()/1000)) && lastSeq === 0 && !haveSummary) {
                    // Only every ~10 minutes (600k ms)
                    return (nowMs - (entry.lastManifestFetchMs||0)) > 600000;
                }
                // ENDED phase: if finalized and no new seqs expected, poll rarely
                if (!active && warEnd > 0 && entry.finalized) {
                    return (nowMs - (entry.lastManifestFetchMs||0)) > 300000; // 5m
                }
                // ACTIVE base interval 25s; tighten to 7s if we saw new seq recently (<30s)
                const recentUpdate = entry.updatedAt && (nowMs - entry.updatedAt) < 30000;
                const base = recentUpdate ? 7000 : 25000;
                return (nowMs - (entry.lastManifestFetchMs||0)) > base;
            } catch(_) { return true; }
        },
        // Idempotent request to backend to lazily materialize storage artifacts for a war
        ensureWarArtifacts: async (rankedWarId, factionId) => {
            if (api._shouldBailDueToIpRateLimit('ensureWarArtifacts')) return null;
            try {
                const warId = String(rankedWarId);
                state._ensureWarArtifactsMs = state._ensureWarArtifactsMs || {};
                const now = Date.now();
                // 15s client throttle mirrors backend guard to avoid stampedes
                if (state._ensureWarArtifactsMs[warId] && now - state._ensureWarArtifactsMs[warId] < 15000) {
                    return null;
                }
                state._ensureWarArtifactsMs[warId] = now;
                const res = await api.get('ensureWarArtifacts', { rankedWarId: warId, factionId });
                state.rankedWarEnsureMeta = state.rankedWarEnsureMeta || {};
                state.rankedWarEnsureMeta[warId] = { ts: now, ok: true, res: (res || null) };
                return res;
            } catch (e) {
                try {
                    state.rankedWarEnsureMeta = state.rankedWarEnsureMeta || {};
                    state.rankedWarEnsureMeta[String(rankedWarId)] = { ts: Date.now(), ok: false, error: e?.message || String(e) };
                } catch(_) { /* noop */ }
                return null;
            }
        },

        // Safe helper: prefer GET (cheap) and only call ensure when explicitly forced. Returns { storage } or null.
        ensureWarArtifactsSafe: async (rankedWarId, factionId, opts = {}) => {
            if (api._shouldBailDueToIpRateLimit('ensureWarArtifactsSafe')) return null;
            const warId = String(rankedWarId);
            const force = !!opts.force || !!opts.forceEnsure;
            // Try cheap GET first
            try {
                const urls = await api.getWarStorageUrls(warId, factionId).catch(() => null);
                if (urls && (urls.summaryUrl || urls.manifestUrl || urls.attacksUrl)) return { storage: urls, fromGet: true };
            } catch(_) { /* ignore and allow optional ensure below */ }
            if (!force) return null;
            // If forced, call ensure (still throttled client-side by ensureWarArtifacts implementation)
            try {
                await api.ensureWarArtifacts(warId, factionId);
                const after = await api.getWarStorageUrls(warId, factionId, { ensure: true }).catch(()=>null);
                if (after && (after.summaryUrl || after.manifestUrl || after.attacksUrl)) return { storage: after, fromEnsure: true };
            } catch(_) {}
            return null;
        }
    };

    //======================================================================
    // 5. UI MODULE
    //======================================================================
    const ui = {
        _formatStatus(rec, meta) {
             let label = (rec.canonical || rec.status || '—').trim();
             // Default: 0 means no explicit sub-rank override (fall back to lexical heuristics)
             let subRank = 0;
             let remainingText = '';
             let sortVal = 0;
             const now = Date.now();

             // Resolve 'until' timestamp (normalize to ms)
             let untilMs = 0;
             // Prefer meta.hospitalUntil if available (often more fresh/specific for hospital)
             // Also accept `rec.rawUntil` (API seconds) and `rec.arrivalMs` (ms) as common sources.
             let rawUntil = (meta && meta.hospitalUntil) || rec.until || rec.rawUntil;
             if (rawUntil) {
                 // Heuristic: if < 1e12 (year 2001 in ms), assume seconds (Torn API uses seconds)
                 if (rawUntil < 1000000000000) untilMs = rawUntil * 1000;
                 else untilMs = rawUntil;
             }

             // Helper to extract destination from description
             const extractDest = (desc, prefix) => {
                 if (!desc) return '';
                 // Capture until end of string or opening parenthesis (start of duration/time)
                 const regex = new RegExp(`${prefix}\\s+(.+?)(?:$|\\()`, 'i');
                 const match = desc.match(regex);
                 return match ? match[1].trim() : '';
             };
             
             // Resolve destination from description, status, or meta
             const resolveDest = (desc, prefix) => {
                 let d = extractDest(desc, prefix);
                 if (!d) d = extractDest(rec.status, prefix);
                 if (!d && meta && meta.dest) d = meta.dest;
                 if (!d && rec.dest) d = rec.dest;
                 return d;
             };

             // Travel Logic
             if (label === 'Traveling' || label === 'Returning' || label === 'Travel') {
                const isReturn = label === 'Returning';
                const desc = rec.description || '';
                const dest = resolveDest(desc, isReturn ? 'from' : 'to');
                const abbr = utils.abbrevDest(dest) || dest;
                const arrow = isReturn ? '←' : '→';
                
                // Duration logic
                if (untilMs > now) {
                    const diff = Math.ceil((untilMs - now) / 1000);
                    sortVal = diff;
                    const h = Math.floor(diff / 3600);
                    const m = Math.floor((diff % 3600) / 60);
                    remainingText = ` dur. ${h}h ${m}m`;
                }
                
                label = `${arrow} ${abbr}`;
                
                // Sub-ranks follow the ranked-war heuristic (higher = more urgent for sorting)
                // Travel: high priority so it sorts above Abroad/Hospital.
                if (isReturn) subRank = 40; else subRank = 45; // Traveling/Returning
             }
             else if (label === 'Abroad') {
                 const desc = rec.description || '';
                 const dest = resolveDest(desc, 'In');
                 const abbr = utils.abbrevDest(dest) || dest;
                 label = `In ${abbr}`;
                 subRank = 50;
             }
             else if (label === 'HospitalAbroad') {
                 const desc = rec.description || '';
                 const dest = resolveDest(desc, 'in');
                 const abbr = utils.abbrevDest(dest) || dest;
                 label = `${abbr}`;
                 // Hospital abroad should sort highly (second only to in-hospital)
                 subRank = 90;
                 if (untilMs > now) {
                    const diff = Math.ceil((untilMs - now) / 1000);
                    sortVal = diff;
                    if (diff > 0) { remainingText = ` ${utils.formatTimeHMS(diff)}`; }
                 }
             }
             else if (label === 'Hospital') {
                // Hospital should be considered the highest-priority status for sorting
                subRank = 100;
                if (untilMs > now) {
                    const diff = Math.ceil((untilMs - now) / 1000);
                    sortVal = diff;
                    if (diff > 0) { label = `${utils.formatTimeHMS(diff)}`; }
                 }
             }
             else if (label === 'Okay') {
                subRank = 15;
             }
             
             return { label, remainingText, sortVal, subRank };
        },
        _refreshStatusTimes() {
            try {
                const unified = state.unifiedStatus || {};
                // Streamlined selectors to target only content rows and avoid headers
                const cells = document.querySelectorAll(
                    '.table-body > li .status, ' + // Faction/Members list rows
                    '.tab-menu-cont ul > li .status' // Ranked war rows
                );

                cells.forEach(cell => {
                    const row = cell.closest('li') || cell.closest('.table-row');
                    if (!row) return;

                    // Strict Header Guards (though selectors should prevent this)
                    if (cell.closest('.table-header')) return;
                    if (cell.classList.contains('tdm-rank-sort-header')) return;
                    if (cell.closest('.tdm-rank-sort-header')) return;

                    let id = row.dataset.id || row.dataset.tdmOpponentId || row.dataset.opponentId;
                    if (!id) {
                        const link = row.querySelector('a[href*="XID="]');
                        if (link) {
                            const m = link.href.match(/XID=(\d+)/);
                            if (m) id = m[1];
                        }
                    }
                    if (!id) return;
                    const rec = unified[id];
                    if (!rec) return;
                    const meta = state.rankedWarChangeMeta ? state.rankedWarChangeMeta[id] : null;

                    const { label, remainingText, sortVal, subRank } = ui._formatStatus(rec, meta);

                    // Preserve inner structure (colors) by targeting the first child if present
                    const target = cell.firstElementChild || cell;
                    let disp = (label || '') + (remainingText || '');
                    
                    if ((target.textContent || '') !== disp || cell.dataset.tdmPhase !== (rec.canonical || rec.phase) || cell.dataset.tdmConf !== (rec.confidence || '')) {
                        target.textContent = disp;
                        cell.dataset.tdmPhase = rec.canonical || rec.phase || '';
                        cell.dataset.tdmConf = rec.confidence || '';
                    }

                    cell.dataset.sortValue = sortVal;
                    if (subRank > 0) cell.dataset.subRank = subRank;
                    else delete cell.dataset.subRank;
                });
            } catch(_) {}
        },
        _getFFColor(value) {
            let r, g, b;
            if (value <= 1) {
                r = 0x28; g = 0x28; b = 0xc6;
            } else if (value <= 3) {
                const t = (value - 1) / 2;
                r = 0x28;
                g = Math.round(0x28 + (0xc6 - 0x28) * t);
                b = Math.round(0xc6 - (0xc6 - 0x28) * t);
            } else if (value <= 5) {
                const t = (value - 3) / 2;
                r = Math.round(0x28 + (0xc6 - 0x28) * t);
                g = Math.round(0xc6 - (0xc6 - 0x28) * t);
                b = 0x28;
            } else {
                r = 0xc6; g = 0x28; b = 0x28;
            }
            return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase();
        },
        _getContrastColor(hex) {
            if (!hex) return 'black';
            const r = parseInt(hex.slice(1, 3), 16);
            const g = parseInt(hex.slice(3, 5), 16);
            const b = parseInt(hex.slice(5, 7), 16);
            const brightness = r * 0.299 + g * 0.587 + b * 0.114;
            return brightness > 126 ? 'black' : 'white';
        },
        // Remove native title tooltips on touch devices (PDA) to prevent sticky popups
        _sanitizeTouchTooltips(root=document) {
            try {
                const isTouch = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0);
                if (!isTouch) return;
                root.querySelectorAll('[title]').forEach(el => {
                    if (!el.getAttribute) return;
                    const t = el.getAttribute('title');
                    if (!t) return;
                    // Preserve semantics
                    if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', t);
                    el.removeAttribute('title');
                });
            } catch(_) {}
        },
        _coerceLevelDisplayMode(mode) {
            return LEVEL_DISPLAY_MODES.includes(mode) ? mode : DEFAULT_LEVEL_DISPLAY_MODE;
        },
        isLevelOverlayEnabled(opts = {}) {
            if (opts.ignoreFeatureFlag === true) return true;
            try { return utils.isFeatureEnabled('rankWarEnhancements.overlay'); } catch(_) { return false; }
        },
        getLevelDisplayMode(opts = {}) {
            if (!ui.isLevelOverlayEnabled(opts)) return DEFAULT_LEVEL_DISPLAY_MODE;
            const stored = state.ui?.levelDisplayMode;
            return ui._coerceLevelDisplayMode(stored);
        },
        setLevelDisplayMode(mode, opts = {}) {
            const resolved = ui._coerceLevelDisplayMode(mode);
            const prev = ui._coerceLevelDisplayMode(state.ui?.levelDisplayMode);
            state.ui.levelDisplayMode = resolved;
            if (!opts.skipPersist) {
                try { storage.set(state.ui.levelDisplayModeKey, resolved); } catch(_) {}
            }
            if (opts.log !== false && prev !== resolved) {
                try { tdmlogger('info', '[LevelOverlay] mode change', { prev, next: resolved, source: opts.source || 'user' }); } catch(_) {}
            }
            if ((prev !== resolved || opts.forceRefresh) && opts.skipRefresh !== true) {
                try {
                    if (typeof ui.queueLevelOverlayRefresh === 'function') {
                        ui.queueLevelOverlayRefresh({ reason: 'mode-change' });
                    }
                } catch(_) { /* noop */ }
            }
            return resolved;
        },
        cycleLevelDisplayMode(opts = {}) {
            const current = ui.getLevelDisplayMode({ ignoreFeatureFlag: opts.ignoreFeatureFlag });
            const idx = LEVEL_DISPLAY_MODES.indexOf(current);
            const next = LEVEL_DISPLAY_MODES[(idx + 1) % LEVEL_DISPLAY_MODES.length];
            return ui.setLevelDisplayMode(next, opts);
        },
        _levelOverlayRefresh: {
            _pending: false,
            _last: 0,
            _minIntervalMs: 180,
            _scopes: null,
            schedule(opts = {}) {
                if (opts && opts.scope instanceof Node) {
                    this._scopes = this._scopes || new Set();
                    this._scopes.add(opts.scope);
                }
                if (opts && Array.isArray(opts.scopes)) {
                    this._scopes = this._scopes || new Set();
                    opts.scopes.forEach(scope => {
                        if (scope instanceof Node) this._scopes.add(scope);
                    });
                }
                if (this._pending) return;
                const now = Date.now();
                const wait = Math.max(0, this._minIntervalMs - (now - (this._last || 0)));
                this._pending = true;
                setTimeout(() => {
                    this._pending = false;
                    this._last = Date.now();
                    let scopes = null;
                    if (this._scopes && this._scopes.size) {
                        scopes = Array.from(this._scopes);
                        this._scopes.clear();
                    }
                    try {
                        ui._withRankedWarSortPause(() => ui.updateAllLevelCells({ scopes }), 'level-overlay-refresh');
                    } catch (err) {
                        try { tdmlogger('warn', '[LevelOverlay] refresh failed', err); } catch(_) {}
                    }
                }, wait);
            }
        },
        queueLevelOverlayRefresh(opts = {}) {
            if (!ui.isLevelOverlayEnabled(opts)) {
                if (opts.forceReset) {
                    try {
                        ui._withRankedWarSortPause(() => ui.updateAllLevelCells({ forceReset: true, ignoreFeatureFlag: true }), 'level-overlay-reset');
                    } catch(_) {}
                }
                return;
            }
            ui._levelOverlayRefresh.schedule(opts);
        },
        _collectLevelCellRoots(opts = {}) {
            const seen = new Set();
            const add = (node) => {
                if (!node || typeof node.querySelectorAll !== 'function') return;
                if (seen.has(node)) return;
                seen.add(node);
            };
            if (opts.scope) add(opts.scope);
            if (Array.isArray(opts.scopes)) opts.scopes.forEach(add);
            const tables = state.dom?.rankwarfactionTables;
            if (tables && tables.length) Array.from(tables).forEach(add);
            if (state.dom?.rankwarContainer) add(state.dom.rankwarContainer);
            if (state.dom?.factionListContainer) add(state.dom.factionListContainer);
            if (!seen.size) add(document);
            return Array.from(seen);
        },
        _gatherLevelCells(root) {
            try {
                return Array.from(root.querySelectorAll('.level, .lvl')).filter(cell => cell.closest('.members-list > li, .members-cont > li, .table-body > li, .table-body > .table-row'));
            } catch(_) {
                return [];
            }
        },
        _readLevelCellMeta(cell) {
            const meta = { level: null, text: '' };
            if (!cell) return meta;
            try {
                const rawText = (cell.textContent || '').trim();
                meta.text = rawText;
                const parsed = parseInt(rawText.replace(/[^0-9]/g,''), 10);
                // If we've previously recorded an original level, prefer that and do not overwrite it.
                if (cell && cell.dataset && cell.dataset.tdmOrigLevel) {
                    const orig = Number(cell.dataset.tdmOrigLevel);
                    if (Number.isFinite(orig)) {
                        meta.level = orig;
                    }
                } else if (Number.isFinite(parsed)) {
                    meta.level = parsed;
                    if (meta.level != null && cell && cell.dataset) cell.dataset.tdmOrigLevel = String(meta.level);
                }
                if (rawText && !cell.dataset.tdmOrigText) cell.dataset.tdmOrigText = rawText;
            } catch(_) {}
            return meta;
        },
        _extractPlayerIdFromRow(row) {
            try {
                if (!row) return null;
                const datasetId = row.dataset?.tdmOpponentId || row.dataset?.opponentId || row.dataset?.playerId || row.dataset?.id || null;
                if (datasetId) {
                    if (row.dataset) row.dataset.tdmOpponentId = datasetId;
                    return String(datasetId);
                }
                const link = row.querySelector('a[href*="XID="]');
                if (link && link.href) {
                    const match = link.href.match(/[?&]XID=(\d+)/i);
                    if (match && match[1]) {
                        if (row.dataset) row.dataset.tdmOpponentId = match[1];
                        return match[1];
                    }
                }
                return null;
            } catch(_) { return null; }
        },
        _computeLevelDisplay({ mode, level, ff, bsp }) {
            const fallbackDisplay = level != null ? String(level) : '—';
            const result = { display: fallbackDisplay, sortValue: Number.isFinite(level) ? level : null, overlayActive: mode !== 'level' };
            if (mode === 'ff') {
                const display = ff && ff.ffValue != null ? (formatFairFightValue(ff.ffValue) || String(ff.ffValue)) : '—';
                result.display = display;
                result.sortValue = (ff && ff.ffValue != null) ? Number(ff.ffValue) : Number.NEGATIVE_INFINITY;
                return result;
            }
            if (mode === 'ff-bs') {
                const display = (ff && (ff.bsHuman || (ff.bsEstimate != null ? utils.formatBattleStats(ff.bsEstimate) : null))) || '—';
                result.display = display;
                result.sortValue = (ff && ff.bsEstimate != null) ? Number(ff.bsEstimate) : Number.NEGATIVE_INFINITY;
                return result;
            }
            if (mode === 'bsp') {
                const display = (bsp && (bsp.formattedTbs || (bsp.tbs != null ? utils.formatBattleStats(bsp.tbs) : null) || (bsp.score != null ? utils.formatBattleStats(bsp.score) : null))) || '—';
                result.display = display;
                result.sortValue = (bsp && bsp.tbs != null) ? Number(bsp.tbs) : ((bsp && bsp.score != null) ? Number(bsp.score) : Number.NEGATIVE_INFINITY);
                return result;
            }
            result.overlayActive = false;
            return result;
        },
        _formatTooltipAge(ms) {
            try {
                if (!ms) return null;
                const seconds = Math.floor(ms / 1000);
                if (!Number.isFinite(seconds) || seconds <= 0) return null;
                return utils.formatAgoShort(seconds);
            } catch(_) { return null; }
        },
        _buildLevelTooltipText({ level, ff, bsp }) {
            try {
                const lines = [];
                if (Number.isFinite(level)) lines.push(`Lvl: ${level}`);
                
                if (ff) {
                    const parts = [];
                    if (ff.ffValue != null) {
                        const ffDisplay = formatFairFightValue(ff.ffValue) || String(ff.ffValue);
                        parts.push(`FF: ${ffDisplay}`);
                    }
                    const bsText = ff.bsHuman || (ff.bsEstimate != null ? utils.formatBattleStats(ff.bsEstimate) : null);
                    if (bsText) {
                        parts.push(`BS: ${bsText}`);
                    }
                    if (parts.length > 0) {
                        const age = ui._formatTooltipAge(ff.lastUpdatedMs);
                        if (age) parts.push(`(${age})`);
                        lines.push(parts.join(' '));
                    }
                }
                
                if (bsp) {
                    const parts = [];
                    const bspText = bsp.formattedTbs || (bsp.tbs != null ? utils.formatBattleStats(bsp.tbs) : null) || (bsp.score != null ? utils.formatBattleStats(bsp.score) : null);
                    if (bspText) {
                        parts.push(`BSP: ${bspText}`);
                    }
                    if (bsp.tbsBalanced != null) {
                        if (parts.length > 0) parts.push('-');
                        parts.push(utils.formatBattleStats(bsp.tbsBalanced));
                    }
                    if (parts.length > 0) {
                        const age = ui._formatTooltipAge(bsp.timestampMs);
                        if (age) parts.push(`(${age})`);
                        lines.push(parts.join(' '));
                    }
                }
                
                return lines.length ? lines.join('\n') : 'Level stats unavailable';
            } catch(_) {
                return 'Level stats unavailable';
            }
        },
        _ensureLevelCellHandlers(cell) {
            if (!cell || cell._tdmLevelBound) return;
            const longPressMs = state.ui?.levelCellLongPressMs || LEVEL_CELL_LONGPRESS_MS;
            const clearTimer = () => {
                if (cell._tdmLevelTouchTimer) {
                    clearTimeout(cell._tdmLevelTouchTimer);
                    cell._tdmLevelTouchTimer = null;
                }
            };
            const resetSuppression = (delayed = false) => {
                const clear = () => {
                    if (cell.dataset) {
                        delete cell.dataset.tdmLongPressActive;
                        delete cell.dataset.tdmSuppressClick;
                    }
                };
                if (delayed) setTimeout(clear, 220); else clear();
            };
            const handleClick = (event) => {
                if (!ui.isLevelOverlayEnabled()) return;
                if (cell.dataset?.tdmSuppressClick === '1') {
                    event.preventDefault();
                    event.stopPropagation();
                    resetSuppression(true);
                    return;
                }
                ui.cycleLevelDisplayMode({ source: 'level-cell' });
            };
            const handlePointerDown = (event) => {
                if (!ui.isLevelOverlayEnabled()) return;
                if (event.pointerType !== 'touch' && event.pointerType !== 'pen') return;
                clearTimer();
                resetSuppression();
                cell._tdmLevelTouchTimer = setTimeout(() => {
                    if (cell.dataset) {
                        cell.dataset.tdmLongPressActive = '1';
                        cell.dataset.tdmSuppressClick = '1';
                    }
                }, longPressMs);
            };
            const handlePointerEnd = () => {
                if (cell.dataset?.tdmLongPressActive === '1') {
                    resetSuppression(true);
                } else {
                    resetSuppression();
                }
                clearTimer();
            };
            cell.addEventListener('click', handleClick);
            cell.addEventListener('pointerdown', handlePointerDown);
            cell.addEventListener('pointerup', handlePointerEnd);
            cell.addEventListener('pointercancel', handlePointerEnd);
            cell.addEventListener('pointerleave', handlePointerEnd);
            cell._tdmLevelBound = true;
        },
        _teardownLevelCell(cell) {
            if (!cell) return;
            cell.classList.add('tdm-level-cell');
            cell.classList.remove('tdm-level-cell--overlay');
            if (cell.dataset) {
                const origText = cell.dataset.tdmOrigText || cell.dataset.tdmDisplay;
                if (origText && cell.textContent !== origText) cell.textContent = origText;
                delete cell.dataset.tdmDisplay;
                delete cell.dataset.tdmLevelMode;
            }
            try { cell.removeAttribute('data-sort-value'); } catch(_) {}
            try { cell.style && cell.style.removeProperty('--tdm-level-overlay-color'); } catch(_) {}
        },
        _renderLevelCell(cell, mode) {
            try {
                const row = cell.closest('li, .table-row, tr');
                const meta = ui._readLevelCellMeta(cell);
                const playerId = ui._extractPlayerIdFromRow(row);
                const ff = playerId ? utils.readFFScouter(playerId) : null;
                const bsp = playerId ? utils.readBSP(playerId) : null;
                const display = ui._computeLevelDisplay({ mode, level: meta.level, ff, bsp });
                const originalText = (cell.dataset && cell.dataset.tdmOrigText) ? cell.dataset.tdmOrigText : (meta.text || '');
                const resolvedText = display.overlayActive ? (display.display || '—') : (originalText || display.display || '—');
                cell.classList.add('tdm-level-cell');
                ui._ensureLevelCellHandlers(cell);

                let indicatorText = '';
                if (display.overlayActive) {
                    if (mode === 'ff') indicatorText = 'FF';
                    else if (mode === 'ff-bs') indicatorText = 'FFBS';
                    else if (mode === 'bsp') indicatorText = 'BSP';
                }

                let needsUpdate = false;
                if (cell.childNodes.length === 0) needsUpdate = true;
                else if (cell.childNodes[0].nodeType === 3 && cell.childNodes[0].nodeValue !== resolvedText) needsUpdate = true;
                else if (indicatorText && (!cell.childNodes[1] || cell.childNodes[1].textContent !== indicatorText)) needsUpdate = true;
                else if (!indicatorText && cell.childNodes.length > 1) needsUpdate = true;

                if (needsUpdate) {
                    cell.textContent = '';
                    cell.appendChild(document.createTextNode(resolvedText));
                    if (indicatorText) {
                        const ind = document.createElement('span');
                        ind.className = 'tdm-level-indicator';
                        ind.textContent = indicatorText;
                        cell.appendChild(ind);
                    }
                }

                if (display.overlayActive) {
                    const newVal = display.display || '—';
                    if (cell.dataset.tdmDisplay !== newVal) cell.dataset.tdmDisplay = newVal;
                    cell.classList.add('tdm-level-cell--overlay');
                } else {
                    cell.classList.remove('tdm-level-cell--overlay');
                    if (cell.dataset && 'tdmDisplay' in cell.dataset) delete cell.dataset.tdmDisplay;
                }

                // Apply FF Scouter coloring logic
                // We use ff.ffValue because normalizeFfRecord returns { ffValue: ... }
                // We apply this in 'level' mode as well if data is available, to replicate FF Scouter behavior
                const shouldColor = (mode === 'level' || mode === 'ff' || mode === 'ff-bs') && ff && Number.isFinite(ff.ffValue);

                if (shouldColor) {
                    const hexColor = ui._getFFColor(ff.ffValue);
                    const textColor = ui._getContrastColor(hexColor);
                    
                    // Convert to RGBA for 50% transparency
                    const r = parseInt(hexColor.slice(1, 3), 16);
                    const g = parseInt(hexColor.slice(3, 5), 16);
                    const b = parseInt(hexColor.slice(5, 7), 16);
                    const rgbaColor = `rgba(${r}, ${g}, ${b}, 0.5)`;

                    cell.style.backgroundColor = rgbaColor;
                    cell.style.color = textColor;
                    cell.style.setProperty('--tdm-level-overlay-color', textColor);
                } else {
                    // Reset inline styles if not coloring
                    cell.style.backgroundColor = '';
                    cell.style.color = '';
                    
                    if (display.overlayActive) {
                        if (cell.style && !cell.style.getPropertyValue('--tdm-level-overlay-color')) {
                            try {
                                const color = window.getComputedStyle(cell).color;
                                if (color) cell.style.setProperty('--tdm-level-overlay-color', color);
                            } catch(_) {}
                        }
                    } else {
                        try { cell.style.removeProperty('--tdm-level-overlay-color'); } catch(_) {}
                    }
                }
                if (display.sortValue != null && Number.isFinite(display.sortValue)) {
                    const sv = String(display.sortValue);
                    if (cell.getAttribute('data-sort-value') !== sv) cell.setAttribute('data-sort-value', sv);
                } else {
                    if (cell.hasAttribute('data-sort-value')) cell.removeAttribute('data-sort-value');
                }
                if (cell.dataset) {
                    const pid = playerId || '';
                    if (cell.dataset.playerId !== pid) cell.dataset.playerId = pid;
                    if (cell.dataset.tdmLevelMode !== mode) cell.dataset.tdmLevelMode = mode;
                }
                const origLevel = (cell && cell.dataset && cell.dataset.tdmOrigLevel) ? Number(cell.dataset.tdmOrigLevel) : null;
                const tooltip = ui._buildLevelTooltipText({ level: (meta.level != null ? meta.level : origLevel), ff, bsp });
                if (tooltip) {
                    if (cell.title !== tooltip) cell.title = tooltip;
                    const aria = cell.getAttribute('aria-label');
                    if (aria !== tooltip) cell.setAttribute('aria-label', tooltip);
                }
            } catch(_) { /* non-fatal */ }
        },
        _updateLevelHeaderLabels(mode, overlayEnabled) {
            try {
                const labelMap = { level: 'Level', ff: 'FF', 'ff-bs': 'FF BS', bsp: 'BSP' };
                const label = labelMap[mode] || 'Level';
                const selectors = [
                    '.tab-menu-cont .members-cont .white-grad .level',
                    '.tab-menu-cont .members-cont .white-grad .lvl',
                    '.f-war-list .table-header .level',
                    '.f-war-list .table-header .lvl',
                    '.table-header .level',
                    '.table-header .lvl'
                ];
                selectors.forEach(selector => {
                    document.querySelectorAll(selector).forEach(header => {
                        if (!header.dataset.tdmOrigLabel) {
                            header.dataset.tdmOrigLabel = (header.textContent || 'Level').trim();
                        }
                        const desiredHTML = 'Lvl/BS';
                        
                        // Non-destructive text update to preserve sort icons
                        let textUpdated = false;
                        // 1. Try direct text nodes
                        for (const node of header.childNodes) {
                            if (node.nodeType === 3 && node.nodeValue.trim()) {
                                // Replace plain text node with an inline span that preserves the sort icon sibling
                                const span = document.createElement('span');
                                span.innerHTML = desiredHTML;
                                header.replaceChild(span, node);
                                textUpdated = true;
                                break;
                            }
                        }
                        // 2. Try nested div (common in Torn headers: div > div(text) + div(icon))
                        if (!textUpdated) {
                            const textDiv = Array.from(header.children).find(c => !c.className.includes('sortIcon'));
                            if (textDiv) {
                                if (textDiv.innerHTML !== desiredHTML) textDiv.innerHTML = desiredHTML;
                                textUpdated = true;
                            }
                        }
                        // 3. Fallback: if empty or just text, set content. If icon exists but no text node found, prepend text.
                        if (!textUpdated) {
                            const icon = header.querySelector('[class*="sortIcon"]');
                            if (icon) {
                                const span = document.createElement('span');
                                span.innerHTML = desiredHTML;
                                header.insertBefore(span, icon);
                            } else {
                                header.innerHTML = desiredHTML;
                            }
                        }

                        // Ensure sort icon exists
                        let icon = header.querySelector('[class*="sortIcon"]');
                        if (!icon) {
                            // Try to clone from a sibling header
                            const sibling = header.parentElement ? header.parentElement.querySelector('[class*="sortIcon"]') : null;
                            if (sibling) {
                                icon = sibling.cloneNode(true);
                                icon.className = sibling.className.replace(/activeIcon\S*/g, '').replace(/asc\S*/g, '').replace(/desc\S*/g, '');
                                header.appendChild(icon);
                            } else {
                                // Create generic if no sibling found (using classes from user report as best guess)
                                icon = document.createElement('div');
                                icon.className = 'sortIcon___SmuX8';
                                header.appendChild(icon);
                            }
                        }

                        if (overlayEnabled) {
                            if (header.dataset.tdmLevelMode !== label) {
                                header.dataset.tdmLevelMode = label;
                            }
                        } else {
                            if ('tdmLevelMode' in header.dataset) {
                                delete header.dataset.tdmLevelMode;
                            }
                        }

                        const tip = `Level/BS column (${label}) – click any cell to cycle`;
                        if (header.title !== tip) header.title = tip;
                        if (header.getAttribute('aria-label') !== tip) header.setAttribute('aria-label', tip);
                        
                        if (header.getAttribute('data-tdm-overlay-level-header') !== '1') {
                            header.setAttribute('data-tdm-overlay-level-header', '1');
                        }
                    });
                });
            } catch(_) { /* noop */ }
        },
        _updateSortIcons(table, activeField, direction) {
            try {
                if (!table) return;
                const headers = table.querySelectorAll('.table-header > *, .white-grad > *');
                headers.forEach(header => {
                    const field = header.dataset.tdmSortKey || ui._detectRankedWarSortField(header);
                    const icon = header.querySelector('[class*="sortIcon"]');
                    if (!icon) return;
                    
                    // Remove active/direction classes
                    const classes = Array.from(icon.classList);
                    const activeClass = classes.find(c => c.startsWith('activeIcon'));
                    const ascClass = classes.find(c => c.startsWith('asc'));
                    const descClass = classes.find(c => c.startsWith('desc'));
                    
                    if (activeClass) icon.classList.remove(activeClass);
                    if (ascClass) icon.classList.remove(ascClass);
                    if (descClass) icon.classList.remove(descClass);
                    
                    if (field === activeField) {
                        const activeCls = 'activeIcon___pGiua';
                        const ascCls = 'asc___e08kZ';
                        const descCls = 'desc___S5bx1';
                        
                        icon.classList.add(activeCls);
                        icon.classList.add(direction === 'asc' ? ascCls : descCls);
                    }
                });
            } catch(_) { /* noop */ }
        },
        updateAllLevelCells(opts = {}) {
            try {
                const overlayEnabled = opts.forceReset ? false : ui.isLevelOverlayEnabled({ ignoreFeatureFlag: opts.ignoreFeatureFlag });
                const mode = overlayEnabled ? ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) : DEFAULT_LEVEL_DISPLAY_MODE;
                const roots = ui._collectLevelCellRoots(opts);
                roots.forEach(root => {
                    ui._gatherLevelCells(root).forEach(cell => {
                        if (overlayEnabled) {
                            state.ui = state.ui || {};
                            state.ui._overlaySortPaused = true;
                            ui._renderLevelCell(cell, mode);
                        } else {
                            ui._teardownLevelCell(cell);
                        }
                    });
                });
                ui._updateLevelHeaderLabels(mode, overlayEnabled);
                if (overlayEnabled && ui._isRankWarOverlaySortActive()) {
                    try { ui._applyRankedWarOverlaySort({ toggle: false, direction: state.ui.rankWarOverlaySortDir, reason: 'overlay-render' }); } catch(_) {}
                }
                if (overlayEnabled && state.ui) delete state.ui._overlaySortPaused;
                if (!overlayEnabled) {
                    try {
                        ui._collectMembersListTables().forEach(table => { if (table?.dataset) delete table.dataset.tdmOverlaySorted; });
                        document.querySelectorAll('.f-war-list.members-list .table-header .lvl, .f-war-list.members-list .table-header .level').forEach(header => {
                            if (header.dataset) delete header.dataset.tdmOverlaySortDir;
                        });
                        ui._collectRankedWarTables().forEach(table => { if (table?.dataset) delete table.dataset.tdmOverlaySorted; });
                        document.querySelectorAll('.tab-menu-cont .white-grad .lvl, .tab-menu-cont .white-grad .level, .tab-menu-cont .table-header .lvl, .tab-menu-cont .table-header .level').forEach(header => {
                            if (header.dataset) delete header.dataset.tdmOverlaySortDir;
                        });
                        if (state?.ui && 'rankWarOverlaySortDir' in state.ui) delete state.ui.rankWarOverlaySortDir;
                    } catch(_) { /* optional cleanup */ }
                }
                if (ui._areRankedWarFavoritesEnabled()) {
                    const shouldRepinFavorites = true;
                    if (shouldRepinFavorites) {
                        try { ui._pinFavoritesInAllVisibleTables(); } catch(_) { /* noop */ }
                    }
                }
            } catch(_) { /* noop */ }
        },
        _isRankWarOverlaySortActive() {
            try {
                if (state.ui?._overlaySortPaused) return false;
                return typeof state.ui?.rankWarOverlaySortDir === 'string' && state.ui.rankWarOverlaySortDir.length > 0;
            } catch(_) { return false; }
        },
        _disableRankedWarOverlaySort(opts = {}) {
            try {
                if (state?.ui && 'rankWarOverlaySortDir' in state.ui) delete state.ui.rankWarOverlaySortDir;
                const tables = opts.table ? [opts.table] : ui._collectRankedWarTables(opts.scope);
                tables.forEach(table => {
                    if (!table) return;
                    if (table.dataset) delete table.dataset.tdmOverlaySorted;
                    table.querySelectorAll('.white-grad .level, .white-grad .lvl, .table-header .level, .table-header .lvl').forEach(header => {
                        if (header?.dataset) delete header.dataset.tdmOverlaySortDir;
                    });
                });
                try { ui.requestRankedWarFavoriteRepin?.({ delays: [0, 160, 400] }); } catch(_) {}
            } catch(_) { /* noop */ }
        },
        _collectMembersListTables(container) {
            try {
                const set = new Set();
                const roots = [];
                if (container && typeof container.querySelectorAll === 'function') {
                    roots.push(...container.querySelectorAll('.f-war-list.members-list'));
                    if (container.matches && container.matches('.f-war-list.members-list')) roots.push(container);
                }
                if (!roots.length) roots.push(...document.querySelectorAll('.f-war-list.members-list'));
                roots.forEach(node => { if (node && !set.has(node)) { set.add(node); } });
                return Array.from(set);
            } catch(_) {
                return [];
            }
        },
        _applyMembersListOverlaySort(table, opts = {}) {
            try {
                if (!table) return false;
                if (!ui.isLevelOverlayEnabled() || ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) === 'level') return false;
                const body = table.querySelector('.table-body');
                if (!body) return false;
                const rows = Array.from(body.querySelectorAll(':scope > li.table-row'));
                if (!rows.length) return false;
                const mode = ui.getLevelDisplayMode({ ignoreFeatureFlag: true });
                const entries = [];
                rows.forEach((row, index) => {
                    const cell = row.querySelector('.level, .lvl');
                    if (!cell) return;
                    const meta = ui._readLevelCellMeta(cell);
                    const playerId = ui._extractPlayerIdFromRow(row);
                    const ff = playerId ? utils.readFFScouter(playerId) : null;
                    const bsp = playerId ? utils.readBSP(playerId) : null;
                    const computed = ui._computeLevelDisplay({ mode, level: meta.level, ff, bsp });
                    const sortable = Number.isFinite(computed.sortValue) ? computed.sortValue : Number.NEGATIVE_INFINITY;
                    const fallback = Number.isFinite(meta.level) ? meta.level : Number.NEGATIVE_INFINITY;
                    entries.push({ row, sortable, fallback, index });
                });
                if (!entries.length) return false;
                const header = table.querySelector('.table-header .lvl, .table-header .level');
                let direction = header?.dataset?.tdmOverlaySortDir || 'desc';
                if (typeof opts.direction === 'string') direction = opts.direction;
                else if (opts.toggle !== false) direction = direction === 'asc' ? 'desc' : 'asc';
                if (header?.dataset) header.dataset.tdmOverlaySortDir = direction;
                
                // Update sort icons
                ui._updateSortIcons(table, 'level', direction);

                const multiplier = direction === 'asc' ? 1 : -1;
                entries.sort((a, b) => {
                    const av = Number.isFinite(a.sortable) ? a.sortable : a.fallback;
                    const bv = Number.isFinite(b.sortable) ? b.sortable : b.fallback;
                    if (!Number.isFinite(av) && !Number.isFinite(bv)) return a.index - b.index;
                    if (!Number.isFinite(av)) return 1;
                    if (!Number.isFinite(bv)) return -1;
                    if (av === bv) return a.index - b.index;
                    return (av - bv) * multiplier;
                });
                const frag = document.createDocumentFragment();
                entries.forEach(entry => frag.appendChild(entry.row));
                body.appendChild(frag);
                try { ui._pinFavoritesInFactionList(table); } catch(_) {}
                if (table.dataset) table.dataset.tdmOverlaySorted = '1';
                return true;
            } catch(_) {
                return false;
            }
        },
        _ensureMembersListOverlaySortHandlers(container) {
            try {
                const tables = ui._collectMembersListTables(container);
                if (!tables.length) return;
                tables.forEach(table => {
                    const headers = table.querySelectorAll('.table-header .lvl, .table-header .level');
                    headers.forEach(header => {
                        if (!header || header._tdmOverlaySortBound) return;
                        const handler = (event) => {
                            if (!ui.isLevelOverlayEnabled() || ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) === 'level') return;
                            const applied = ui._applyMembersListOverlaySort(table, { toggle: true });
                            if (applied) {
                                event.preventDefault();
                                event.stopPropagation();
                                if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
                            }
                        };
                        header.addEventListener('click', handler, true);
                        header._tdmOverlaySortBound = true;
                    });
                });
            } catch(_) { /* noop */ }
        },
        _collectRankedWarTables(container) {
            try {
                const roots = [];
                if (container && typeof container.querySelectorAll === 'function') {
                    container.querySelectorAll('.tab-menu-cont').forEach(node => roots.push(node));
                    if (container.matches && container.matches('.tab-menu-cont')) roots.push(container);
                }
                if (!roots.length && state.dom?.rankwarfactionTables) {
                    state.dom.rankwarfactionTables.forEach(node => roots.push(node));
                }
                if (!roots.length) document.querySelectorAll('.tab-menu-cont').forEach(node => roots.push(node));
                const seen = new Set();
                return roots.filter(node => {
                    if (!node || seen.has(node)) return false;
                    seen.add(node);
                    return true;
                });
            } catch(_) {
                return [];
            }
        },
        _resolveRankedWarListEl(table) {
            if (!table) return null;
            const primary = table.querySelector('.members-list, .members-cont, ul.members-list, ul.members-cont');
            if (!primary) return null;
            if (primary.matches('ul')) return primary;
            const scoped = primary.querySelector(':scope > ul');
            if (scoped) return scoped;
            const nested = primary.querySelector('ul');
            return nested || primary;
        },
        _applyRankedWarOverlaySort(opts = {}) {
            try {
                if (!ui.isLevelOverlayEnabled() || ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) === 'level') return false;
                const tables = ui._collectRankedWarTables();
                if (!tables.length) return false;
                const mode = ui.getLevelDisplayMode({ ignoreFeatureFlag: true });
                if (!state.ui) state.ui = {};
                const prior = (opts.originHeader?.dataset?.tdmOverlaySortDir) || state.ui.rankWarOverlaySortDir;
                let direction = prior || 'desc';
                if (typeof opts.direction === 'string' && opts.direction) direction = opts.direction;
                else if (opts.toggle !== false) direction = prior ? (prior === 'asc' ? 'desc' : 'asc') : 'desc';
                state.ui.rankWarOverlaySortDir = direction;
                let mutated = false;
                tables.forEach(table => {
                    if (ui._isRankedWarCustomSortActive(table)) {
                        if (table.dataset.tdmSortField !== 'level' && opts.toggle !== true) {
                            if (table?.dataset) delete table.dataset.tdmOverlaySorted;
                            return;
                        }
                    } else if (opts.toggle !== true) {
                        const pref = ui._loadRankedWarSortPreference(table);
                        if (pref && pref.field && pref.field !== 'level') {
                            ui._refreshRankedWarSortForTable(table, { reason: 'overlay-restore-pref' });
                            return;
                        }
                    }
                    const sorted = ui._applyRankedWarCustomSort({ table, field: 'level', direction, reason: 'overlay', mode });
                    if (sorted) mutated = true;
                });
                return mutated;
            } catch(_) {
                return false;
            }
        },
        _ensureRankedWarOverlaySortHandlers(container) {
            try {
                const tables = ui._collectRankedWarTables(container);
                if (!tables.length) return;
                tables.forEach(table => {
                    const headers = table.querySelectorAll('.white-grad .level, .white-grad .lvl, .table-header .level, .table-header .lvl');
                    headers.forEach(header => {
                        if (!header || header._tdmOverlaySortBound) return;
                        const handler = (event) => {
                            if (!ui.isLevelOverlayEnabled() || ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) === 'level') return;
                            event.preventDefault();
                            event.stopPropagation();
                            if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
                            ui._applyRankedWarOverlaySort({ originHeader: header, toggle: true });
                        };
                        header.addEventListener('click', handler, true);
                        header._tdmOverlaySortBound = true;
                        if (header.dataset) header.dataset.tdmOverlayLevelHeader = '1';
                    });
                    const resetCells = table.querySelectorAll('.white-grad > *, .white-grad .table-cell, .white-grad li, .table-header > *, .table-header .table-cell, .table-header li');
                    resetCells.forEach(cell => {
                        if (!cell) return;
                        if (cell.matches('[data-tdm-overlay-level-header="1"], .level, .lvl')) return;
                        if (cell._tdmOverlayResetBound) return;
                        const disableHandler = () => {
                            if (!ui.isLevelOverlayEnabled() || ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) === 'level') return;
                            if (!ui._isRankWarOverlaySortActive()) return;
                            ui._disableRankedWarOverlaySort({ table });
                            try { ui.requestRankedWarFavoriteRepin?.(); } catch(_) {}
                        };
                        ['pointerdown', 'click'].forEach(evtType => {
                            try { cell.addEventListener(evtType, disableHandler, true); } catch(_) {}
                        });
                        cell._tdmOverlayResetBound = true;
                    });
                });
            } catch(_) { /* noop */ }
        },
        _ensureRankedWarSortHandlers(container) {
            try {
                const tables = ui._collectRankedWarTables(container);
                if (!tables.length) return;
                tables.forEach(table => {
                    // Fix: Selector must match div headers in ranked war tables (direct children of .white-grad)
                    const headers = table.querySelectorAll('.white-grad > div, .white-grad .table-header > *, .table-header > *');
                    headers.forEach(header => {
                        if (!header || header._tdmRankSortBound) return;
                        const field = ui._detectRankedWarSortField(header);
                        if (!field) return;
                        header.dataset.tdmSortKey = field;
                        const handler = (event) => {
                            try {
                                if (event?.type === 'keydown') {
                                    const key = event.key || event.code || '';
                                    if (key && key !== 'Enter' && key !== ' ' && key !== 'Spacebar') return;
                                }
                                if (event?.type === 'click' && event.button && event.button !== 0) return;
                                
                                // If clicking the Level header (which has overlay enabled), ignore this handler
                                // The overlay handler will take care of it
                                if (header.dataset?.tdmOverlayLevelHeader === '1') {
                                    const overlayActive = ui.isLevelOverlayEnabled() && ui.getLevelDisplayMode({ ignoreFeatureFlag: true }) !== 'level';
                                    if (overlayActive) return;
                                }

                                event?.preventDefault?.();
                                event?.stopPropagation?.();
                                if (typeof event?.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
                                
                                // Disable overlay sort if active, since we are sorting another column
                                if (ui._isRankWarOverlaySortActive()) {
                                    ui._disableRankedWarOverlaySort({ table });
                                }

                                const sortField = header.dataset?.tdmSortKey || field;
                                if (!sortField) return;
                                const sortDir = ui._toggleRankedWarSortDirection(table, sortField);
                                
                                // Sync sort to all ranked war tables
                                const allTables = ui._collectRankedWarTables();
                                allTables.forEach(t => {
                                    ui._refreshRankedWarSortForTable(t, { field: sortField, direction: sortDir, reason: 'header-click' });
                                });
                            } catch(_) { /* non-fatal */ }
                        };
                        header.addEventListener('click', handler, true);
                        header.addEventListener('keydown', handler, true);
                        header._tdmRankSortBound = true;
                        header.classList.add('tdm-rank-sort-header');
                    });
                    ui._ensureRankedWarSortObserver(table);
                });
            } catch(_) { /* noop */ }
        },
        _toggleRankedWarSortDirection(table, field) {
            try {
                if (!table || !field) return 'desc';
                const priorField = table.dataset?.tdmSortField;
                const priorDir = table.dataset?.tdmSortDir;
                if (priorField === field) {
                    return priorDir === 'asc' ? 'desc' : 'asc';
                }
                return ui._defaultRankedWarSortDirection(field);
            } catch(_) {
                return 'desc';
            }
        },
        _defaultRankedWarSortDirection(field) {
            switch (field) {
                case 'member':
                case 'status':
                    return 'asc';
                default:
                    return 'desc';
            }
        },
        _defaultRankedWarSortField() {
            return null;
        },
        _ensureRankedWarDefaultSort(table) {
            try {
                if (!table) return;
                const activeField = table.dataset?.tdmSortField;
                if (activeField) return;
                ui._refreshRankedWarSortForTable(table, { reason: 'default' });
            } catch(_) { /* noop */ }
        },
        _isRankedWarCustomSortActive(table) {
            try {
                if (!table) return false;
                return typeof table.dataset?.tdmSortField === 'string' && table.dataset.tdmSortField.length > 0;
            } catch(_) {
                return false;
            }
        },
        _areRankedWarFavoritesEnabled() {
            try {
                if (featureFlagController?.isEnabled?.('rankWarEnhancements.favorites') === false) return false;
                return true;
            } catch(_) {
                return true;
            }
        },
        _isRankedWarSortDebugEnabled() { return false; },
        _isRankedWarSortPaused() {
            try {
                return (state?.ui?._rankWarSortPauseDepth || 0) > 0;
            } catch(_) {
                return false;
            }
        },
        _getRankedWarSortPreferenceStore() {
            try {
                if (!state.ui) state.ui = {};
                if (!(state.ui._rankWarSortPrefs instanceof Map)) {
                    state.ui._rankWarSortPrefs = new Map();
                }
                return state.ui._rankWarSortPrefs;
            } catch(_) {
                return new Map();
            }
        },
        _rankedWarSortPreferenceKey(table) {
            try {
                if (!table) return null;
                
                // Stability fix: Prefer side-based key to ensure persistence across DOM updates where factionId might be delayed
                const isLeft = table.classList?.contains('left');
                const isRight = table.classList?.contains('right');
                if (isLeft || isRight) {
                    const key = `side:${isLeft ? 'left' : 'right'}`;
                    if (table.dataset) table.dataset.tdmSortPrefKey = key;
                    return key;
                }

                const factionId = ui._resolveRankedWarTableFactionId?.(table);
                if (factionId != null) return `faction:${String(factionId)}`;
                if (table.dataset?.factionId) return `faction:${String(table.dataset.factionId)}`;
                if (table.dataset?.tdmSortPrefKey) return table.dataset.tdmSortPrefKey;
                
                if (!state.ui) state.ui = {};
                const nextId = (state.ui._rankWarSortPrefCounter = (state.ui._rankWarSortPrefCounter || 0) + 1);
                const fallbackKey = `table:${nextId}`;
                if (table.dataset) table.dataset.tdmSortPrefKey = fallbackKey;
                return fallbackKey;
            } catch(_) {
                return null;
            }
        },
        _loadRankedWarSortPreference(table) {
            try {
                const key = ui._rankedWarSortPreferenceKey(table);
                if (!key) return null;
                const store = ui._getRankedWarSortPreferenceStore();
                return store.get(key) || null;
            } catch(_) {
                return null;
            }
        },
        _storeRankedWarSortPreference(table, field, direction) {
            try {
                if (!field) return;
                const key = ui._rankedWarSortPreferenceKey(table);
                if (!key) return;
                const store = ui._getRankedWarSortPreferenceStore();
                store.set(key, { field, direction: direction === 'asc' ? 'asc' : 'desc' });
            } catch(_) { /* noop */ }
        },
        _withRankedWarSortPause(fn, reason = 'unspecified') {
            if (typeof fn !== 'function') return;
            if (!state.ui) state.ui = {};
            const scheduleRelease = () => {
                try {
                    const runRelease = () => {
                        if (!state.ui) state.ui = {};
                        const next = Math.max(0, (state.ui._rankWarSortPauseDepth || 1) - 1);
                        state.ui._rankWarSortPauseDepth = next;
                    };
                    setTimeout(runRelease, 0); // allow MutationObserver microtasks to flush before resuming
                } catch(_) { /* noop */ }
            };
            try {
                const nextDepth = (state.ui._rankWarSortPauseDepth || 0) + 1;
                state.ui._rankWarSortPauseDepth = nextDepth;
                const result = fn();
                if (result && typeof result.then === 'function') {
                    return result.finally(() => scheduleRelease());
                }
                scheduleRelease();
                return result;
            } catch(err) {
                scheduleRelease();
                throw err;
            }
        },
        _logRankedWarSort() {},
        _refreshRankedWarSortForTable(table, opts = {}) {
            try {
                if (!table) return;
                const reason = opts.reason || 'refresh';
                let field = opts.field;
                let dir = opts.direction;
                if (!field) field = table.dataset?.tdmSortField;
                if (!dir) dir = table.dataset?.tdmSortDir;
                if (!field) {
                    const pref = ui._loadRankedWarSortPreference(table);
                    if (pref?.field) {
                        field = pref.field;
                        if (!dir && pref.direction) dir = pref.direction;
                    }
                }
                if (!field) field = ui._defaultRankedWarSortField();
                if (!dir) dir = ui._defaultRankedWarSortDirection(field);

                // Fix: If sorting by a non-level field, disable the overlay sort enforcement to prevent fighting
                if (field !== 'level' && state.ui && state.ui.rankWarOverlaySortDir) {
                    delete state.ui.rankWarOverlaySortDir;
                }

                ui._withRankedWarSortPause(() => {
                    ui._applyRankedWarCustomSort({ table, field, direction: dir, reason });
                    ui._updateSortIcons(table, field, dir);
                    if (ui._areRankedWarFavoritesEnabled()) {
                        try {
                            const factionId = ui._resolveRankedWarTableFactionId(table);
                            ui._pinFavoritesInTable(table, factionId);
                        } catch(_) { /* noop */ }
                    }
                }, `refresh:${reason}`);
            } catch(_) { /* noop */ }
        },
        _scheduleRankedWarSortSync(table, reason = 'unspecified', delayMs = 40) {
            try {
                if (!table) return;
                if (ui._isRankedWarSortPaused()) return;
                if (table._tdmSortSyncHandle) {
                    clearTimeout(table._tdmSortSyncHandle);
                }
                // Use dynamic delay to avoid thrashing when external scripts inject many nodes
                table._tdmSortSyncHandle = setTimeout(() => {
                    if (ui._isRankedWarSortPaused()) {
                        table._tdmSortSyncHandle = null;
                        return;
                    }

                    try { ui._refreshRankedWarSortForTable(table, { reason }); } catch(_) { /* noop */ }
                    table._tdmSortSyncHandle = null;
                }, Math.max(10, Number(delayMs) || 40));
            } catch(_) { /* noop */ }
        },
        _ensureRankedWarSortObserver(table) {
            try {
                if (!table || table._tdmSortObserver) return;
                const list = ui._resolveRankedWarListEl(table);
                if (!list) return;
                const observer = new MutationObserver(mutations => {
                    try {
                        if (!Array.isArray(mutations)) return;
                        const changed = mutations.some(m => m && m.type === 'childList');
                        if (!changed) return;
                        if (ui._isRankedWarSortPaused()) return;

                        // Count added nodes across mutation records to detect external-script bursts
                        const addedCount = mutations.reduce((sum, m) => sum + (m.addedNodes ? m.addedNodes.length : 0), 0);
                        // If many nodes are being added (likely from another userscript), increase debounce
                        const delay = addedCount > 8 ? Math.min(600, 40 + addedCount * 20) : 40;
                        ui._scheduleRankedWarSortSync(table, 'observer-childList', delay);
                    } catch(_) { /* noop */ }
                });
                observer.observe(list, { childList: true });
                table._tdmSortObserver = observer;
            } catch(_) { /* noop */ }
        },
        _detectRankedWarSortField(header) {
            try {
                if (!header) return null;
                const direct = header.dataset?.tdmSortKey
                    || header.dataset?.sortField
                    || header.dataset?.column
                    || header.dataset?.columnId
                    || header.dataset?.columnName;
                if (direct) {
                    const normalized = ui._normalizeRankedWarSortField(direct);
                    if (normalized) return normalized;
                }
                const classes = Array.from(header.classList || []);
                for (const cls of classes) {
                    const normalized = ui._normalizeRankedWarSortField(cls);
                    if (normalized) return normalized;
                }
                const text = header.textContent ? header.textContent.trim().toLowerCase() : '';
                if (!text) return null;
                if (text.includes('level') || text.includes('lv')) return 'level';
                if (text.includes('member')) return 'member';
                if (text.includes('status')) return 'status';
                if (text.includes('points')) return 'points';
                return null;
            } catch(_) {
                return null;
            }
        },
        _normalizeRankedWarSortField(value) {
            if (!value) return null;
            const normalized = String(value).toLowerCase();
            if (normalized === 'lvl' || normalized === 'level' || normalized === 'bs') return 'level';
            if (normalized === 'member' || normalized === 'members' || normalized === 'name') return 'member';
            if (normalized === 'status' || normalized === 'statuses') return 'status';
            if (normalized === 'points' || normalized === 'point' || normalized === 'score') return 'points';
            return null;
        },
        _buildRankedWarRowMeta(row, ctx = {}) {
            const meta = {
                playerId: null,
                playerIdNum: Number.NaN,
                name: '',
                nameSort: '',
                overlaySort: Number.NEGATIVE_INFINITY,
                level: Number.NEGATIVE_INFINITY,
                statusText: '',
                statusRank: 0,
                points: Number.NEGATIVE_INFINITY,
                isFavorite: false
            };
            try {
                if (!row) return meta;
                const playerId = ui._extractPlayerIdFromRow(row);
                if (playerId) {
                    meta.playerId = String(playerId);
                    const pidNum = Number(playerId);
                    if (Number.isFinite(pidNum)) meta.playerIdNum = pidNum;
                }
                meta.isFavorite = row?.dataset?.tdmFavorite === '1';
                const nameNode = row.querySelector('.member a[href*="profiles.php"], .member span, .member');
                const rawName = nameNode ? utils.sanitizePlayerName?.(nameNode.textContent || '', meta.playerId) || nameNode.textContent || '' : '';
                meta.name = rawName ? rawName.trim() : '';
                meta.nameSort = meta.name.toLowerCase();
                const levelCell = row.querySelector('.level, .lvl');
                const overlayMode = ctx.mode || ui.getLevelDisplayMode?.({ ignoreFeatureFlag: true }) || 'level';
                if (levelCell) {
                    const cellMeta = ui._readLevelCellMeta(levelCell) || {};
                    if (Number.isFinite(cellMeta.level)) meta.level = cellMeta.level;
                    const ff = meta.playerId ? utils.readFFScouter?.(meta.playerId) : null;
                    const bsp = meta.playerId ? utils.readBSP?.(meta.playerId) : null;
                    const computed = ui._computeLevelDisplay?.({ mode: overlayMode, level: cellMeta.level, ff, bsp });
                    if (computed && Number.isFinite(computed.sortValue)) meta.overlaySort = computed.sortValue;
                }
                const parseNumeric = (selectors) => {
                    try {
                        for (const sel of selectors) {
                            const node = typeof sel === 'string' ? row.querySelector(sel) : null;
                            if (!node) continue;
                            const dataVal = node.getAttribute('data-value');
                            const raw = dataVal != null ? dataVal : (node.textContent || '');
                            if (!raw) continue;
                            const cleaned = raw.replace(/[^0-9.\-]/g, '');
                            if (!cleaned) continue;
                            const num = Number(cleaned);
                            if (Number.isFinite(num)) return num;
                        }
                    } catch(_) { /* noop */ }
                    return Number.NaN;
                };
                meta.points = parseNumeric(['.points', '[data-points]']);
                const statusNode = row.querySelector('.status');
                meta.statusText = statusNode ? statusNode.textContent?.trim() || '' : '';
                if (statusNode && statusNode.dataset.sortValue) {
                    meta.statusTime = parseInt(statusNode.dataset.sortValue, 10);
                } else {
                    meta.statusTime = 0;
                }
                
                if (statusNode && statusNode.dataset.subRank) {
                    meta.statusRank = parseInt(statusNode.dataset.subRank, 90);
                } else {
                    const status = meta.statusText.toLowerCase();
                    // Ensure hospital-related statuses get top priority. Check 'hospital' before 'abroad'
                    if (status.includes('hospital')) meta.statusRank = 100;
                    else if (status.includes('abroad')) meta.statusRank = 90;
                    else if (status.includes('travel')) meta.statusRank = 40;
                    else if (status.includes('jail')) meta.statusRank = 70;
                    else if (status.includes('okay')) meta.statusRank = 80;
                    else meta.statusRank = 0;
                }
            } catch(_) { /* noop */ }
            return meta;
        },
        _compareRankedWarSortValues(field, aMeta, bMeta, direction) {
            const asc = direction === 'asc' ? 1 : -1;
            const compareNumber = (av, bv) => {
                const aValid = Number.isFinite(av);
                const bValid = Number.isFinite(bv);
                if (aValid && bValid) {
                    if (av !== bv) return (av - bv) * asc;
                    return 0;
                }
                if (aValid && !bValid) return -1 * asc;
                if (!aValid && bValid) return 1 * asc;
                return 0;
            };
            switch (field) {
                case 'member': {
                    const cmp = aMeta.nameSort.localeCompare(bMeta.nameSort);
                    if (cmp !== 0) return cmp * asc;
                    break;
                }
                case 'status': {
                    const rankCmp = compareNumber(aMeta.statusRank, bMeta.statusRank);
                    if (rankCmp !== 0) return rankCmp;
                    const timeCmp = compareNumber(aMeta.statusTime, bMeta.statusTime);
                    if (timeCmp !== 0) return timeCmp;
                    const textCmp = aMeta.statusText.localeCompare(bMeta.statusText);
                    if (textCmp !== 0) return textCmp * asc;
                    break;
                }
                case 'points':
                    return compareNumber(aMeta.points, bMeta.points);
                case 'level':
                default: {
                    const av = Number.isFinite(aMeta.overlaySort) ? aMeta.overlaySort : aMeta.level;
                    const bv = Number.isFinite(bMeta.overlaySort) ? bMeta.overlaySort : bMeta.level;
                    const levelCmp = compareNumber(av, bv);
                    if (levelCmp !== 0) return levelCmp;
                    break;
                }
            }
            return 0;
        },
        _applyRankedWarCustomSort(opts = {}) {
            try {
                const { table, field, direction, mode, reason = 'unspecified' } = opts;
                if (!table || !field) return false;
                
                const list = ui._resolveRankedWarListEl(table);
                if (!list) return false;
                
                const rowSelector = ':scope > li, :scope > .table-row';
                const rows = Array.from(list.querySelectorAll(rowSelector));
                if (!rows.length) return false;
                
                let factionId = ui._resolveRankedWarTableFactionId(table);
                if (table.dataset) {
                    if (factionId != null) table.dataset.factionId = String(factionId);
                    else if (table.dataset.factionId) factionId = table.dataset.factionId;
                }
                const favoritesEnabled = ui._areRankedWarFavoritesEnabled();
                const favSet = favoritesEnabled && factionId != null ? new Set(Object.keys(ui.getFavoritesForFaction(factionId) || {})) : new Set();
                const actualDir = direction === 'asc' ? 'asc' : 'desc';
                if (favoritesEnabled) {
                    rows.forEach(row => {
                        try {
                            const pid = ui._extractPlayerIdFromRow(row);
                            const isFav = pid && favSet.has(String(pid));
                            ui._setFavoriteRowHighlight(row, !!isFav);
                        } catch(_) { /* noop */ }
                    });
                }
                ui._storeRankedWarSortPreference(table, field, actualDir);
                
                const entries = rows.map((row, index) => ({
                    row,
                    index,
                    meta: ui._buildRankedWarRowMeta(row, { mode, index })
                }));
                const isEntryFavorite = (entry) => entry?.row?.dataset?.tdmFavorite === '1';
                const originalOrder = rows.slice();
                entries.sort((a, b) => {
                    if (favoritesEnabled) {
                        const favA = isEntryFavorite(a);
                        const favB = isEntryFavorite(b);
                        if (favA !== favB) return favA ? -1 : 1;
                    }
                    const cmp = ui._compareRankedWarSortValues(field, a.meta, b.meta, actualDir);
                    if (cmp !== 0) return cmp;
                    return a.index - b.index;
                });
                const reordered = entries.some((entry, idx) => entry.row !== originalOrder[idx]);
                if (reordered) {
                    const frag = document.createDocumentFragment();
                    entries.forEach(entry => frag.appendChild(entry.row));
                    list.appendChild(frag);
                }
                if (table.dataset) {
                    table.dataset.tdmSortField = field;
                    table.dataset.tdmSortDir = actualDir;
                }
                ui._updateRankedWarHeaderSortIndicators(table, field, actualDir);
                
                return reordered;
            } catch(_) {
                return false;
            }
        },
        _updateRankedWarHeaderSortIndicators(table, field, direction) {
            try {
                if (!table) return;
                // Fix: Selector must match div headers in ranked war tables
                const headers = table.querySelectorAll('.white-grad > div, .white-grad .table-header > *, .table-header > *');
                headers.forEach(header => {
                    const key = header?.dataset?.tdmSortKey || ui._detectRankedWarSortField(header);
                    if (!key) return;
                    header.classList.remove('tdm-sort-asc', 'tdm-sort-desc', 'tdm-sort-active');
                    if (key === field) {
                        header.classList.add('tdm-sort-active');
                        header.classList.add(direction === 'asc' ? 'tdm-sort-asc' : 'tdm-sort-desc');
                        if (header.dataset) header.dataset.tdmSortDir = direction;
                    } else if (header.dataset) {
                        delete header.dataset.tdmSortDir;
                    }
                });
            } catch(_) { /* noop */ }
        },
        _refreshRankedWarSortForFaction(factionId) {
            try {
                const tables = ui._collectRankedWarTables();
                if (!tables.length) return;
                tables.forEach(table => {
                    const tableFactionId = ui._resolveRankedWarTableFactionId(table);
                    if (factionId != null && tableFactionId != null && String(tableFactionId) !== String(factionId)) return;
                    ui._refreshRankedWarSortForTable(table);
                });
            } catch(_) { /* noop */ }
        },
        _refreshRankedWarSortForAll() {
            try {
                ui._refreshRankedWarSortForFaction(null);
            } catch(_) { /* noop */ }
        },
        _favStorageKey(factionId) {
            try { return `tdm.favorites.faction_${String(factionId ?? 'global')}`; } catch(_) { return 'tdm.favorites.faction_global'; }
        },
        getFavoritesForFaction(factionId) {
            try {
                const key = ui._favStorageKey(factionId);
                const map = storage.get(key, {});
                return (map && typeof map === 'object') ? { ...map } : {};
            } catch(_) { return {}; }
        },
        isFavorite(playerId, factionId) {
            try {
                const favs = ui.getFavoritesForFaction(factionId);
                return !!(favs && favs[String(playerId)]);
            } catch(_) { return false; }
        },
        _setFavoriteRowHighlight(row, isFavorite) {
            if (!row) return;
            const active = !!isFavorite;
            try { row.classList?.toggle('tdm-favorite-row', active); } catch(_) {}
            if (row.dataset) {
                if (active) row.dataset.tdmFavorite = '1';
                else delete row.dataset.tdmFavorite;
            }
        },
        _collectRowsByPlayerId(root) {
            const map = new Map();
            try {
                if (!root) return map;
                const rows = root.querySelectorAll('li, .table-row');
                rows.forEach(row => {
                    const id = ui._extractPlayerIdFromRow(row);
                    if (id) map.set(String(id), row);
                });
            } catch(_) {}
            return map;
        },
        _resolveRankedWarTableFactionId(tableContainer, visibleFactions) {
            try {
                if (!tableContainer) return null;
                const datasetId = tableContainer.dataset?.factionId;
                if (datasetId) return datasetId;
                const vis = visibleFactions || utils.getVisibleRankedWarFactionIds?.() || {};
                const linkCandidates = tableContainer.querySelectorAll?.('a[href*="factions.php"]') || [];
                for (const link of linkCandidates) {
                    const parsed = utils.parseFactionIdFromHref?.(link?.href);
                    if (parsed) return parsed;
                }
                const isLeft = tableContainer.classList?.contains('left');
                const isRight = tableContainer.classList?.contains('right');
                if (isLeft && vis.leftId) return vis.leftId;
                if (isRight && vis.rightId) return vis.rightId;
                const siblings = Array.from(tableContainer.parentElement?.querySelectorAll?.('.tab-menu-cont') || []);
                if (vis.ids?.length && siblings.length) {
                    const idx = siblings.indexOf(tableContainer);
                    if (idx >= 0 && idx < vis.ids.length && vis.ids[idx]) return vis.ids[idx];
                }
                if (vis.ids?.length === 1) return vis.ids[0];
                return null;
            } catch(_) { return null; }
        },
        _getFactionIdForMembersList(container) {
            try {
                const toStr = (val) => (val != null ? String(val) : null);
                if (container?.dataset?.factionId) return toStr(container.dataset.factionId);
                const attr = container?.getAttribute?.('data-faction-id');
                if (attr) return toStr(attr);
                const ancestor = container?.closest?.('[data-faction-id]');
                if (ancestor?.dataset?.factionId) return toStr(ancestor.dataset.factionId);
                const urlId = state.page?.url?.searchParams?.get('ID');
                if (urlId) return toStr(urlId);
                if (state.page?.isMyFactionPage && state.user?.factionId) return toStr(state.user.factionId);
                const headerLink = document.querySelector('.faction-info-head a[href*="factions.php"]');
                const parsed = headerLink ? utils.parseFactionIdFromHref?.(headerLink.href) : null;
                if (parsed) return toStr(parsed);
                return state.user?.factionId != null ? String(state.user.factionId) : null;
            } catch(_) { return state.user?.factionId != null ? String(state.user.factionId) : null; }
        },
        _syncFavoriteHeartsForPlayer(playerId, factionId) {
            try {
                if (!ui._areRankedWarFavoritesEnabled()) return;
                const pid = String(playerId);
                const selector = `.tdm-fav-heart[data-player-id='${pid}']`;
                document.querySelectorAll(selector).forEach(heart => {
                    const heartFaction = heart.getAttribute('data-faction-id') || heart.dataset.factionId || '';
                    if (factionId && heartFaction && String(factionId) !== String(heartFaction)) return;
                    const effective = heartFaction || factionId || null;
                    const fav = ui.isFavorite(pid, effective);
                    heart.setAttribute('aria-pressed', fav ? 'true' : 'false');
                    heart.classList.toggle('tdm-fav-heart--active', fav);
                    ui._setFavoriteRowHighlight(heart.closest('li, .table-row'), fav);
                });
            } catch(_) { /* noop */ }
        },
        _pinFavoritesEverywhere() {
            if (!ui._areRankedWarFavoritesEnabled()) return;
            try { ui._pinFavoritesInAllVisibleTables(); } catch(_) {}
            try { ui._pinFavoritesInFactionList(); } catch(_) {}
        },
        _pinFavoritesInAllVisibleTables() {
            try {
                if (!ui._areRankedWarFavoritesEnabled()) return;
                ui._refreshRankedWarSortForAll();
            } catch(_) { /* noop */ }
        },
        requestRankedWarFavoriteRepin() {
            try {
                if (!ui._areRankedWarFavoritesEnabled()) return;
                ui._refreshRankedWarSortForAll();
                setTimeout(() => ui._refreshRankedWarSortForAll(), 150);
            } catch(_) { /* noop */ }
        },
        _pinFavoritesInTable(tableContainer, factionId) {
            try {
                if (!ui._areRankedWarFavoritesEnabled()) return;
                if (!tableContainer) return;
                let resolvedFactionId = factionId ?? tableContainer.dataset?.factionId ?? ui._resolveRankedWarTableFactionId(tableContainer);
                if (resolvedFactionId == null) return;
                resolvedFactionId = String(resolvedFactionId);
                if (tableContainer.dataset) tableContainer.dataset.factionId = resolvedFactionId;
                const list = ui._resolveRankedWarListEl(tableContainer) || tableContainer.querySelector('.members-list, .members-cont');
                if (!list) return;
                const rowSelector = ':scope > li, :scope > .table-row';
                const rows = Array.from(list.querySelectorAll(rowSelector));
                if (!rows.length) return;
                const favRows = [];
                const normalRows = [];
                let needsReorder = false;
                let seenNonFav = false;
                rows.forEach(row => {
                    const isFav = row.dataset?.tdmFavorite === '1';
                    if (isFav) {
                        if (seenNonFav) needsReorder = true;
                        favRows.push(row);
                    } else {
                        seenNonFav = true;
                        normalRows.push(row);
                    }
                    ui._setFavoriteRowHighlight(row, isFav);
                });
                
                if (!favRows.length) return;
                if (!needsReorder) return;
                const frag = document.createDocumentFragment();
                favRows.forEach(row => frag.appendChild(row));
                normalRows.forEach(row => frag.appendChild(row));
                list.appendChild(frag);
            } catch(_) { /* noop */ }
        },
        _pinFavoritesInFactionList(container) {
            try {
                if (!ui._areRankedWarFavoritesEnabled()) return;
                const root = container || state.dom?.factionListContainer;
                if (!root) return;
                const factionId = ui._getFactionIdForMembersList(root);
                if (!factionId) return;
                const body = root.matches?.('.table-body') ? root : root.querySelector('.table-body');
                if (!body) return;
                const rows = Array.from(body.querySelectorAll(':scope > li.table-row'));
                if (!rows.length) return;
                const favs = ui.getFavoritesForFaction(factionId);
                const favSet = new Set(Object.keys(favs || {}));
                if (!favSet.size) {
                    rows.forEach(row => ui._setFavoriteRowHighlight(row, false));
                    return;
                }
                const favRows = [];
                const normalRows = [];
                let needsReorder = false;
                let seenNonFav = false;
                rows.forEach(row => {
                    const pid = ui._extractPlayerIdFromRow(row);
                    const isFav = pid && favSet.has(String(pid));
                    if (isFav) {
                        if (seenNonFav) needsReorder = true;
                        favRows.push(row);
                    } else {
                        seenNonFav = true;
                        normalRows.push(row);
                    }
                    ui._setFavoriteRowHighlight(row, isFav);
                });
                if (!favRows.length) return;
                if (!needsReorder) return;
                const frag = document.createDocumentFragment();
                favRows.forEach(row => frag.appendChild(row));
                normalRows.forEach(row => frag.appendChild(row));
                body.appendChild(frag);
            } catch(_) { /* noop */ }
        },
        toggleFavorite(playerId, factionId, el) {
            try {
                if (!ui._areRankedWarFavoritesEnabled()) return false;
                if (!playerId) return false;
                const factionKey = factionId != null ? String(factionId) : null;
                const key = ui._favStorageKey(factionKey);
                const favs = ui.getFavoritesForFaction(factionKey);
                const id = String(playerId);
                const was = !!favs[id];
                if (was) delete favs[id]; else favs[id] = { addedAt: Date.now() };

                // Immediate UI feedback
                if (el?.classList) {
                    el.classList.toggle('tdm-fav-heart--active', !was);
                    el.setAttribute('aria-pressed', (!was).toString());
                }
                ui._syncFavoriteHeartsForPlayer(id, factionKey);

                // Persist and run heavier repin/sort asynchronously to keep UI snappy
                setTimeout(() => { try { storage.set(key, favs); } catch(_) {} }, 50);
                setTimeout(() => { try { ui._pinFavoritesEverywhere(); ui._refreshRankedWarSortForFaction(factionKey); } catch(_) {} }, 120);

                return !was;
            } catch(_) { return false; }
        },
        _ensureRankedWarFavoriteHeart(subrow, playerId, factionId) {
            if (!subrow || !playerId || !factionId) return;
            try {
                if (!ui._areRankedWarFavoritesEnabled()) {
                    const existing = subrow.querySelector('.tdm-fav-heart');
                    if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
                    return;
                }
                let heart = subrow.querySelector('.tdm-fav-heart');
                if (!heart) {
                    heart = utils.createElement('button', { className: 'tdm-fav-heart', innerHTML: '\u2665', title: 'Favorite', type: 'button' });
                    heart.style.marginRight = '2px';
                    heart.style.marginLeft = '2px';
                    heart.style.border = 'none';
                    heart.style.background = 'transparent';
                    heart.style.cursor = 'pointer';
                    heart.addEventListener('click', (e) => {
                        try {
                            e.preventDefault();
                            e.stopPropagation();
                            ui.toggleFavorite(playerId, factionId, heart);
                        } catch(_) {}
                    });
                    subrow.insertAdjacentElement('afterbegin', heart);
                }
                heart.dataset.playerId = String(playerId);
                heart.dataset.factionId = String(factionId);
                const favActive = ui.isFavorite(playerId, factionId);
                heart.setAttribute('aria-pressed', favActive ? 'true' : 'false');
                heart.classList.toggle('tdm-fav-heart--active', favActive);
                ui._setFavoriteRowHighlight(subrow.closest('li, .table-row'), favActive);
            } catch(_) { /* noop */ }
        },
        _ensureMembersListFavoriteHeart(row, factionId) {
            if (!row) return;
            try {
                if (!ui._areRankedWarFavoritesEnabled()) {
                    const existing = row.querySelector('.tdm-fav-heart');
                    if (existing && existing.parentNode) existing.parentNode.removeChild(existing);
                    return;
                }
                const playerId = ui._extractPlayerIdFromRow(row);
                if (!playerId) return;
                const memberCell = row.querySelector('.member') || row.querySelector('.table-body .table-cell');
                if (!memberCell) return;
                let heart = memberCell.querySelector('.tdm-fav-heart');
                if (!heart) {
                    heart = utils.createElement('button', { className: 'tdm-fav-heart', innerHTML: '\u2665', title: 'Favorite', type: 'button' });
                    heart.style.marginRight = '6px';
                    heart.style.border = 'none';
                    heart.style.background = 'transparent';
                    heart.style.cursor = 'pointer';
                    heart.addEventListener('click', (e) => {
                        try {
                            e.preventDefault();
                            e.stopPropagation();
                            ui.toggleFavorite(playerId, factionId, heart);
                        } catch(_) {}
                    });
                    memberCell.insertBefore(heart, memberCell.firstChild);
                }
                if (factionId != null) heart.dataset.factionId = String(factionId);
                heart.dataset.playerId = String(playerId);
                const favActive = ui.isFavorite(playerId, factionId);
                heart.setAttribute('aria-pressed', favActive ? 'true' : 'false');
                heart.classList.toggle('tdm-fav-heart--active', favActive);
                ui._setFavoriteRowHighlight(row, favActive);
            } catch(_) { /* noop */ }
        },
        ensureBadgeDock() {
            // Reclaim existing dock if script re-injected
            if (!state.ui.badgeDockEl) {
                const existing = document.querySelector('.torn-badge-dock');
                if (existing) {
                    state.ui.badgeDockEl = existing;
                    state.ui.badgeDockItemsEl = existing.querySelector('.torn-badge-dock__block');
                    state.ui.badgeDockActionsEl = existing.querySelector('.torn-badge-dock__actions');
                }
            }
            // Hard de-duplication: if multiple docks somehow exist (race / double inject), keep the first and remove the rest
            try {
                const docks = document.querySelectorAll('.torn-badge-dock');
                if (docks && docks.length > 1) {
                    for (let i = 1; i < docks.length; i++) {
                        docks[i].remove();
                    }
                    // Rebind references to the surviving dock
                    const first = docks[0];
                    if (first) {
                        state.ui.badgeDockEl = first;
                        state.ui.badgeDockItemsEl = first.querySelector('.torn-badge-dock__block');
                        state.ui.badgeDockActionsEl = first.querySelector('.torn-badge-dock__actions');
                    }
                }
            } catch(_) { /* noop */ }
            if (!state.ui.badgeDockEl) {
                const dock = document.createElement('div');
                dock.className = 'torn-badge-dock';
                Object.assign(dock.style, {
                    position: 'fixed',
                    left: '18px',
                    right: 'auto',
                    bottom: '18px',
                    zIndex: '2000',
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: 'flex-start',
                    gap: '6px',
                    pointerEvents: 'none',
                    width: 'max-content',
                    transformOrigin: 'bottom left'
                });

                const actions = document.createElement('div');
                actions.className = 'torn-badge-dock__actions';
                Object.assign(actions.style, {
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: 'flex-start',
                    gap: '4px',
                    pointerEvents: 'auto'
                });

                const block = document.createElement('div');
                block.className = 'torn-badge-dock__block';
                Object.assign(block.style, {
                    display: 'flex',
                    flexDirection: 'column',
                    alignItems: 'flex-start',
                    gap: '4px',
                    pointerEvents: 'auto',
                    padding: '0',
                    borderRadius: '10px',
                    background: 'transparent',
                    border: 'none',
                    boxShadow: 'none',
                    maxWidth: 'none',
                    fontSize: '10px',
                    width: 'max-content'
                });

                dock.appendChild(actions);
                dock.appendChild(block);
                document.body.appendChild(dock);
                state.ui.badgeDockEl = dock;
                state.ui.badgeDockActionsEl = actions;
                state.ui.badgeDockItemsEl = block;
            } else {
                Object.assign(state.ui.badgeDockEl.style, {
                    left: '18px',
                    right: 'auto',
                    alignItems: 'flex-start',
                    gap: '6px',
                    pointerEvents: 'none',
                    width: 'max-content'
                });
                if (!state.ui.badgeDockItemsEl) state.ui.badgeDockItemsEl = state.ui.badgeDockEl.querySelector('.torn-badge-dock__block');
                if (!state.ui.badgeDockActionsEl) state.ui.badgeDockActionsEl = state.ui.badgeDockEl.querySelector('.torn-badge-dock__actions');
                if (state.ui.badgeDockActionsEl) {
                    Object.assign(state.ui.badgeDockActionsEl.style, {
                        alignItems: 'flex-start',
                        gap: '4px',
                        pointerEvents: 'auto'
                    });
                }
                if (state.ui.badgeDockItemsEl) {
                    Object.assign(state.ui.badgeDockItemsEl.style, {
                        alignItems: 'flex-start',
                        gap: '4px',
                        padding: '0',
                        maxWidth: 'none',
                        fontSize: '10px',
                        background: 'transparent',
                        border: 'none',
                        borderRadius: '10px',
                        boxShadow: 'none',
                        pointerEvents: 'auto',
                        width: 'max-content'
                    });
                }
            }

            try { if (window.__TDM_SINGLETON__) window.__TDM_SINGLETON__.badgeDockEnsures++; } catch(_) {}

            return state.ui.badgeDockEl;
        },
        _composeDockBadgeStyle(overrides = {}) {
            return Object.assign({
                display: 'inline-flex',
                alignItems: 'center',
                gap: '3px',
                padding: '2px 5px',
                borderRadius: '8px',
                background: 'rgba(12, 18, 28, 0.9)',
                border: '1px solid rgba(255,255,255,0.08)',
                color: '#e3ebf5',
                fontSize: '10px',
                fontWeight: '600',
                letterSpacing: '0.015em',
                lineHeight: '1.05',
                whiteSpace: 'nowrap',
                boxShadow: '0 4px 12px rgba(0,0,0,0.22)',
                pointerEvents: 'auto'
            }, overrides || {});
        },
        // Unified badge/timer ensure orchestrator.
        // to avoid scattered duplication logic and racing ensure calls.
        ensureBadgesSuite(force = false) {
            try {
                // Lightweight throttle: skip if we already ensured within the last animation frame unless force is true
                const now = performance.now();
                if (!force && state._lastBadgesSuiteAt && (now - state._lastBadgesSuiteAt) < 30) return; // ~1 frame @ 60fps
                state._lastBadgesSuiteAt = now;
            } catch(_) {}
            const leader = state.script?.isLeaderTab !== false; // treat undefined as true initially
            // Always start by ensuring dock + toggle (they internally dedupe)
            if (leader) {
                ui.ensureBadgeDock();
                ui.ensureBadgeDockToggle();
            } else {
                // Non-leader tabs only reclaim existing dock to prevent duplication
                ui.ensureBadgeDock();
            }

            // Core timers / badges (leader ensures structure; non-leader only updates existing)
            const exec = (ensureFn, updateFallback) => {
                try {
                    if (leader) ensureFn(); else if (updateFallback) updateFallback();
                } catch(_) {}
            };
            // Ordered badge ensures (explicit ordering preserved for stable dock layout):
            // 1) API usage counter
            exec(ui.ensureApiUsageBadge, ui.updateApiUsageBadge);
            // 2) Inactivity timer
            exec(ui.ensureInactivityTimer, null);
            // 3) Opponent status
            exec(ui.ensureOpponentStatus, null);
            // 4) Faction score
            exec(ui.ensureFactionScoreBadge, ui.updateFactionScoreBadge);
            // 5) User score
            exec(ui.ensureUserScoreBadge, ui.updateUserScoreBadge);
            // 6) Dibs/Deals
            exec(ui.ensureDibsDealsBadge, ui.updateDibsDealsBadge);
            // 7) Chain watcher badge (kept last so it's visually stable and can expand)
            exec(ui.ensureChainWatcherBadge, ui.updateChainWatcherBadge);
            // Chain timer intentionally not part of the primary badge order; ensure separately (keeps top-left logical grouping)
            exec(ui.ensureChainTimer, () => { /* chain timer skipped on passive tab to avoid duplication */ });
        },
        ensureBadgeDockToggle() {
            ui.ensureBadgeDock();
            // Dedupe existing toggles (can accumulate if script reinjected)
            try {
                const toggles = state.ui.badgeDockEl ? state.ui.badgeDockEl.querySelectorAll('.torn-badge-dock__toggle') : [];
                if (toggles && toggles.length > 1) {
                    for (let i = 1; i < toggles.length; i++) toggles[i].remove();
                }
                if (!state.ui.badgeDockToggleEl && toggles && toggles.length === 1) {
                    state.ui.badgeDockToggleEl = toggles[0];
                    return state.ui.badgeDockToggleEl;
                }
            } catch(_) { /* noop */ }

            if (!state.ui.badgeDockToggleEl) {
                const toggle = document.createElement('button');
                toggle.type = 'button';
                toggle.className = 'torn-badge-dock__toggle';
                Object.assign(toggle.style, {
                    border: '2px solid #ffcc00',
                    borderRadius: '4px',
                    background: 'linear-gradient(to bottom, #00b300, #008000)',
                    boxSizing: 'border-box',
                    color: '#d7e6ff',
                    width: '26px',
                    height: '26px',
                    display: 'flex',
                    alignItems: 'center',
                    justifyContent: 'center',
                    cursor: 'pointer',
                    boxShadow: '0 3px 10px rgba(0,0,0,0.35)',
                    transition: 'transform 0.12s ease, opacity 0.12s ease',
                    pointerEvents: 'auto',
                    alignSelf: 'flex-start'
                });
                toggle.style.fontSize = '12px';

                // Initial visual state: will be overridden after we read persisted value
                toggle.title = 'Collapse badge dock';
                toggle.textContent = '⤢';
                toggle.addEventListener('click', () => {
                    const collapsed = state.ui.badgeDockEl?.dataset.collapsed === 'true';
                    ui.setBadgeDockCollapsed(!collapsed, { persist: true });
                });

                state.ui.badgeDockToggleEl = toggle;
                const dock = state.ui.badgeDockEl;
                if (dock) {
                    // Position the toggle button below the dock container
                    dock.appendChild(toggle);
                }
                // Restore persisted collapsed state (defaults to false if not set)
                let persistedCollapsed = false;
                try { persistedCollapsed = !!storage.get(state.ui.badgeDockCollapsedKey, false); } catch(_) {}
                ui.setBadgeDockCollapsed(persistedCollapsed, { skipPersist: true });
            }

            return state.ui.badgeDockToggleEl;
        },
        ensureBadgeDockItems() {
            ui.ensureBadgeDock();
            return state.ui.badgeDockItemsEl;
        },
        setBadgeDockCollapsed(collapsed, opts = {}) {
            ui.ensureBadgeDock();
            if (!state.ui.badgeDockEl) return;

            state.ui.badgeDockEl.dataset.collapsed = collapsed ? 'true' : 'false';
            if (state.ui.badgeDockItemsEl) {
                state.ui.badgeDockItemsEl.style.display = collapsed ? 'none' : 'flex';
            }
            if (state.ui.badgeDockToggleEl) {
                state.ui.badgeDockToggleEl.textContent = collapsed ? '⤢' : '⤡';
                state.ui.badgeDockToggleEl.title = collapsed ? 'Expand badge dock' : 'Collapse badge dock';
                state.ui.badgeDockToggleEl.style.transform = collapsed ? 'rotate(180deg)' : 'rotate(0deg)';
            }
            if (!opts.skipPersist && opts.persist !== false) {
                try { storage.set(state.ui.badgeDockCollapsedKey, !!collapsed); } catch(_) {}
            }
        },
        toggleDebugOverlayMinimized: () => {
            const overlay = ui.ensureDebugOverlayContainer();
            if (!overlay) return;
            ui.setDebugOverlayMinimized(!state.ui.debugOverlayMinimized);
        },
        setDebugOverlayMinimized: (minimized, opts = {}) => {
            const next = !!minimized;
            const prev = !!state.ui.debugOverlayMinimized;
            state.ui.debugOverlayMinimized = next;
            if (!opts.skipPersist && prev !== next) {
                try { storage.set(state.ui.debugOverlayMinimizedKey, next); } catch(_) {}
            }
            const overlay = document.getElementById('tdm-live-track-overlay');
            if (!overlay) return;
            overlay.dataset.minimized = next ? 'true' : 'false';
            const wrap = overlay._wrapRef || overlay.querySelector('[data-tdm-overlay-wrap="1"]') || null;
            const body = overlay._innerBodyRef || overlay.querySelector('#tdm-live-track-overlay-body') || null;
            const help = overlay._helpRef || (wrap ? wrap.querySelector('#tdm-live-track-overlay-help') : null);
            const buttonBar = overlay._buttonBarRef || (wrap ? wrap.querySelector('[data-tdm-overlay-button-bar="1"]') : null);
            const copyBtn = overlay._copyBtnRef || (buttonBar ? buttonBar.querySelector('button[data-role="tdm-overlay-copy"]') : null);
            if (body) {
                body.style.display = next ? 'none' : '';
            }
            if (wrap) {
                if (next) {
                    wrap.style.paddingTop = '0';
                    wrap.style.minHeight = '';
                    wrap.style.minWidth = '';
                    wrap.style.paddingRight = '0';
                    wrap.style.display = 'inline-flex';
                    wrap.style.alignItems = 'center';
                } else {
                    wrap.style.paddingTop = '20px';
                    wrap.style.minHeight = '';
                    wrap.style.minWidth = '';
                    wrap.style.paddingRight = '';
                    wrap.style.display = 'block';
                    wrap.style.alignItems = '';
                }
            }
            const btn = state.ui.debugOverlayMinimizeEl || (wrap ? wrap.querySelector('#tdm-live-track-overlay-minimize') : null);
            if (btn) {
                const label = next ? 'Restore debug overlay' : 'Minimize debug overlay';
                btn.textContent = next ? '🐞' : '[ _ ]';
                btn.title = label;
                btn.setAttribute('aria-label', label);
            }
            if (help) {
                help.style.display = next ? 'none' : 'inline-flex';
            }
            if (buttonBar) {
                if (next) {
                    buttonBar.style.position = 'static';
                    buttonBar.style.top = '';
                    buttonBar.style.right = '';
                    buttonBar.style.gap = '0';
                    buttonBar.style.alignItems = 'center';
                } else {
                    buttonBar.style.position = 'absolute';
                    buttonBar.style.top = '4px';
                    buttonBar.style.right = '4px';
                    buttonBar.style.gap = '4px';
                    buttonBar.style.alignItems = 'center';
                }
            }
            if (copyBtn) {
                copyBtn.style.display = next ? 'none' : 'inline-flex';
            }
            const expandedStyle = overlay.dataset.tdmOverlayExpandedStyle;
            if (next) {
                overlay.style.background = 'transparent';
                overlay.style.boxShadow = 'none';
                overlay.style.padding = '0';
                overlay.style.borderRadius = '0';
                overlay.style.maxWidth = 'none';
                overlay.style.position = 'fixed';
                overlay.style.top = '8px';
                overlay.style.left = '8px';
                overlay.style.zIndex = '99999';
                overlay.style.color = '#fff';
                overlay.style.font = '11px/1.3 monospace';
                overlay.style.cursor = 'default';
                overlay.style.display = 'block';
                overlay.style.opacity = '1';
            } else {
                if (expandedStyle) {
                    overlay.setAttribute('style', expandedStyle);
                } else {
                    overlay.style.background = 'rgba(0,0,0,.65)';
                    overlay.style.boxShadow = '0 0 4px rgba(0,0,0,.4)';
                    overlay.style.padding = '6px 8px 4px';
                    overlay.style.borderRadius = '6px';
                    overlay.style.maxWidth = '300px';
                    overlay.style.position = 'fixed';
                    overlay.style.top = '8px';
                    overlay.style.left = '8px';
                    overlay.style.zIndex = '99999';
                    overlay.style.color = '#fff';
                    overlay.style.font = '11px/1.3 monospace';
                    overlay.style.cursor = 'default';
                }
                overlay.style.display = 'block';
                overlay.style.opacity = '1';
            }
        },
        ensureDebugOverlayContainer: (opts = {}) => {
            let el = document.getElementById('tdm-live-track-overlay');
            const created = !el;
            // Initialize persisted minimized state once per load
            try {
                if (state.ui._overlayMinInitDone !== true) {
                    const persisted = storage.get('debugOverlayMinimized', null);
                    if (persisted === true || persisted === false) {
                        state.ui.debugOverlayMinimized = persisted;
                    }
                    state.ui._overlayMinInitDone = true;
                }
            } catch(_) {}
            if (!el) {
                el = document.createElement('div');
                el.id = 'tdm-live-track-overlay';
                el.style.cssText = 'position:fixed;top:8px;left:8px;z-index:99999;background:rgba(0,0,0,.65);color:#fff;font:11px/1.3 monospace;padding:6px 8px 4px;border-radius:6px;max-width:300px;box-shadow:0 0 4px rgba(0,0,0,.4);cursor:default;';
                el.dataset.tdmOverlayExpandedStyle = el.getAttribute('style');
                document.body.appendChild(el);
            } else if (!el.dataset.tdmOverlayExpandedStyle) {
                const current = el.getAttribute('style');
                el.dataset.tdmOverlayExpandedStyle = current && current.trim().length ? current : 'position:fixed;top:8px;left:8px;z-index:99999;background:rgba(0,0,0,.65);color:#fff;font:11px/1.3 monospace;padding:6px 8px 4px;border-radius:6px;max-width:300px;box-shadow:0 0 4px rgba(0,0,0,.4);cursor:default;';
            }
            let wrap = el._wrapRef;
            if (!wrap || !wrap.isConnected) {
                wrap = el.querySelector('[data-tdm-overlay-wrap="1"]');
                if (!wrap) {
                    wrap = document.createElement('div');
                    wrap.dataset.tdmOverlayWrap = '1';
                    wrap.style.position = 'relative';
                    wrap.style.pointerEvents = 'auto';
                    while (el.firstChild) {
                        wrap.appendChild(el.firstChild);
                    }
                    el.appendChild(wrap);
                }
                el._wrapRef = wrap;
            }

            let buttonBar = wrap.querySelector('[data-tdm-overlay-button-bar="1"]');
            if (!buttonBar) {
                buttonBar = document.createElement('div');
                buttonBar.dataset.tdmOverlayButtonBar = '1';
                wrap.insertBefore(buttonBar, wrap.firstChild);
            }
            buttonBar.style.position = 'absolute';
            buttonBar.style.top = '4px';
            buttonBar.style.right = '4px';
            buttonBar.style.display = 'flex';
            buttonBar.style.gap = '4px';
            buttonBar.style.alignItems = 'center';
            buttonBar.style.pointerEvents = 'auto';
            el._buttonBarRef = buttonBar;

            let help = wrap.querySelector('#tdm-live-track-overlay-help');
            if (!help) {
                help = document.createElement('span');
                help.id = 'tdm-live-track-overlay-help';
                help.textContent = '?';
            }
            help.title = 'tick=total cycle; api/build/apply=sub-sections; drift=timer slip vs planned; tpm=transitions/min; skipped=unchanged ticks; landed transients=recent arrivals.';
            Object.assign(help.style, { background: '#374151', color: '#fff', borderRadius: '50%', width: '16px', height: '16px', fontSize: '10px', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'help', pointerEvents: 'auto', marginRight: '2px', position: 'static', top: 'auto', left: 'auto', padding: '0' });

            let copyBtn = buttonBar.querySelector('button[data-role="tdm-overlay-copy"]');
            if (!copyBtn) {
                copyBtn = document.createElement('button');
                copyBtn.type = 'button';
                copyBtn.dataset.role = 'tdm-overlay-copy';
                copyBtn.textContent = '⧉';
                copyBtn.title = 'Copy diagnostics lines to clipboard';
                copyBtn.setAttribute('aria-label', 'Copy diagnostics');
                Object.assign(copyBtn.style, { background: '#2563eb', color: '#fff', border: 'none', padding: '2px 4px', fontSize: '10px', cursor: 'pointer', borderRadius: '4px', lineHeight: '1', pointerEvents: 'auto' });
                copyBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    try {
                        const body = el._innerBodyRef || el;
                        const text = body.innerText || body.textContent || '';
                        navigator.clipboard.writeText(text.trim());
                        copyBtn.textContent = '✔';
                        setTimeout(() => { copyBtn.textContent = '⧉'; }, 1200);
                    } catch (_) {
                        copyBtn.textContent = '✖';
                        setTimeout(() => { copyBtn.textContent = '⧉'; }, 1500);
                    }
                });
            }
            el._copyBtnRef = copyBtn;

            let minBtn = wrap.querySelector('#tdm-live-track-overlay-minimize');
            if (!minBtn) {
                minBtn = document.createElement('button');
                minBtn.id = 'tdm-live-track-overlay-minimize';
                minBtn.type = 'button';
                Object.assign(minBtn.style, { background: '#1f2937', color: '#e5e7eb', border: 'none', padding: '2px 5px', fontSize: '10px', cursor: 'pointer', borderRadius: '4px', lineHeight: '1', pointerEvents: 'auto' });
                minBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    e.preventDefault();
                    const now = Date.now();
                    // Simple debounce: ignore rapid double clicks within 300ms
                    if (state.ui._lastOverlayToggle && (now - state.ui._lastOverlayToggle) < 300) return;
                    state.ui._lastOverlayToggle = now;
                    ui.toggleDebugOverlayMinimized();
                    try { storage.set('debugOverlayMinimized', !!state.ui.debugOverlayMinimized); } catch(_) {}
                });
                minBtn.textContent = state.ui.debugOverlayMinimized ? '🐞' : '[ _ ]';
                const initLabel = state.ui.debugOverlayMinimized ? 'Restore debug overlay' : 'Minimize debug overlay';
                minBtn.title = initLabel;
                minBtn.setAttribute('aria-label', initLabel);
            }
            if (!buttonBar.contains(copyBtn)) buttonBar.appendChild(copyBtn);
            buttonBar.insertBefore(help, copyBtn);
            if (!buttonBar.contains(minBtn)) buttonBar.appendChild(minBtn);
            else if (minBtn !== buttonBar.lastChild) buttonBar.appendChild(minBtn);
            state.ui.debugOverlayMinimizeEl = minBtn;
            el._helpRef = help;

            let inner = el._innerBodyRef;
            if (!inner || !inner.isConnected) {
                inner = wrap.querySelector('#tdm-live-track-overlay-body');
                if (!inner) {
                    inner = document.createElement('div');
                    inner.id = 'tdm-live-track-overlay-body';
                    wrap.appendChild(inner);
                }
                el._innerBodyRef = inner;
            }

            ui.setDebugOverlayMinimized(state.ui.debugOverlayMinimized, { skipPersist: true });
            if (created) {
                try { tdmlogger('info', '[LiveTrackOverlay] Container created'); } catch(_) {}
            }
            // Avoid forcing visible flash if caller requested skipShow (used on PDA minimized state restoration)
            // Passive ensures (background refresh / cadence updates) should not un-hide or flash the overlay
            const passive = !!opts.passive;
            if (!opts.skipShow) {
                try {
                    // If user has explicitly minimized, suppress passive reopening and respect persistent preference
                    if (state.ui.debugOverlayMinimized) {
                        if (!passive) {
                            // Non-passive explicit call (e.g., user toggling) still ensures container exists but keeps it minimized
                            el.style.display = 'block';
                            // In minimized mode we rely on setDebugOverlayMinimized styling; avoid opacity flicker
                        }
                    } else {
                        el.style.display = 'block';
                        el.style.opacity = '1';
                    }
                } catch(_) {}
            }
            try { if (window.__TDM_SINGLETON__) window.__TDM_SINGLETON__.overlayEnsures++; } catch(_) {}
            // Prevent persistent native tooltips on touch devices
            try { ui._sanitizeTouchTooltips(el); } catch(_) {}
            return el;
        },
        updateDebugOverlayFingerprints: () => {
            try {
                if (!state.debug?.apiLogs && !state.debug?.cadence) return; // only show when debug logging is enabled
                const overlay = ui.ensureDebugOverlayContainer({ passive: true });
                if (!overlay) return;
                const wrap = overlay._wrapRef || overlay;
                let body = overlay._innerBodyRef;
                if (!body) {
                    body = document.createElement('div');
                    body.dataset.tdmOverlayBody = '1';
                    body.style.marginTop = '18px';
                    overlay._innerBodyRef = body;
                    wrap.appendChild(body);
                }
                let section = body.querySelector('[data-fp-section="1"]');
                if (!section) {
                    section = document.createElement('div');
                    section.dataset.fpSection = '1';
                    section.style.marginTop = '4px';
                    section.style.borderTop = '1px solid rgba(255,255,255,.12)';
                    section.style.paddingTop = '4px';
                    body.appendChild(section);
                }
                const dFp = state._fingerprints?.dibs || '-';
                const mFp = state._fingerprints?.medDeals || '-';
                const dAge = state._fingerprintsMeta?.dibsChangedAt ? (Date.now() - state._fingerprintsMeta.dibsChangedAt) : null;
                const mAge = state._fingerprintsMeta?.medDealsChangedAt ? (Date.now() - state._fingerprintsMeta.medDealsChangedAt) : null;
                const fmtAge = (ms) => (ms == null) ? '—' : (ms < 1000 ? ms + 'ms' : (ms < 60000 ? (ms/1000).toFixed(1)+'s' : Math.round(ms/60000)+'m'));
                section.innerHTML = `\n<strong>Fingerprints</strong>\n<pre style="white-space:pre-wrap;margin:2px 0 0;font-size:10px;line-height:1.3;">dibs: ${dFp}  (age ${fmtAge(dAge)})\nmed : ${mFp}  (age ${fmtAge(mAge)})</pre>`;

                // War status augmentation
                try {
                    const warMeta = (state.rankedWarLastSummaryMeta) || {};
                    const warSource = state.rankedWarLastSummarySource || '-';
                    const manifestFp = warMeta.manifestFingerprint || warMeta.manifestFP || '-';
                    const summaryFp = warMeta.summaryFingerprint || warMeta.summaryFP || '-';
                    const summaryVer = warMeta.summaryVersion != null ? warMeta.summaryVersion : (warMeta.version != null ? warMeta.version : '-');
                    const lastApplyMs = warMeta.appliedAtMs ? (Date.now() - warMeta.appliedAtMs) : null;
                    const attacksCache = state.rankedWarAttacksCache || {};
                    let attacksMetaLine = '';
                    try {
                        // Grab first war cache entry (active war) heuristically
                        const firstKey = Object.keys(attacksCache)[0];
                        if (firstKey) {
                            const ac = attacksCache[firstKey];
                            const lastSeq = ac?.lastSeq ?? ac?.lastSequence ?? '-';
                            const cnt = Array.isArray(ac?.attacks) ? ac.attacks.length : (ac?.attacks ? Object.keys(ac.attacks).length : 0);
                            attacksMetaLine = `attacks: war=${firstKey} seq=${lastSeq} count=${cnt}`;
                        }
                    } catch(_) {}
                    const ageTxt = fmtAge(lastApplyMs);
                    section.innerHTML += `\n<strong>War Status</strong>\n<pre style="white-space:pre-wrap;margin:2px 0 0;font-size:10px;line-height:1.3;">src=${warSource} ver=${summaryVer}\nsummary=${summaryFp}\nmanifest=${manifestFp}\nlastApply=${ageTxt}${attacksMetaLine? '\n'+attacksMetaLine:''}</pre>`;
                } catch(_) { /* ignore war status overlay errors */ }
            } catch(_) { /* noop */ }
        },
        ensureDebugOverlayStyles: (enabled = true) => {
            try {
                const styleId = 'tdm-live-track-overlay-override';
                const blocker = document.querySelector('style[data-tdm-overlay-hide]');
                if (blocker) blocker.remove();
                let styleEl = document.getElementById(styleId);
                if (enabled) {
                    if (!styleEl) {
                        styleEl = document.createElement('style');
                        styleEl.id = styleId;
                        styleEl.textContent = '#tdm-live-track-overlay{display:block !important;opacity:1 !important;visibility:visible !important;}';
                        document.head.appendChild(styleEl);
                    }
                } else if (styleEl) {
                    styleEl.remove();
                }
            } catch(_) {}
        },

        // Brief window to force-show badges on tab focus using cached values while data refreshes
        _badgesForceShowUntil: 0,
        updateApiCadenceInfo: (opts = {}) => {
            try {
                const throttleState = state.uiCadenceInfoThrottle || (state.uiCadenceInfoThrottle = { lastRender: 0, pending: null });
                const now = Date.now();
                if (!opts.force) {
                    const elapsed = now - (throttleState.lastRender || 0);
                    const minInterval = 900; // soften rapid DOM churn on slower devices
                    if (elapsed < minInterval) {
                        if (!throttleState.pending) {
                            const delay = Math.max(150, minInterval - elapsed);
                            throttleState.pending = utils.registerTimeout(setTimeout(() => {
                                throttleState.pending = null;
                                try { ui.updateApiCadenceInfo({ force: true }); } catch(_) { /* ignore re-entrant errors */ }
                            }, delay));
                        }
                        return;
                    }
                }
                throttleState.lastRender = now;
                if (throttleState.pending) {
                    utils.unregisterTimeout(throttleState.pending);
                    throttleState.pending = null;
                }
                const line = document.getElementById('tdm-last-faction-refresh');
                const poll = document.getElementById('tdm-polling-status');
                const opponentLine = document.getElementById('tdm-opponent-poll-line');
                const extraStatus = document.getElementById('tdm-additional-factions-status');
                const extraSummary = document.getElementById('tdm-additional-factions-summary');
                const s = state.script || {};

                const extraRaw = utils.coerceStorageString(storage.get('tdmExtraFactionPolls', ''), '');
                const extraList = utils.parseFactionIdList(extraRaw);
                if (extraStatus) extraStatus.textContent = extraList.length ? `Polling ${extraList.length} extra faction${extraList.length === 1 ? '' : 's'}.` : 'No extra factions configured.';
                if (extraSummary) extraSummary.textContent = extraList.length ? `IDs: ${extraList.join(', ')}` : '—';
                // Avoid overwriting user edits; cadence updater intentionally does not touch the input field.

                if (opponentLine) {
                    const oppId = state.lastOpponentFactionId || state?.warData?.opponentId || null;
                    if (!oppId) {
                        opponentLine.textContent = 'Opponent polling: no opponent detected.';
                    } else {
                        const active = !!s.lastOpponentPollActive;
                        const reason = s.lastOpponentPollReason || (active ? 'active' : 'paused');
                        const agoTxt = (() => {
                            if (!s.lastOpponentPollAt) return '';
                            const diffSec = Math.floor((Date.now() - s.lastOpponentPollAt) / 1000);
                            if (diffSec < 0) return '';
                            if (diffSec < 60) return `${diffSec}s ago`;
                            return utils.formatAgoShort(Math.floor(s.lastOpponentPollAt / 1000));
                        })();
                        let descriptor;
                        const oppLabel = `faction ${oppId}`;
                        if (active) {
                            if (reason === 'war-active') descriptor = `active (ranked war, ${oppLabel})`;
                            else if (reason === 'war-pre') descriptor = `active (upcoming war, ${oppLabel})`;
                            else descriptor = `active (forced list, ${oppLabel})`;
                        } else if (reason === 'paused') {
                            descriptor = `paused (war inactive, ${oppLabel})`;
                        } else if (reason === 'none') {
                            descriptor = 'paused (no opponent detected)';
                        } else {
                            descriptor = `paused (${reason})`;
                        }
                        const suffix = agoTxt ? ` • last fetch ${agoTxt}` : '';
                        opponentLine.textContent = `Opponent polling: ${descriptor}${suffix}`;
                    }
                }

                if (line) {
                    const parts = [];
                    if (s.lastFactionRefreshAttemptMs) {
                        const sec = Math.floor((Date.now() - s.lastFactionRefreshAttemptMs) / 1000);
                        const ago = sec < 60 ? `${sec}s ago` : utils.formatAgoShort(Math.floor(s.lastFactionRefreshAttemptMs/1000));
                        const ids = Array.isArray(s.lastFactionRefreshAttemptIds) ? s.lastFactionRefreshAttemptIds.slice(0,3).join(',') : '';
                        parts.push(`attempt ${ago}${ids ? ` [${ids}]` : ''}`);
                    }
                    if (s.lastFactionRefreshFetchMs) {
                        const sec = Math.floor((Date.now() - s.lastFactionRefreshFetchMs) / 1000);
                        const ago = sec < 60 ? `${sec}s ago` : utils.formatAgoShort(Math.floor(s.lastFactionRefreshFetchMs/1000));
                        parts.push(`fetch ${ago}`);
                    }
                    if (s.lastFactionRefreshBackgroundMs) {
                        const sec = Math.floor((Date.now() - s.lastFactionRefreshBackgroundMs) / 1000);
                        const ago = sec < 60 ? `${sec}s ago` : utils.formatAgoShort(Math.floor(s.lastFactionRefreshBackgroundMs/1000));
                        parts.push(`bg ${ago}`);
                    }
                    if (s.lastFactionRefreshSkipReason) {
                        parts.push(`skip: ${s.lastFactionRefreshSkipReason}`);
                    }
                    const txt = `Last faction refresh: ${parts.length ? parts.join(' | ') : '—'}`;
                    line.textContent = txt;
                }

                if (poll) {
                    try {
                        const trackingEnabled = storage.get('tdmActivityTrackingEnabled', false);
                        const idleOverride = utils.isActivityKeepActiveEnabled();
                        const active = (s.isWindowActive !== false) || idleOverride;
                        const status = active ? 'active' : 'paused (tab hidden)';
                        let suffix = '';
                        if (idleOverride) suffix = ' [Idle tracking: Torn API only]';
                        else if (trackingEnabled) suffix = ' [Activity tracking]';
                        poll.textContent = `Polling status: ${status}${suffix}`;
                    } catch(_) {}
                }
            } catch(_) { /* noop */ }
        },
        showTosComplianceModal: () => {
            const { modal, header, controls, tableWrap, footer } = ui.createReportModal({ id: 'tdm-tos-modal', title: 'Torn API Terms of Service Compliance', maxWidth: '760px' });
            // Intro blurb
            const intro = utils.createElement('div', { style: { fontSize: '0.9em', lineHeight: '1.4', margin: '4px 0 10px 0', color: '#ddd' } });
            intro.textContent = 'This userscript is designed to comply with Torn\'s API Terms of Service. Review the summary below and ensure your usage aligns with Torn\'s policies.';
            controls.appendChild(intro);
            const rows = [
                { aspect: 'Data Storage', details: 'Cached locally in browser; backend endpoints receive only the payloads needed for faction coordination.' },
                { aspect: 'Data Sharing', details: 'Faction-wide: dibs, war metrics (attack counts & timing), user notes, organized crimes metadata.' },
                { aspect: 'Purpose of Use', details: 'Competitive advantage & coordination during ranked wars.' },
                { aspect: 'Key Storage', details: 'Custom API keys stay in browser storage. Backend services use the key transiently to mint Firebase tokens and do not persist the raw value.' },
                { aspect: 'Key Access Level', details: 'Required custom scopes: factions.basic, factions.members, factions.rankedwars, factions.chain, users.basic, users.attacks. Legacy Limited keys remain supported during migration.' }
            ];
            ui.renderReportTable(tableWrap, { columns: [
                { key: 'aspect', label: 'Aspect', width: '170px' },
                { key: 'details', label: 'Details', width: 'auto', render: r => r.details }
            ], rows, tableId: 'tdm-tos-table' });
            footer.innerHTML = '<span style="color:#bbb;font-size:0.8em;">Always follow Torn\'s official API Terms. Misuse of API data can result in revocation.</span>';
            const ackBtn = utils.createElement('button', { style: { marginTop: '8px', background: '#2e7d32', color: 'white', border: 'none', padding: '6px 14px', borderRadius: '4px', cursor: 'pointer', fontWeight: 'bold' }, textContent: 'Acknowledge & Close' });
            ackBtn.onclick = () => {
                try { storage.set(TDM_TOS_ACK_KEY, Date.now()); } catch(_) {}
                modal.style.display = 'none';
            };
            controls.appendChild(ackBtn);
        },
        ensureTosComplianceLink: () => {
            try {
                const container = state.dom.customControlsContainer || document.querySelector('.dibs-system-main-controls');
                if (!container) return;
                if (container.querySelector('#tdm-tos-link')) return; // already
                const link = utils.createElement('a', { id: 'tdm-tos-link', style: { marginLeft: '8px', fontSize: '11px', cursor: 'pointer', textDecoration: 'underline', color: '#9ecbff' }, textContent: 'TOS' });
                link.onclick = (e) => { e.preventDefault(); ui.showTosComplianceModal(); };
                container.appendChild(link);
            } catch(_) { /* noop */ }
        },
        // Generic, reusable report modal scaffold to keep a consistent look & feel across modals
        createReportModal: function({ id, title, width = '90%', maxWidth = '1000px' } = {}) {
            let modal = document.getElementById(id);
            if (!modal) {
                modal = utils.createElement('div', {
                    id,
                    style: {
                        position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)',
                        backgroundColor: config.CSS.colors.modalBg, border: `2px solid ${config.CSS.colors.modalBorder}`,
                        borderRadius: '4px', padding: '8px', zIndex: 10000, color: 'white',
                        width, maxWidth, minWidth: '320px', maxHeight: '80vh', overflowY: 'auto', overflowX: 'auto',
                        boxShadow: '0 4px 8px rgba(0,0,0,0.5)'
                    }
                });
                document.body.appendChild(modal);
            }
            modal.innerHTML = '';
            modal.style.display = 'block';

            const closeBtn = utils.createElement('button', {
                className: `${id}-close`,
                style: {
                    position: 'absolute', top: '8px', right: '12px', background: config.CSS.colors.error,
                    color: 'white', border: 'none', borderRadius: '4px', padding: '4px 10px', cursor: 'pointer',
                    fontWeight: 'bold', zIndex: 10001
                },
                textContent: 'X'
            });
            const header = utils.createElement('h2', { style: { marginTop: '0', marginRight: '28px' }, textContent: title || 'Report' });
            const controls = utils.createElement('div', { id: `${id}-controls`, style: { marginBottom: '8px' } });
            const tableWrap = utils.createElement('div', { id: `${id}-table`, style: { width: '100%' } });
            const footer = utils.createElement('div', { id: `${id}-footer`, style: { marginTop: '12px', fontSize: '0.95em', color: '#aaa' } });

            modal.appendChild(closeBtn);
            modal.appendChild(header);
            modal.appendChild(controls);
            modal.appendChild(tableWrap);
            modal.appendChild(footer);

            if (!modal._tdmCloseBound) {
                modal.addEventListener('click', (event) => {
                    if (event.target.classList.contains(`${id}-close`)) {
                        modal.style.display = 'none';
                    }
                });
                modal._tdmCloseBound = true;
            }

            const setLoading = (msg) => {
                const el = document.getElementById(`${id}-loading`) || utils.createElement('div', { id: `${id}-loading` });
                el.textContent = msg || 'Loading...';
                el.style.margin = '6px 0';
                el.style.color = '#ccc';
                controls.appendChild(el);
            };
            const clearLoading = () => { const el = document.getElementById(`${id}-loading`); if (el) el.remove(); };
            const setError = (msg) => {
                clearLoading();
                controls.appendChild(utils.createElement('div', { style: { color: config.CSS.colors.error }, textContent: msg || 'Unexpected error' }));
            };

            return { modal, header, controls, tableWrap, footer, setLoading, clearLoading, setError };
        },

        // Reusable sortable table renderer. Columns accept: key, label, width, align, render(row), sortValue(row)
        renderReportTable: function(container, { columns, rows, defaultSort = { key: columns?.[0]?.key, asc: true }, tableId, manualSort = false, onSortChange } = {}) {
            const table = utils.createElement('table', {
                id: tableId || undefined,
                style: { width: '100%', minWidth: '900px', borderCollapse: 'collapse', background: '#222', color: 'white' }
            });
            const thead = utils.createElement('thead');
            const tbody = utils.createElement('tbody');

            let sortKey = defaultSort?.key;
            let sortAsc = !!defaultSort?.asc;

            const asSortable = (val) => {
                if (val instanceof Date) return val.getTime();
                if (typeof val === 'number') return val;
                if (typeof val === 'boolean') return val ? 1 : 0;
                const s = (val == null ? '' : String(val));
                const n = Number(s);
                return isFinite(n) && s.trim() !== '' ? n : s.toLowerCase();
            };

            const headerRow = utils.createElement('tr', { style: { background: '#333' } });
            columns.forEach(col => {
                const th = utils.createElement('th', {
                    style: { padding: '6px', cursor: 'pointer', textAlign: col.align || 'left', width: col.width || undefined }
                }, [document.createTextNode(col.label)]);
                th.dataset.sort = col.key;
                th.onclick = () => {
                    const key = th.getAttribute('data-sort');
                    if (sortKey === key) sortAsc = !sortAsc; else { sortKey = key; sortAsc = true; }
                    if (manualSort && typeof onSortChange === 'function') {
                        onSortChange(sortKey, sortAsc);
                    } else {
                        renderRows();
                    }
                };
                headerRow.appendChild(th);
            });
            thead.appendChild(headerRow);

            const renderCell = (col, row) => {
                try { return (typeof col.render === 'function') ? col.render(row) : (row[col.key] ?? ''); } catch(_) { return ''; }
            };
            const sortVal = (col, row) => {
                try { return (typeof col.sortValue === 'function') ? col.sortValue(row) : asSortable(row[col.key]); } catch(_) { return asSortable(row[col.key]); }
            };
            const renderRows = () => {
                const col = columns.find(c => c.key === sortKey) || columns[0];
                const data = manualSort ? rows : [...rows].sort((a, b) => {
                    const A = sortVal(col, a);
                    const B = sortVal(col, b);
                    if (A === B) return 0;
                    return sortAsc ? (A > B ? 1 : -1) : (A < B ? 1 : -1);
                });
                tbody.innerHTML = '';
                for (const r of data) {
                    const tr = utils.createElement('tr', { style: { background: '#2c2c2c', color: 'white' } });
                    for (const c of columns) {
                        const td = utils.createElement('td', { style: { padding: '6px', color: 'white', textAlign: (c.align || 'left') } });
                        const val = renderCell(c, r);
                        if (val instanceof Node) td.appendChild(val); else td.textContent = val == null ? '' : String(val);
                        tr.appendChild(td);
                    }
                    tbody.appendChild(tr);
                }
            };

            table.appendChild(thead);
            table.appendChild(tbody);
            container.innerHTML = '';
            container.appendChild(table);
            renderRows();
            return { rerender: renderRows, getSort: () => ({ key: sortKey, asc: sortAsc }) };
        },
        updatePageContext: () => {
            state.page.url = new URL(window.location.href);
            state.dom.factionListContainer = document.querySelector('.f-war-list.members-list');
            state.dom.customControlsContainer = document.querySelector('.dibs-system-main-controls');
            state.dom.rankwarContainer = document.querySelector('div.desc-wrap.warDesc___qZfyO');
            if (state.dom.rankwarContainer) { state.dom.rankwarmembersWrap = state.dom.rankwarContainer.querySelector('.faction-war.membersWrap___NbYLx'); }
            // Scope ranked war tables to the war container to avoid unrelated elements using the same class
            state.dom.rankwarfactionTables = state.dom.rankwarContainer
                ? state.dom.rankwarContainer.querySelectorAll('.tab-menu-cont')
                : document.querySelectorAll('.tab-menu-cont');
            state.dom.rankBox = document.querySelector('.rankBox___OzP3D');

            state.page.isFactionProfilePage = state.page.url.href.includes(`factions.php?step=profile`);
            state.page.isMyFactionPrivatePage = state.page.url.href.includes('factions.php?step=your');
            state.page.isRankedWarPage = !!state.dom.rankwarContainer;
            state.page.isMyFactionYourInfoTab = state.page.url.hash.includes('tab=info') && state.page.isMyFactionPrivatePage;
            state.page.isFactionPage = state.page.url.href.includes(`factions.php`);
            const isMyFactionById = state.page.isFactionPage && state.user.factionId && state.page.url.searchParams.get('ID') === state.user.factionId;
            state.page.isMyFactionProfilePage = isMyFactionById && (state.page.url.searchParams.get('step') === 'your' || state.page.url.searchParams.get('step') === 'profile');
            state.page.isMyFactionPage = state.page.isMyFactionProfilePage || state.page.isMyFactionPrivatePage || (state.page.isRankedWarPage && state.factionPull && state.user.factionId && state.factionPull.id?.toString() === state.user.factionId);
            state.page.isAttackPage = state.page.url.href.includes('loader.php?sid=attack&user2ID=');

            // Cache preferred chain DOM sources (highest fidelity first) so per-second updater avoids repeated heavy queries.
            // 1. Primary: chain-box (provides center-stat count + timeleft)
            const chainBox = document.querySelector('.chain-box');
            if (chainBox) {
                state.dom.chainBoxEl = chainBox;
                state.dom.chainBoxCountEl = chainBox.querySelector('.chain-box-center-stat');
                state.dom.chainBoxTimeEl = chainBox.querySelector('.chain-box-timeleft');
            } else {
                state.dom.chainBoxEl = state.dom.chainBoxCountEl = state.dom.chainBoxTimeEl = null;
            }
            // 2. Secondary: compact bar-stats strip (large/medium/small screen variants)
            //    We locate a bar-stats container whose text includes 'Chain:' then capture value/time if present.
            let barStats = null;
            try {
                const candidates = document.querySelectorAll('.bar-stats___E_LqA');
                for (const c of candidates) {
                    if (/Chain:/i.test(c.textContent || '')) { barStats = c; break; }
                }
            } catch(_) { /* noop */ }
            if (barStats) {
                state.dom.barChainStatsEl = barStats;
                state.dom.barChainValueEl = barStats.querySelector('.bar-value___uxnah');
                // Time may be direct <p> or nested inside span.barDescWrapper___* with parentheses
                state.dom.barChainTimeEl = barStats.querySelector('.bar-timeleft___B9RGV');
            } else {
                state.dom.barChainStatsEl = state.dom.barChainValueEl = state.dom.barChainTimeEl = null;
            }
        },
        updateAllPages: () => {
            utils.perf.start('updateAllPages');
            utils.perf.start('updateAllPages.updatePageContext');
            ui.updatePageContext();
            utils.perf.stop('updateAllPages.updatePageContext');

            if (state.page.isAttackPage) {
                utils.perf.start('updateAllPages.injectAttackPageUI');
                // Handle async promise rejection to prevent "Uncaught (in promise)" errors
                ui.injectAttackPageUI().catch(err => {
                    try { tdmlogger('error', `[UI] injectAttackPageUI failed: ${err}`); } catch(_) {}
                });
                utils.perf.stop('updateAllPages.injectAttackPageUI');
            }
            if (state.page.isRankedWarPage) {
                utils.perf.start('updateAllPages.updateRankedWarUI');
                ui._renderEpoch.schedule();
                utils.perf.stop('updateAllPages.updateRankedWarUI');
            }
            if (state.page.isMyFactionPage) {
                // Faction cap banner logic now unified inside handlers.checkTermedWarScoreCap
            }
            if (state.dom.factionListContainer) {
                utils.perf.start('updateAllPages.updateFactionPageUI');
                // Gate Members List updates through a dedicated epoch scheduler to reduce churn
                ui._renderEpochMembers.schedule();
                utils.perf.stop('updateAllPages.updateFactionPageUI');
            }
            utils.perf.start('updateAllPages.updateRetalsButtonCount');
            ui.updateRetalsButtonCount();
            utils.perf.stop('updateAllPages.updateRetalsButtonCount');
            utils.perf.start('updateAllPages.ensureBadgesSuite');
            ui.ensureBadgesSuite();
            utils.perf.stop('updateAllPages.ensureBadgesSuite');
            // Ensure TOS link present where controls live (not part of dock suite)
            ui.ensureTosComplianceLink();
            utils.perf.stop('updateAllPages');
        },
        ensureChainTimer: () => {
            if (!storage.get('chainTimerEnabled', true)) { ui.removeChainTimer(); return; }
            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;
            // Dedupe any stray duplicates (same id)
            try {
                const dups = document.querySelectorAll('#tdm-chain-timer');
                if (dups.length > 1) {
                    for (let i = 1; i < dups.length; i++) dups[i].remove();
                }
                if (!state.ui.chainTimerEl && dups.length === 1) {
                    state.ui.chainTimerEl = dups[0];
                    state.ui.chainTimerValueEl = state.ui.chainTimerEl.querySelector('.tdm-chain-timer-value');
                }
            } catch(_) { /* noop */ }
            if (!state.ui.chainTimerEl) {
                const wrapper = utils.createElement('div', {
                    id: 'tdm-chain-timer',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(18,28,38,0.9)',
                        border: '1px solid rgba(140,255,200,0.35)',
                        color: '#d9fff0',
                        gap: '6px',
                        padding: '4px 8px'
                    })
                });
                const label = utils.createElement('span', {
                    textContent: 'Chain',
                    style: { textTransform: 'uppercase', fontSize: '9px', letterSpacing: '0.08em', opacity: 0.78, fontWeight: '700' }
                });
                const value = utils.createElement('span', {
                    className: 'tdm-chain-timer-value',
                    textContent: '--:--',
                    style: { fontSize: '12px', fontWeight: '700', letterSpacing: '0.05em', fontVariantNumeric: 'tabular-nums', color: '#9fffc9' }
                });
                wrapper.appendChild(label);
                wrapper.appendChild(value);
                items.appendChild(wrapper);
                state.ui.chainTimerEl = wrapper;
                state.ui.chainTimerValueEl = value;
            } else if (state.ui.chainTimerEl.parentNode !== items) {
                items.appendChild(state.ui.chainTimerEl);
            }
            // Start/refresh updater
            ui.startChainTimerUpdater();
        },
        removeChainTimer: () => {
            if (state.ui.chainTimerIntervalId) { try { utils.unregisterInterval(state.ui.chainTimerIntervalId); } catch(_) {} state.ui.chainTimerIntervalId = null; }
            if (state.ui.chainTimerEl) { state.ui.chainTimerEl.remove(); state.ui.chainTimerEl = null; }
            if (state.ui.chainTimerValueEl) state.ui.chainTimerValueEl = null;
        },
        startChainTimerUpdater: () => {
            if (!state.ui.chainTimerEl) return;
            if (state.ui.chainTimerIntervalId) return; // already running
            // Lightweight re-discovery every few seconds if elements disappear (layout changes / responsive breakpoints)
            let lastDomRefreshMs = 0;
            const refreshDomRefsIfNeeded = () => {
                const now = Date.now();
                if (now - lastDomRefreshMs < 4000) return; // refresh at most every 4s
                lastDomRefreshMs = now;
                if (!state.dom.chainBoxEl || !document.body.contains(state.dom.chainBoxEl)) {
                    const cb = document.querySelector('.chain-box');
                    if (cb) {
                        state.dom.chainBoxEl = cb;
                        state.dom.chainBoxCountEl = cb.querySelector('.chain-box-center-stat');
                        state.dom.chainBoxTimeEl = cb.querySelector('.chain-box-timeleft');
                    }
                }
                if (!state.dom.barChainStatsEl || !document.body.contains(state.dom.barChainStatsEl)) {
                    let barStats = null;
                    try {
                        const candidates = document.querySelectorAll('.bar-stats___E_LqA');
                        for (const c of candidates) { if (/Chain:/i.test(c.textContent || '')) { barStats = c; break; } }
                    } catch(_) {}
                    if (barStats) {
                        state.dom.barChainStatsEl = barStats;
                        state.dom.barChainValueEl = barStats.querySelector('.bar-value___uxnah');
                        state.dom.barChainTimeEl = barStats.querySelector('.bar-timeleft___B9RGV');
                    }
                }
            };

            const parseMmSs = (txt) => {
                if (!txt) return null;
                const m = txt.match(/^(\d{1,2}):(\d{2})$/);
                if (!m) return null;
                const mm = parseInt(m[1], 10); const ss = parseInt(m[2], 10);
                if (!Number.isFinite(mm) || !Number.isFinite(ss)) return null;
                return mm * 60 + ss;
            };

            const readPreferredDom = () => {
                // Returns { current:null|number, remainingSec:null|number }
                // Priority 1: chain-box
                if (state.dom.chainBoxEl) {
                    let current = null; let remainingSec = null;
                    try {
                        const ct = state.dom.chainBoxCountEl?.textContent?.trim().replace(/[,\s]/g,'');
                        if (ct && /^\d+$/.test(ct)) current = parseInt(ct,10);
                    } catch(_) {}
                    try {
                        const ttxt = state.dom.chainBoxTimeEl?.textContent?.trim();
                        const rem = parseMmSs(ttxt === '00:00' ? '' : ttxt);
                        if (rem != null && rem > 0) remainingSec = rem;
                    } catch(_) {}
                    if (current != null || remainingSec != null) return { current, remainingSec };
                }
                // Priority 2: bar-stats
                if (state.dom.barChainStatsEl) {
                    let current = null; let remainingSec = null;
                    try {
                        // Forms: "866/1k" or "866 / 1k" possibly with separate text nodes.
                        const raw = state.dom.barChainValueEl?.textContent || '';
                        // Remove spaces & commas for numeric extraction
                        const cleaned = raw.replace(/[,\s]/g,'');
                        const m = cleaned.match(/^(\d+)/);
                        if (m) current = parseInt(m[1],10);
                    } catch(_) {}
                    try {
                        const ttxt = state.dom.barChainTimeEl?.textContent?.trim();
                        const rem = parseMmSs(ttxt === '00:00' ? '' : ttxt);
                        if (rem != null && rem > 0) remainingSec = rem;
                    } catch(_) {}
                    if (current != null || remainingSec != null) return { current, remainingSec };
                }
                return { current: null, remainingSec: null };
            };

            const setDisplay = (remainingSec, current) => {
                if (!state.ui.chainTimerEl) return;
                const valueEl = state.ui.chainTimerValueEl || state.ui.chainTimerEl.querySelector('.tdm-chain-timer-value');
                if (remainingSec > 0 && current > 0) {
                    const mm = Math.floor(remainingSec / 60);
                    const ss = (remainingSec % 60).toString().padStart(2,'0');
                    if (valueEl) {
                        valueEl.textContent = `(${current}) ${mm}:${ss}`;
                        // Dynamic color thresholds:
                        //  >120s  -> green (default)
                        // 61-120s -> orange
                        //  <=60s  -> red
                        let color = '#9fffc9'; // default green
                        let border = '1px solid rgba(140,255,200,0.35)';
                        if (remainingSec <= 60) {
                            color = '#ff6666';
                            border = '1px solid rgba(255,102,102,0.55)';
                        } else if (remainingSec <= 120) {
                            color = '#ffb347';
                            border = '1px solid rgba(255,179,71,0.55)';
                        }
                        try {
                            valueEl.style.color = color;
                            // Subtle emphasis: pulse transition (no CSS animation to keep simple) by adjusting parent border/color.
                            state.ui.chainTimerEl.style.border = border;
                        } catch(_) { /* styling best-effort */ }
                        // Tooltip update for clarity
                        try { state.ui.chainTimerEl.title = `Chain: ${current} | Remaining: ${mm}:${ss}`; } catch(_) {}
                    }
                    ui._orchestrator.setDisplay(state.ui.chainTimerEl, 'inline-flex');
                } else {
                    if (valueEl) valueEl.textContent = '--:--';
                    ui._orchestrator.setDisplay(state.ui.chainTimerEl, 'none');
                }
            };

            const tick = async () => {
                refreshDomRefsIfNeeded();
                const nowSec = Math.floor(Date.now()/1000);
                // Read DOM preferred values
                const dom = readPreferredDom();
                let current = Number(state.ui.chainFallback.current||0);
                if (dom.current != null && dom.current > 0) {
                    current = dom.current;
                    // Keep fallback current in sync if DOM is ahead
                    if (dom.current > (state.ui.chainFallback.current||0)) state.ui.chainFallback.current = dom.current;
                }
                // Derive remaining seconds
                let remaining = 0;
                if (dom.remainingSec != null) {
                    remaining = dom.remainingSec;
                    // Update fallback epoch using fresher DOM timer
                    state.ui.chainFallback.timeoutEpoch = nowSec + dom.remainingSec;
                } else {
                    const timeout = Number(state.ui.chainFallback.timeoutEpoch||0);
                    remaining = timeout > nowSec ? (timeout - nowSec) : 0;
                    if (remaining <= 0 && current > 0) {
                        const endEpoch = Number(state.ui.chainFallback.endEpoch||0);
                        if (endEpoch > nowSec) remaining = endEpoch - nowSec;
                    }
                }
                setDisplay(remaining, current);
            };

            // Initial tick and interval
            tick();
            state.ui.chainTimerIntervalId = utils.registerInterval(setInterval(tick, 1000));
        },
        ensureInactivityTimer: () => {
            if (!storage.get('inactivityTimerEnabled', false)) { ui.removeInactivityTimer(); return; }
            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;
            // Dedupe duplicates
            try {
                const dups = document.querySelectorAll('#tdm-inactivity-timer');
                if (dups.length > 1) {
                    for (let i = 1; i < dups.length; i++) dups[i].remove();
                }
                if (!state.ui.inactivityTimerEl && dups.length === 1) {
                    state.ui.inactivityTimerEl = dups[0];
                    state.ui.inactivityTimerValueEl = state.ui.inactivityTimerEl.querySelector('.tdm-inactivity-value');
                }
            } catch(_) { /* noop */ }

            if (!state.ui.inactivityTimerEl) {
                const wrapper = utils.createElement('div', {
                    id: 'tdm-inactivity-timer',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(32, 24, 6, 0.9)',
                        border: '1px solid rgba(255, 214, 102, 0.32)',
                        color: '#ffe082',
                        gap: '6px',
                        padding: '4px 8px'
                    })
                });
                const label = utils.createElement('span', {
                    textContent: 'Inactivity',
                    style: { textTransform: 'uppercase', fontSize: '9px', letterSpacing: '0.08em', opacity: 0.78, fontWeight: '700' }
                });
                const value = utils.createElement('span', {
                    className: 'tdm-inactivity-value',
                    textContent: '00:00',
                    style: {
                        fontSize: '12px',
                        fontWeight: '700',
                        letterSpacing: '0.05em',
                        fontVariantNumeric: 'tabular-nums',
                        color: '#ffeb3b'
                    }
                });
                wrapper.appendChild(label);
                wrapper.appendChild(value);
                items.appendChild(wrapper);
                state.ui.inactivityTimerEl = wrapper;
                state.ui.inactivityTimerValueEl = value;
            } else if (state.ui.inactivityTimerEl.parentNode !== items) {
                items.appendChild(state.ui.inactivityTimerEl);
            }

            ui._orchestrator.setDisplay(state.ui.inactivityTimerEl, 'inline-flex');
            ui.startInactivityUpdater();
        },
        removeInactivityTimer: () => {
            if (state.ui.inactivityTimerIntervalId) { try { utils.unregisterInterval(state.ui.inactivityTimerIntervalId); } catch(_) {} state.ui.inactivityTimerIntervalId = null; }
            if (state.ui.inactivityTimerEl) { state.ui.inactivityTimerEl.remove(); state.ui.inactivityTimerEl = null; }
            if (state.ui.inactivityTimerValueEl) state.ui.inactivityTimerValueEl = null;
        },
        startInactivityUpdater: () => {
            if (!state.ui.inactivityTimerEl) return;
            if (state.ui.inactivityTimerIntervalId) return;
            const tick = () => {
                const ms = Date.now() - (state.script.lastActivityTime || Date.now());
                // set color orange after 4 minutes, red after 5 minutes
                const valueEl = state.ui.inactivityTimerValueEl || state.ui.inactivityTimerEl.querySelector('.tdm-inactivity-value');
                if (valueEl) {
                    valueEl.textContent = utils.formatTimeHMS(totalSec);
                    let color = '#ffeb3b';
                    if (totalSec >= 300) color = '#ff5370';
                    else if (totalSec >= 240) color = '#ffb74d';
                    valueEl.style.color = color;
                }
            };
            tick();
            state.ui.inactivityTimerIntervalId = utils.registerInterval(setInterval(tick, 1000));
        },
        ensureOpponentStatus: () => {
            if (!storage.get('opponentStatusTimerEnabled', true)) { ui.removeOpponentStatus(); return; }
            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;
            try {
                const dups = document.querySelectorAll('#tdm-opponent-status');
                if (dups.length > 1) {
                    for (let i = 1; i < dups.length; i++) dups[i].remove();
                }
                if (!state.ui.opponentStatusEl && dups.length === 1) {
                    state.ui.opponentStatusEl = dups[0];
                    state.ui.opponentStatusValueEl = state.ui.opponentStatusEl.querySelector('.tdm-opponent-status-value');
                }
            } catch(_) { /* noop */ }

            if (!state.ui.opponentStatusEl) {
                const wrapper = utils.createElement('div', {
                    id: 'tdm-opponent-status',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(12, 24, 36, 0.9)',
                        border: '1px solid rgba(112, 197, 255, 0.35)',
                        color: '#c8ecff',
                        gap: '6px',
                        padding: '4px 8px'
                    })
                });
                // Keep the dock compact — don't show a full "Opponent" label to save space.
                // Some hosts of older UI expect a span, so create an empty placeholder with minimal footprint.
                const label = utils.createElement('span', {
                    textContent: '',
                    style: { display: 'none' }
                });
                const value = utils.createElement('span', {
                    className: 'tdm-opponent-status-value',
                    innerHTML: '<span style="opacity:0.6">No active dib</span>',
                    style: { fontSize: '12px', fontWeight: '700', letterSpacing: '0.05em', color: '#9dd6ff' }
                });
                wrapper.appendChild(label);
                wrapper.appendChild(value);
                items.appendChild(wrapper);
                state.ui.opponentStatusEl = wrapper;
                state.ui.opponentStatusValueEl = value;
                ui._orchestrator.setDisplay(wrapper, 'inline-flex');
            } else if (state.ui.opponentStatusEl.parentNode !== items) {
                items.appendChild(state.ui.opponentStatusEl);
            }

            ui.startOpponentStatusUpdater();
        },
        ensureApiUsageBadge: () => {
            if (!storage.get('apiUsageCounterEnabled', false)) { ui.removeApiUsageBadge(); return; }
            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;
            try {
                const dups = document.querySelectorAll('#tdm-api-usage');
                if (dups.length > 1) {
                    for (let i = 1; i < dups.length; i++) dups[i].remove();
                }
                if (!state.ui.apiUsageEl && dups.length === 1) {
                    state.ui.apiUsageEl = dups[0];
                    state.ui.apiUsageValueEl = state.ui.apiUsageEl.querySelector('.tdm-api-usage-value');
                    state.ui.apiUsageDetailEl = state.ui.apiUsageEl.querySelector('.tdm-api-usage-detail');
                }
            } catch(_) { /* noop */ }

            if (!state.ui.apiUsageEl) {
                const wrapper = utils.createElement('div', {
                    id: 'tdm-api-usage',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(12, 26, 11, 0.9)',
                        border: '1px solid rgba(126, 206, 102, 0.38)',
                        color: '#c8f7ab',
                        gap: '6px'
                    })
                });
                const label = utils.createElement('span', {
                    textContent: 'API',
                    style: { fontWeight: '700', textTransform: 'uppercase', letterSpacing: '0.08em', fontSize: '9px', opacity: 0.78 }
                });
                const value = utils.createElement('span', {
                    className: 'tdm-api-usage-value',
                    textContent: '0',
                    style: { fontSize: '11px', fontWeight: '700', letterSpacing: '0.04em', fontVariantNumeric: 'tabular-nums', color: '#dcff93' }
                });
                const detail = utils.createElement('span', {
                    className: 'tdm-api-usage-detail',
                    textContent: 'C0/B0',
                    style: { fontSize: '10px', opacity: 0.75 }
                });
                wrapper.appendChild(label);
                wrapper.appendChild(value);
                wrapper.appendChild(detail);
                items.appendChild(wrapper);
                state.ui.apiUsageEl = wrapper;
                state.ui.apiUsageValueEl = value;
                state.ui.apiUsageDetailEl = detail;
            } else if (state.ui.apiUsageEl.parentNode !== items) {
                items.appendChild(state.ui.apiUsageEl);
            }

            if (handlers?.debouncedUpdateApiUsageBadge) { handlers.debouncedUpdateApiUsageBadge(); } else { ui.updateApiUsageBadge(); }
        },
        ensureAttackModeBadge: () => {
            // Respect user-configured visibility
            if (!storage.get('attackModeBadgeEnabled', true)) { const ex = document.getElementById('tdm-attack-mode'); if (ex) ex.remove(); state.ui.attackModeEl = null; return; }
            // Compact badge next to other chat header widgets
            const chatRoot = document.querySelector('.root___lv7vM');
            if (!chatRoot) return;
            const isRanked = (state.warData?.warType === 'Ranked War');
            const warActive = !!(state.lastRankWar && utils.isWarActive?.(state.lastRankWar.id));
            // Only show attack mode during active ranked wars
            if (!isRanked || !warActive) {
                const existing = document.getElementById('tdm-attack-mode');
                if (existing) existing.remove();
                state.ui.attackModeEl = null;
                return;
            }
            if (!isRanked) {
                const existing = document.getElementById('tdm-attack-mode');
                if (existing) existing.remove();
                state.ui.attackModeEl = null;
                return;
            }
            const fs = (state.script && state.script.factionSettings) || {};
            const mode = (fs.options && fs.options.attackMode) || fs.attackMode || 'FFA';
            // Always read-only badge; no admin editing from header
            const isAdmin = false;

            const existing = document.getElementById('tdm-attack-mode');
            if (!existing) {
                const el = utils.createElement('div', {
                    id: 'tdm-attack-mode',
                    className: 'tdm-text-halo',
                    title: 'Faction attack mode',
                    style: { display: 'inline-flex', alignItems: 'center', gap: '6px', marginRight: '8px', color: '#ffd166', fontWeight: '700', fontSize: '12px', padding: '0 2px' }
                });
                chatRoot.insertBefore(el, chatRoot.firstChild);
                state.ui.attackModeEl = el;
            } else if (existing.parentNode !== chatRoot) {
                chatRoot.insertBefore(existing, chatRoot.firstChild);
                state.ui.attackModeEl = existing;
            } else {
                state.ui.attackModeEl = existing;
            }

            if (!state.ui.attackModeEl) return;

            // If already rendered with same admin state, update in place and return
            const prev = state.ui._attackModeRendered || {};
            if (prev.isAdmin === isAdmin) {
                const span = state.ui.attackModeEl.querySelector('.tdm-attack-mode-value');
                if (span) {
                    if (span.textContent !== String(mode)) span.textContent = String(mode);
                    state.ui._attackModeRendered = { mode, isAdmin };
                    return;
                }
            }

            // Full (re)render
            state.ui.attackModeEl.innerHTML = '';
            const label = utils.createElement('span', { textContent: 'Atk Mode:' });
            let valueNode;
            // Always read-only span
            valueNode = utils.createElement('span', { className: 'tdm-attack-mode-value', textContent: mode, style: { color: '#fff' } });
            state.ui.attackModeEl.appendChild(label);
            state.ui.attackModeEl.appendChild(valueNode);
            state.ui._attackModeRendered = { mode, isAdmin };
        },
        removeAttackModeBadge: () => {
            const el = document.getElementById('tdm-attack-mode');
            if (el) el.remove();
            state.ui.attackModeEl = null;
        },
        ensureUserScoreBadge: () => {
            const enabled = storage.get('userScoreBadgeEnabled', true);
            const existing = document.getElementById('tdm-user-score');
                        if (!enabled) { if (existing) ui._orchestrator.setDisplay(existing, 'none'); return; }
                        const warId = state.lastRankWar?.id;
                        const inWindow = warId && utils.isWarInActiveOrGrace?.(warId, 6);
                        // Hide entirely if war not active or in grace window
                        if (!inWindow) { if (existing) ui._orchestrator.setDisplay(existing, 'none'); return; }
                        const force = ui._badgesForceShowUntil && Date.now() < ui._badgesForceShowUntil;

            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;

            let el = existing;
            if (!el) {
                const initialLabel = (() => {
                    const cached = storage.get('badge.user');
                    const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                    const isWar = (warId || null) === (cached?.warId || null);
                    return (!isExpired && isWar && cached?.v) ? cached.v : 'Me: 0';
                })();
                el = utils.createElement('div', {
                    id: 'tdm-user-score',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(14, 33, 24, 0.88)',
                        border: '1px solid rgba(110, 204, 163, 0.42)',
                        color: '#9ef5c8'
                    }),
                    title: 'Your personal score this war'
                }, [document.createTextNode(initialLabel)]);
                items.appendChild(el);
            } else if (el.parentNode !== items) {
                items.appendChild(el);
            }
            state.ui.userScoreBadgeEl = el;
            ui._orchestrator.setDisplay(el, 'inline-flex');
            ui.updateUserScoreBadge();
        },
        updateUserScoreBadge: async () => {
            const el = document.getElementById('tdm-user-score');
            if (!el) return;
            const warId = state.lastRankWar?.id;
            const inWindow = warId && utils.isWarInActiveOrGrace?.(warId, 6);
            const force = ui._badgesForceShowUntil && Date.now() < ui._badgesForceShowUntil;
            if (!inWindow) { ui._orchestrator.setDisplay(el, 'none'); return; }
            // Determine score type and compute user's current score
            const scoreType = state.warData?.scoreType || 'Respect';
            let value = 0;
            let assists = 0;
            let usedLightweight = false;

            // Enhancement #1: Use lightweight userScore if available and sufficient
            if (state.userScore) {
                 // Support both short keys (r, s) and long keys (respect, successful) from backend
                 if (scoreType === 'Respect') { value = Number(state.userScore.r || state.userScore.respect || 0); usedLightweight = true; }
                 else if (scoreType === 'Attacks') { value = Number(state.userScore.s || state.userScore.successful || 0); usedLightweight = true; }
                 else if (scoreType === 'Respect (no chain)') { value = Number(state.userScore.rnc || state.userScore.respectNoChain || 0); usedLightweight = true; }
                 else if (scoreType === 'Respect (no bonus)') { value = Number(state.userScore.rnb || state.userScore.respectNoBonus || 0); usedLightweight = true; }
                 
                 if (usedLightweight) assists = Number(state.userScore.as || state.userScore.assists || 0);
            } 
            // TODO: Prune now that userScore comes from back end
            if (!usedLightweight) {
                try {
                    const rows = await utils.getSummaryRowsCached(warId, state.user.factionId);
                    // Anti-flicker: if rows empty (fetch fail?), try to preserve cache
                    if ((!rows || rows.length === 0)) {
                         const cached = storage.get('badge.user');
                         const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                         const isWar = (warId || null) === (cached?.warId || null);
                         // If we have a valid non-zero cache, use it
                         if (!isExpired && isWar && cached.v && !/Me:\s*0\b/.test(cached.v)) {
                             ui._orchestrator.setText(el, cached.v);
                             ui._orchestrator.setDisplay(el, storage.get('userScoreBadgeEnabled', true) ? 'inline-flex' : 'none');
                             return;
                         }
                    }
                    const me = rows.find(r => String(r.attackerId) === String(state.user.tornId));
                    value = utils.computeScoreFromRow(me, scoreType);
                    assists = Number(me?.assistCount || 0);
                } catch(_) {}
            }
            // During force window, if computed value is 0 but we have a cached label, prefer cached and avoid overwriting it
            if (force && (!value || value === 0)) {
                const cached = storage.get('badge.user');
                const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                const isWar = (warId || null) === (cached?.warId || null);
                const v = (!isExpired && isWar) ? cached.v : null;
                if (v) { ui._orchestrator.setText(el, v); ui._orchestrator.setDisplay(el, storage.get('userScoreBadgeEnabled', true) ? 'inline-flex' : 'none'); return; }
            }
            // --- Anti-flicker & precision formatting ---
            state._scoreCaches = state._scoreCaches || {};
            const cacheKey = 'user';
            const prev = state._scoreCaches[cacheKey];
            const typeAbbr = (scoreType === 'Attacks') ? 'Hits' : (scoreType.startsWith('Respect') ? 'R' : scoreType);
            const formatted = utils.formatScore ? utils.formatScore(value, scoreType) : String(value);
            const shouldUpdate = !prev || prev.type !== scoreType || utils.scores?.shouldUpdate(prev.raw, value) || (prev.assists !== assists);
            if (shouldUpdate) {
                let label = `Me: ${formatted} ${typeAbbr}`;
                if (assists > 0) label += ` / ${assists} A`;
                ui._orchestrator.setText(el, label);
                state._scoreCaches[cacheKey] = { raw: value, formatted, type: scoreType, assists, ts: Date.now() };
                // Avoid persisting a zero overwriting a good cached label during force window
                if (!(force && (!value || value === 0))) { storage.set('badge.user', { v: label, ts: Date.now(), warId }); }
            } else {
                // preserve prior visible text if we temporarily can’t recompute
                if (!el.textContent || /Me:\s*0\b/.test(el.textContent)) {
                    const cached = storage.get('badge.user');
                    const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                    const isWar = (warId || null) === (cached?.warId || null);
                    const v = (!isExpired && isWar) ? cached.v : null;
                    if (v) ui._orchestrator.setText(el, v);
                }
            }
            ui._orchestrator.setDisplay(el, storage.get('userScoreBadgeEnabled', true) ? 'inline-flex' : 'none');
        },
        removeUserScoreBadge: () => {
            const el = document.getElementById('tdm-user-score');
            if (el) el.remove();
            state.ui.userScoreBadgeEl = null;
        },
        ensureFactionScoreBadge: () => {
            const enabled = storage.get('factionScoreBadgeEnabled', true);
            const existing = document.getElementById('tdm-faction-score');
            if (!enabled) { if (existing) ui._orchestrator.setDisplay(existing, 'none'); return; }
            const warId = state.lastRankWar?.id;
            const inWindow = warId && utils.isWarInActiveOrGrace?.(warId, 6);
            if (!inWindow) { if (existing) ui._orchestrator.setDisplay(existing, 'none'); return; }
            const force = ui._badgesForceShowUntil && Date.now() < ui._badgesForceShowUntil;

            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;

            let el = existing;
            if (!el) {
                const initialLabel = (() => {
                    const cached = storage.get('badge.faction');
                    const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                    const isWar = (warId || null) === (cached?.warId || null);
                    return (!isExpired && isWar && cached?.v) ? cached.v : 'Faction: 0';
                })();
                el = utils.createElement('div', {
                    id: 'tdm-faction-score',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(16, 26, 44, 0.9)',
                        border: '1px solid rgba(104, 162, 247, 0.38)',
                        color: '#a8cfff'
                    }),
                    title: 'Our faction score this war'
                }, [document.createTextNode(initialLabel)]);
                items.appendChild(el);
            } else if (el.parentNode !== items) {
                items.appendChild(el);
            }
            state.ui.factionScoreBadgeEl = el;
            ui._orchestrator.setDisplay(el, 'inline-flex');
            ui.updateFactionScoreBadge();
        },
        updateFactionScoreBadge: async () => {
            const el = document.getElementById('tdm-faction-score');
            if (!el) return;
            const warId = state.lastRankWar?.id;
            const inWindow = warId && utils.isWarInActiveOrGrace?.(warId, 6);
            const force = ui._badgesForceShowUntil && Date.now() < ui._badgesForceShowUntil;
            if (!inWindow) { ui._orchestrator.setDisplay(el, 'none'); return; }
            const scoreType = state.warData?.scoreType || 'Respect';
            // New simplified logic: rely solely on lastRankWar.factions which is authoritative & freshest
            let total = 0;
            try {
                const lw = state.lastRankWar;
                if (lw && Array.isArray(lw.factions)) {
                    const ourFac = lw.factions.find(f => String(f.id) === String(state.user.factionId));
                    if (ourFac && typeof ourFac.score === 'number') {
                        total = Number(ourFac.score) || 0;
                    }
                }
            } catch(_) { /* noop */ }
            // During force window, if computed total is 0 but we have a cached label, prefer cached and avoid overwriting it
            if (force && (!total || total === 0)) {
                const cached = storage.get('badge.faction');
                const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                const isWar = (warId || null) === (cached?.warId || null);
                const v = (!isExpired && isWar) ? cached.v : null;
                if (v) { ui._orchestrator.setText(el, v); ui._orchestrator.setDisplay(el, storage.get('factionScoreBadgeEnabled', true) ? 'inline-flex' : 'none'); return; }
            }
            // --- Anti-flicker & precision formatting ---
            state._scoreCaches = state._scoreCaches || {};
            const cacheKey = 'faction';
            const prev = state._scoreCaches[cacheKey];
            const formatted = utils.formatScore ? utils.formatScore(total, scoreType) : String(total);
            const shouldUpdate = !prev || prev.type !== scoreType || utils.scores?.shouldUpdate(prev.raw, total);
            if (shouldUpdate) {
                const label = `Faction: ${formatted}`;
                ui._orchestrator.setText(el, label);
                state._scoreCaches[cacheKey] = { raw: total, formatted, type: scoreType, ts: Date.now() };
                if (!(force && (!total || total === 0))) { storage.set('badge.faction', { v: label, ts: Date.now(), warId }); }
            } else {
                if (!el.textContent || /Faction:\s*0\b/.test(el.textContent)) {
                    const cached = storage.get('badge.faction');
                    const isExpired = !cached || (Date.now() - (cached.ts || 0) > 2 * 60 * 60 * 1000);
                    const isWar = (warId || null) === (cached?.warId || null);
                    const v = (!isExpired && isWar) ? cached.v : null;
                    if (v) ui._orchestrator.setText(el, v);
                }
            }
            ui._orchestrator.setDisplay(el, storage.get('factionScoreBadgeEnabled', true) ? 'inline-flex' : 'none');
        },
        removeFactionScoreBadge: () => {
            const el = document.getElementById('tdm-faction-score');
            if (el) el.remove();
            state.ui.factionScoreBadgeEl = null;
        },
        updateApiUsageBadge: () => {
            const el = state.ui.apiUsageEl || document.getElementById('tdm-api-usage');
            if (!el) return;
            const total = Number(state.session.apiCalls || 0);
            const client = Number(state.session.apiCallsClient || 0);
            const backend = Number(state.session.apiCallsBackend || 0);
            const valueEl = state.ui.apiUsageValueEl || el.querySelector('.tdm-api-usage-value');
            if (valueEl) {
                const prev = valueEl.textContent;
                const next = total.toString();
                if (prev !== next) valueEl.textContent = next;
            }
            const detailEl = state.ui.apiUsageDetailEl || el.querySelector('.tdm-api-usage-detail');
            if (detailEl) {
                const combo = `C${client}/B${backend}`;
                if (detailEl.textContent !== combo) detailEl.textContent = combo;
            }
            el.title = `Torn API calls (session, user key)\n• Client: ${client}\n• Backend: ${backend}\n• Total: ${total}`;
            ui._orchestrator.setDisplay(el, storage.get('apiUsageCounterEnabled', false) ? 'inline-flex' : 'none');
        },
        removeApiUsageBadge: () => {
            if (state.ui.apiUsageEl) { state.ui.apiUsageEl.remove(); state.ui.apiUsageEl = null; }
            state.ui.apiUsageValueEl = null;
            state.ui.apiUsageDetailEl = null;
        },
        ensureDibsDealsBadge: () => {
            if (!storage.get('dibsDealsBadgeEnabled', true)) { ui.removeDibsDealsBadge(); return; }
            // Removed war active/grace gating so badge can show pre-war (setup / staging phase)
            const warId = state.lastRankWar?.id; // May be undefined pre-war; still proceed.
            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;
            let el = document.getElementById('tdm-dibs-deals');
            if (!el) {
                el = utils.createElement('div', {
                    id: 'tdm-dibs-deals',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(36, 24, 6, 0.88)',
                        border: '1px solid rgba(247, 183, 65, 0.4)',
                        color: '#fcd77d'
                    }),
                    title: 'Active dibs and med deals'
                }, [document.createTextNode('Dibs: 0, Deals: 0')]);
                items.appendChild(el);
            } else if (el.parentNode !== items) {
                items.appendChild(el);
            }
            state.ui.dibsDealsBadgeEl = el;
            // Immediate paint using raw updater (debounced variant may be pending already)
            ui.updateDibsDealsBadge();
            // Start periodic refresh (idempotent) always (lightweight)
            if (!state._dibsDealsInterval) {
                state._dibsDealsInterval = utils.registerInterval(setInterval(() => {
                    try {
                        // Prefer debounced variant if initialized; fallback to direct
                        if (handlers?.debouncedUpdateDibsDealsBadge) handlers.debouncedUpdateDibsDealsBadge(); else ui.updateDibsDealsBadge();
                    } catch(_) {}
                }, 8000));
            }
        },
        updateDibsDealsBadge: () => {
            const el = document.getElementById('tdm-dibs-deals');
            if (!el) return;
            if (!storage.get('dibsDealsBadgeEnabled', true)) { ui._orchestrator.setDisplay(el, 'none'); return; }
            // War activity window check removed; we allow badge visibility regardless of active/grace status
            // Count active dibs (dibsActive true) and active med deals (medDeals entries with isMedDeal true)
            let activeDibs = 0;
            try {
                if (Array.isArray(state.dibsData)) {
                    for (const d of state.dibsData) if (d && d.dibsActive) activeDibs++;
                }
            } catch(_) {}
            let activeDeals = 0;
            try {
                for (const v of Object.values(state.medDeals || {})) {
                    if (v && v.isMedDeal) activeDeals++;
                }
            } catch(_) {}
            // Stabilization: if backend temporarily returned empty structures (race) retain last non-zero counts for a short grace window to avoid flicker
            const GRACE_MS = 15000; // 15s retention of last non-zero display when new counts drop to zero unexpectedly
            if (!state._dibsDealsLast) state._dibsDealsLast = { dibs: 0, deals: 0, ts: 0 };
            const nowMs = Date.now();
            const hadPrev = (state._dibsDealsLast.dibs > 0 || state._dibsDealsLast.deals > 0);
            const countsNowZero = activeDibs === 0 && activeDeals === 0;
            if (countsNowZero && hadPrev && (nowMs - state._dibsDealsLast.ts) < GRACE_MS) {
                // Retain previous non-zero snapshot (assume transient fetch gap) but slowly age out
                activeDibs = state._dibsDealsLast.dibs;
                activeDeals = state._dibsDealsLast.deals;
            } else if (activeDibs > 0 || activeDeals > 0) {
                state._dibsDealsLast = { dibs: activeDibs, deals: activeDeals, ts: nowMs };
            }
            // Build display parts only for non-zero counts
            const parts = [];
            if (activeDibs > 0) parts.push(`Dibs: ${activeDibs}`);
            if (activeDeals > 0) parts.push(`Deals: ${activeDeals}`);
            if (parts.length === 0) {
                // Hide the badge entirely when no active dibs or deals are present to avoid noise
                ui._orchestrator.setDisplay(el, 'none');
                try { if (el.title !== 'No active dibs or med deals') ui._orchestrator.setTitle(el, 'No active dibs or med deals'); } catch(_) {}
                return;
            }
            const text = parts.join(', ');
            if (el.textContent !== text) ui._orchestrator.setText(el, text);
            ui._orchestrator.setDisplay(el, 'inline-flex');
            ui._orchestrator.setTitle(el, `${activeDibs} active dib${activeDibs===1?'':'s'}${activeDeals>0?` | ${activeDeals} active deal${activeDeals===1?'':'s'}`:''}`);
            try { if (window.__TDM_SINGLETON__) window.__TDM_SINGLETON__.dibsDealsUpdates++; } catch(_) {}
        },
        removeDibsDealsBadge: () => {
            const el = document.getElementById('tdm-dibs-deals');
            if (el) el.remove();
            state.ui.dibsDealsBadgeEl = null;
            if (state._dibsDealsInterval) { try { utils.unregisterInterval(state._dibsDealsInterval); } catch(_) {} state._dibsDealsInterval = null; }
        },
        // ChainWatcher Badge
        ensureChainWatcherBadge: () => {
            // Respect user toggle
            if (!storage.get('chainWatcherBadgeEnabled', true)) { ui.removeChainWatcherBadge(); return; }
            // Do not create unless explicitly enabled via presence of selections
            ui.ensureBadgeDock();
            ui.ensureBadgeDockToggle();
            const items = ui.ensureBadgeDockItems();
            if (!items) return;
            let el = document.getElementById('tdm-chain-watchers');
            if (!el) {
                el = utils.createElement('div', {
                    id: 'tdm-chain-watchers',
                    style: ui._composeDockBadgeStyle({ background: 'rgba(10,36,66,0.9)', border: '1px solid rgba(59,130,246,0.3)', color: '#9fd3ff' }),
                    title: 'Selected Chain Watchers'
                }, [document.createTextNode('Watchers: —')]);
                items.appendChild(el);
            } else if (el.parentNode !== items) {
                items.appendChild(el);
            }
            state.ui.chainWatcherBadgeEl = el;
            ui.updateChainWatcherBadge();
            // Start periodic status refresher while badge is present
            try {
                if (state.ui.chainWatcherIntervalId) { try { utils.unregisterInterval(state.ui.chainWatcherIntervalId); } catch(_) {} state.ui.chainWatcherIntervalId = null; }
                state.ui.chainWatcherIntervalId = utils.registerInterval(setInterval(() => {
                    try { ui.updateChainWatcherDisplayedStatuses && ui.updateChainWatcherDisplayedStatuses(); } catch(_) {}
                }, 2500)); // refresh every 2.5s
            } catch(_) {}
        },
        updateChainWatcherBadge: () => {
            // Respect user toggle
            if (!storage.get('chainWatcherBadgeEnabled', true)) { ui.removeChainWatcherBadge(); return; }
            const el = document.getElementById('tdm-chain-watchers');
            if (!el) return;
            const stored = storage.get('chainWatchers', []);
            const membersById = (Array.isArray(state.factionMembers) ? state.factionMembers : []).reduce((acc, m) => { if (m && m.id) acc[String(m.id)] = m; return acc; }, {});
            const names = Array.isArray(stored) ? stored.map(s => {
                const name = s && s.name ? s.name : (s || '');
                const id = String(s && (s.id || s.tornId || s) || '');
                return { id, name };
            }).filter(Boolean) : [];
            if (!names.length) { ui._orchestrator.setDisplay(el, 'none'); return; }
            // Build DOM: "Watchers: " + colored name spans
            while (el.firstChild) el.removeChild(el.firstChild);
            el.appendChild(document.createTextNode('Watchers: '));
            names.forEach((n, idx) => {
                const span = document.createElement('span');
                span.className = 'tdm-chainwatcher-badge-name';
                span.textContent = n.name;
                try { span.dataset.memberId = String(n.id); } catch(_) {}
                // Apply color based on live member status (if available)
                const mem = membersById[String(n.id)];
                try { if (utils.addLastActionStatusColor && typeof utils.addLastActionStatusColor === 'function') utils.addLastActionStatusColor(span, mem); } catch(_) {}
                el.appendChild(span);
                if (idx < names.length - 1) el.appendChild(document.createTextNode(', '));
            });
            ui._orchestrator.setDisplay(el, 'inline-flex');
            try { ui._orchestrator.setTitle(el, names.map(n => n.name).join(', ')); } catch(_) {}
            // Also update the small header display with names in yellow (plain text)
            try {
                const hdr = document.getElementById('tdm-chainwatcher-header-names');
                if (hdr) hdr.textContent = names.map(n => n.name).join(', ');
            } catch(_) {}
        },
        // Refresh displayed status spans for chain watcher UI elements (checkbox list + badge)
        updateChainWatcherDisplayedStatuses: () => {
            try {
                // Update checkbox list spans
                const members = Array.isArray(state.factionMembers) ? state.factionMembers : [];
                for (const m of members) {
                    const id = String(m.id);
                    // Color the name span instead of rendering textual status
                    const nameSpan = document.querySelector(`#tdm-chainwatcher-checkbox-list span.tdm-chainwatcher-name[data-member-id="${id}"]`);
                    if (nameSpan) {
                        try { if (utils.addLastActionStatusColor && typeof utils.addLastActionStatusColor === 'function') utils.addLastActionStatusColor(nameSpan, m); } catch(_) {}
                    }
                }
                // Update options in hidden select (if present)
                const chainSelect = document.getElementById('tdm-chainwatcher-select');
                if (chainSelect) {
                    const opts = Array.from(chainSelect.options || []);
                    for (const opt of opts) {
                        const id = String(opt.value);
                        const mem = members.find(mm => String(mm.id) === id);
                        const baseName = mem && mem.name ? `${mem.name} [${id}]` : opt.textContent.split(' - ')[0];
                        opt.textContent = baseName;
                    }
                }
                // Refresh badge text
                try { ui.updateChainWatcherBadge && ui.updateChainWatcherBadge(); } catch(_) {}
            } catch(_) {}
        },
        updateChainWatcherMeta: (meta) => {
            try {
                const el = document.getElementById('tdm-chainwatcher-meta');
                if (!el) return;
                if (!meta || !meta.lastWriter) {
                    el.textContent = 'Last updated: —';
                    return;
                }
                const lw = meta.lastWriter || {};
                const name = lw.username || (lw.tornId ? `Torn#${lw.tornId}` : (lw.uid ? `uid:${lw.uid}` : 'Unknown'));
                // Firestore serialized shape: { _seconds, _nanoseconds }
                let updatedAt = null;
                try {
                    if (meta && meta.updatedAt && typeof meta.updatedAt._seconds === 'number') {
                        updatedAt = new Date(meta.updatedAt._seconds * 1000 + Math.floor((meta.updatedAt._nanoseconds || 0) / 1e6));
                    }
                } catch (_) { updatedAt = null; }
                const timeStr = updatedAt && !Number.isNaN(updatedAt.getTime()) ? updatedAt.toLocaleString(undefined, { hour12: false }) + ' LT' : 'Invalid Date';
                el.textContent = `Last updated: ${name} @ ${timeStr}`;
                // Also update header names to match stored watchers if present
                try {
                    const stored = storage.get('chainWatchers', []);
                    const names = Array.isArray(stored) ? stored.map(s => s && s.name ? s.name : (s || '')).filter(Boolean) : [];
                    const hdr = document.getElementById('tdm-chainwatcher-header-names');
                    if (hdr) hdr.textContent = names.length ? names.join(', ') : '—';
                } catch(_) {}
            } catch(_) { /* noop */ }
        },
        removeChainWatcherBadge: () => {
            const el = document.getElementById('tdm-chain-watchers');
            if (el) el.remove();
            state.ui.chainWatcherBadgeEl = null;
            if (state.ui.chainWatcherIntervalId) { try { utils.unregisterInterval(state.ui.chainWatcherIntervalId); } catch(_) {} state.ui.chainWatcherIntervalId = null; }
        },
        // Optional: prune cache to last 10 wars to avoid unlimited growth
        removeOpponentStatus: () => {
            if (state.ui.opponentStatusIntervalId) { try { utils.unregisterInterval(state.ui.opponentStatusIntervalId); } catch(_) {} state.ui.opponentStatusIntervalId = null; }
            if (state.ui.opponentStatusEl) { state.ui.opponentStatusEl.remove(); state.ui.opponentStatusEl = null; }
            if (state.ui.opponentStatusValueEl) state.ui.opponentStatusValueEl = null;
        },
        
        // TDM_REF: hospital refocus reconciliation
        // Force a lightweight reconciliation of hospital timers & opponent status cache
        // after the tab regains visibility (prevents long-sleep drift / stale hospital displays).
        forceHospitalCountdownRefocus: () => {
            try {
                const nowSec = Math.floor(Date.now() / 1000);
                let changed = false;
                // Reconcile medDeals hospital expirations
                if (state.medDeals && typeof state.medDeals === 'object') {
                    for (const [oid, meta] of Object.entries(state.medDeals)) {
                        if (!meta || typeof meta !== 'object') continue;
                        if (meta.activeType === 'status' && typeof meta.hospitalUntil === 'number' && meta.hospitalUntil > 0) {
                            if (meta.hospitalUntil <= nowSec) {
                                // Expired while tab hidden: normalize to Okay
                                meta.prevStatus = meta.prevStatus || 'Hospital';
                                meta.newStatus = 'Okay';
                                meta.hospitalUntil = 0;
                                changed = true;
                            }
                        }
                    }
                    if (changed) {
                        try { state._mutate.setMedDeals({ ...state.medDeals }, { source: 'hospital-refocus' }); }
                        catch(_) { try { storage.set('medDeals', state.medDeals); } catch(_) {} }
                    }
                }
                // Force opponent status widget immediate refresh
                if (state.ui && state.ui.opponentStatusCache) {
                    state.ui.opponentStatusCache.lastFetch = 0; // so next tick refetches
                }
                if (state._opponentStatusStable) {
                    state._opponentStatusStable.lastRendered = 0; // allow immediate rerender
                }
                // Touch any active alert hospital countdowns (simple text recompute)
                const alerts = document.querySelectorAll('.tdm-alert');
                alerts.forEach(el => {
                    const hospMatch = /Hosp\s+(\d+):(\d{2})/.exec(el.textContent || '');
                    if (hospMatch) {
                        // If underlying meta expired, swap to Okay (cheap heuristic)
                        if (/hospital/i.test(el.textContent) || true) {
                            // Derive an opponent id if embedded
                            const idMatch = /(Opp|Target)\s*#?(\d{1,9})/i.exec(el.textContent || '');
                            let expired = false;
                            if (idMatch) {
                                const oid = idMatch[2];
                                const meta = state.medDeals?.[oid];
                                if (meta && meta.hospitalUntil === 0) expired = true;
                                else if (meta && typeof meta.hospitalUntil === 'number' && meta.hospitalUntil < nowSec) expired = true;
                            }
                            if (expired) {
                                try {
                                    el.textContent = el.textContent.replace(/Hosp\s+\d+:\d{2}/, 'Okay');
                                } catch(_) {}
                            }
                        }
                    }
                });
            } catch(_) { /* non-fatal */ }
        },
        startOpponentStatusUpdater: () => {
            if (!state.ui.opponentStatusEl) return;
            if (state.ui.opponentStatusIntervalId) return;

            // Consolidated throttling state
            if (!state._opponentStatusStable) {
                state._opponentStatusStable = { lastCanon: null, lastActivity: null, lastRendered: 0, lastId: null, lastDest: null };
                state._opponentStatusMinRenderIntervalMs = 1200; // min 1.2s between identical renders
                state._opponentStatusForceIntervalMs = 8000; // hard refresh every 8s even if unchanged
                // Canonicalization enhancement: treat Travel 'in <place>' as Abroad.
                // Removed legacy opponent status canonicalization wrapper.
            }

            const getMyActiveDibOpponentId = () => {
                try {
                    if (Array.isArray(state.dibsData)) {
                        const dib = state.dibsData.find(d => d && d.dibsActive && d.userId === state.user.tornId);
                        if (dib?.opponentId) return dib.opponentId;
                    }
                } catch(_) { /* noop */ }
                return null;
            };

            const tick = async () => {
                const dibOppId = getMyActiveDibOpponentId();
                const oppId = dibOppId ? String(dibOppId) : null;
                if (!oppId) {
                    const valueEl = state.ui.opponentStatusValueEl || state.ui.opponentStatusEl.querySelector('.tdm-opponent-status-value');
                    if (valueEl) {
                        valueEl.innerHTML = '<span style="opacity:0.6">No active dib</span>';
                        valueEl.style.color = '#94b9d6';
                    }
                    if (state.ui.opponentStatusEl) ui._orchestrator.setDisplay(state.ui.opponentStatusEl, 'none');
                    state.ui.opponentStatusCache = { lastFetch: 0, untilEpoch: 0, text: '', opponentId: null, canonical: null, activity: null, dest: null, unified: null };
                    return;
                }

                // Try to reuse cached hospital release time for the same opponent
                const nowMs = Date.now();
                if (!state.ui.opponentStatusCache) {
                    state.ui.opponentStatusCache = { lastFetch: 0, untilEpoch: 0, text: '', opponentId: oppId, canonical: null, activity: null, dest: null, unified: null };
                } else if (state.ui.opponentStatusCache.opponentId !== oppId) {
                    state.ui.opponentStatusCache = { lastFetch: 0, untilEpoch: 0, text: '', opponentId: oppId, canonical: null, activity: null, dest: null, unified: null };
                }

                // Prefer seeding from a local active dib if available. This avoids flicker
                // by populating the cache early with name/faction/last-action/activity hints.
                try {
                    if (Array.isArray(state.dibsData) && state.dibsData.length) {
                        const dib = state.dibsData.find(d => String(d.opponentId) === String(oppId) && !!d.dibsActive);
                        if (dib) {
                            // Normalize possible field name variants
                            const name = dib.opponentname || dib.opponentName || dib.opponent || dib.username || null;
                            if (name) state.ui.opponentStatusCache.name = String(name);
                            const opFac = dib.opponentFactionId || dib.opponentFaction || dib.opponentFactionID || null;
                            if (opFac) state.ui.opponentStatusCache.opponentFactionId = String(opFac);

                            // last action timestamp: prefer explicit last_action.lastAction.timestamp fields
                            // Avoid seeding from the dib creation timestamp (dibbedAt/lastActionTimestamp used during optimistic creates)
                            // which would make "Last action" display show the age of the dib rather than the player's real last action.
                            try {
                                // Candidate timestamp sources in descending reliability when present in dibsData
                                const candidateTsRaw = (dib.last_action && (dib.last_action.timestamp || dib.last_action)) || (dib.lastAction && (dib.lastAction.timestamp || dib.lastAction)) || null;
                                let candidateTs = 0;
                                if (candidateTsRaw && typeof candidateTsRaw === 'object' && (candidateTsRaw._seconds || candidateTsRaw.seconds)) {
                                    candidateTs = Number(candidateTsRaw._seconds || candidateTsRaw.seconds || 0) || 0;
                                } else if (typeof candidateTsRaw === 'number') {
                                    candidateTs = candidateTsRaw > 1e12 ? Math.floor(candidateTsRaw / 1000) : Math.floor(candidateTsRaw);
                                }

                                // Compare against dibbedAt (if present). If candidate equals dibbedAt (or is implausibly close)
                                // it's likely the optimistic dib creation time and not a genuine last action — skip it.
                                const dibbedRaw = dib.dibbedAt || dib.dibbed_at || dib.createdAt || dib.created || null;
                                let dibbedTs = 0;
                                if (dibbedRaw && typeof dibbedRaw === 'object' && (dibbedRaw._seconds || dibbedRaw.seconds)) {
                                    dibbedTs = Number(dibbedRaw._seconds || dibbedRaw.seconds || 0) || 0;
                                } else if (typeof dibbedRaw === 'number') {
                                    dibbedTs = dibbedRaw > 1e12 ? Math.floor(dibbedRaw / 1000) : Math.floor(dibbedRaw);
                                }

                                // If candidate exists use it to seed cached lastActionTs. We don't try to
                                // detect optimistic dib creation time here — authoritative sources (status/session)
                                // will be preferred later for rendering.
                                if (candidateTs > 0) {
                                    state.ui.opponentStatusCache.lastActionTs = candidateTs;
                                }
                            } catch (_) { /* non-fatal */ }

                            // activity hint (status at dib)
                            const act = dib.opponentStatusAtDib || dib.opponent_status_at_dib || null;
                            if (act) state.ui.opponentStatusCache.activity = act;
                        }
                    }
                } catch (_) { /* non-fatal */ }
                let canonicalHint = state.ui.opponentStatusCache.canonical || null;
                let activityHint = state.ui.opponentStatusCache.activity || null;
                // If activity hint came from cached dibsData, ensure it's recent enough to trust
                try {
                    const cachedTs = Number(state.ui.opponentStatusCache?.lastActionTs || 0) || 0;
                    if (activityHint && cachedTs) {
                        const nowSec = Math.floor(Date.now()/1000);
                        const age = nowSec - cachedTs;
                        const ACTIVITY_HINT_STALE = 120; // 2 minutes
                        if (age > ACTIVITY_HINT_STALE) activityHint = null;
                    }
                } catch(_) { /* noop */ }
                let destHint = state.ui.opponentStatusCache.dest || null;

                // Only refresh every 10s
                if (nowMs - state.ui.opponentStatusCache.lastFetch >= 10000) {
                    state.ui.opponentStatusCache.lastFetch = nowMs;
                    try {
                        // Prefer cached opponent faction members first. Use seeded opponentFactionId
                        // (from dibsData) if available so lookups succeed earlier and avoid stale fallbacks.
                        const tf = state.tornFactionData || {};
                        const oppFactionId = state.ui.opponentStatusCache?.opponentFactionId || state.lastOpponentFactionId || state?.warData?.opponentId;
                        const entry = oppFactionId ? tf[oppFactionId] : null;
                        let status = null;
                        if (entry?.data?.members) {
                            const arr = Array.isArray(entry.data.members) ? entry.data.members : Object.values(entry.data.members);
                            const m = arr.find(x => String(x.id) === String(oppId));
                            if (m) {
                                // Prefer the precomputed unifiedStatus entry (if present) for canonical/activity
                                // but ensure we attach the faction member's last_action (timestamp + status)
                                // so activity and lastActionTs come from the same snapshot.
                                const unifiedRec = state.unifiedStatus?.[String(oppId)] || null;
                                if (unifiedRec) {
                                    // Build a status-like object from unified + member's last_action if available
                                    status = status || {};
                                    // Use rawState/rawDescription/rawUntil from unified if available, else member.status
                                    if (unifiedRec.rawState) status.state = unifiedRec.rawState;
                                    if (unifiedRec.rawDescription) status.description = unifiedRec.rawDescription;
                                    if (unifiedRec.rawUntil) status.until = unifiedRec.rawUntil;
                                    // Ensure last_action is attached from the member record when possible
                                    if (m.last_action || m.lastAction) {
                                        status.last_action = m.last_action || m.lastAction;
                                        // Also include direct activity hint on cache if present
                                        activityHint = (m.last_action?.status || m.lastAction?.status || activityHint) || activityHint;
                                    }
                                }
                                // If we still don't have a status object, fall back to the member.status
                                if (!status && m.status) {
                                    status = { ...m.status };
                                    if (m.last_action || m.lastAction) status.last_action = m.last_action || m.lastAction;
                                }
                                // Seed the cache with opponent faction id and name if absent
                                try { if (!state.ui.opponentStatusCache.opponentFactionId) state.ui.opponentStatusCache.opponentFactionId = String(m?.faction?.id || m?.faction_id || m?.faction?.faction_id || state.lastOpponentFactionId || ''); } catch(_) {}
                                try { if (!state.ui.opponentStatusCache.name) state.ui.opponentStatusCache.name = m?.name || m?.username || m?.playername || state.ui.opponentStatusCache.name; } catch(_) {}
                            }
                        }
                        if (!status) {
                            // If not found in the primary opponent faction bundle, try to locate the member
                            // in any other loaded faction bundle we have cached. This helps in cases where
                            // we have multiple faction bundles loaded and the member lives in a different one.
                            try {
                                for (const fEntry of Object.values(tf || {})) {
                                    const membersObj2 = fEntry?.data?.members || fEntry?.data?.member || null;
                                    const membersArr2 = membersObj2 ? (Array.isArray(membersObj2) ? membersObj2 : Object.values(membersObj2)) : null;
                                    if (!membersArr2 || !membersArr2.length) continue;
                                    const found = membersArr2.find(x => String(x.id) === String(oppId));
                                    if (found) {
                                        // Use the found member's status and attach last_action if present
                                        if (found.status) status = { ...found.status };
                                        if ((found.last_action || found.lastAction) && status) status.last_action = found.last_action || found.lastAction;
                                        // Seed cache name/opponentFactionId for later usage
                                        try { if (!state.ui.opponentStatusCache.opponentFactionId) state.ui.opponentStatusCache.opponentFactionId = String(found?.faction?.id || found?.faction_id || ''); } catch(_) {}
                                        try { if (!state.ui.opponentStatusCache.name) state.ui.opponentStatusCache.name = found?.name || found?.username || found?.playername || state.ui.opponentStatusCache.name; } catch(_) {}
                                        break;
                                    }
                                }
                            } catch(_) { /* ignore */ }

                            // Optionally refresh opponent faction bundle (members) respecting 10s freshness
                            if (!status && oppFactionId) {
                                try { await api.getTornFaction(state.user.actualTornApiKey, 'members', oppFactionId); } catch (_) {}
                                const e2 = state.tornFactionData?.[oppFactionId];
                                if (e2?.data?.members) {
                                    const arr2 = Array.isArray(e2.data.members) ? e2.data.members : Object.values(e2.data.members);
                                    const m2 = arr2.find(x => String(x.id) === String(oppId));
                                    if (m2?.status) {
                                        status = { ...m2.status };
                                        if (m2.last_action || m2.lastAction) status.last_action = m2.last_action || m2.lastAction;
                                    }
                                }
                            }
                        }
                        if (!status) {
                            // Fallback to cached user status (member-first, 10s TTL)
                            const s = await utils.getUserStatus(oppId);
                            status = s?.raw || (s?.canonical ? { state: s.canonical, until: s.until } : null);
                            if (s?.activity && !activityHint) activityHint = s.activity;
                        }

                        let text = '';
                        let until = 0;
                        let canonicalLocal = null;
                        let destLocal = destHint || null;
                        // activityLocal must come from the same authoritative `status` source when available.
                        // Only fall back to the cached dibs-derived hint if we couldn't obtain a status.
                        let activityLocal = null;
                        let unifiedLocal = null;

                        const stateStrRaw = String(status?.state || '').trim();
                        const stateStr = stateStrRaw;
                        const desc = String(status?.description || '').trim();

                        if (stateStr === 'Hospital') {
                            until = Number(status?.until) || 0;
                            text = desc || 'Hosp';
                            const hospDest = utils.parseHospitalAbroadDestination(desc) || utils.parseUnifiedDestination(desc) || null;
                            if (hospDest) destLocal = hospDest;
                        } else if (stateStr) {
                            text = desc || stateStr;
                            if (stateStr === 'Abroad') {
                                const abroadDest = utils.parseUnifiedDestination(desc) || null;
                                if (abroadDest) destLocal = abroadDest;
                            }
                        } else {
                            text = 'Okay';
                        }

                        const actFromStatus = status?.last_action?.status || status?.lastAction?.status || status?.activity || null;
                        if (actFromStatus) {
                            // Check timestamp for staleness before accepting an activity token.
                            // Prefer a recent last_action timestamp (seconds) else treat activity as stale.
                            let actTs = 0;
                            try {
                                actTs = Number(status?.last_action?.timestamp || status?.lastAction?.timestamp || state.ui.opponentStatusCache?.lastActionTs || 0) || 0;
                                // Normalize ms->s if needed
                                if (actTs > 1e12) actTs = Math.floor(actTs / 1000);
                                else if (actTs > 1e11) actTs = Math.floor(actTs / 1000);
                            } catch (_) { actTs = 0; }
                            const nowSec = Math.floor(Date.now() / 1000);
                            const age = actTs > 0 ? (nowSec - actTs) : Number.POSITIVE_INFINITY;
                            const ACTIVITY_STALE_SECONDS = 120; // 2 minutes
                            if (actTs && Number.isFinite(age) && age <= ACTIVITY_STALE_SECONDS) {
                                activityLocal = actFromStatus;
                            } else {
                                // stale or missing last_action timestamp on the authoritative status ->
                                // do not mix with a dibs-derived activity. Leave activityLocal null.
                                activityLocal = null;
                            }
                        }

                        if (status && stateStr) {
                            try {
                                const payload = { id: oppId, status };
                                if (activityLocal) payload.last_action = { status: activityLocal };
                                const prevUnified = state.ui.opponentStatusCache.unified || null;
                                unifiedLocal = utils.buildUnifiedStatusV2(payload, prevUnified || undefined);
                            } catch(_) {
                                unifiedLocal = null;
                            }
                        }

                        if (unifiedLocal) {
                            canonicalLocal = unifiedLocal.canonical || canonicalLocal;
                            if (unifiedLocal.dest) destLocal = unifiedLocal.dest;
                               // unifiedLocal.activity is coming from the same `status` record; prefer it
                               // (we already validated freshness above), otherwise leave activityLocal as-is.
                               if (unifiedLocal.activity) activityLocal = unifiedLocal.activity;
                        }

                        if (destLocal && /torn\s*(city)?/i.test(destLocal)) destLocal = null;

                        if (stateStr === 'Hospital') {
                            const nonTornDest = destLocal && !/torn\s*(city)?/i.test(destLocal);
                            if (nonTornDest) {
                                if (!canonicalLocal || canonicalLocal === 'Hospital') canonicalLocal = 'HospitalAbroad';
                            } else if (!canonicalLocal) {
                                canonicalLocal = 'Hospital';
                            }
                        }

                        if (!canonicalLocal && stateStr) {
                            const mapState = stateStr.toLowerCase();
                            if (mapState === 'traveling') canonicalLocal = 'Travel';
                            else if (mapState === 'returning') canonicalLocal = 'Returning';
                            else if (mapState === 'abroad') canonicalLocal = 'Abroad';
                            else if (mapState === 'hospitalabroad') canonicalLocal = 'HospitalAbroad';
                            else if (mapState === 'hospital') canonicalLocal = 'Hospital';
                            else if (mapState === 'jail') canonicalLocal = 'Jail';
                            else if (mapState === 'okay') canonicalLocal = 'Okay';
                        }

                        state.ui.opponentStatusCache.untilEpoch = until;
                        state.ui.opponentStatusCache.text = text || 'Okay';
                        state.ui.opponentStatusCache.canonical = canonicalLocal || null;
                        state.ui.opponentStatusCache.activity = activityLocal || null;
                        state.ui.opponentStatusCache.dest = destLocal || null;
                        state.ui.opponentStatusCache.unified = unifiedLocal || null;
                        canonicalHint = state.ui.opponentStatusCache.canonical;
                        activityHint = state.ui.opponentStatusCache.activity;
                        destHint = state.ui.opponentStatusCache.dest;
                    } catch (_) { /* noop */ }
                }

                const until = state.ui.opponentStatusCache.untilEpoch;
                const baseText = state.ui.opponentStatusCache.text || '';
                const cacheEntry = state.session.userStatusCache?.[String(oppId)];
                const unifiedRecCache = state.unifiedStatus?.[String(oppId)] || null;
                // If we don't have a unified record, try to find the member inside any loaded faction bundle
                // so we can construct canonical/activity from the bundle when available.
                let memberFromBundle = null;
                if (!unifiedRecCache && state.tornFactionData) {
                    try {
                        for (const fEntry of Object.values(state.tornFactionData || {})) {
                            const membersObj = fEntry?.data?.members || fEntry?.data?.member || null;
                            const membersArr = membersObj ? (Array.isArray(membersObj) ? membersObj : Object.values(membersObj)) : null;
                            if (!membersArr || !membersArr.length) continue;
                            const mm = membersArr.find(x => String(x?.id) === String(oppId));
                            if (mm) { memberFromBundle = mm; break; }
                        }
                    } catch(_) { memberFromBundle = null; }
                }
                // If we have an until timer it's very likely Hospital — treat as canonical Hospital
                // Prefer unified records (faction bundle) first, then bundle member-derived status if we found it,
                // then session cache, and finally any cached hint.
                let canon = unifiedRecCache?.canonical || (memberFromBundle ? (utils.buildUnifiedStatusV2(memberFromBundle)?.canonical || null) : null) || cacheEntry?.canonical || canonicalHint || (until > 0 ? 'Hospital' : (/(Hosp)/i.test(baseText) ? 'Hospital' : null));
                let activity = unifiedRecCache?.activity || (memberFromBundle ? (memberFromBundle?.last_action?.status || memberFromBundle?.lastAction?.status || null) : null) || cacheEntry?.activity || activityHint || null;
                let dest = unifiedRecCache?.dest || (memberFromBundle ? (utils.buildUnifiedStatusV2(memberFromBundle)?.dest || null) : null) || destHint || null;

                const stable = state._opponentStatusStable;
                const now = Date.now();

                const computeCountdown = () => {
                    if (until > 0) {
                        const nowSec = Math.floor(Date.now() / 1000);
                        const rem = until - nowSec;
                        if (rem > 0) {
                            const mm = Math.floor(rem / 60);
                            const ss = String(rem % 60).padStart(2, '0');
                            return `${mm}:${ss}`;
                        }
                        return null;
                    }
                    const match = baseText.match(/(\d+:\d{2})/);
                    return match ? match[1] : null;
                };

                const countdown = computeCountdown();
                if (!dest && state.ui.opponentStatusCache.unified?.dest) dest = state.ui.opponentStatusCache.unified.dest;
                if (dest && /torn\s*(city)?/i.test(dest)) dest = null;
                const destAbbrev = dest ? utils.abbrevDest(dest) : '';
                const actToken = activity ? utils.abbrevActivity(activity) : null;

                // Prefer canonical abbreviation when it conveys more useful information
                // than a generic baseText like 'Okay'. If canonical is meaningful
                // (e.g., 'Abroad' -> 'Abrd'), prefer it over baseText.
                const canonAbbrev = canon ? utils.abbrevStatus(canon) : null;
                const fallbackText = (canonAbbrev && String(canonAbbrev).toLowerCase() !== 'okay') ? (canonAbbrev || baseText) : (baseText || (canonAbbrev || 'Okay'));
                // Helper to avoid duplicating similar short tokens (e.g. 'Ok' vs 'Okay')
                const shouldAppendActivity = (base, token) => {
                    if (!base || !token) return false;
                    // normalize into meaningful word tokens, drop common connectors like 'now'
                    const stop = new Set(['now','last','action','the','a','an','to','in','on','of']);
                    const normalizeTokens = (txt) => String(txt||'').toLowerCase().replace(/[^a-z\s]/g, ' ').split(/\s+/).filter(Boolean).map(w => (w==='ok' ? 'okay' : w)).filter(w => !stop.has(w));
                    const bTokens = normalizeTokens(base);
                    const tTokens = normalizeTokens(token);
                    if (!bTokens.length || !tTokens.length) return true;
                    // If every token in token is present or contained in a base token, treat as redundant
                    const allContained = tTokens.every(t => bTokens.some(b => b === t || b.includes(t) || t.includes(b) || (b.startsWith('ok') && t.startsWith('ok'))));
                    return !allContained;
                };
                let composed = '';

                if (canon === 'HospitalAbroad') {
                    const baseLabel = (destAbbrev ? `${destAbbrev} Hosp` : 'Abrd Hosp').trim();
                    const label = countdown ? `${baseLabel} ${countdown}` : baseLabel;
                    // For hospital states prefer the hospital label/countdown only — activity token isn't useful here
                    composed = label;
                } else if (canon === 'Hospital') {
                    const baseLabel = 'Hosp';
                    const label = countdown ? `${baseLabel} ${countdown}` : baseLabel;
                    // For hospital states prefer the hospital label/countdown only — activity token isn't useful here
                    composed = label;
                } else if (canon === 'Abroad') {
                    const baseLabel = destAbbrev || 'Abrd';
                    composed = (actToken && shouldAppendActivity(baseLabel, actToken)) ? `${baseLabel} ${actToken}` : baseLabel;
                } else {
                    let baseLabel = fallbackText;
                    if (canon) {
                        const abbrev = utils.abbrevStatus(canon);
                        if (abbrev) baseLabel = abbrev;
                    }
                    if (/^Hosp/i.test(baseLabel) && countdown) {
                        baseLabel = `${baseLabel.replace(/\s+\d+:\d{2}.*/, '')} ${countdown}`.trim();
                    }
                    composed = actToken ? `${baseLabel} ${actToken}` : baseLabel;
                }

                if (!composed || !composed.trim()) {
                    composed = fallbackText || 'Okay';
                }
                composed = composed.replace(/\s+/g, ' ').trim();

                const changed = (
                    (canon && (canon !== stable.lastCanon || activity !== stable.lastActivity || oppId !== stable.lastId || (dest || null) !== (stable.lastDest || null))) ||
                    (!canon && stable.lastCanon != null)
                );
                const since = now - (stable.lastRendered || 0);
                if (changed || since >= state._opponentStatusMinRenderIntervalMs || since >= state._opponentStatusForceIntervalMs) {
                    stable.lastCanon = canon || null;
                    stable.lastActivity = activity || null;
                    stable.lastRendered = now;
                    stable.lastId = oppId;
                    stable.lastDest = dest || null;
                    const href = `https://www.torn.com/loader.php?sid=attack&user2ID=${oppId}`;
                    const valueEl = state.ui.opponentStatusValueEl || state.ui.opponentStatusEl.querySelector('.tdm-opponent-status-value');
                    if (valueEl) {
                        // Compute opponent display name (prefer seeded dibsData, then snapshot, faction members, session cache, DOM)
                        // Persist chosen name into opponentStatusCache to avoid rapid source-flip flicker.
                        let oppNameFull = '';

                        // Simplified name resolution: prefer seeded dibs name, then session cache, then snapshot, else fallback to ID.
                        try {
                            const seeded = state.ui.opponentStatusCache?.name;
                            const sessionName = state.session?.userStatusCache?.[oppId]?.name;
                            const snap = state.rankedWarTableSnapshot?.[oppId];
                            const snapName = snap ? (snap.name || snap.username || null) : null;
                            oppNameFull = String(seeded || sessionName || snapName || `ID ${oppId}` || `ID ${oppId}`);
                        } catch (_) {
                            oppNameFull = `ID ${oppId}`;
                        }
                        // Store chosen name on the opponentStatusCache so subsequent renders are stable
                        try {
                            state.ui.opponentStatusCache = state.ui.opponentStatusCache || {};
                            state.ui.opponentStatusCache.name = oppNameFull;
                        } catch(_) {}
                        // Expose sanitized name on the wrapper element to make it available to other handlers
                        try {
                            if (state.ui.opponentStatusEl && state.ui.opponentStatusEl.dataset) state.ui.opponentStatusEl.dataset.opponentName = oppNameFull;
                        } catch(_) {}

                        // Limit display name to 12 chars for badge
                        let displayName = String(oppNameFull || '').trim();
                        const displayShort = displayName.length > 12 ? displayName.slice(0, 12) + '\u2026' : displayName;

                        // Attempt to find last-action timestamp from various caches.
                        // Prefer authoritative sources (status from API), then faction member bundle (tornFactionData / unified),
                        // then session cache, snapshot, and finally seeded dibsData cache.
                        let lastActionTs = 0;
                        try {
                            lastActionTs = Number(status?.last_action?.timestamp || status?.lastAction?.timestamp || 0) || 0;
                            // If not present on status, try to read from the faction bundle member record
                            if (!lastActionTs) {
                                const oppFacId = state.ui.opponentStatusCache?.opponentFactionId || state.lastOpponentFactionId || state?.warData?.opponentFactionId || null;
                                if (oppFacId) {
                                    const bundle = state.tornFactionData?.[oppFacId];
                                    const membersObj = bundle?.data?.members || bundle?.data?.member || null;
                                    const membersArr = membersObj ? (Array.isArray(membersObj) ? membersObj : Object.values(membersObj)) : null;
                                    const mm = membersArr ? membersArr.find(x => String(x.id) === String(oppId)) : null;
                                    if (mm) lastActionTs = Number(mm?.last_action?.timestamp || mm?.lastAction?.timestamp || 0) || 0;
                                }
                            }
                            // session cache then snapshot then seeded cache
                            if (!lastActionTs) lastActionTs = Number(state.session?.userStatusCache?.[oppId]?.last_action?.timestamp || state.session?.userStatusCache?.[oppId]?.lastAction?.timestamp || 0) || 0;
                            if (!lastActionTs) lastActionTs = Number(state.rankedWarTableSnapshot?.[oppId]?.last_action?.timestamp || state.rankedWarTableSnapshot?.[oppId]?.lastAction?.timestamp || 0) || 0;
                            if (!lastActionTs) lastActionTs = Number(state.ui.opponentStatusCache?.lastActionTs || 0) || 0;
                        } catch(_) { lastActionTs = Number(state.ui.opponentStatusCache?.lastActionTs || 0) || 0; }
                        // Normalize ms -> seconds if needed
                        if (lastActionTs > 1e12) lastActionTs = Math.floor(lastActionTs / 1000);
                        else if (lastActionTs > 1e11) lastActionTs = Math.floor(lastActionTs / 1000);

                        // Build badge nodes: username (limited), last-action inline, then composed status
                        try {
                            valueEl.innerHTML = '';
                            const nameNode = document.createElement('span');
                            nameNode.className = 'tdm-opponent-name';
                            nameNode.textContent = displayShort;
                            nameNode.title = displayName;
                            nameNode.style.fontWeight = '700';
                                    // Keep tight spacing — reduce name->status gap
                                    nameNode.style.marginRight = '4px';
                            nameNode.style.maxWidth = '140px';
                            nameNode.style.overflow = 'hidden';
                            nameNode.style.textOverflow = 'ellipsis';
                            nameNode.style.whiteSpace = 'nowrap';

                            const lastEl = document.createElement('span');
                            lastEl.className = 'tdm-last-action-inline';
                            // Always include the element to keep badge layout stable and
                            // provide a consistent click target. Store timestamp or '0'.
                            if (lastActionTs && Number.isFinite(lastActionTs) && lastActionTs > 0) {
                                lastEl.setAttribute('data-last-ts', String(Math.floor(lastActionTs)));
                                try { lastEl.textContent = utils.formatAgoShort(lastActionTs) || ''; } catch(_) { lastEl.textContent = ''; }
                                try { lastEl.title = `Last Action: ${utils.formatAgoLong ? utils.formatAgoLong(lastActionTs) : ''}`; } catch(_) { lastEl.title = ''; }
                            } else {
                                lastEl.setAttribute('data-last-ts', '0');
                                lastEl.textContent = '';
                                lastEl.title = '';
                            }
                            lastEl.style.display = 'inline-block';
                            lastEl.style.marginLeft = '4px';
                            lastEl.style.marginRight = '4px';
                            lastEl.style.cursor = 'pointer';
                            // Click should send last action details to chat (if supported)
                            try {
                                lastEl.onclick = (e) => {
                                    try { e.preventDefault(); e.stopPropagation(); } catch(_) {}
                                    const tsAttr = lastEl.getAttribute('data-last-ts');
                                    const short = tsAttr && Number(tsAttr) > 0 ? (utils.formatAgoShort ? utils.formatAgoShort(Number(tsAttr)) : '') : '';
                                    const statusLabelForChat = activity || (canon ? utils.abbrevStatus(canon) : null);
                                    ui.sendLastActionToChat && ui.sendLastActionToChat(oppId, oppNameFull, statusLabelForChat || null, short || null);
                                };
                            } catch(_) {}
                            // Attempt to attach color class based on cached data
                            try {
                                // Try several places for an authoritative member object so last-action color can be applied.
                                const mem = Array.isArray(state.factionMembers) ? state.factionMembers.find(m => String(m.id) === String(oppId)) : null;
                                if (mem && utils.addLastActionStatusColor) {
                                    utils.addLastActionStatusColor(lastEl, mem);
                                } else {
                                    // Fallback: check seeded opponent faction bundle (tornFactionData) for member details
                                    try {
                                        const oppFacId = state.ui.opponentStatusCache?.opponentFactionId || null;
                                        if (oppFacId) {
                                            const bundle = state.tornFactionData?.[oppFacId];
                                            const membersObj = bundle?.data?.members || bundle?.data?.member || null;
                                            const membersArr = membersObj ? (Array.isArray(membersObj) ? membersObj : Object.values(membersObj)) : null;
                                            const m2 = membersArr ? membersArr.find(x => String(x.id) === String(oppId)) : null;
                                            if (m2 && utils.addLastActionStatusColor) utils.addLastActionStatusColor(lastEl, m2);
                                        }
                                    } catch(_) { /* ignore */ }
                                }
                            } catch(_) {}

                            const statusNode = document.createElement('span');
                            statusNode.className = 'tdm-opponent-status-text';
                            statusNode.style.color = '#9dd6ff';
                            statusNode.textContent = composed;

                            valueEl.appendChild(nameNode);
                            valueEl.appendChild(lastEl);
                            valueEl.appendChild(statusNode);

                            // When any part of the opponent badge is clicked open a detailed dib popup
                            try {
                                if (state.ui.opponentStatusEl) {
                                    state.ui.opponentStatusEl.onclick = (e) => {
                                        try { e.preventDefault(); e.stopPropagation(); } catch(_) {}
                                        ui.openOpponentPopup && ui.openOpponentPopup(oppId, oppNameFull);
                                    };
                                }
                            } catch(_) {}
                        } catch (ex) {
                            // Fallback to original simple display
                            valueEl.innerHTML = `<span style="color:#9dd6ff;text-decoration:underline;">${composed}</span>`;
                        }
                    }
                    ui._orchestrator.setDisplay(state.ui.opponentStatusEl, 'inline-flex');
                }
            };

            tick();
            state.ui.opponentStatusIntervalId = utils.registerInterval(setInterval(tick, 1000));
        },
        _ensureRankedWarRowInlineOrder: (row, subrow) => {
            if (!row || !subrow) return;
            try {
                const notesBtnEl = subrow.querySelector('.note-button');
                let la = subrow.querySelector('.tdm-last-action-inline');
                if (!la) {
                    la = utils.createElement('span', { className: 'tdm-last-action-inline', textContent: '' });
                    try {
                        la.style.cursor = 'pointer';
                        la.dataset.tdmLastClick = '1';
                        la.onclick = (e) => {
                            try {
                                e.preventDefault();
                                e.stopPropagation();
                                const link = row.querySelector('a[href*="profiles.php?XID="]');
                                const id = link ? (link.href.match(/XID=(\d+)/) || [])[1] : null;
                                const name = link ? (link.textContent || '').trim() : null;
                                const ts = Number(la.getAttribute('data-last-ts') || 0);
                                const short = utils.formatAgoShort(ts) || '';
                                const status = (row && utils.getActivityStatusFromRow) ? utils.getActivityStatusFromRow(row) : null;
                                ui.sendLastActionToChat(id, name, status, short);
                            } catch (_) {}
                        };
                    } catch (_) {}
                    if (notesBtnEl) notesBtnEl.insertAdjacentElement('afterend', la);
                    else subrow.appendChild(la);
                } else if (notesBtnEl && la.previousElementSibling !== notesBtnEl) {
                    notesBtnEl.insertAdjacentElement('afterend', la);
                }
                if (la) {
                    let eta = subrow.querySelector('.tdm-travel-eta');
                    if (!eta) {
                        eta = utils.createElement('span', { className: 'tdm-travel-eta', textContent: '' });
                        la.insertAdjacentElement('afterend', eta);
                    } else if (eta.previousElementSibling !== la) {
                        la.insertAdjacentElement('afterend', eta);
                    }
                }
                const lastActionRow = row.querySelector('.last-action-row');
                if (lastActionRow) {
                    try {
                        const raw = (lastActionRow.textContent || '').trim();
                        const cleaned = raw.replace(/^Last Action:\s*/i, '').trim();
                        if (cleaned) row.setAttribute('data-last-action', cleaned);
                        lastActionRow.remove();
                    } catch (_) {}
                }
            } catch (_) {}
        },
        _ensureRankedWarRowSkeleton: (row, isCurrentTableOurFaction) => {
            if (!row) return { subrow: null, existed: false };
            let subrow = row.querySelector('.dibs-notes-subrow');
            const existed = !!subrow;
            if (!subrow) subrow = utils.createElement('div', { className: 'dibs-notes-subrow' });
            if (!isCurrentTableOurFaction) {
                if (!subrow.querySelector('.dibs-btn')) {
                    subrow.appendChild(utils.createElement('button', { className: 'btn dibs-btn btn-dibs-inactive tdm-soften', textContent: 'Dibs' }));
                }
                if (!subrow.querySelector('.btn-med-deal-default')) {
                    subrow.appendChild(utils.createElement('button', { className: 'btn btn-med-deal-default tdm-soften', textContent: 'Med Deal', style: { display: 'none' } }));
                }
                utils.ensureNoteButton(subrow);
                let retalContainer = subrow.querySelector('.tdm-retal-container');
                if (!retalContainer) {
                    // Use a non-growing flex container constrained to the subrow so it
                    // can't push past the row on narrow viewports (PDA). Ensure it can
                    // shrink when space is tight by allowing min-width:0.
                    retalContainer = utils.createElement('div', { className: 'tdm-retal-container', style: { flex: '0 0 auto', display: 'flex', justifyContent: 'flex-end', maxWidth: '100%', minWidth: '0', boxSizing: 'border-box' } });
                    subrow.appendChild(retalContainer);
                }
                if (!retalContainer.querySelector('.retal-btn')) {
                    retalContainer.appendChild(utils.createElement('button', { className: 'btn retal-btn tdm-soften', textContent: 'Retal', style: { display: 'none' } }));
                }
            } else {
                utils.ensureNoteButton(subrow, { disabled: true });
            }
            if (!existed) row.appendChild(subrow);
            ui._ensureRankedWarRowInlineOrder(row, subrow);
            return { subrow, existed };
        },
        _buildRankedWarMemberLookup: (data) => {
            const lookup = new Map();
            if (!data) return lookup;
            const members = data.members || data.faction?.members || null;
            if (!members) return lookup;
            const memberArray = Array.isArray(members) ? members : Object.values(members);
            memberArray.forEach(member => {
                const id = String(member?.id || member?.player_id || member?.user_id || member?.player?.id || '');
                if (id) lookup.set(id, member);
            });
            return lookup;
        },
        _updateRankedWarNotesButton: (subrow, opponentId, opponentName) => {
            if (!subrow || !opponentId) return;
            const notesBtn = subrow.querySelector('.note-button');
            if (!notesBtn) return;
            const userNote = (state.userNotes && typeof state.userNotes === 'object') ? state.userNotes[opponentId] : null;
            const noteText = userNote?.noteContent || '';
            utils.updateNoteButtonState(notesBtn, noteText);
            if (notesBtn.title !== noteText) notesBtn.title = noteText;
            if (notesBtn.getAttribute('aria-label') !== noteText) notesBtn.setAttribute('aria-label', noteText);
            notesBtn.onclick = (e) => ui.openNoteModal(opponentId, opponentName, noteText, e.currentTarget);
            notesBtn.disabled = false;
        },
        _updateRankedWarDibsAndMedDeal: (subrow, opponentId, opponentName) => {
            if (!subrow || !opponentId) return;
            const dibsBtn = subrow.querySelector('.dibs-btn');
            if (dibsBtn && utils.updateDibsButton) utils.updateDibsButton(dibsBtn, opponentId, opponentName);
            let medDealBtn = subrow.querySelector('.btn-med-deal-default');
            if (!medDealBtn) {
                try {
                    medDealBtn = utils.createElement('button', { className: 'btn btn-med-deal-default tdm-soften', textContent: 'Med Deal', style: { display: 'none' } });
                    const dibsBtnForInsert = subrow.querySelector('.dibs-btn');
                    const notesBtnForInsert = subrow.querySelector('.note-button');
                    if (dibsBtnForInsert && dibsBtnForInsert.nextSibling) {
                        dibsBtnForInsert.parentNode.insertBefore(medDealBtn, dibsBtnForInsert.nextSibling);
                    } else if (notesBtnForInsert) {
                        notesBtnForInsert.parentNode.insertBefore(medDealBtn, notesBtnForInsert);
                    } else {
                        subrow.appendChild(medDealBtn);
                    }
                } catch (_) {}
            }
            if (medDealBtn && utils.updateMedDealButton) utils.updateMedDealButton(medDealBtn, opponentId, opponentName);
        },
        _updateRankedWarRetalButton: (row, subrow, opponentId, opponentName) => {
            if (!row || !subrow || !opponentId) return;
            const retalBtn = subrow.querySelector('.retal-btn');
            if (!retalBtn) return;
            const meta = state.rankedWarChangeMeta[opponentId];
            const hasAlert = !!(meta && (meta.activeType || meta.pendingText));
            if (!hasAlert) ui.updateRetaliationButton(retalBtn, opponentId, opponentName);
        },
        _updateRankedWarTravelStatus: (row, subrow, opponentId, opponentName) => {
            if (!row || !subrow || !opponentId) return;
            try {
                const unified = state.unifiedStatus?.[opponentId] || null;
                let etaEl = subrow.querySelector('.tdm-travel-eta');
                if (!unified || !unified.canonical) {
                    if (etaEl && etaEl.parentNode) etaEl.parentNode.removeChild(etaEl);
                    return;
                }
                const canon = unified.canonical;
                const dest = unified.dest || '';
                const mins = unified.durationMins || 0;
                const plane = unified.plane || '';
                const prevUnified = state._previousUnifiedStatus?.[opponentId] || null;
                if (plane) { utils.ensureBusinessUpgrade(unified, unified.id, { prevRec: prevUnified }); }
                const confidence = unified.confidence || 'LOW';
                const arrivalMs = Number(unified.arrivalMs) || 0;
                const isTravel = canon === 'Travel';
                const isReturning = canon === 'Returning';
                const isAbroad = canon === 'Abroad';
                const isHospAbroad = canon === 'HospitalAbroad';
                const isLanded = unified.landedTornRecent || unified.landedAbroadRecent || unified.landedGrace;
                if (!(isTravel || isReturning || isAbroad || isHospAbroad || isLanded)) {
                    if (etaEl && etaEl.parentNode) etaEl.parentNode.removeChild(etaEl);
                    return;
                }
                if (!etaEl) {
                    etaEl = document.createElement('span');
                    etaEl.className = 'tdm-travel-eta';
                    try { etaEl.dataset.oppId = String(opponentId); } catch (_) {}
                    const inline = subrow.querySelector('.tdm-last-action-inline');
                    if (inline && inline.nextSibling) inline.parentNode.insertBefore(etaEl, inline.nextSibling);
                    else if (inline) inline.parentNode.appendChild(etaEl);
                    else subrow.appendChild(etaEl);
                }
                let line = '';
                if (isAbroad) {
                    line = `Abroad${dest ? ' ' + utils.abbrevDest(dest) : ''}`;
                } else if (isHospAbroad) {
                    line = `HospAbroad${dest ? ' ' + utils.abbrevDest(dest) : ''}`;
                } else if (isLanded) {
                    const arrow = unified.isreturn ? '\u2190' : '\u2192';
                    line = `${arrow} ${utils.abbrevDest(dest) || dest} Landed`;
                } else if (isTravel || isReturning) {
                    const arrow = isReturning || unified.isreturn ? '\u2190' : '\u2192';
                    if (confidence === 'HIGH' && arrivalMs) {
                        const now = Date.now();
                        let remMs = arrivalMs - now;
                        if (remMs < 0) remMs = 0;
                        const remTotalMin = Math.ceil(remMs / 60000);
                        const rh = Math.floor(remTotalMin / 60);
                        const rm = remTotalMin % 60;
                        const remStr = rh > 0 ? `${rh}h${rm ? ' ' + rm + 'm' : ''}` : `${remTotalMin}m`;
                        line = `${arrow} ${utils.abbrevDest(dest) || dest} LAND~${remStr}`.trim();
                    } else {
                        const durStr = mins > 0 ? (Math.floor(mins / 60) ? `${Math.floor(mins / 60)}h${mins % 60 ? ' ' + (mins % 60) + 'm' : ''}` : `${mins}m`) : '?';
                        line = `${arrow} ${utils.abbrevDest(dest) || dest} dur. ${durStr}`.trim();
                    }
                }
                line = line.replace(/\s+/g, ' ').trim();
                if (etaEl.textContent !== line) etaEl.textContent = line;
                if (confidence === 'HIGH') {
                    etaEl.classList.remove('tdm-travel-lowconf');
                    etaEl.classList.add('tdm-travel-conf');
                } else {
                    etaEl.classList.remove('tdm-travel-conf');
                    etaEl.classList.add('tdm-travel-lowconf');
                }
                let tooltip = '';
                if (isLanded && unified.landedatms) {
                    const landedLocal = new Date(unified.landedatms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
                    tooltip = `${line} * ${plane} * Landed ${landedLocal}`;
                } else if (mins > 0) {
                    tooltip = `${line} * ${plane} * Flight ${mins}m`;
                } else {
                    tooltip = line;
                }
                if (etaEl.title !== tooltip) etaEl.title = tooltip;
                try {
                    const shouldHaveHandler = !(isAbroad || isHospAbroad) && (confidence === 'HIGH') && (isTravel || isReturning) && !!arrivalMs;
                    if (shouldHaveHandler) {
                        if (!etaEl._tdmTravelClickHandler) {
                            etaEl._tdmTravelClickHandler = function (ev) {
                                try {
                                    if (isAbroad || isHospAbroad) return;
                                    if (confidence !== 'HIGH' || !(isTravel || isReturning) || !arrivalMs) return;
                                    const minsLeft = Math.max(0, Math.round((arrivalMs - Date.now()) / 60000));
                                    const flightStr = minsLeft > 0 ? `${minsLeft}m left` : 'Landed';
                                    const etaUtc = arrivalMs ? new Date(arrivalMs).toUTCString().split(' ')[4].slice(0, 5) : '';
                                    const newContent = `Travel: ${utils.abbrevDest(dest) || dest} - ${flightStr}${etaUtc ? ' (ETA UTC ' + etaUtc + ')' : ''}`;
                                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                                    const resolvedOpponentName = userLink?.textContent?.trim() || opponentName || '';
                                    ui.openNoteModal(opponentId, resolvedOpponentName, newContent, etaEl);
                                } catch (e) { tdmlogger('error', '[travel eta click err]', e); }
                            };
                            etaEl.addEventListener('click', etaEl._tdmTravelClickHandler);
                        }
                        etaEl._tdmTravelClickBound = true;
                    } else {
                        if (etaEl._tdmTravelClickHandler) {
                            try { etaEl.removeEventListener('click', etaEl._tdmTravelClickHandler); } catch (_) {}
                            etaEl._tdmTravelClickHandler = null;
                        }
                        etaEl._tdmTravelClickBound = false;
                    }
                } catch (_) { /* ignore travel handler attach/remove issues */ }
            } catch (_) { /* ignore travel icon errors */ }
        },
        processRankedWarTables: async () => {
            const factionTables = state.dom.rankwarfactionTables;
            if (!factionTables || !factionTables.length) {
                return;
            }
            tdmlogger('debug', '[processRankedWarTables] found tables', {factionTablesCount: factionTables.length});

            state.script.hasProcessedRankedWarTables = true;
            const ourFactionName = state.factionPull?.name;

            const opponentFactionLink = state.dom.rankBox?.querySelector('.nameWp___EX6gT .opponentFactionName___vhESM');
            const currentFactionLink = state.dom.rankBox?.querySelector('.nameWp___EX6gT .currentFactionName___eq7n8');

            const opponentFactionName = opponentFactionLink ? opponentFactionLink.textContent.trim() : 'N/A';
            const currentFactionName = currentFactionLink ? currentFactionLink.textContent.trim() : 'N/A';

            const overlayScopes = [];
            const favoritesEnabled = ui._areRankedWarFavoritesEnabled();
            const ffscouterIds = new Set();

            factionTables.forEach(tableContainer => {
                overlayScopes.push(tableContainer);
                let isCurrentTableOurFaction = false;
                const tableFactionId = ui._resolveRankedWarTableFactionId(tableContainer);
                if (tableFactionId != null && tableContainer?.dataset) {
                    tableContainer.dataset.factionId = String(tableFactionId);
                }
                if (tableContainer.classList.contains('left')) {
                    if (ourFactionName && opponentFactionName === ourFactionName) isCurrentTableOurFaction = true;
                } else if (tableContainer.classList.contains('right')) {
                    if (ourFactionName && currentFactionName === ourFactionName) isCurrentTableOurFaction = true;
                }

                tableContainer.querySelectorAll('.members-list > li, .members-cont > li').forEach(row => {
                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                    if (!userLink) {
                        tdmlogger('debug', '[processRankedWarTables] Row skipped: no userLink', row);
                        return;
                    }
                    // Extract user ID
                    const match = userLink.href.match(/XID=(\d+)/);
                    const userId = match ? match[1] : null;
                    if (userId) ffscouterIds.add(userId);
                    // tdmlogger('debug', '[processRankedWarTables] Processing row', { userId, isCurrentTableOurFaction });

                    // (Add a log if timeline/status update would be called here)
                    // Example: if (shouldUpdateTimeline) { tdmlogger('debug', '[processRankedWarTables] Would update timeline for', userId); }
                    const skeleton = ui._ensureRankedWarRowSkeleton(row, isCurrentTableOurFaction);
                    const subrow = skeleton.subrow;
                    if (!subrow) {
                        tdmlogger('debug', '[processRankedWarTables] Row skipped: skeleton failed', { userId });
                        return;
                    }
                    if (favoritesEnabled && userId && tableFactionId) {
                        try { ui._ensureRankedWarFavoriteHeart(subrow, userId, tableFactionId); } catch(_) {}
                    }
                });
                try { ui._ensureRankedWarDefaultSort(tableContainer); } catch(_) {}
                try { ui._ensureRankedWarSortObserver(tableContainer); } catch(_) {}
            });
            try { ui._refreshRankedWarSortForAll(); } catch(_) {}
            if (favoritesEnabled) {
                try { ui.requestRankedWarFavoriteRepin?.({ delays: [0, 150, 400] }); } catch(_) {}
            }
            
            // Trigger FFScouter fetch for collected IDs
            if (ffscouterIds.size > 0) {
                api.fetchFFScouterStats?.(Array.from(ffscouterIds));
            }

            ui._ensureRankedWarOverlaySortHandlers(state.dom.rankwarContainer || document);
            ui._ensureRankedWarSortHandlers(state.dom.rankwarContainer || document);
            try { ui.queueLevelOverlayRefresh({ scopes: overlayScopes, reason: 'ranked-war-process' }); } catch(_) {}
                ui._renderEpoch.schedule(); // Gate updates through render epochs for stability
            // Ensure observers/tickers are running
            try { ui.ensureRankedWarAlertObserver(); } catch(_) {}
            try { ui.ensureActivityAlertTicker && ui.ensureActivityAlertTicker(); } catch(_) {}
            try { ui.ensureLastActionTicker && ui.ensureLastActionTicker(); } catch(_) {}

            // Seed faction bundles for both visible factions on this page exactly once per render
            try {
                const vis = utils.getVisibleRankedWarFactionIds?.();
                const ids = (vis && vis.ids) ? vis.ids.filter(Boolean) : [];
                if (ids.length) {
                    const now = Date.now();
                    const last = state.script.lastProcessTableBundleMs || 0;
                    const cadence = state.script.factionBundleRefreshMs || config.DEFAULT_FACTION_BUNDLE_REFRESH_MS;
                    if (!last || (now - last) >= Math.max(config.MIN_GLOBAL_FETCH_INTERVAL_MS || 2000, cadence / 3)) {
                        state.script.lastProcessTableBundleMs = now;
                        api.refreshFactionBundles?.({ pageFactionIds: ids, source: 'rankedWarTable' }).catch(e => {
                            try { tdmlogger('warn', `[processRankedWarTables] bundle refresh failed: ${e}`); } catch(_) {}
                        });
                    }
                }
            } catch(_) { /* noop */ }
        },
        processFactionPageMembers: async (container) => {
            const members = container.querySelectorAll('.table-body > li.table-row');
            if (!members.length) {  
                return; 
            }

            const factionId = ui._getFactionIdForMembersList(container);
            if (factionId && container && container.dataset) container.dataset.factionId = String(factionId);

            const ffscouterIds = new Set();
            const headerUl = container.querySelector('.table-header');
            if (headerUl) {
                // Ensure a small member-index header exists (for rows that include a per-row index element)
                if (!headerUl.querySelector('#col-header-member-index')) {
                    // Try to insert before the member header if present, otherwise append
                    const memberEl = headerUl.querySelector('.member');
                    const idxEl = utils.createElement('li', { id: 'col-header-member-index', className: 'table-cell table-header-column', innerHTML: `<span></span>` });
                    idxEl.style.minWidth = '3%'; idxEl.style.maxWidth = '3%';
                    if (memberEl) headerUl.insertBefore(idxEl, memberEl);
                    else headerUl.appendChild(idxEl);
                }

                // Ensure separate headers for Dibs/Deals and Notes
                if (!headerUl.querySelector('#col-header-dibs-deals')) {
                    headerUl.appendChild(utils.createElement('li', { id: 'col-header-dibs-deals', className: 'table-cell table-header-column', innerHTML: `<span>Dibs/Deals</span>` }));
                }
                if (!headerUl.querySelector('#col-header-notes')) {
                    headerUl.appendChild(utils.createElement('li', { id: 'col-header-notes', className: 'table-cell table-header-column', innerHTML: `<span>Notes</span>` }));
                }
            }

            members.forEach(row => {
                // Ensure status cell has marker class for refresh
                const statusCell = row.querySelector('.status');
                if (statusCell && !statusCell.classList.contains('tdm-status-cell')) {
                    statusCell.classList.add('tdm-status-cell');
                }

                let dibsDealsContainer = row.querySelector('.tdm-dibs-deals-container');
                if (!dibsDealsContainer) {
                    dibsDealsContainer = utils.createElement('div', { className: 'table-cell tdm-dibs-deals-container torn-divider divider-vertical' });
                    const dibsCell = utils.createElement('div', { className: 'dibs-cell' });
                    dibsCell.appendChild(utils.createElement('button', { className: 'btn dibs-button tdm-soften', textContent: 'Dibs' }));
                    dibsCell.appendChild(utils.createElement('button', { className: 'btn med-deal-button tdm-soften', style: { display: 'none' }, textContent: 'Med Deal' }));
                    dibsDealsContainer.appendChild(dibsCell);
                    row.appendChild(dibsDealsContainer);
                }

                let notesContainer = row.querySelector('.tdm-notes-container');
                if (!notesContainer) {
                    notesContainer = utils.createElement('div', { className: 'table-cell tdm-notes-container torn-divider divider-vertical' });
                    const notesCell = utils.createElement('div', { className: 'notes-cell' });
                    utils.ensureNoteButton(notesCell, { withLabel: true });
                    notesContainer.appendChild(notesCell);
                    row.appendChild(notesContainer);
                }

                try { ui._ensureMembersListFavoriteHeart(row, factionId); } catch(_) {}
                
                const pid = ui._extractPlayerIdFromRow(row);
                if (pid) ffscouterIds.add(pid);
            });
            
            if (ffscouterIds.size > 0) {
                api.fetchFFScouterStats?.(Array.from(ffscouterIds));
            }

            ui._ensureMembersListOverlaySortHandlers(container);
            // Coalesce follow-up UI updates via epoch to avoid immediate flush
            ui._renderEpochMembers.schedule();
            try { ui.queueLevelOverlayRefresh({ scope: container, reason: 'faction-members-process' }); } catch(_) {}
            try { ui._pinFavoritesInFactionList(container); } catch(_) {}
        },

        // Batches per-row updates and consumes recent-window timeline samples to render stable signals
        updateRankedWarUI: async () => {
            try {
                const now = Date.now();
                const metricsRoot = state.metrics || (state.metrics = {});
                const tracker = metricsRoot.uiRankedWarUi || (metricsRoot.uiRankedWarUi = { total: 0, perMinute: 0, recent: [], history: [] });
                tracker.total = (tracker.total || 0) + 1;
                tracker.lastTs = now;
                tracker.recent = Array.isArray(tracker.recent) ? tracker.recent.filter(ts => (now - ts) <= 60000) : [];
                tracker.recent.push(now);
                tracker.perMinute = tracker.recent.length;
                tracker.history = Array.isArray(tracker.history) ? tracker.history : [];
                tracker.history.push(now);
                if (tracker.history.length > 5) tracker.history.splice(0, tracker.history.length - 5);
                if (state?.debug?.cadence && (!tracker._lastLog || (now - tracker._lastLog) >= 30000)) {
                    tracker._lastLog = now;
                    try { tdmlogger('debug', '[ui.updateRankedWarUI]', { total: tracker.total, perMinute: tracker.perMinute }); } catch (_) {}
                }
            } catch (_) { /* non-fatal metrics capture */ }
            const factionTables = state.dom.rankwarfactionTables;
            if (!factionTables) { 
                return; 
                }
            const favoritesEnabled = ui._areRankedWarFavoritesEnabled();
            ui._ensureRankedWarOverlaySortHandlers(state.dom.rankwarContainer || document);
            ui._ensureRankedWarSortHandlers(state.dom.rankwarContainer || document);
            const ourFactionName = state.factionPull?.name;
            const opponentFactionLink = state.dom.rankBox?.querySelector('.nameWp___EX6gT .opponentFactionName___vhESM');
            const currentFactionLink = state.dom.rankBox?.querySelector('.nameWp___EX6gT .currentFactionName___eq7n8');
            const opponentFactionName = opponentFactionLink ? opponentFactionLink.textContent.trim() : 'N/A';
            const currentFactionName = currentFactionLink ? currentFactionLink.textContent.trim() : 'N/A';

            // Determine warKey once for this render (used to fetch timeline windows)
            let warKey = null;
            try {
                const pageKeyRaw = utils.getCurrentWarPageKey?.();
                warKey = pageKeyRaw ? pageKeyRaw.replace(/[^a-z0-9_\-]/gi, '_') : (state.lastRankWar?.id ? `id_${String(state.lastRankWar.id)}` : null);
            } catch(_) { warKey = state.lastRankWar?.id ? `id_${String(state.lastRankWar.id)}` : null; }

            factionTables.forEach((tableContainer) => {
                let isCurrentTableOurFaction = false;
                if (tableContainer.classList.contains('left')) {
                    if (ourFactionName && opponentFactionName === ourFactionName) isCurrentTableOurFaction = true;
                } else if (tableContainer.classList.contains('right')) {
                    if (ourFactionName && currentFactionName === ourFactionName) isCurrentTableOurFaction = true;
                }
                const tableFactionId = ui._resolveRankedWarTableFactionId(tableContainer);
                if (tableFactionId != null && tableContainer?.dataset) {
                    tableContainer.dataset.factionId = String(tableFactionId);
                }

                // Pass 0: Ensure skeleton subrows/placeholders for ALL rows (lightweight, avoids incomplete rows)
                try {
                    const allRows = Array.from(tableContainer.querySelectorAll('.members-list > li, .members-cont > li'));
                    allRows.forEach(row => {
                        const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                        if (!userLink) return;
                        ui._ensureRankedWarRowSkeleton(row, isCurrentTableOurFaction);
                    });
                } catch(_) { /* skeleton ensure pass */ }

                // Collect opponent IDs visible in this container to compute a recent-window stable snapshot in one pass
                let idsInContainer = [];
                try {
                    idsInContainer = Array.from(tableContainer.querySelectorAll('.members-list > li a[href*="profiles.php?XID="], .members-cont > li a[href*="profiles.php?XID="]'))
                        .map(a => (a.href.match(/XID=(\d+)/) || [null, null])[1])
                        .filter(Boolean);
                    // Ensure uniqueness
                    idsInContainer = Array.from(new Set(idsInContainer));
                } catch(_) { idsInContainer = []; }

                let stableById = {};
                try {
                    if (idsInContainer.length && ui._tdmConfig?.timeline?.enabled) {
                        const windowMap = {};
                        stableById = ui._recentWindow?.deriveStableMap(windowMap) || {};
                    }
                } catch(_) { stableById = {}; }

                const tf = state.tornFactionData || {};
                const visibleFactionIds = utils.getVisibleRankedWarFactionIds?.() || {};
                const leftData = visibleFactionIds.leftId ? tf[visibleFactionIds.leftId]?.data : null;
                const rightData = visibleFactionIds.rightId ? tf[visibleFactionIds.rightId]?.data : null;
                const leftMemberLookup = ui._buildRankedWarMemberLookup(leftData);
                const rightMemberLookup = ui._buildRankedWarMemberLookup(rightData);

                // Process all rows in this container
                const rows = Array.from(tableContainer.querySelectorAll('.members-list > li, .members-cont > li'));
                const cap = 120; // safety incase more than 100 members
                let processed = 0;

                rows.forEach(row => {
                    if (processed >= cap) return;
                    processed++;
                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                    if (!userLink) return;
                    const opponentId = userLink.href.match(/XID=(\d+)/)[1];
                    const opponentName = utils.sanitizePlayerName(userLink.textContent, opponentId, { fallbackPrefix: 'Opponent' });
                    let subrow = row.querySelector('.dibs-notes-subrow');
                    if (!subrow) return;
                    if (favoritesEnabled && tableFactionId) {
                        try { ui._ensureRankedWarFavoriteHeart(subrow, opponentId, tableFactionId); } catch(_) {}
                    }

                    // Apply compact last-action/status tag derived from timeline recent-window to reduce flicker
                    try {
                        const stable = stableById?.[opponentId] || null;
                        if (stable && stable.tag) {
                            if (row.getAttribute('data-last-action') !== stable.tag) {
                                row.setAttribute('data-last-action', stable.tag);
                            }
                        }
                        if (row.hasAttribute('data-refreshing')) row.removeAttribute('data-refreshing');
                    } catch(_) { /* non-fatal */ }

                    ui._updateRankedWarNotesButton(subrow, opponentId, opponentName);

                    if (!isCurrentTableOurFaction) {
                        ui._updateRankedWarDibsAndMedDeal(subrow, opponentId, opponentName);
                        ui._updateRankedWarRetalButton(row, subrow, opponentId, opponentName);
                    }

                    // --- Our inline last-action renderer (colored) START ---
                    try {
                        const inline = row.querySelector('.tdm-last-action-inline');
                        if (inline) {
                            const containerIsLeft = !!row.closest('.tab-menu-cont.left, .members-cont.left');
                            const primaryLookup = containerIsLeft ? leftMemberLookup : rightMemberLookup;
                            const secondaryLookup = containerIsLeft ? rightMemberLookup : leftMemberLookup;
                            const member = primaryLookup.get(opponentId) || secondaryLookup.get(opponentId) || null;
                            const la = member?.last_action || member?.lastAction || null;
                            const relProvided = (la && (la.relative || la.last_action_relative || la.rel)) ? (la.relative || la.last_action_relative || la.rel) : '';
                            const ts = Number(la?.timestamp || 0);
                            const stat = String(la?.status || '').trim();
                            let cls = 'tdm-last-action-inline';
                            if (/online/i.test(stat)) cls += ' tdm-la-online';
                            else if (/idle/i.test(stat)) cls += ' tdm-la-idle';
                            else if (/offline/i.test(stat)) cls += ' tdm-la-offline';
                            if (inline.className !== cls) inline.className = cls;
                            const relStable = Number.isFinite(ts) && ts > 0 ? utils.formatAgoShort(ts) : relProvided;
                            const txt = relStable || '';
                            if (inline.textContent !== txt) inline.textContent = txt;
                            if (Number.isFinite(ts) && ts > 0) {
                                const prevTs = inline.getAttribute('data-last-ts');
                                const tsStr = String(ts);
                                if (prevTs !== tsStr) inline.setAttribute('data-last-ts', tsStr);
                                const full = `Last Action: ${utils.formatAgoFull(ts)}`;
                                if (inline.title !== full) inline.title = full;
                            } else {
                                inline.removeAttribute('data-last-ts');
                                if (inline.title) inline.title = '';
                            }
                        }
                    } catch(_) { /* non-fatal */ }
                    // --- Our inline last-action renderer (colored) END ---
                    // Refactored: Use only unified status V2 record for travel/status/ETA rendering
                    ui._updateRankedWarTravelStatus(row, subrow, opponentId, opponentName);
                });
                try { ui._ensureRankedWarDefaultSort(tableContainer); } catch(_) {}
                try { ui._ensureRankedWarSortObserver(tableContainer); } catch(_) {}
            });
            if (favoritesEnabled) {
                try { ui.requestRankedWarFavoriteRepin?.({ delays: [0, 200, 600] }); } catch(_) {}
            }
            try {
                // ui.queueLevelOverlayRefresh({ scopes: Array.from(factionTables), reason: 'ranked-war-update' });
            } catch(_) { /* optional overlay refresh */ }
        },

        // TDM Timeline: simplified config and KV adapter
        _tdmConfig: {
            timeline: (() => {
                const cfg = { enabled: true, sampleEveryMs: 10 * 1000, horizonMs: 36 * 60 * 60 * 1000, maxEntriesPerOpponent: 300 };
                try {
                    const hasStorage = (typeof storage !== 'undefined' && storage && typeof storage.get === 'function');
                    // Timeline override removed – always treated as disabled for legacy sampler
                    const override = false; // disables legacy timeline sampling path
                    cfg.enabled = !!override;
                    // Sampling override
                    try {
                        const sampleMs = null; // legacy sample interval retired
                        if (Number.isFinite(sampleMs) && sampleMs > 0) cfg.sampleEveryMs = Number(sampleMs);
                    } catch(_) { /* keep default */ }
                } catch(_) { /* keep defaults */ }
                return cfg;
            })()
        },

        _kv: {
            _db: null,
            _idbSupported: (typeof window !== 'undefined') && !!window.indexedDB,
            _quotaExceeded: false,
            // Approximate size accounting & eviction
            _approxBytes: 0, // running estimate of stored value bytes
            _keySizes: new Map(), // key -> size bytes
            _sizeScanComplete: false,
            _evicting: false,
            _lastUsageUpdate: 0,
            _usageUpdateDebounceMs: 1500,
            // Per-op log threshold (raise default to reduce noise)
            _logThreshMs: (function(){ try { return Number(storage.get('idbLogThresholdMs', 120)) || 120; } catch(_) { return 120; } })(),
            _largeThreshBytes: (function(){ try { return Number(storage.get('idbLargeItemBytes', 200*1024)) || (200*1024); } catch(_) { return 200*1024; } })(),
            // Log rate limiting: max lines per window
            _logWindowMs: (function(){ try { return Number(storage.get('idbLogWindowMs', 2000)) || 2000; } catch(_) { return 2000; } })(),
            _logMaxPerWindow: (function(){ try { return Number(storage.get('idbLogMaxPerWindow', 50)) || 50; } catch(_) { return 50; } })(),
            _logWinStart: 0,
            _logWinCount: 0,
            // Debug auto-off after N ms when state.debug.idbLogs is true
            _debugOnSince: 0,
            _debugAutoOffMs: (function(){ try { return Number(storage.get('idbDebugAutoOffMs', 30000)) || 30000; } catch(_) { return 30000; } })(),
            // listKeys cache (per-prefix TTL)
            _listCache: new Map(), // prefix -> { ts, keys }
            _listCacheTtlMs: (function(){ try { return Number(storage.get('idbListCacheTtlMs', 60000)) || 60000; } catch(_) { return 60000; } })(),
            // In-memory read-through cache
            _memCache: new Map(), // key -> { ts, v }
            _memTtlMs: (function(){ try { return Number(storage.get('idbMemCacheTtlMs', 3000)) || 3000; } catch(_) { return 3000; } })(),
            // Write dedupe and coalescing
            _lastWriteHash: new Map(), // key -> stringified value
            _pendingSets: new Map(), // key -> { v, timer, promise, resolve }
            _coalesceMs: (function(){ try { return Number(storage.get('idbCoalesceMs', 150)) || 150; } catch(_) { return 150; } })(),
            _coalesceEnabled: (function(){ try { return storage.get('idbCoalesceEnabled', true) !== false; } catch(_) { return true; } })(),
            // Delete the entire tdm-store IndexedDB database
            async deleteDb() {
                return new Promise((resolve) => {
                    if (!this._idbSupported) return resolve(false);
                    try {
                        // Close the db if open
                        if (this._db) {
                            try { this._db.close(); } catch(_) {}
                            this._db = null;
                        }
                        const req = window.indexedDB.deleteDatabase('tdm-store');
                        req.onsuccess = () => {
                            this._sizeScanComplete = false;
                            this._approxBytes = 0;
                            this._keySizes = new Map();
                            this._listCache = new Map();
                            this._memCache = new Map();
                            resolve(true);
                        };
                        req.onerror = () => resolve(false);
                        req.onblocked = () => resolve(false);
                    } catch(_) { resolve(false); }
                });
            },
            _sizeOf(v) {
                try {
                    if (v == null) return 0;
                    if (typeof v === 'string') return v.length;
                    // estimate JSON size cost
                    return JSON.stringify(v).length;
                } catch(_) { return 0; }
            },
            _maxBytes() {
                try {
                    const mb = Number(storage.get('tdmIdbMaxSizeMB', '')) || 0; // blank / 0 => unlimited (browser managed)
                    if (!mb) return 0;
                    return mb * 1024 * 1024;
                } catch(_) { return 0; }
            },
            _scheduleUsageUpdate() {
                try {
                    const now = Date.now();
                    if (now - this._lastUsageUpdate < this._usageUpdateDebounceMs) return;
                    this._lastUsageUpdate = now;
                    setTimeout(() => { try { ui.updateIdbUsageLine && ui.updateIdbUsageLine(); } catch(_) {} }, 250);
                } catch(_) {}
            },
            async _ensureSizeScan() {
                if (this._sizeScanComplete) return;
                try {
                    const db = await this._openDb();
                    if (!db) { this._sizeScanComplete = true; return; }
                    const keys = await this.listKeys('');
                    let total = 0;
                    // Limit deep scan time – only scan first 200 keys synchronously
                    const slice = keys.slice(0, 200);
                    for (const k of slice) {
                        try {
                            const v = await this.getItem(k);
                            const sz = this._sizeOf(v);
                            this._keySizes.set(k, sz);
                            total += sz;
                        } catch(_) {}
                    }
                    this._approxBytes = total; // partial (will refine as keys touched)
                    this._sizeScanComplete = true;
                } catch(_) { this._sizeScanComplete = true; }
            },
            _updateApproxOnSet(key, val, knownSz) {
                try {
                    const sz = (typeof knownSz === 'number') ? knownSz : this._sizeOf(val);
                    const prev = this._keySizes.get(key) || 0;
                    this._keySizes.set(key, sz);
                    this._approxBytes += (sz - prev);
                    if (this._approxBytes < 0) this._approxBytes = 0;
                    this._scheduleUsageUpdate();
                } catch(_) {}
            },
            _updateApproxOnRemove(key) {
                try {
                    const prev = this._keySizes.get(key) || 0;
                    if (prev) this._approxBytes = Math.max(0, this._approxBytes - prev);
                    this._keySizes.delete(key);
                    this._scheduleUsageUpdate();
                } catch(_) {}
            },
            async _maybeEvict() {
                try {
                    const maxBytes = this._maxBytes();
                    if (!maxBytes || this._evicting) return;
                    if (this._approxBytes <= maxBytes) return;
                    this._evicting = true;
                    await this._ensureSizeScan();
                    const keys = await this.listKeys('');
                    const items = [];
                    for (const k of keys) {
                        let sz = this._keySizes.get(k);
                        if (sz == null) {
                            try { const v = await this.getItem(k); sz = this._sizeOf(v); this._keySizes.set(k, sz); } catch(_) { sz = 0; }
                        }
                        items.push({ k, sz });
                    }
                    // Sort largest first (greedy remove big blobs) – could refine with LRU later
                    items.sort((a,b) => b.sz - a.sz);
                    const target = Math.floor(maxBytes * 0.9); // leave headroom
                    let removed = 0, removedBytes = 0;
                    for (const it of items) {
                        if (this._approxBytes <= target) break;
                        try { await this.removeItem(it.k); removed++; removedBytes += it.sz; } catch(_) {}
                    }
                    try { tdmlogger('warn', '[IDB] Evicted ${removed} item(s) (~${(removedBytes/1024).toFixed(1)} KB) to honor max ${Math.round(maxBytes/1024/1024)} MB'); } catch(_) {}
                } catch(e) { try { tdmlogger('warn', '[IDB] eviction error', e); } catch(_) {} }
                finally { this._evicting = false; this._scheduleUsageUpdate(); }
            },
            _isDebugOn() {
                try { return !!(state && state.debug && state.debug.idbLogs); } catch(_) { return false; }
            },
            _isSlowLogEnabled() {
                try { return !!(typeof storage !== 'undefined' && storage && storage.get('idbSlowLogsEnabled', false)); } catch(_) { return false; }
            },
            _maybeAutoDisableDebug() {
                try {
                    if (!this._isDebugOn()) { this._debugOnSince = 0; return; }
                    const now = Date.now();
                    if (!this._debugOnSince) this._debugOnSince = now;
                    if (now - this._debugOnSince > this._debugAutoOffMs) {
                        // Turn off and persist flag off if used
                        try { state.debug.idbLogs = false; } catch(_) {}
                        try { window.localStorage && window.localStorage.setItem('tdm.debugIdbLogs', 'false'); } catch(_) {}
                        this._debugOnSince = 0;
                        try { tdmlogger('warn', '[IDB] auto-disabled verbose IDB logs after timeout'); } catch(_) {}
                    }
                } catch(_) { /* noop */ }
            },
            _shouldPerOpLog(dt) {
                try {
                    const verbose = this._isDebugOn();
                    const slow = this._isSlowLogEnabled();
                    this._maybeAutoDisableDebug();
                    // Only log when explicitly enabled (verbose or slow). Threshold-based logging is disabled by default.
                    if (!verbose && !slow) return false;
                    if (!verbose && slow && !(dt >= this._logThreshMs)) return false;
                    const now = Date.now();
                    if (!this._logWinStart || (now - this._logWinStart) > this._logWindowMs) {
                        this._logWinStart = now; this._logWinCount = 0;
                    }
                    if (this._logWinCount >= this._logMaxPerWindow) return false;
                    this._logWinCount += 1;
                    return true;
                } catch(_) { return false; }
            },
            _openDb() {
                if (!this._idbSupported) return Promise.resolve(null);
                if (this._db) return Promise.resolve(this._db);
                const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                return new Promise((resolve) => {
                    try {
                        const req = window.indexedDB.open('tdm-store', 1);
                        req.onupgradeneeded = (e) => {
                            const db = e.target.result;
                            if (!db.objectStoreNames.contains('kv')) db.createObjectStore('kv');
                        };
                        req.onsuccess = (e) => {
                            this._db = e.target.result;
                            try { ui._stats.idbOps.open += 1; } catch(_) {}
                            const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                            const dt = t1 - t0;
                            if (this._shouldPerOpLog(dt * 0.34)) { tdmlogger('log', `[IDB] open ok in ${dt.toFixed(1)} ms`); }
                            resolve(this._db);
                        };
                        req.onerror = () => {
                            const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                            const dt = t1 - t0;
                            tdmlogger('warn', `[IDB] open failed in ${dt.toFixed(1)} ms`);
                            resolve(null);
                        };
                    } catch(_) { resolve(null); }
                });
            },
            async getItem(key) {
                const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                // In-memory cache
                try {
                    const mc = this._memCache.get(key);
                    if (mc && (Date.now() - mc.ts) < this._memTtlMs) {
                        return mc.v;
                    }
                } catch(_) {}
                try {
                    const db = await this._openDb();
                    if (!db) {
                        try { return window.localStorage.getItem(key); } catch(_) { return null; }
                    }
                    return await new Promise((resolve) => {
                        try {
                            const tx = db.transaction(['kv'], 'readonly');
                            const store = tx.objectStore('kv');
                            const req = store.get(key);
                            req.onsuccess = (e) => {
                                const val = e.target.result ?? null;
                                try {
                                    ui._stats.idbOps.get += 1;
                                    const sz = this._sizeOf(val);
                                    ui._stats.idbBytes.read += sz;
                                    const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                                    const dt = t1 - t0;
                                    if (this._shouldPerOpLog(dt)) { tdmlogger('log', `[IDB] get ${key} in ${dt.toFixed(1)} ms, bytes=${sz}`); }
                                    const warnLarge = this._isDebugOn() || (typeof storage !== 'undefined' && storage && storage.get('idbWarnLarge', false));
                                    if (warnLarge && sz >= this._largeThreshBytes) tdmlogger('warn', `[IDB][large-read] ${key} bytes=${sz}`);
                                    ui._stats.maybeLogIdbSummary();
                                } catch(_) {}
                                try { this._memCache.set(key, { ts: Date.now(), v: val }); } catch(_) {}
                                resolve(val);
                            };
                            req.onerror = () => {
                                const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                                const dt = t1 - t0;
                                tdmlogger('warn', `[IDB] get ${key} failed in ${dt.toFixed(1)} ms`);
                                resolve(null);
                            };
                        } catch(_) { resolve(null); }
                    });
                } catch(_) { return null; }
            },
            async setItem(key, value) {
                // Update in-memory cache immediately to keep readers consistent
                try { this._memCache.set(key, { ts: Date.now(), v: value }); } catch(_) {}
                const doCommit = async (val) => {
                    const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                    try {
                        const db = await this._openDb();
                        if (!db) {
                            try {
                                const vStr = (typeof val === 'string') ? val : JSON.stringify(val);
                                // Dedup localStorage writes
                                const last = this._lastWriteHash.get(key);
                                if (last === vStr) return;
                                window.localStorage.setItem(key, vStr);
                                this._lastWriteHash.set(key, vStr);
                                try { ui._stats.idbOps.set += 1; ui._stats.idbBytes.write += this._sizeOf(vStr); } catch(_) {}
                            } catch(err) { this._quotaExceeded = true; }
                            return;
                        }
                        await new Promise((resolve) => {
                            try {
                                // Dedup IndexedDB writes by stringifying for hash only
                                let vStr = null;
                                try { vStr = (typeof val === 'string') ? val : JSON.stringify(val); } catch(_) { vStr = null; }
                                const last = this._lastWriteHash.get(key);
                                if (vStr && last === vStr) { resolve(); return; }
                                const tx = db.transaction(['kv'], 'readwrite');
                                const store = tx.objectStore('kv');
                                const req = store.put(val, key);
                                req.onsuccess = () => {
                                    try {
                                        if (vStr) this._lastWriteHash.set(key, vStr);
                                        ui._stats.idbOps.set += 1;
                                        const sz = this._sizeOf(val);
                                        ui._stats.idbBytes.write += sz;
                                        const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                                        const dt = t1 - t0;
                                        if (this._shouldPerOpLog(dt)) { tdmlogger('log', `[IDB] set ${key} in ${dt.toFixed(1)} ms, bytes=${sz}`); }
                                        const warnLarge = this._isDebugOn() || (typeof storage !== 'undefined' && storage && storage.get('idbWarnLarge', false));
                                        if (warnLarge && sz >= this._largeThreshBytes) tdmlogger('warn', `[IDB][large-write] ${key} bytes=${sz}`);
                                        ui._stats.maybeLogIdbSummary();
                                        // size accounting & eviction
                                        this._updateApproxOnSet(key, val, sz);
                                        this._maybeEvict();
                                    } catch(_) {}
                                    resolve();
                                };
                                req.onerror = () => { this._quotaExceeded = true; resolve(); };
                            } catch(_) { resolve(); }
                        });
                    } catch(_) { /* no-op */ }
                };
                // Coalesce rapid successive writes per key
                if (this._coalesceEnabled && this._coalesceMs > 0) {
                    const prev = this._pendingSets.get(key);
                    if (prev) {
                        // update value and reset timer
                        prev.v = value;
                        utils.unregisterTimeout(prev.timer);
                        prev.timer = utils.registerTimeout(setTimeout(async () => {
                            try { await doCommit(prev.v); prev.resolve(); } finally { this._pendingSets.delete(key); }
                        }, this._coalesceMs));
                        return prev.promise;
                    }
                    let resolvePromise;
                    const p = new Promise((res) => { resolvePromise = res; });
                    const timer = utils.registerTimeout(setTimeout(async () => {
                        try { await doCommit(value); resolvePromise(); } finally { this._pendingSets.delete(key); }
                    }, this._coalesceMs));
                    this._pendingSets.set(key, { v: value, timer, promise: p, resolve: resolvePromise });
                    return p;
                }
                // No coalescing
                return doCommit(value);
            },
            async removeItem(key) {
                const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                try {
                    const db = await this._openDb();
                    if (!db) {
                        try { window.localStorage.removeItem(key); } catch(_) {}
                        return;
                    }
                    await new Promise((resolve) => {
                        try {
                            const tx = db.transaction(['kv'], 'readwrite');
                            const store = tx.objectStore('kv');
                            const req = store.delete(key);
                            req.onsuccess = () => {
                                try {
                                    ui._stats.idbOps.remove += 1;
                                    const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                                    const dt = t1 - t0;
                                    if (this._shouldPerOpLog(dt)) { tdmlogger('log', `[IDB] remove ${key} in ${dt.toFixed(1)} ms`); }
                                    ui._stats.maybeLogIdbSummary();
                                    this._updateApproxOnRemove(key);
                                } catch(_) {}
                                try { this._memCache.delete(key); this._lastWriteHash.delete(key); } catch(_) {}
                                resolve();
                            };
                            req.onerror = () => resolve();
                        } catch(_) { resolve(); }
                    });
                } catch(_) { /* noop */ }
            },
            async listKeys(prefix = '') {
                const keys = [];
                const t0 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                try {
                    // Cache hit?
                    try {
                        const c = this._listCache.get(prefix);
                        if (c && (Date.now() - c.ts) < this._listCacheTtlMs) {
                            // Return a shallow copy to avoid accidental mutation
                            return [...c.keys];
                        }
                    } catch(_) {}
                    const db = await this._openDb();
                    if (!db) {
                        try {
                            for (let i = 0; i < window.localStorage.length; i++) {
                                const k = window.localStorage.key(i);
                                if (!k) continue;
                                if (!prefix || k.startsWith(prefix)) keys.push(k);
                            }
                        } catch(_) {}
                        try { ui._stats.idbOps.list += 1; } catch(_) {}
                        const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                        const dt = t1 - t0;
                        if (this._shouldPerOpLog(dt)) { tdmlogger('log', `[IDB] listKeys(ls) prefix='${prefix}' in ${dt.toFixed(1)} ms, count=${keys.length}`); }
                        ui._stats.maybeLogIdbSummary();
                        try { this._listCache.set(prefix, { ts: Date.now(), keys: [...keys] }); } catch(_) {}
                        return keys;
                    }
                    await new Promise((resolve) => {
                        try {
                            const tx = db.transaction(['kv'], 'readonly');
                            const store = tx.objectStore('kv');
                            // Prefer getAllKeys when available, else fallback to cursor
                            if (store.getAllKeys) {
                                const req = store.getAllKeys();
                                req.onsuccess = (e) => {
                                    const all = e.target.result || [];
                                    for (const k of all) {
                                        if (!prefix || String(k).startsWith(prefix)) keys.push(String(k));
                                    }
                                    try { ui._stats.idbOps.list += 1; } catch(_) {}
                                    resolve();
                                };
                                req.onerror = () => resolve();
                            } else {
                                const req = store.openCursor();
                                req.onsuccess = (e) => {
                                    const cursor = e.target.result;
                                    if (cursor) {
                                        const k = String(cursor.key);
                                        if (!prefix || k.startsWith(prefix)) keys.push(k);
                                        cursor.continue();
                                    } else {
                                        try { ui._stats.idbOps.list += 1; } catch(_) {}
                                        resolve();
                                    }
                                };
                                req.onerror = () => resolve();
                            }
                        } catch(_) { resolve(); }
                    });
                    const t1 = (typeof performance !== 'undefined' && performance.now) ? performance.now() : Date.now();
                    const dt = t1 - t0;
                    if (this._shouldPerOpLog(dt)) { tdmlogger('log', `[IDB] listKeys prefix='${prefix}' in ${dt.toFixed(1)} ms, count=${keys.length}`); }
                    ui._stats.maybeLogIdbSummary();
                    try { this._listCache.set(prefix, { ts: Date.now(), keys: [...keys] }); } catch(_) {}
                } catch(_) { /* swallow */ }
                return keys;
            },
            async removeByPrefix(prefix) {
                try {
                    const keys = await this.listKeys(prefix);
                    for (const k of keys) {
                        await this.removeItem(k);
                    }
                    try { this._listCache.delete(prefix); } catch(_) {}
                } catch(_) { /* noop */ }
            }
        },

        // Lightweight persistence helper using localStorage with TTL and optional war scoping
        _persist: {
            set(key, value, opts = {}) {
                try {
                    const payload = {
                        v: value,
                        ts: Date.now(),
                        warId: opts.warId ?? null,
                    };
                    window.localStorage.setItem(key, JSON.stringify(payload));
                } catch(_) { /* ignore quota */ }
            },
            get(key, opts = {}) {
                try {
                    const raw = window.localStorage.getItem(key);
                    if (!raw) return null;
                    const payload = JSON.parse(raw);
                    if (opts.warId != null && payload && payload.warId != null && String(payload.warId) !== String(opts.warId)) {
                        return null; // scoped to a different war
                    }
                    if (opts.maxAgeMs != null && payload && typeof payload.ts === 'number') {
                        const age = Date.now() - payload.ts;
                        if (age > opts.maxAgeMs) return null;
                    }
                    return payload ? payload.v : null;
                } catch(_) { return null; }
            },
            remove(key) {
                try { window.localStorage.removeItem(key); } catch(_) {}
            }
        },

        // Lightweight in-memory stats (opt-in via storage flag) for timeline writes
        _stats: {
            tl2Writes: 0,
            tl2Bytes: 0,
            // New: IndexedDB op stats
            idbOps: { get: 0, set: 0, remove: 0, list: 0, open: 0 },
            idbBytes: { read: 0, write: 0 },
            _lastSummary: 0,
            maybeLogIdbSummary() {
                try {
                    const now = Date.now();
                    // Summarize at most every 30s
                    if (now - (this._lastSummary || 0) < 60000) return;
                    this._lastSummary = now;
                    // Only log summaries if verbose debug is on, or explicit flag enabled
                    try {
                        const verbose = !!(state && state.debug && state.debug.idbLogs);
                        const enabled = (typeof storage !== 'undefined' && storage) ? !!storage.get('idbSummaryEnabled', false) : false;
                        if (!verbose && !enabled) return;
                        // Suppress when tab hidden or not on ranked war page
                        if (document.hidden || !(state && state.page && state.page.isRankedWarPage)) return;
                    } catch(_) {}
                    const ops = this.idbOps || {};
                    const bytes = this.idbBytes || {};
                    const totalOps = (ops.get||0)+(ops.set||0)+(ops.remove||0)+(ops.list||0)+(ops.open||0);
                    if (!totalOps) return;
                    tdmlogger('debug', `[IDB][summary]`, { ops, bytes });
                    // Reset running counters to avoid unbounded growth; keep open count cumulative
                    this.idbOps.get = 0; this.idbOps.set = 0; this.idbOps.remove = 0; this.idbOps.list = 0;
                    this.idbBytes.read = 0; this.idbBytes.write = 0;
                } catch(_) { /* noop */ }
            }
        },

        // Timeline sampler: collects light-weight status/activity snapshots per opponent
        // Timeline logic is now fully unified; legacy tracking, segment writing, and status schema removed.
        _timeline: {
            // Only unified status records and canonical status mapping are used.
            async updateUnifiedStatusRecord({ id, stateStr, descStr, kv, now }) {
                // Centralized status update logic; see above for details.
                // ...implementation unchanged...
            },
            async readEntries({ id, kv }) {
                // Return current unified status snapshot for compatibility.
                try {
                    const rec = await kv.getItem(`tdm.tl2.status_unified.id_${String(id)}`);
                    if (!rec) return [];
                    return [{ t: rec.updated || rec.ct1 || Date.now(), sc: rec.canonical || '', a: '', s: 'u' }];
                } catch(_) { return []; }
            },
            async getRecentWindow({ warKey, ids, kv, windowMs }) {
                const out = {};
                for (const id of ids) {
                    out[id] = await this.readEntries({ id, kv });
                }
                return out;
            },
            async getStateAt({ id, kv }) {
                try {
                    const rec = await kv.getItem(`tdm.tl2.status_unified.id_${String(id)}`);
                    if (!rec) return { sc: '', a: '', laTs: 0 };
                    return { sc: rec.canonical || '', a: '', laTs: 0 };
                } catch(_) { return { sc: '', a: '', laTs: 0 }; }
            }
        },
        
        // --- Render Orchestrator: batch DOM updates to avoid flicker and animation reset ---
        _orchestrator: {
            _pending: new Map(),
            _scheduled: false,
            applyOrQueue(el, props) {
                if (!el || !props) return;
                const prev = this._pending.get(el) || {};
                this._pending.set(el, { ...prev, ...props });
                if (!this._scheduled) {
                    this._scheduled = true;
                    const raf = (typeof window !== 'undefined' && window.requestAnimationFrame) ? window.requestAnimationFrame.bind(window) : null;
                    (raf || setTimeout)(() => this.flush(), 0);
                }
            },
            _apply(el, props) {
                try {
                    if ('display' in props && el.style.display !== props.display) el.style.display = props.display;
                    if ('disabled' in props && el.disabled !== props.disabled) el.disabled = props.disabled;
                    if ('className' in props && el.className !== props.className) el.className = props.className;
                    if ('textContent' in props && el.textContent !== props.textContent) el.textContent = props.textContent;
                    if ('innerHTML' in props && el.innerHTML !== props.innerHTML) el.innerHTML = props.innerHTML;
                    if ('title' in props && el.title !== props.title) el.title = props.title;
                    if ('style' in props && props.style && typeof props.style === 'object') {
                        for (const [k, v] of Object.entries(props.style)) {
                            if (el.style[k] !== v) el.style[k] = v;
                        }
                    }
                } catch(_) { /* noop */ }
            },
            flush() {
                this._scheduled = false;
                const entries = Array.from(this._pending.entries());
                this._pending.clear();
                for (const [el, props] of entries) {
                    this._apply(el, props);
                }
            },
            setText(el, text) { this.applyOrQueue(el, { textContent: String(text ?? '') }); },
            setClass(el, className) { this.applyOrQueue(el, { className }); },
            setTitle(el, title) { this.applyOrQueue(el, { title: String(title ?? '') }); },
            setDisplay(el, display) { this.applyOrQueue(el, { display }); },
            setDisabled(el, disabled) { this.applyOrQueue(el, { disabled: !!disabled }); },
            setStyle(el, styleObj) { this.applyOrQueue(el, { style: styleObj || {} }); },
            setHtml(el, html) { this.applyOrQueue(el, { innerHTML: String(html ?? '') }); }
        },

        // --- Recent-window consumers: produce stable, flicker-resistant row signals ---
        _recentWindow: {
            // Given a map id => [{t, sc, a, s}], compute a stable representation per id
            deriveStableMap(windowMap) {
                const out = {};
                if (!windowMap || typeof windowMap !== 'object') return out;
                const now = Date.now();
                for (const [id, entries] of Object.entries(windowMap)) {
                    if (!Array.isArray(entries) || entries.length === 0) continue;
                    // Sort by time ascending just in case; guard against large arrays
                    const arr = entries.slice(-50).sort((x,y) => (x.t||0) - (y.t||0));
                    const last = arr[arr.length - 1];
                    // Confidence: prefer RW samples over API within a short window
                    // Also enforce a minimum age for changes (debounce) to avoid transient blips
                    const ageMs = now - (last.t || 0);
                    const recent = arr.filter(e => (now - (e.t||0)) <= 5000);
                    const hasRecentRw = recent.some(e => e.s === 'rw');
                    const hasRecentApiOnly = recent.length > 0 && !hasRecentRw;
                    // Determine blip: if only API changed in the last 3s and disagrees with prior RW sample
                    let blip = false;
                    if (hasRecentApiOnly) {
                        const lastRw = [...arr].reverse().find(e => e.s === 'rw');
                        if (lastRw && (lastRw.sc !== last.sc || lastRw.a !== last.a) && ((now - lastRw.t) <= 3000)) blip = true;
                    }
                    // Derive a compact tag, e.g., "Hosp 3:12", "Abroad·Fly", "Okay", "Jail"
                    let tag = '';
                    const sc = last.sc || '';
                    const a = last.a || '';
                    // Travel/Hospital formatting heuristics
                    if (/Hospital/i.test(sc)) {
                        // Keep simple; viewer may already render countdown elsewhere
                        tag = 'Hosp';
                    } else if (/Travel|Abroad/i.test(sc)) {
                        tag = 'Abroad' + (a ? `·${utils.abbrevActivity(a)}` : '');
                    } else if (sc) {
                        tag = utils.abbrevStatus(sc) + (a ? `·${utils.abbrevActivity(a)}` : '');
                    } else {
                        tag = a ? utils.abbrevActivity(a) : 'Okay';
                    }
                    const confidence = hasRecentRw ? 'high' : (ageMs <= 15000 ? 'medium' : 'low');
                    out[id] = { tag, confidence, blip, ts: last.t, src: last.s };
                }
                return out;
            }
        },

        // Lightweight render epoch scheduler to coalesce frequent updates
        _renderEpoch: {
            _pending: false,
            _last: 0,
            _minIntervalMs: 150,
            schedule() {
                if (this._pending) return;
                const now = Date.now();
                const wait = Math.max(0, this._minIntervalMs - (now - this._last));
                this._pending = true;
                const cb = async () => {
                    this._pending = false;
                    this._last = Date.now();
                    try { await ui.updateRankedWarUI(); } catch(e) {
                        try { tdmlogger('error', `[updateRankedWarUI] failed: ${e}`); } catch(_) {}
                    }
                };
                const raf = (typeof window !== 'undefined' && window.requestAnimationFrame) ? window.requestAnimationFrame.bind(window) : null;
                if (wait === 0) {
                    (raf || setTimeout)(cb, 0);
                } else {
                    setTimeout(() => (raf || setTimeout)(cb, 0), wait);
                }
            }
        },

        // Lightweight render epoch scheduler for Faction Members List updates
        _renderEpochMembers: {
            _pending: false,
            _last: 0,
            _minIntervalMs: 150,
            schedule() {
                if (this._pending) return;
                const now = Date.now();
                const wait = Math.max(0, this._minIntervalMs - (now - this._last));
                this._pending = true;
                const cb = async () => {
                    this._pending = false;
                    this._last = Date.now();
                    try {
                        if (state.dom.factionListContainer) {
                            await ui.processFactionPageMembers(state.dom.factionListContainer);
                            ui.updateFactionPageUI(state.dom.factionListContainer);
                        }
                    } catch(_) {}
                };
                const raf = (typeof window !== 'undefined' && window.requestAnimationFrame) ? window.requestAnimationFrame.bind(window) : null;
                if (wait === 0) {
                    (raf || setTimeout)(cb, 0);
                } else {
                    setTimeout(() => (raf || setTimeout)(cb, 0), wait);
                }
            }
        },

        // --- Ranked War Alerts: Snapshot and Observer ---
        ensureRankedWarAlertObserver: () => {
            // Run alerts for any war type. If no war containers/tables are present, disconnect and exit gracefully.
            const containers = (state.dom.rankwarfactionTables && state.dom.rankwarfactionTables.length)
                ? state.dom.rankwarfactionTables
                : document.querySelectorAll('.tab-menu-cont');
            
            if (!containers.length) {
                
                try {
                    if (state._rankedWarObserver) { state._rankedWarObserver.disconnect(); }
                    if (state._rankedWarScoreObserver) { state._rankedWarScoreObserver.disconnect(); }
                } catch(_) { /* noop */ }
                state._rankedWarObserver = null;
                state._rankedWarScoreObserver = null;
                return;
            }
            const ourFactionName = state.factionPull?.name;
            const opponentFactionLink = state.dom.rankBox?.querySelector('.nameWp___EX6gT .opponentFactionName___vhESM');
            const currentFactionLink = state.dom.rankBox?.querySelector('.nameWp___EX6gT .currentFactionName___eq7n8');
            const opponentFactionName = opponentFactionLink ? opponentFactionLink.textContent.trim() : '';
            const currentFactionName = currentFactionLink ? currentFactionLink.textContent.trim() : '';
            
            // Build candidate list (prefer enemy-faction, within war container)
            const containerRoot = state.dom.rankwarContainer || document;
            const enemyCandidates = Array.from(containerRoot.querySelectorAll('.tab-menu-cont.enemy-faction .members-list, .tab-menu-cont.enemy-faction .members-cont'));
            const allCandidates = enemyCandidates.length
                ? enemyCandidates
                : Array.from(containers)
                    .map(c => c.querySelector('.members-list') || c.querySelector('.members-cont'))
                    .filter(Boolean);
            

            // Helper to classify if a container is "ours" using label mapping (fallback when class is missing)
            const isOurSide = (tabContainer) => {
                // Prefer explicit CSS classes if present
                if (tabContainer.closest('.tab-menu-cont')?.classList.contains('our-faction')) return true;
                if (tabContainer.closest('.tab-menu-cont')?.classList.contains('enemy-faction')) return false;
                // Fallback to left/right mapping with labels
                const side = tabContainer.closest('.tab-menu-cont')?.classList.contains('left') ? 'left' : (tabContainer.closest('.tab-menu-cont')?.classList.contains('right') ? 'right' : '');
                if (side === 'left') return !!(ourFactionName && opponentFactionName === ourFactionName);
                if (side === 'right') return !!(ourFactionName && currentFactionName === ourFactionName);
                return false; // default to opponent side if unknown
            };

            // One-time restore of persisted travel meta per load
            if (!state._travelMetaRestored) {
                state._travelMetaRestored = true;
                try { ui._restorePersistedTravelMeta?.(); } catch(_) {}
            }

            // Prioritize selection:
            // 1) opponents with TDM subrows (pick the last to prefer deeper index)
            // 2) opponents with any rows (pick the last)
            let opponentTableEl = null;
            const opponentWithSubrows = allCandidates
                .map(members => ({ members, container: members.closest('.tab-menu-cont') }))
                .filter(x => x.members.querySelector('li .dibs-notes-subrow') && !isOurSide(x.members));
            if (opponentWithSubrows.length) {
                const chosen = opponentWithSubrows[opponentWithSubrows.length - 1];
                opponentTableEl = chosen.members;
                const idx = Array.from(containers).indexOf(chosen.container);
                
            } else {
                const opponentAnyRows = allCandidates
                    .map(members => ({ members, container: members.closest('.tab-menu-cont') }))
                    .filter(x => x.members.querySelector('> li') && !isOurSide(x.members));
                if (opponentAnyRows.length) {
                    const chosen = opponentAnyRows[opponentAnyRows.length - 1];
                    opponentTableEl = chosen.members;
                    const idx = Array.from(containers).indexOf(chosen.container);
                    
                }
            }
            const table = opponentTableEl;
            // Determine the actual list element that owns the LIs (some DOMs use div.members-list > ul > li)
            const listEl = table && (table.matches('ul') ? table : (table.querySelector(':scope > ul') || table.querySelector('ul') || table));
            if (table) {
                try {
                    const cont = table.closest('.tab-menu-cont');
                    const liCount = listEl ? listEl.querySelectorAll(':scope > li').length : 0;
                    tdmlogger('debug', `[Observer] selected container class="${cont?.className || ''}" usingListEl=<${(listEl?.tagName || '').toLowerCase()}> rows=${liCount}`);
                } catch(_) {}
            }
            // Attempt to restore prior snapshot and alert meta from local persistence (persist across reloads)
            const pageKeyRaw = utils.getCurrentWarPageKey?.();
            const pageKey = pageKeyRaw ? pageKeyRaw.replace(/[^a-z0-9_\-]/gi,'_') : null;
            const warKey = pageKey || (state.lastRankWar?.id ? `id_${String(state.lastRankWar.id)}` : null);
            if (warKey) {
                try {
                    const metaKey = `tdm.rw_meta_${warKey}`;
                    const snapKey = `tdm.rw_snap_${warKey}`;
                    const rawMetaObj = storage.get(metaKey);
                    const rawMeta = (rawMetaObj && rawMetaObj.warId === state.lastRankWar?.id && Date.now() - rawMetaObj.ts < 2 * 60 * 60 * 1000) ? rawMetaObj.v : undefined;
                    const rawSnapObj = storage.get(snapKey);
                    const rawSnap = (rawSnapObj && rawSnapObj.warId === state.lastRankWar?.id && Date.now() - rawSnapObj.ts < 2 * 60 * 60 * 1000) ? rawSnapObj.v : undefined;
                    if (!state.rankedWarChangeMeta && rawMeta) {
                        if (rawMeta && typeof rawMeta === 'object') state.rankedWarChangeMeta = rawMeta;
                    }
                    if ((!state.rankedWarTableSnapshot || Object.keys(state.rankedWarTableSnapshot).length === 0) && rawSnap) {
                        if (rawSnap && typeof rawSnap === 'object') state.rankedWarTableSnapshot = rawSnap;
                    }
                    // Prune expired metas on restore based on TTLs used in rendering
                    if (state.rankedWarChangeMeta && typeof state.rankedWarChangeMeta === 'object') {
                        const now = Date.now();
                        for (const [id, meta] of Object.entries(state.rankedWarChangeMeta)) {
                            const type = meta?.activeType;
                            const ttl = type === 'travel' ? 60*60*1000 : ((type === 'retalDone' || type === 'score') ? 60000 : 120000);
                            if (!type || !meta?.ts || (now - meta.ts) >= ttl) delete state.rankedWarChangeMeta[id];
                        }
                    }
                } catch(_) { /* ignore restore issues */ }
            }
            // Initialize snapshot once or when empty
            if (!state.rankedWarTableSnapshot || Object.keys(state.rankedWarTableSnapshot).length === 0) {
                const snap = {};
                const srcPrefInit = 'rw';
                const apiOnlyInit = srcPrefInit === 'api';
                const tfInit = state.tornFactionData || {};
                const oppFactionIdInit = state?.warData?.opponentFactionId || state?.warData?.opponentId || null;
                (listEl || table).querySelectorAll(':scope > li').forEach(row => {
                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                    if (!userLink) return;
                    const id = userLink.href.match(/XID=(\d+)/)?.[1];
                    if (!id) return;
                    // Prefer API-sourced status when timeline source is API-only
                    let member = null;
                    try {
                        if (oppFactionIdInit && tfInit[oppFactionIdInit]?.data?.members) {
                            const arr = Array.isArray(tfInit[oppFactionIdInit].data.members)
                                ? tfInit[oppFactionIdInit].data.members
                                : Object.values(tfInit[oppFactionIdInit].data.members);
                            member = arr.find(x => String(x.id) === String(id)) || null;
                        }
                    } catch(_) {}
                    const domStatusInit = apiOnlyInit ? '' : utils.getStatusTextFromRow(row);
                    const statusObjInit = member?.status;
                    const statusTextInit = statusObjInit?.description || domStatusInit || '';
                    snap[id] = {
                        status: statusTextInit,
                        activity: utils.getActivityStatusFromRow(row),
                        retal: !!state.retaliationOpportunities[id],
                        ts: Date.now()
                    };
                });
                state.rankedWarTableSnapshot = snap;
                tdmlogger('debug', `[Observer] snapshot initialized for ${Object.keys(snap).length} rows`);
                // Initialize scoreboard snapshot from lastRankWar factions (storage authoritative)
                try {
                    const lw = state.lastRankWar;
                    if (lw && Array.isArray(lw.factions)) {
                        const ourFac = lw.factions.find(f => String(f.id) === String(state.user.factionId));
                        const oppFac = lw.factions.find(f => String(f.id) !== String(state.user.factionId));
                        if (ourFac || oppFac) {
                            const snap = { opp: Number(oppFac?.score || 0), our: Number(ourFac?.score || 0) };
                            state.rankedWarScoreSnapshot = snap;
                            tdmlogger('debug', `[Observer] score snapshot init (lastRankWar) opp=${snap.opp} our=${snap.our}`);
                        }
                    }
                } catch(_) { /* noop */ }
            }
            // Build a throttled scanner to avoid floods
            const runScan = () => {
                const scanTime = new Date().toLocaleTimeString();
                let anyChange = false;
                const rowsAll = (listEl || table).querySelectorAll(':scope > li');
                // Scan all rows
                const rows = Array.from(rowsAll);
                
                let loggedCount = 0;
                // Check scoreboard changes once per scan and create alerts before row loop
                try {
                    // Use lastRankWar updates (assumed refreshed by polling) as source of truth
                    const lw = state.lastRankWar;
                    if (lw && Array.isArray(lw.factions)) {
                        const ourFac = lw.factions.find(f => String(f.id) === String(state.user.factionId));
                        const oppFac = lw.factions.find(f => String(f.id) !== String(state.user.factionId));
                        if (ourFac || oppFac) {
                            const nextScores = { opp: Number(oppFac?.score || 0), our: Number(ourFac?.score || 0) };
                            const hadBoth = !!(ourFac && oppFac);
                            const prevScores = state.rankedWarScoreSnapshot || { opp: null, our: null };
                            const prevOpp = (prevScores.opp == null ? nextScores.opp : prevScores.opp);
                            const prevOur = (prevScores.our == null ? nextScores.our : prevScores.our);

                            // If we don't yet have both factions resolved, delay baseline stamping to avoid false +0 logs
                            if (!hadBoth) {
                                // Still persist partial so that when other side appears we can compute properly
                                state.rankedWarScoreSnapshot = { opp: prevOpp, our: prevOur };
                            } else {
                                let deltaOpp = Math.max(0, nextScores.opp - (prevOpp || 0));
                                let deltaOur = Math.max(0, nextScores.our - (prevOur || 0));

                                // Baseline catch-up: if our previous was 0/null but opponent already had a delta earlier, treat first non-zero our score as a catch-up delta exactly once
                                if ((prevOur == null || prevOur === 0) && nextScores.our > 0 && !state._scoreboardInitializedOur) {
                                    deltaOur = nextScores.our; // full catch-up
                                    state._scoreboardInitializedOur = true;
                                }
                                if ((prevOpp == null || prevOpp === 0) && nextScores.opp > 0 && !state._scoreboardInitializedOpp) {
                                    deltaOpp = nextScores.opp;
                                    state._scoreboardInitializedOpp = true;
                                }

                                // Anomaly detection: opponent shows large positive but our delta repeatedly zero while our absolute > 0
                                try {
                                    if (!state._scoreboardAnomalyCount) state._scoreboardAnomalyCount = 0;
                                    const anomaly = (nextScores.our > 0 && deltaOur === 0 && deltaOpp > 0 && prevOur === 0);
                                    if (anomaly) {
                                        state._scoreboardAnomalyCount += 1;
                                        if (state._scoreboardAnomalyCount <= 3) {
                                            tdmlogger('debug', `[Scoreboard][Anomaly] ourFac score=${nextScores.our} prevOur=${prevOur} deltaOur=0 while opp delta=${deltaOpp}`);
                                        }
                                    }
                                } catch(_) {}

                                if (deltaOpp > 0 || deltaOur > 0) {
                                    anyChange = true;
                                    state.rankedWarScoreSnapshot = nextScores;
                                    tdmlogger('info', `[Scoreboard] Change (lastRankWar) opp +${deltaOpp}, our +${deltaOur} (totals opp=${nextScores.opp}, our=${nextScores.our})`);
                                } else {
                                    // Ensure snapshot persists even with no delta (so nulls don't re-trigger catch-up incorrectly)
                                    state.rankedWarScoreSnapshot = { opp: nextScores.opp, our: nextScores.our };
                                }
                            }
                        }
                    }
                } catch(_) { /* noop */ }

                rows.forEach(row => {
                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                    if (!userLink) return;
                    const id = userLink.href.match(/XID=(\d+)/)?.[1];
                    if (!id) return;
                    const opponentName = utils.sanitizePlayerName(userLink.textContent, id, { fallbackPrefix: 'Opponent' });
                    const prev = state.rankedWarTableSnapshot[id] || {};
                    // Prefer cached opponent member data over DOM text when available
                    const tf = state.tornFactionData || {};
                    // Only use the current page's opponent faction id; avoid stale lastOpponentFactionId to prevent cross-war contamination
                    const oppFactionId = state?.warData?.opponentFactionId || state?.warData?.opponentId || null;
                    let member = null;
                    if (oppFactionId && tf[oppFactionId]?.data?.members) {
                        const arr = Array.isArray(tf[oppFactionId].data.members)
                            ? tf[oppFactionId].data.members
                            : Object.values(tf[oppFactionId].data.members);
                        member = arr.find(x => String(x.id) === String(id)) || null;
                    }
                    // Respect timeline source preference strictly when set to API-only
                    const srcPrefStrict = 'rw';
                    const apiOnly = srcPrefStrict === 'api';
                    const domStatus = apiOnly ? '' : utils.getStatusTextFromRow(row);
                    const statusObj = member?.status;
                    let statusText = statusObj?.description || domStatus || '';
                    const prevUnified = state.unifiedStatus?.[id] || null;
                    const memberForCanon = statusObj
                        ? { id, status: statusObj, last_action: member?.last_action || member?.lastAction || null }
                        : null;
                    const currRec = memberForCanon
                        ? utils.buildUnifiedStatusV2(memberForCanon, prevUnified)
                        : utils.buildUnifiedStatusV2({ state: domStatus, description: domStatus });
                    // Canonicalized status to smooth FF Scouter rewrites (e.g., Hosp countdowns, "in CI", "T CI")
                    let currCanon = currRec?.canonical || '';
                    // Derive DOM-only canonical for mismatch detection (API vs page)
                    // canonicalizeStatus removed. Use buildUnifiedStatusV2 for canonical status records.
                    const domRec = utils.buildUnifiedStatusV2({ state: domStatus, description: domStatus });
                    const domCanon = domRec?.canonical || '';
                    const currStatus = statusText;
                    // If API says Okay but DOM still clearly shows Hospital (countdown) and prior hospital meta not expired, trust DOM to avoid premature early-release alert
                    const existingMetaForMismatch = state.rankedWarChangeMeta[id];
                    const prevHospUntil = existingMetaForMismatch && typeof existingMetaForMismatch.hospitalUntil === 'number' ? existingMetaForMismatch.hospitalUntil : 0;
                    const nowSecMismatch = Math.floor(Date.now() / 1000);
                    const domShowsHospital = /hosp/i.test(domStatus || '');
                    const apiSaysOkay = /okay/i.test(currCanon || '') || /okay/i.test(statusObj?.description || '');
                    const hospitalTimeRemaining = prevHospUntil > nowSecMismatch ? (prevHospUntil - nowSecMismatch) : 0;
                    let suppressedEarlyHosp = false;
                    if (!apiOnly && domShowsHospital && apiSaysOkay && hospitalTimeRemaining > 30) {
                        // Treat as still Hospital
                        currCanon = 'Hospital';
                        statusText = domStatus;
                        suppressedEarlyHosp = true;
                    }
                    const statusUntil = Number(statusObj?.until) || 0;
                    const currActivity = (member?.last_action?.status) ? member.last_action.status : utils.getActivityStatusFromRow(row);
                    // Phased timeline sampling (non-blocking, respects runtime toggle)
                    try {
                        const explicit = null; // legacy toggle disabled
                        const cfg = ui._tdmConfig?.timeline || {};
                        const enabled = (explicit !== null) ? !!explicit : !!cfg.enabled;
                        if (enabled) {
                            const pageKey = utils.getCurrentWarPageKey?.();
                            const warKey = pageKey ? pageKey.replace(/[^a-z0-9_\-]/gi,'_') : (state.lastRankWar?.id ? `id_${String(state.lastRankWar.id)}` : 'unknown');
                            // (timeline sampling removed)
                        }
                    } catch(_) { /* ignore sampling errors */ }
                    const currRetal = !!state.retaliationOpportunities[id];
                    // Per-row points delta to detect which opponent scored
                    const currPoints = utils.getPointsFromRow(row);
                    const hadPrevPoints = typeof prev.points === 'number';
                    const pointsDelta = (typeof currPoints === 'number' && hadPrevPoints && currPoints > prev.points) ? (currPoints - prev.points) : 0;

                    // Row state log (debug only)
                    // logRow(`[TDM][RowState] ${opponentName} [${id}] status="${currStatus}" activity="${currActivity}" retal=${currRetal} @ ${scanTime}`);

                    // Compare canonical to avoid flicker on countdown-only changes
                    // Bugfix: compare state-to-state; do not re-canonicalize using previous description text
                    const prevCanon = prev.statusCanon || prev.canon || '';
                    // Add previous-status validity TTL to suppress stale transitions
                    const prevStatusValidTtlMs = (ui._tdmConfig?.timeline?.prevStatusValidTtlMs != null)
                        ? Number(ui._tdmConfig.timeline.prevStatusValidTtlMs)
                        : (30 * 60 * 1000); // default 30m
                    const prevTs = (state.rankedWarChangeMeta?.[id]?.ts) || 0;
                    const nowForPrev = Date.now();
                    const prevStillValid = prevTs && (nowForPrev - prevTs) <= prevStatusValidTtlMs;
                    const statusChanged = !!(prevCanon && currCanon && prevCanon !== currCanon && prevStillValid);
                    const activityChanged = !!(prev.activity && currActivity && prev.activity !== currActivity);
                    const retalChanged = typeof prev.retal === 'boolean' ? (prev.retal !== currRetal) : !!currRetal;
                    anyChange = anyChange || statusChanged || activityChanged || retalChanged;
                    // Console logs for changes (follow suppression rule for activity Idle/Online -> Offline)
                    if (state.debug && state.debug.rowLogs) {
                        if (statusChanged) {
                            tdmlogger('debug', `[StatusChange] ${opponentName} [${id}] ${prev.status} -> ${currStatus} @ ${new Date().toLocaleTimeString()}`);
                        }
                        if (activityChanged && !((prev.activity === 'Idle' || prev.activity === 'Online') && currActivity === 'Offline')) {
                            try {
                                if (state?.debug?.activityLogs || storage.get('debugActivity', false)) {
                                    tdmlogger('debug', `[ActivityChange] ${opponentName} [${id}] ${prev.activity} -> ${currActivity} @ ${new Date().toLocaleTimeString()}`);
                                }
                            } catch(_) {}
                        }
                        if (retalChanged) {
                            try { tdmlogger('debug', `[RetalChange] ${opponentName} [${id}] ${prev.retal ? 'ON' : 'OFF'} -> ${currRetal ? 'ON' : 'OFF'} @ ${new Date().toLocaleTimeString()}`); } catch(_) {}
                        }
                    }

                    // If user is currently in Hospital, clear any lingering score bump effects for this player
                    try {
                        if (/^hospital$/i.test(String(currCanon || ''))) {
                            const idStr = String(id);
                            if (state._scoreBumpTimers && state._scoreBumpTimers[idStr]) {
                                const prev = state._scoreBumpTimers[idStr];
                                try { if (prev.fade) utils.unregisterTimeout(prev.fade); } catch(_) {}
                                try { if (prev.redToOrange) utils.unregisterTimeout(prev.redToOrange); } catch(_) {}
                                try { if (prev.persist) utils.unregisterTimeout(prev.persist); } catch(_) {}
                                delete state._scoreBumpTimers[idStr];
                            }
                            const scoreEl = row.querySelector('.points___TQbnu, .points');
                            try { if (scoreEl) scoreEl.classList.remove('tdm-score-bump', 'fade', 'tdm-score-bump-orange'); } catch(_) {}
                        }
                    } catch(_) {}

                    // Apply score border highlight orthogonally — never affects alertType
                    if (pointsDelta > 0) {
                        try {
                            const scoreEl = row.querySelector('.points___TQbnu, .points');
                            if (scoreEl) {
                                // Reset any previous timers and orange state if re-bumped
                                const idStr = String(id);
                                state._scoreBumpTimers = state._scoreBumpTimers || {};
                                if (state._scoreBumpTimers[idStr]) {
                                    const prev = state._scoreBumpTimers[idStr];
                                    try { if (prev.fade) utils.unregisterTimeout(prev.fade); } catch(_) {}
                                    try { if (prev.redToOrange) utils.unregisterTimeout(prev.redToOrange); } catch(_) {}
                                    try { if (prev.persist) utils.unregisterTimeout(prev.persist); } catch(_) {}
                                }
                                scoreEl.classList.remove('tdm-score-bump-orange');
                                scoreEl.classList.remove('fade');
                                scoreEl.classList.add('tdm-score-bump');

                                // Short red flash then transition to orange persistent highlight
                                const fadeTimer = utils.registerTimeout(setTimeout(() => { try { scoreEl.classList.add('fade'); } catch(_) {} }, 19500));
                                const redToOrangeTimer = utils.registerTimeout(setTimeout(() => {
                                    try { scoreEl.classList.remove('tdm-score-bump', 'fade'); } catch(_) {}
                                    try { scoreEl.classList.add('tdm-score-bump-orange'); } catch(_) {}
                                }, 20000));
                                // Ensure the combined time (red + orange + fade) is no more than 5 minutes (300000ms) total
                                // red phase starts immediately and lasts ~20s; persist removal should fire at 5min from now
                                const TOTAL_MS = 5 * 60 * 1000; // 300000 ms
                                const persistTimer = utils.registerTimeout(setTimeout(() => {
                                    try { scoreEl.classList.remove('tdm-score-bump', 'fade', 'tdm-score-bump-orange'); } catch(_) {}
                                    delete state._scoreBumpTimers[idStr];
                                }, TOTAL_MS));

                                state._scoreBumpTimers[idStr] = { fade: fadeTimer, redToOrange: redToOrangeTimer, persist: persistTimer };
                            }
                        } catch(_) {}
                    }

                    let alertType = null, alertText = '', alertData = {};
                    let freshChange = false; // true only when a new change is detected in this scan
                    const existingMeta = state.rankedWarChangeMeta[id];
                    // Priority 1: Retal — render countdown here to avoid legacy/observer flicker
                    if (currRetal) {
                        // Defensive: only render active retaliation opportunities with positive remaining time
                        const ret = state.retaliationOpportunities[id];
                        const nowSec = Math.floor(Date.now() / 1000);
                        const rem = (ret && typeof ret.retaliationEndTime === 'number') ? Math.floor(ret.retaliationEndTime - nowSec) : null;
                        if (rem == null || rem <= 0) {
                            // expired or malformed — treat as no retaliation
                        } else {
                            alertType = 'retal';
                            const mm = Math.floor(rem / 60);
                            const ss = String(rem % 60).padStart(2, '0');
                            alertText = `Retal👉${mm}:${ss}`;
                            freshChange = true;
                        }
                    }
                    // Priority 2b: Remove any lingering old score meta beyond previous behavior
                    else if (existingMeta?.activeType === 'score') {
                        const ttlMs = 60000;
                        if (Date.now() - (existingMeta.ts || 0) >= ttlMs) {
                            if (state.rankedWarChangeMeta[id]?.activeType === 'travel') { try { ui._kv.removeItem(`tdm.travel.${id}`); } catch(_) {} }
                            delete state.rankedWarChangeMeta[id];
                        }
                    }
                    // Cleanup stickies: if abroad hospital meta exists but target is no longer Hospital, remove it
                    else if (existingMeta?.activeType === 'status' && existingMeta.hospitalAbroad && !/^hospital$/i.test(currCanon || '')) {
                        if (state.rankedWarChangeMeta[id]?.activeType === 'travel') { try { ui._kv.removeItem(`tdm.travel.${id}`); } catch(_) {} }
                        delete state.rankedWarChangeMeta[id];
                    }
                    // Priority 3: Active travel meta (60 minutes; degrade display if stale)
                    else if (existingMeta?.activeType === 'travel') {
                        const meta = existingMeta;
                        const nowMs = Date.now();
                        // Keep travel meta visible until ETA passes (with small buffer) when we have a confident ETA.
                        // If no ETA, fallback to a generous TTL to avoid leaks.
                        const hasEta = Number.isFinite(meta?.etaMs) && meta.etaMs > 0;
                        const bufferMs = 5 * 60 * 1000; // 5 minutes buffer after ETA
                        const fallbackTtlMs = 2 * 60 * 60 * 1000; // 2 hours if we lack ETA
                        const age = nowMs - (meta.ts || 0);
                        const withinTtl = hasEta ? (nowMs <= (meta.etaMs + bufferMs)) : (age < fallbackTtlMs);
                        if (withinTtl) {
                            // Keep meta but no longer render via alert button
                            alertType = null;
                            alertText = '';
                            alertData = meta;
                        } else {
                            if (state.rankedWarChangeMeta[id]?.activeType === 'travel') { try { ui._kv.removeItem(`tdm.travel.${id}`); } catch(_) {} }
                            delete state.rankedWarChangeMeta[id];
                        }
                    }
                    // Priority 3: Status change (always show for any status change; travel has extra logic)
                    else if ((prevCanon && currCanon && prevCanon !== currCanon) || (!prevCanon && currCanon === 'Hospital')) {
                        // Early Hospital Out: Previously in Hospital with >60s remaining, now Okay
                        const nowSecEarly = Math.floor(Date.now() / 1000);
                        const hadHospMeta = existingMeta && existingMeta.activeType === 'status' && /hosp/i.test(existingMeta.newStatus || '') && typeof existingMeta.hospitalUntil === 'number';
                        // Raw row text still includes 'hosp'? Then don't treat as released yet (prevents flicker false positives)
                        const rawStillHosp = /hosp/i.test(statusText || '');
                        // Only flag early release if: we previously had Hospital meta with >60s remaining, canonical now says Okay, AND raw text no longer shows Hospital
                        const earlyRelease = hadHospMeta && !rawStillHosp && !suppressedEarlyHosp && (existingMeta.hospitalUntil - nowSecEarly) > 60 && (/^okay$/i.test(currCanon));
                        if (earlyRelease) {
                            alertType = 'earlyHospOut';
                            alertText = 'EarlyHospOut👉';
                            alertData = { prevStatus: 'Hospital', newStatus: 'Okay', hospitalUntil: existingMeta.hospitalUntil };
                            freshChange = true;
                        }
                        // Special handling for travel
                        if (!alertType && currCanon === 'Travel') {
                            const prevWasGate = !prevCanon || /^(okay|abroad|hospital)$/i.test(prevCanon);
                            const existing = state.rankedWarChangeMeta[id];
                            const nowMsTravel = Date.now();
                            // Debounce: require two consecutive scans seeing Travel before committing (unless no previous snapshot)
                            const prevSnapshotCanon = prevCanon;
                            const confirmNeeded = prevWasGate && (!existing || existing.activeType !== 'travel');
                            if (confirmNeeded) {
                                // If previous snapshot already recorded Travel (prevCanon === 'Travel'), we confirm now; otherwise store a provisional marker and skip
                                if (prevSnapshotCanon !== 'Travel') {
                                    state.rankedWarChangeMeta[id] = { activeType: 'travelPending', ts: nowMsTravel };
                                    alertType = null; // travel handled inline
                                    alertText = '';
                                    alertData = state.rankedWarChangeMeta[id];
                                } else {
                                    // Confirmed departure
                                    const leftAtMs = nowMsTravel;
                                    const dest = utils.parseUnifiedDestination(statusText);
                                    const mins = dest ? utils.travel.getMinutes(dest) : 0;
                                    if (!dest || !mins) utils.travel.logUnknownDestination(statusText);
                                    const etaMs = utils.travel.computeEtaMs(leftAtMs, mins);
                                    const etaLocal = etaMs ? new Date(etaMs).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' }) : '';
                                    const text = etaLocal ? utils.travel.formatTravelLine(leftAtMs, mins, statusText) : (statusText || 'Travel');
                                    const etaUTC = (() => { if (!etaMs) return ''; const d = new Date(etaMs); return `${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')} UTC`; })();
                                    const meta = { activeType: 'travel', pendingText: text, ts: leftAtMs, dest, mins, leftAtMs, etaMs, etaUTC, etaLocal, leavingAtMs: leftAtMs, firstSeenMs: state.rankedWarChangeMeta[id]?.firstSeenMs || leftAtMs };
                                    state.rankedWarChangeMeta[id] = meta;
                                    alertType = null;
                                    alertText = '';
                                    alertData = meta;
                                    freshChange = true;
                                }
                            } else if (existing && existing.activeType === 'travelPending') {
                                // Escalate pending to confirmed if still travel after >3s
                                if (nowMsTravel - (existing.ts||0) > 3000) {
                                    const leftAtMs = existing.ts || nowMsTravel;
                                    const dest = utils.parseUnifiedDestination(statusText);
                                    const mins = dest ? utils.travel.getMinutes(dest) : 0;
                                    if (!dest || !mins) utils.travel.logUnknownDestination(statusText);
                                    const etaMs = utils.travel.computeEtaMs(leftAtMs, mins);
                                    const etaLocal = etaMs ? new Date(etaMs).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' }) : '';
                                    const text = etaLocal ? utils.travel.formatTravelLine(leftAtMs, mins, statusText) : (statusText || 'Travel');
                                    const etaUTC = (() => { if (!etaMs) return ''; const d = new Date(etaMs); return `${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')} UTC`; })();
                                    const meta = { activeType: 'travel', pendingText: text, ts: leftAtMs, dest, mins, leftAtMs, etaMs, etaUTC, etaLocal, leavingAtMs: leftAtMs, firstSeenMs: state.rankedWarChangeMeta[id]?.firstSeenMs || leftAtMs };
                                    state.rankedWarChangeMeta[id] = meta;
                                    alertType = null;
                                    alertText = '';
                                    alertData = meta;
                                    freshChange = true;
                                }
                            } else if (existing && existing.activeType === 'travelPendingReturn') {
                                // Similar escalation for return travel pending marker
                                if (nowMsTravel - (existing.ts||0) > 3000) {
                                    const leftAtMs = existing.ts || nowMsTravel;
                                    const dest = existing.dest || null; // Returning from dest
                                    const mins = dest ? (existing.mins || utils.travel.getMinutes(dest)) : 0;
                                    if (!dest || !mins) utils.travel.logUnknownDestination(statusText);
                                    const etaMs = utils.travel.computeEtaMs(leftAtMs, mins);
                                    const etaLocal = etaMs ? new Date(etaMs).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' }) : '';
                                    const text = etaLocal ? utils.travel.formatTravelLine(leftAtMs, mins, statusText) : (statusText || 'Travel');
                                    const etaUTC = (() => { if (!etaMs) return ''; const d = new Date(etaMs); return `${String(d.getUTCHours()).padStart(2,'0')}:${String(d.getUTCMinutes()).padStart(2,'0')} UTC`; })();
                                    const meta = { activeType: 'travel', isReturn:true, pendingText: text, ts: leftAtMs, dest, mins, leftAtMs, etaMs, etaUTC, etaLocal, leavingAtMs: leftAtMs, firstSeenMs: state.rankedWarChangeMeta[id]?.firstSeenMs || leftAtMs };
                                    state.rankedWarChangeMeta[id] = meta;
                                    alertType = null;
                                    alertText = '';
                                    alertData = meta;
                                    freshChange = true;
                                }
                            } else if (/^returning to torn from /i.test(statusText || '')) {
                                // Handle immediate detection of inbound travel (return flight). We treat like travelPendingReturn first pass.
                                const dest = utils.parseUnifiedDestination(statusText);
                                const mins = dest ? utils.travel.getMinutes(dest) : 0;
                                if (!dest || !mins) utils.travel.logUnknownDestination(statusText);
                                const nowMsReturn = Date.now();
                                if (!existing || !/travel/i.test(existing.activeType)) {
                                    // Enrich pending return meta so UI can compute ETA immediately. Mark confident since we have mins+timestamp.
                                    state.rankedWarChangeMeta[id] = { activeType: 'travelPendingReturn', ts: nowMsReturn, leavingAtMs: nowMsReturn, dest, mins, isReturn: true, confident: true };
                                }
                                alertType = null; // inline only
                                const existingReturn = state.rankedWarChangeMeta[id];
                                const leftMsTmp = existingReturn?.ts || nowMsReturn;
                                const etaMsTmp = utils.travel.computeEtaMs(leftMsTmp, existingReturn?.mins || 0);
                                const etaLocalTmp = etaMsTmp ? new Date(etaMsTmp).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' }) : '';
                                alertText = '';
                                alertData = state.rankedWarChangeMeta[id];
                            } else if (apiOnly && statusObj && Number(statusObj.until) === 0) {
                                // API-only mode sometimes reports Travel with until:0. Infer ETA from destination heuristics.
                                const dest = utils.parseUnifiedDestination(statusText);
                                const mins = dest ? utils.travel.getMinutes(dest) : 0;
                                if (!dest || !mins) utils.travel.logUnknownDestination(statusText);
                                const leftAtMs = Date.now();
                                const etaMs = utils.travel.computeEtaMs(leftAtMs, mins);
                                const etaLocal = etaMs ? new Date(etaMs).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' }) : '';
                                const text = etaLocal ? utils.travel.formatTravelLine(leftAtMs, mins, statusText) : (statusText || 'Travel');
                                const meta = { activeType: 'travel', pendingText: text, ts: leftAtMs, dest, mins, leftAtMs, etaMs, etaLocal, leavingAtMs: leftAtMs, firstSeenMs: state.rankedWarChangeMeta[id]?.firstSeenMs || leftAtMs };
                                state.rankedWarChangeMeta[id] = meta;
                                alertType = null;
                                alertText = '';
                                alertData = meta;
                                freshChange = true;
                            } else if (!existing && currCanon === 'Travel') {
                                // Travel meta missing; backfill from legacy timeline disabled – rely on forward detection only
                            } else if (existing && existing.activeType === 'travel') {
                                // After 10 min, show elapsed instead of static ETA? if we lacked reliable ETA or it passed
                                const ageMs = nowMsTravel - (existing.leavingAtMs || existing.ts || 0);
                                let text = existing.pendingText || '';
                                if (existing.etaMs && nowMsTravel > existing.etaMs + 2*60*1000) {
                                    // ETA passed by >2m: degrade to elapsed
                                    const minsElapsed = Math.floor(ageMs/60000);
                                    text = `Travel ${minsElapsed}m+`;
                                } else if (!existing.etaMs && ageMs > 10*60*1000) {
                                    const minsElapsed = Math.floor(ageMs/60000);
                                    text = `Travel ${minsElapsed}m`;
                                }
                                // Safety: if text became blank due to truncation or mutation, rebuild from meta fields
                                if (!text.trim()) {
                                    const abbr = utils.abbrevDest(existing.dest || '') || '';
                                    if (existing.etaLocal && existing.etaLocal !== '??:??') text = `ETA ${existing.etaLocal}`;
                                    else if (abbr) text = `Travel ${abbr}`; else text = 'Travel';
                                    existing.pendingText = text;
                                }
                                alertType = null;
                                existing.pendingText = text; // keep text for inline usage
                                alertText = '';
                                alertData = existing;
                            }
                        } else if (!alertType && (/^Hospital$/i.test(currCanon) || /^HospitalAbroad$/i.test(currCanon))) {
                            // Explicit Hospital message with destination prefix when abroad; show time left if available
                            const nowSec = Math.floor(Date.now() / 1000);
                            const rem = statusUntil > 0 ? Math.max(0, statusUntil - nowSec) : 0;
                            const mm = Math.floor(rem / 60);
                            const ss = String(rem % 60).padStart(2, '0');
                            const timeTxt = rem > 0 ? `${mm}:${ss}` : '';
                            const hadActiveTravel = (state.rankedWarChangeMeta[id]?.activeType === 'travel');
                            const fromWasTravelOrAbroad = /travel|abroad/i.test(prevCanon || '');
                            // Determine if abroad by parsing destination inside hospital description as well
                            let destFromHosp = utils.parseHospitalAbroadDestination(statusText) || '';
                            const hospitalAbroad = !!(hadActiveTravel || fromWasTravelOrAbroad || destFromHosp);
                            let prefix = '';
                            let destForMeta = '';
                            if (hospitalAbroad) {
                                const destFull = destFromHosp || utils.parseUnifiedDestination(statusText) || (state.rankedWarChangeMeta[id]?.dest) || '';
                                destForMeta = destFull;
                                const destAbbrev = utils.abbrevDest(destFull) || '';
                                prefix = destAbbrev ? `${destAbbrev} ` : '';
                            }
                            let label = `${prefix}Hosp`.trim();
                            if (hospitalAbroad && !prefix) label = 'Abrd Hosp';
                            const text = timeTxt ? `${label} ${timeTxt}` : label;
                            // statusUntil already holds the server-provided epoch when hospital ends; hospitalUntil local var was previously undefined (bug)
                            // Note: hospital countdown source of truth is hospitalUntil in this meta; we do not derive it from tdm.status.id_* cache.
                            const meta = { activeType: 'status', pendingText: text, ts: Date.now(), prevStatus: prev.status || prevCanon, newStatus: (hospitalAbroad ? 'HospitalAbroad' : 'Hospital'), hospitalUntil: statusUntil, hospitalAbroad };
                            
                            meta.hospitalUntil = statusUntil;
                            meta.hospitalAbroad = hospitalAbroad;
                            if (destForMeta) meta.dest = destForMeta;
                            state.rankedWarChangeMeta[id] = meta;
                            alertType = 'status';
                            alertText = text;
                            alertData = meta;
                            freshChange = true;
                        } else if (!alertType) {
                            // Generic status change
                            // Suppress trivial Travel↔Okay transitions without a validated prior snapshot
                            const trivialTravelOkay = ((/^(travel)$/i.test(prevCanon) && /^okay$/i.test(currCanon)) || (/^okay$/i.test(prevCanon) && /^(travel)$/i.test(currCanon)));
                            if (trivialTravelOkay && !prevStillValid) {
                                // Skip creating a button; rely on travel meta or next scan
                            } else {
                                alertType = 'status';
                                const fromAbbr = utils.abbrevStatus(prevCanon || '');
                                const toAbbr = utils.abbrevStatus(currCanon || '');
                                alertText = `${fromAbbr}→${toAbbr}`.trim();
                                alertData = { prevStatus: prev.status || prevCanon, newStatus: statusText };
                                freshChange = true;
                            }
                        }
                    }
                    // Priority 4: Activity change (except Idle/Online -> Offline)
                    else if (prev.activity && currActivity && prev.activity !== currActivity) {
                        if (!((prev.activity === 'Idle' || prev.activity === 'Online') && currActivity === 'Offline')) {
                            alertType = 'activity';
                            const abbr = currActivity === 'Online' ? 'On' : currActivity === 'Offline' ? 'Off' : currActivity === 'Idle' ? 'Idle' : (currActivity || '').toString();
                            const firstSeenAtMs = Date.now();
                            alertText = `${abbr} 00:00`;
                            alertData = { prevActivity: prev.activity, newActivity: currActivity, firstSeenAtMs, abbr };
                            freshChange = true;
                        }
                    }

                    // Persist recent status/activity/retal-done alerts (abroad hospital is sticky until status changes or higher-priority alert)
                    if (!alertType) {
                        const meta = state.rankedWarChangeMeta[id];
                        if (meta && meta.activeType !== 'retal') {
                            const isStickyAbroadHosp = (meta.activeType === 'status' && meta.hospitalAbroad === true);
                            // Default TTL 120s; Retal Done is shorter (60s)
                            const ttlMs = (meta.activeType === 'retalDone') ? 60000 : 120000;
                            if (isStickyAbroadHosp || (Date.now() - (meta.ts || 0) < ttlMs)) {
                                if (meta.activeType === 'travelPending' || meta.activeType === 'travelPendingReturn') {
                                    alertType = 'travel';
                                    alertText = meta.pendingText || 'ETA …';
                                } else {
                                    alertType = meta.activeType;
                                }
                                // Live-update hospital countdown if we have an 'until'
                                if (meta.activeType === 'status' && typeof meta.hospitalUntil === 'number' && meta.hospitalUntil > 0) {
                                    const nowSec = Math.floor(Date.now() / 1000);
                                    const rem = Math.max(0, meta.hospitalUntil - nowSec);
                                    const mm = Math.floor(rem / 60);
                                    const ss = String(rem % 60).padStart(2, '0');
                                    const timeTxt = rem > 0 ? `${mm}:${ss}` : '';
                                    let label = 'Hosp';
                                    if (meta.hospitalAbroad) {
                                        const destAbbrev = utils.abbrevDest(meta.dest || '') || '';
                                        label = destAbbrev ? `${destAbbrev} Hosp` : 'Abrd Hosp';
                                    }
                                    alertText = timeTxt ? `${label} ${timeTxt}` : label;
                                } else if (meta.activeType === 'activity') {
                                    const first = meta.firstSeenAtMs || meta.ts || Date.now();
                                    const elapsed = Date.now() - first;
                                    const mm = Math.floor(elapsed / 60000);
                                    const ss = Math.floor((elapsed % 60000) / 1000);
                                    alertText = `${meta.abbr || 'On'} ${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
                                    meta.pendingText = alertText;
                                } else {
                                    alertText = meta.pendingText || '';
                                }
                                alertData = meta;
                            }
                        }
                        // Fallback: show abroad hospital label even without a fresh change if API shows Hospital abroad and no other alert
                        if (!alertType && /^hospital$/i.test(currCanon || '')) {
                            const nowSec = Math.floor(Date.now() / 1000);
                            const rem = statusUntil > 0 ? Math.max(0, statusUntil - nowSec) : 0;
                            const mm = Math.floor(rem / 60);
                            const ss = String(rem % 60).padStart(2, '0');
                            const timeTxt = rem > 0 ? `${mm}:${ss}` : '';
                            const destFull = utils.parseHospitalAbroadDestination(statusText) || utils.parseUnifiedDestination(statusText) || '';
                            if (destFull) {
                                const destAbbrev = utils.abbrevDest(destFull) || '';
                                const label = destAbbrev ? `${destAbbrev} Hosp` : 'Abrd Hosp';
                                // Create/refresh sticky meta
                                const metaSticky = { activeType: 'status', pendingText: timeTxt ? `${label} ${timeTxt}` : label, ts: Date.now(), prevStatus: prev.status || prevCanon, newStatus: 'Hospital', hospitalUntil: statusUntil, hospitalAbroad: true, dest: destFull };
                                state.rankedWarChangeMeta[id] = metaSticky;
                                alertType = 'status';
                                alertText = metaSticky.pendingText;
                                alertData = metaSticky;
                            }
                        }
                    }

                    // Update snapshot
                    // Persist latest row snapshot, including points if available
                    state.rankedWarTableSnapshot[id] = { status: statusText, activity: currActivity, retal: currRetal, statusCanon: currCanon, points: (typeof currPoints === 'number' ? currPoints : (hadPrevPoints ? prev.points : undefined)) };

                    // Update button UI
                    const subrow = row.querySelector('.dibs-notes-subrow');
                    if (!subrow) return;
                    const retalBtn = subrow.querySelector('.retal-btn');
                    if (!retalBtn) return;

                    // Respect global setting to hide alert buttons
                    const alertsEnabled = storage.get('alertButtonsEnabled', true);
                    if (!alertsEnabled) {
                        const desiredClass = 'btn retal-btn tdm-alert-btn tdm-alert-inactive';
                        if (retalBtn.className !== desiredClass) retalBtn.className = desiredClass;
                        if (retalBtn.style.display !== 'none') retalBtn.style.display = 'none';
                        if (retalBtn.disabled !== true) retalBtn.disabled = true;
                        if (retalBtn.textContent !== '') retalBtn.textContent = '';
                        if (retalBtn.title) retalBtn.title = '';
                        if (retalBtn.dataset.tdmClickType) delete retalBtn.dataset.tdmClickType;
                        return;
                    }

                    const showAlerts = storage.get('alertButtonsEnabled', true);

                    // Inject abroadHosp alert (post-processing) if no higher-priority alert selected.
                    if (showAlerts && !alertType) {
                        try {
                            // Prefer existing meta indicating hospitalAbroad or unified status record (rec.canonical === 'abroad_hosp')
                            const metaHospAbroad = state.rankedWarChangeMeta[id]?.hospitalAbroad === true;
                            const unifiedAbroadHosp = !!(rec && rec.canonical === 'HospitalAbroad');
                            if (metaHospAbroad || unifiedAbroadHosp) {
                                alertType = 'abroadHosp';
                                let label = 'Hosp Abroad';
                                let remTxt = '';
                                const until = state.rankedWarChangeMeta[id]?.hospitalUntil;
                                if (typeof until === 'number' && until > 0) {
                                    const nowSec = Math.floor(Date.now()/1000);
                                    const rem = Math.max(0, until - nowSec);
                                    if (rem > 0) {
                                        const mm = Math.floor(rem/60); const ss = String(rem%60).padStart(2,'0');
                                        remTxt = `${mm}:${ss}`;
                                    }
                                }
                                alertText = remTxt ? `${label} ${remTxt}` : label;
                                alertData = { hospitalAbroad: true, hospitalUntil: state.rankedWarChangeMeta[id]?.hospitalUntil };
                            }
                        } catch(_) { /* swallow */ }
                    }

                    // Restrict to allowed button alert types only
                    const allowedAlertTypes = new Set(['retal','retalDone','earlyHospOut','abroadHosp']);
                    if (alertType && showAlerts && allowedAlertTypes.has(alertType)) {
                        // Only stamp a new timestamp when this scan detected a fresh change.
                        if (freshChange) {
                            state.rankedWarChangeMeta[id] = { activeType: alertType, pendingText: alertText, ts: Date.now(), ...alertData };
                        }
                        // --- Travel Fallback Handling START ---
                        // Travel handling removed: inline .tdm-travel-eta now owns travel display.
                        // --- Travel Fallback Handling END ---
                        // Choose a variant class by change type and direction
                        let baseClass = 'btn retal-btn tdm-alert-btn';
                        let variant = 'tdm-alert-retal';
                        let addGlow = false;
                        if (alertType === 'activity') {
                            const from = (alertData.prevActivity || '').toLowerCase();
                            const to = (alertData.newActivity || '').toLowerCase();
                            if ((from === 'offline' || from === 'idle') && to === 'online') { variant = 'tdm-alert-green'; addGlow = true; }
                            else if (from === 'online' && to === 'idle') { variant = 'tdm-alert-grey'; addGlow = false; }
                            else if ((from === 'online' || from === 'idle') && to === 'offline') { variant = 'tdm-alert-grey'; addGlow = false; }
                            else { variant = 'tdm-alert-grey'; addGlow = false; }
                        } else if (alertType === 'status') {
                            const fromS = (alertData.prevStatus || '').toLowerCase();
                            const toS = (alertData.newStatus || '').toLowerCase();
                            const isTravelToAbroadOrOkay = /travel/.test(fromS) && (toS === 'abroad' || toS === 'okay');
                            const isOkayToHosp = fromS === 'okay' && /hosp/i.test(toS);
                            const isHospToOkay = /hosp/i.test(fromS) && toS === 'okay';
                            if (alertData.hospitalAbroad) variant = 'tdm-alert-red';
                            else if (isTravelToAbroadOrOkay) variant = 'tdm-alert-red';
                            else if (isOkayToHosp) variant = 'tdm-alert-grey';
                            else if (isHospToOkay) variant = 'tdm-alert-grey';
                            else {variant = 'tdm-alert-grey'; addGlow = false; }
                        } else if (alertType === 'earlyHospOut') {
                            variant = 'tdm-alert-green';
                            addGlow = true;
                        } else if (alertType === 'retalDone') {
                            
                            variant = 'tdm-alert-grey';
                            addGlow = false;
                        }

                        const desiredDisplay = 'inline-block';
                        const desiredDisabled = false;
                        const desiredClass = `${baseClass} ${variant} ${addGlow ? 'tdm-alert-glow' : ''}`.trim();

                        // Tooltip for overflow and richer info
                        let desiredTitle = alertText;
                        if (alertType === 'status' && alertData?.hospitalUntil) {
                            const until = Number(alertData.hospitalUntil);
                            const localUntil = isFinite(until) && until > 0 ? new Date(until * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
                            desiredTitle = `${alertText}${localUntil ? ` • Until ${localUntil}` : ''}`;
                        }

                        // Apply only if changed to prevent flicker and CSS animation restart
                        if ((!alertText || !alertText.trim()) && state.debug && state.debug.alerts) {
                            try { console.warn('[TDM][AlertRender] Blank alertText after processing', { id, alertType, meta: state.rankedWarChangeMeta[id] }); } catch(_) {}
                        }
                        // Apply via orchestrator to batch DOM writes
                        ui._orchestrator.applyOrQueue(retalBtn, {
                            display: desiredDisplay,
                            disabled: desiredDisabled,
                            className: desiredClass,
                            textContent: alertText,
                            title: desiredTitle
                        });

                        
                        // Click handlers by type: retal sends chat; earlyHospOut sends chat; travel updates note; others no-op.
                        if (retalBtn.dataset.tdmClickType !== alertType) {
                            retalBtn.onclick = (e) => {
                                e.preventDefault(); e.stopPropagation();
                                const metaNow = state.rankedWarChangeMeta[id];
                                if (alertType === 'retal') {
                                    // Send faction chat alert for retaliation
                                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                                    const opponentName = utils.sanitizePlayerName(userLink?.textContent, id, { fallbackPrefix: 'Opponent' });
                                    ui.sendRetaliationAlert(id, opponentName);
                                } else if (alertType === 'earlyHospOut') {
                                    const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                                    const opponentName = userLink?.textContent?.trim() || '';
                                    const facId = state.user.factionId;
                                    const msg = `Early Hospital Release ${opponentName}`;
                                    let chatTextbox = document.querySelector(`#faction-${facId} > div.content___n3GFQ > div.root___WUd1h > textarea`);
                                    const chatButton = document.querySelector(`#channel_panel_button\\:faction-${facId}`);
                                    if (storage.get('pasteMessagesToChatEnabled', true)) {
                                        if (!chatTextbox && chatButton) {
                                            chatButton.click();
                                            setTimeout(() => ui.populateChatMessage(msg),900);
                                        } else if (chatTextbox) {
                                            setTimeout(() => ui.populateChatMessage(msg),600);
                                        }
                                    }
                                } else if (alertType === 'abroadHosp') {
                                    // Placeholder: future chat / tracking action could go here
                                }
                            };
                            retalBtn.dataset.tdmClickType = alertType;
                        }
                    } else {
                        // No alert and no recent meta; clear and hide
                        if (state.rankedWarChangeMeta[id] && state.rankedWarChangeMeta[id].activeType !== 'retal') {
                            const type = state.rankedWarChangeMeta[id].activeType;
                            const ttlMs = type === 'travel' ? (60 * 60 * 1000) : ((type === 'retalDone' || type === 'score') ? 60000 : 120000);
                            if (Date.now() - (state.rankedWarChangeMeta[id].ts || 0) >= ttlMs) {
                                if (state.rankedWarChangeMeta[id]?.activeType === 'travel') { try { ui._kv.removeItem(`tdm.travel.${id}`); } catch(_) {} }
                                delete state.rankedWarChangeMeta[id];
                            }
                        }
                        // Fall back to legacy retal rendering (apply no-op updates when unchanged)
                        const desiredClass = 'btn retal-btn tdm-alert-btn tdm-alert-inactive';
                        ui._orchestrator.applyOrQueue(retalBtn, {
                            display: 'none',
                            disabled: true,
                            className: desiredClass,
                            textContent: '',
                            title: ''
                        });
                        if (retalBtn.dataset.tdmClickType) delete retalBtn.dataset.tdmClickType;
                    }
                });
                if (!anyChange && state.debug && state.debug.rowLogs) {
                    const now = Date.now();
                    if (!state._rankedWarLastNoChangeLogMs || now - state._rankedWarLastNoChangeLogMs > 5000) {
                        state._rankedWarLastNoChangeLogMs = now;
                        
                    }
                }
                state._rankedWarLastScanMs = Date.now();
                // Persist snapshot and meta for reload/hash-change continuity
                try {
                    if (warKey) {
                        // Use debounced cross-tab-safe persistence helpers
                        try { utils._initRwMetaSignalListener?.(); } catch(_) {}
                        try { utils.schedulePersistRankedWarMeta?.(warKey); } catch(_) {}
                        try { utils.schedulePersistRankedWarSnapshot?.(warKey); } catch(_) {}
                    }
                } catch(_) { /* ignore quota */ }
            };
            const scheduleScan = () => {
                if (state._rankedWarScanScheduled) return;
                state._rankedWarScanScheduled = true;
                const coalesceDelay = 120;
                setTimeout(() => {
                    state._rankedWarScanScheduled = false;
                    const minInterval = 800; // ms between scans
                    const now = Date.now();
                    const since = now - (state._rankedWarLastScanMs || 0);
                    if (since < minInterval) {
                        setTimeout(runScan, minInterval - since);
                    } else {
                        runScan();
                    }
                }, coalesceDelay);
            };
            state._rankedWarOnMut = scheduleScan; // keep compatibility with existing calls
            state._rankedWarScheduleScan = scheduleScan;
            if (state._rankedWarObserver) { try { utils.unregisterObserver(state._rankedWarObserver); } catch(_) {} state._rankedWarObserver = null; }
            state._rankedWarObserver = utils.registerObserver(new MutationObserver((mutations) => {
                // Ignore mutations originating from our own UI to reduce noise
                const allOwn = mutations.every(m => {
                    const t = m.target && typeof m.target.closest === 'function' ? m.target : null;
                    if (!t) return false;
                    return !!t.closest('.dibs-notes-subrow, .retal-btn, #tdm-attack-container, #tdm-chain-timer, #tdm-inactivity-timer, #tdm-opponent-status, #tdm-api-usage');
                });
                if (allOwn) return; // skip scheduling
                scheduleScan();
                // Keep badges fresh during scans
                try { ui.updateUserScoreBadge?.(); ui.updateFactionScoreBadge?.(); } catch(_) {}
            }));
            
            state._rankedWarObserver.observe((listEl || table), { childList: true, subtree: true, attributes: true, attributeFilter: ['class','title','aria-label','href'] });
            // Observe rank box score changes to trigger scans promptly
            try {
                const rankBox = state.dom.rankBox || document.querySelector('.rankBox___OzP3D');
                if (rankBox) {
                    if (state._rankedWarScoreObserver) { try { state._rankedWarScoreObserver.disconnect(); } catch(_) {} }
                    state._rankedWarScoreObserver = utils.registerObserver(new MutationObserver(() => {
                        // Debounce frequent slider anim mutations by coalescing
                        scheduleScan();
                    }));
                    state._rankedWarScoreObserver.observe(rankBox, { childList: true, subtree: true, characterData: true });
                }
            } catch(_) { /* noop */ }
            try { ui.ensureTravelTicker(); } catch(_) {}
            // Prime once
            
            // If no changes detected in last scan, log a summary line
            try {
                // Wrap next microtask to allow onMut to complete row processing
                Promise.resolve().then(() => {
                    // This relies on the anyChange variable inside onMut; if needed for future, move to outer scope
                    // For now, a simple periodic summary can be added elsewhere if desired
                });
            } catch(_) {}
        },
        dumpUnifiedStatus: async (id, kv, log=true) => {
            try {
                const key = `tdm.tl2.status.id_${id}`;
                const raw = await kv.getItem(key);
                if (log) tdmlogger('debug', `[dumpUnifiedStatus] ${id} ${raw}`);
                return raw;
            } catch(e) { if (log) tdmlogger('warn', `[dumpUnifiedStatus error] ${id} ${e}`); return null; }
        },
        dumpUnifiedAll: async ({ kv, limit=50, prefix='tdm.tl2.status.id_' }={}) => {
            try {
                const keys = await kv.listKeys(prefix);
                const out = [];
                for (const k of keys.slice(0, limit)) {
                    try { out.push({ k, v: await kv.getItem(k) }); } catch(_) {}
                }
                tdmlogger('info', `[dumpUnifiedAll] { count: ${out.length}, keys: ${out.map(e=>e.k)} }`);
                return out;
            } catch(e) { tdmlogger('warn', `[dumpUnifiedAll error] ${e}`); return []; }
        },
        _restorePersistedTravelMeta: async () => {
            try {
                const now = Date.now();
                // Phase 0: Legacy standalone travel keys (remove soon)
                try {
                    const keys = await ui._kv.listKeys('tdm.travel.');
                    const maxAgeMs = 6 * 60 * 60 * 1000;
                    for (const k of keys) {
                        try {
                            const obj = await ui._kv.getItem(k);
                            if (!obj || typeof obj !== 'object') continue;
                            const age = now - (obj.firstSeenMs || obj.leavingAtMs || obj.ts || 0);
                            if (age > maxAgeMs) { try { ui._kv.removeItem(k); } catch(_) {}; continue; }
                            const id = String(obj.id || k.split('.').pop());
                            if (!id) continue;
                            if (state.rankedWarChangeMeta[id] && state.rankedWarChangeMeta[id].activeType === 'travel') continue;
                            state.rankedWarChangeMeta[id] = {
                                activeType: 'travel',
                                dest: obj.dest || null,
                                mins: Number(obj.mins)||0,
                                leavingAtMs: obj.leavingAtMs || obj.firstSeenMs || obj.ts || (now - age),
                                firstSeenMs: obj.firstSeenMs || obj.leavingAtMs || now,
                                etaMs: obj.etaMs || 0,
                                isReturn: !!obj.isReturn,
                                confident: obj.confident !== false,
                                ts: obj.leavingAtMs || obj.firstSeenMs || obj.ts || Date.now()
                            };
                        } catch(_) { /* legacy item error */ }
                    }
                } catch(_) { /* legacy list error */ }

                // Phase 1: Embedded travelMeta inside last tl2 status segment
                try {
                    const statusKeys = await ui._kv.listKeys('tdm.tl2.status.id_');
                    for (const sk of statusKeys) {
                        try {
                            const id = sk.replace(/^tdm\.tl2\.status\.id_/, '');
                            if (!id) continue;
                            const segs = await ui._kv.getItem(sk);
                            const arr = Array.isArray(segs) ? segs : (segs ? JSON.parse(segs) : []);
                            if (!arr.length) continue;
                            const last = arr[arr.length - 1];
                            const tmeta = last && last.travelMeta;
                            if (!tmeta || typeof tmeta !== 'object') continue;
                            const leftAt = tmeta.at || tmeta.firstSeenMs || 0;
                            if (!leftAt) continue;
                            // Staleness rules
                            if (now - leftAt > 8 * 60 * 60 * 1000) continue;
                            if (tmeta.etaMs && tmeta.etaMs > 0) {
                                if (now - tmeta.etaMs > 4 * 60 * 60 * 1000) continue;
                            }
                            const existing = state.rankedWarChangeMeta[id];
                            if (existing && existing.activeType === 'travel' && existing.firstSeenMs <= (tmeta.firstSeenMs || existing.firstSeenMs)) continue;
                            state.rankedWarChangeMeta[id] = {
                                activeType: (tmeta.landedAtMs && (now - tmeta.landedAtMs) < 2*60*1000) ? 'travelLanded' : 'travel',
                                dest: tmeta.dest || null,
                                mins: tmeta.mins || 0,
                                leavingAtMs: tmeta.at || tmeta.firstSeenMs || leftAt,
                                firstSeenMs: tmeta.firstSeenMs || tmeta.at || leftAt,
                                etaMs: tmeta.etaMs || 0,
                                isReturn: !!tmeta.isReturn,
                                landedAtMs: tmeta.landedAtMs || 0,
                                confident: tmeta.confident !== false,
                                ts: tmeta.at || leftAt
                            };
                        } catch(_) { /* per status key */ }
                    }
                } catch(_) { /* embedded restore error */ }
                // Phase 2: Load per-player persisted phase history (v2)
                try {
                    const keys = await ui._kv.listKeys('tdm.phaseHistory.id_');
                    for (const k of keys) {
                        try {
                            const id = k.replace(/^tdm\.phaseHistory\.id_/, '');
                            if (!id) continue;
                            const arr = await ui._kv.getItem(k);
                            if (!Array.isArray(arr) || !arr.length) continue;
                            state._activityTracking = state._activityTracking || {};
                            state._activityTracking._phaseHistory = state._activityTracking._phaseHistory || {};
                            state._activityTracking._phaseHistory[id] = (state._activityTracking._phaseHistory[id] || []).concat(arr).slice(-100);
                        } catch(_) { /* per-key */ }
                    }
                    if (storage.get('tdmDebugPersist', false)) tdmlogger('info', '[Restore] rehydrated phaseHistory keys=' + (Array.isArray(keys) ? keys.length : 0));
                } catch(_) { /* ignore */ }
                try { ui._renderEpoch.schedule(); } catch(_) {}
            } catch(_) { /* ignore top-level */ }
        },
        ensureActivityAlertTicker: () => {
            if (state.ui.activityTickerIntervalId) return; // already running
            state.ui.activityTickerIntervalId = utils.registerInterval(setInterval(() => {
                try {
                    if (!state.page.isRankedWarPage) return;
                    const metas = state.rankedWarChangeMeta;
                    if (!metas) return;
                    const now = Date.now();
                    for (const id in metas) {
                        const meta = metas[id];
                        if (!meta || meta.activeType !== 'activity' || !meta.firstSeenAtMs) continue;
                        const elapsed = now - meta.firstSeenAtMs;
                        const mm = Math.floor(elapsed / 60000);
                        const ss = Math.floor((elapsed % 60000) / 1000);
                        const label = `${meta.abbr || 'On'} ${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`;
                        if (meta.pendingText !== label) {
                            meta.pendingText = label;
                            try {
                                const link = document.querySelector(`a[href*="profiles.php?XID=${id}"]`);
                                if (link) {
                                    const row = link.closest('li') || link.closest('tr');
                                    const btn = row && row.querySelector('.dibs-notes-subrow .retal-btn');
                                    if (btn && btn.textContent !== label) btn.textContent = label;
                                }
                            } catch(_) { /* noop */ }
                        }
                    }
                } catch(_) { /* ignore ticker iteration errors */ }
            }, 1000));
        },
        // Keep last-action tooltips dynamic using stored timestamps
        ensureLastActionTicker: () => {
            if (state.ui.lastActionTickerIntervalId) return;
            state.ui.lastActionTickerIntervalId = utils.registerInterval(setInterval(() => {
                try {
                    if (!state.page.isRankedWarPage) return;
                    document.querySelectorAll('.tdm-last-action-inline[data-last-ts]').forEach(el => {
                        const ts = Number(el.getAttribute('data-last-ts') || 0);
                        if (Number.isFinite(ts) && ts > 0) {
                            const full = `Last Action: ${utils.formatAgoFull(ts)}`;
                            if (el.title !== full) el.title = full;
                            // Also refresh the short label to stay current (granularity: s/m/h/d)
                            const shortTxt = utils.formatAgoShort(ts) || '';
                            if (el.textContent !== shortTxt) el.textContent = shortTxt;
                            // Ensure click-to-chat handler exists once
                            try {
                                if (!el.dataset.tdmLastClick) {
                                    el.style.cursor = 'pointer';
                                    el.onclick = (e) => {
                                        try {
                                            e.preventDefault(); e.stopPropagation();
                                            const row = el.closest('li, tr');
                                            const link = row ? row.querySelector('a[href*="profiles.php?XID="]') : null;
                                            const id = link ? (link.href.match(/XID=(\d+)/) || [])[1] : null;
                                            const name = link ? (link.textContent || '').trim() : (el.dataset?.name || null);
                                            const ts = Number(el.getAttribute('data-last-ts') || 0);
                                            const short = utils.formatAgoShort(ts) || '';
                                            const status = (row && utils.getActivityStatusFromRow) ? utils.getActivityStatusFromRow(row) : null;
                                            ui.sendLastActionToChat(id || null, name || null, status || null, short || null);
                                        } catch(_) { /* noop */ }
                                    };
                                    el.dataset.tdmLastClick = '1';
                                }
                            } catch(_) { /* noop */ }
                        }
                    });
                } catch(_) { /* ignore */ }
            }, 1000));
        },

        ensureTravelTicker: () => {
            try {
                if (ui._travelTickerInterval) return;
                ui._travelTickerInterval = utils.registerInterval(setInterval(() => {
                    try {
                        const nodes = document.querySelectorAll('.tdm-travel-eta');
                        if (!nodes.length) return;
                        const now = Date.now();
                        nodes.forEach(el => {
                            try {
                                const row = el.closest('li');
                                if (!row) return;
                                const userLink = row.querySelector('a[href*="profiles.php?XID="]');
                                const id = userLink?.href?.match(/XID=(\d+)/)?.[1];
                                if (!id) return;
                                const meta = state.rankedWarChangeMeta[id];
                                if (!meta || (meta.activeType !== 'travel' && meta.activeType !== 'travelLanded')) return;
                                const destAbbr = meta.dest ? (utils.abbrevDest(meta.dest) || meta.dest.split(/[\s,]/)[0]) : '';
                                const arrow = meta.isReturn ? '\u2190' : '\u2192';
                                const leaving = meta.leavingAtMs || meta.ts || meta.firstSeenMs;
                                const leftLocal = leaving ? new Date(leaving).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false }) : '';
                                // Upgrade confidence if mins appear
                                if (el.classList.contains('tdm-travel-lowconf')) {
                                    if (meta.mins && meta.mins > 0 && meta.leavingAtMs) {
                                        meta.confident = true;
                                        el.classList.remove('tdm-travel-lowconf');
                                        el.style.transition = 'opacity 0.4s';
                                        el.style.opacity = '0.2';
                                        setTimeout(()=>{ el.style.opacity='1'; },30);
                                    }
                                }
                                if (meta.activeType === 'travel') {
                                    const mins = Number(meta.mins)||0;
                                    if (mins>0 && meta.leavingAtMs) {
                                        const etaMs = meta.etaMs || utils.travel.computeEtaMs(meta.leavingAtMs, mins);
                                        meta.etaMs = etaMs;
                                        let remMs = etaMs - now;
                                        const remMin = Math.max(0, Math.ceil(remMs/60000));
                                        const rh = Math.floor(remMin/60); const rm = remMin % 60;
                                        const remStr = rh>0?`${rh}h${rm? ' ' + rm + 'm':''}`:`${remMin}m`;
                                        const planeType = (state.unifiedStatus?.[id]?.plane) || (utils.getKnownPlaneTypeForId?.(id) || 'light_aircraft');
                                        tdmlogger('debug', '[BusinessDetect] User plane type detected:', { id, planeType });
                                        // Keep inline travel text compact (no 'LEFT <time>')
                                        let newText = `${arrow} ${destAbbr} *${planeType}* LAND~${remStr}`.trim();
                                        if (remMs <= 0) {
                                            meta.activeType = 'travelLanded';
                                            meta.landedAtMs = now;
                                            const landedLocal = new Date(now).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false });
                                            newText = `${arrow} ${destAbbr} Landed at ${landedLocal}`;
                                        }
                                        if (el.textContent !== newText) el.textContent = newText;
                                    }
                                }
                                if (meta.activeType === 'travelLanded') {
                                    const elapsedMs = now - (meta.landedAtMs || now);
                                    if (elapsedMs > 60000) {
                                        const em = Math.floor(elapsedMs/60000);
                                        if (!/\+\d+m$/.test(el.textContent)) el.textContent = `${el.textContent} +${em}m`;
                                        if (elapsedMs > 115000) { // ~1m55s expire
                                            try { ui._kv.removeItem(`tdm.travel.${id}`); } catch(_) {}
                                            delete state.rankedWarChangeMeta[id];
                                            el.style.transition='opacity .4s';
                                            el.style.opacity='0';
                                            setTimeout(()=>{ try { el.remove(); } catch(_) {}; },420);
                                        }
                                    }
                                }
                            } catch(_) { /* per element */ }
                        });
                    } catch(_) { /* loop */ }
                }, 60000));
            } catch(_) { /* ignore */ }
        },

        updateFactionPageUI: (container) => {
            // --- Faction Header Metrics (Dibs & Med Deals Summary) ---
            try {
                if (state.page.isFactionPage && !state.page.isMyFactionPage) {
                    let header = document.querySelector('#tdm-faction-header-metrics');
                    if (!header) {
                        const anchor = document.querySelector('.faction-info-wrap');
                        if (anchor && anchor.parentElement) {
                            header = utils.createElement('div', { id: 'tdm-faction-header-metrics', style: { margin: '6px 0 4px 0', padding: '4px 6px', background: '#2d2c2c', border: '1px solid #444', borderRadius: '4px', fontSize: '0.75em', display: 'flex', gap: '12px', flexWrap: 'wrap' } });
                            anchor.parentElement.insertBefore(header, anchor.nextSibling);
                        }
                    }
                    if (header) {
                        const myIdStr = String(state.user?.tornId || '');
                        const activeDibs = (state.dibsData || []).filter(d => d.dibsActive);
                        const myDibsCount = activeDibs.filter(d => String(d.userId) === myIdStr).length;
                        const medDealsArr = Object.values(state.medDeals || {}).filter(s => s && s.isMedDeal);
                        const myMedDeals = medDealsArr.filter(s => String(s.medDealForUserId) === myIdStr).length;
                        const totalMedDeals = medDealsArr.length;
                        const html = `
                            <span style="color:#4caf50">My Dibs: ${myDibsCount}</span>
                            <span style="color:#ff9800">All Dibs: ${activeDibs.length}</span>
                            <span style="color:#2196f3">My Med Deals: ${myMedDeals}</span>
                            <span style="color:#9c27b0">All Med Deals: ${totalMedDeals}</span>
                        `;
                        if (header.innerHTML.trim() !== html.trim()) header.innerHTML = html;
                    }
                }
            } catch(_) { /* non-fatal */ }
            const members = container.querySelectorAll('.f-war-list .table-body > li.table-row');
            if (!members.length) { 
                return; 
            }

            members.forEach(memberLi => {
                const dibsDealsContainer = memberLi.querySelector('.tdm-dibs-deals-container');
                const notesContainer = memberLi.querySelector('.tdm-notes-container');
                if (!dibsDealsContainer && !notesContainer) return;

                const memberIdLink = memberLi.querySelector('a[href*="profiles.php?XID="]');
                if (!memberIdLink) return;

                const opponentId = memberIdLink.href.match(/XID=(\d+)/)[1];
                const opponentName = utils.sanitizePlayerName(memberIdLink.textContent, opponentId, { fallbackPrefix: 'Opponent' });

                const dibsCell = dibsDealsContainer ? dibsDealsContainer.querySelector('.dibs-cell') : null;
                const notesCell = notesContainer ? notesContainer.querySelector('.notes-cell') : null;

                // --- Update Dibs & Med Deal Cell ---
                // Note: column hiding on own-faction page handled centrally by updateColumnVisibilityStyles
                if (dibsDealsContainer) {
                    const dibsButton = dibsCell?.querySelector('.dibs-button');
                    const medDealButton = dibsCell?.querySelector('.med-deal-button');

                    // Update buttons if present (harmless if the column is hidden by CSS)
                    if (dibsButton && utils.updateDibsButton) utils.updateDibsButton(dibsButton, opponentId, opponentName);
                    if (medDealButton && utils.updateMedDealButton) utils.updateMedDealButton(medDealButton, opponentId, opponentName);
                }

                // --- Conditional Notes Cell Update START ---
                const noteButton = notesCell.querySelector('.note-button');
                const userNote = state.userNotes[opponentId];
                
                if (noteButton) {
                    const content = userNote?.noteContent || '';
                    utils.updateNoteButtonState(noteButton, content);
                    noteButton.onclick = (e) => ui.openNoteModal(opponentId, opponentName, content, e.currentTarget);
                    noteButton.disabled = false;
                }
                // --- Conditional Notes Cell Update END ---
            });
        },

        injectAttackPageUI: async () => {
            const opponentId = new URLSearchParams(window.location.search).get('user2ID');
            if (!opponentId) return;

            ui.createSettingsButton();
            const appHeaderWrapper = document.querySelector('.playersModelWrap___dkqHO');
            if (!appHeaderWrapper) return;
            let attackContainer = document.getElementById('tdm-attack-container');
            if (!attackContainer) {
                attackContainer = utils.createElement('div', { id: 'tdm-attack-container', style: { margin: '10px', padding: '10px', background: '#2d2c2c', borderRadius: '5px', border: '1px solid #444', textAlign: 'center', color: 'white' } });
                appHeaderWrapper.insertAdjacentElement('afterend', attackContainer);
            }

            // Robustly derive opponentName from attack page header — handle multiple id/class patterns & fallbacks
            let opponentName = null;
            try {
                const playersEl = document.querySelector('.players___eKiHL');
                if (playersEl) {
                    // Candidate selectors include legacy 'playername_', Torn's '-name' suffixes, and common user-name classes
                    const candidates = playersEl.querySelectorAll('span[id^="playername_"], span[id$="-name"], span.user-name, span.userName___loAWK, a[href*="profiles.php?XID="]');
                    if (candidates && candidates.length) {
                        // Prefer the last non-empty candidate (opponent name usually appears second)
                        for (let i = candidates.length - 1; i >= 0; i--) {
                            const txt = (candidates[i].textContent || '').trim();
                            if (!txt) continue;
                            if (/^back to profile$/i.test(txt) || /^profile$/i.test(txt)) continue;
                            opponentName = txt;
                            break;
                        }
                    }
                }
            } catch(_) { opponentName = null; }
            // Fallback to known local dibs data or explicit ID if still missing
            if (!opponentName) {
                try {
                    const dib = Array.isArray(state.dibsData) ? state.dibsData.find(d => d && String(d.opponentId) === String(opponentId) && (d.opponentname || d.opponentName)) : null;
                    if (dib) opponentName = dib.opponentname || dib.opponentName || null;
                } catch(_) { opponentName = null; }
            }
            const safeOpponentName = opponentName || `ID ${opponentId}`;
            opponentName = utils.sanitizePlayerName(safeOpponentName, opponentId);

            // --- FIX START: Build new content in a fragment to prevent flicker ---
            const contentFragment = document.createDocumentFragment();

            // Defer Score Cap Notification (non-blocking UI)
            // Buttons render immediately; warning is verified asynchronously and updated in place
            setTimeout(() => {
                const existingWarning = attackContainer.querySelector('.score-cap-warning');
                // Individual cap banner on attack page only after user cap reached
                if (!state.user.hasReachedScoreCap) { if (existingWarning) existingWarning.remove(); }
                (async () => {
                    try {
                        const oppFactionId = state.warData?.opponentFactionId || state.lastOpponentFactionId || state?.warData?.opponentId;
                        if (!oppFactionId) { if (existingWarning) existingWarning.remove(); return; }
                        // Prefer cached opponent faction members to determine membership
                        const tf = state.tornFactionData || {};
                        let inOppFaction = false;
                        const entry = tf[oppFactionId];
                        const readMembers = (data) => {
                            const m = data?.members || data?.member || data?.faction?.members;
                            if (!m) return [];
                            return Array.isArray(m) ? m : Object.values(m);
                        };
                        if (entry?.data) {
                            const arr = readMembers(entry.data);
                            inOppFaction = !!arr.find(x => String(x.id) === String(opponentId));
                        }
                        // If not found and freshness window allows, try a light members refresh
                        if (!inOppFaction) {
                            try { await api.getTornFaction(state.user.actualTornApiKey, 'members', oppFactionId); } catch(_) {}
                            const e2 = (state.tornFactionData || {})[oppFactionId];
                            if (e2?.data) {
                                const arr2 = readMembers(e2.data);
                                inOppFaction = !!arr2.find(x => String(x.id) === String(opponentId));
                            }
                        }
                        if (inOppFaction && state.user.hasReachedScoreCap) {
                            if (!existingWarning) {
                                const scoreCapWarning = utils.createElement('div', {
                                    className: 'score-cap-warning',
                                    style: { padding: '10px', marginBottom: '10px', backgroundColor: config.CSS.colors.error, color: 'white', borderRadius: '5px', fontWeight: 'bold' },
                                    textContent: 'Your individual score cap is reached. Do not attack.'
                                });
                                attackContainer.insertBefore(scoreCapWarning, attackContainer.firstChild);
                            }
                        } else if (existingWarning) {
                            existingWarning.remove();
                        }
                    } catch (error) {
                        tdmlogger('warn', `[Failed to verify opponent's faction for score cap warning] ${error}`);
                    }
                })();
            }, 0);

            try {
                // Build the button rows
                const opponentDibs = state.dibsData.find(d => d.opponentId === opponentId && d.dibsActive);
                const opponentMedDeal = state.medDeals[opponentId];
                const opponentNote = state.userNotes[opponentId];

                const buttonRow = utils.createElement('div', { style: { display: 'flex', gap: '4px', justifyContent: 'center', flexWrap: 'nowrap', marginBottom: '8px' } });
                // ... (Button creation logic is the same)
                const dibsBtn = utils.createElement('button', { className: 'btn dibs-btn', style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', borderColor: '#004d4f !important', borderRadius: '3px' } });
                if (utils.updateDibsButton || opponentDibs) utils.updateDibsButton(dibsBtn, opponentId, opponentName, { opponentPolicyCheck: true });
                buttonRow.appendChild(dibsBtn);

                if (state.warData.warType === 'Termed War') {
                    const medDealBtn = utils.createElement('button', { className: 'btn med-deal-btn', style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', borderColor: '#004d4f !important', borderRadius: '3px' } });
                    if (utils.updateMedDealButton || opponentMedDeal) utils.updateMedDealButton(medDealBtn, opponentId, opponentName);
                    buttonRow.appendChild(medDealBtn);
                }

                const noteContent = opponentNote?.noteContent || '';
                const notesBtn = utils.createElement('button', { textContent: noteContent || 'Note', title: noteContent, className: 'btn ' + (noteContent.trim() !== '' ? 'active-note-button' : 'inactive-note-button'), style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', borderColor: '#004d4f !important', borderRadius: '3px' }, onclick: (e) => ui.openNoteModal(opponentId, opponentName, noteContent, e.currentTarget) });
                buttonRow.appendChild(notesBtn);

                // Always create a Retal button placeholder; updater will control visibility
                const retalBtn = utils.createElement('button', { className: 'btn retal-btn btn-retal-inactive', style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', borderColor: '#004d4f !important', borderRadius: '3px', marginLeft: 'auto', display: 'none' }, disabled: true, onclick: () => ui.sendRetaliationAlert(opponentId, opponentName) });
                buttonRow.appendChild(retalBtn);
                ui.updateRetaliationButton(retalBtn, opponentId, opponentName);
                contentFragment.appendChild(buttonRow);

                const assistRow = utils.createElement('div', { style: { display: 'flex', gap: '4px', justifyContent: 'center', flexWrap: 'wrap' } });
                assistRow.appendChild(utils.createElement('span', { textContent: 'Need Assistance:', style: { alignSelf: 'center', fontSize: '0.9em', color: '#ffffffff', marginRight: '2px' } }));
                const assistanceButtons = [{ text: 'Smoke/Flash (Speed)', message: 'Need Smoke/Flash on' }, { text: 'Tear/Pepper (Dex)', message: 'Need Tear/Pepper on' }, { text: 'Help Kill', message: 'Help Kill' }, { text: 'Target Down', message: 'Target Down' }];
                assistanceButtons.forEach(btnInfo => {
                    assistRow.appendChild(utils.createElement('button', { className: 'btn req-assist-button', textContent: btnInfo.text, onclick: () => ui.sendAssistanceRequest(btnInfo.message, opponentId, opponentName), style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', borderColor: '#004d4f !important', borderRadius: '3px' } }));
                });
                contentFragment.appendChild(assistRow);

                // Replace only the button/assist rows, leaving the warning intact
                const rowsToRemove = attackContainer.querySelectorAll('div:not(.score-cap-warning)');
                rowsToRemove.forEach(row => row.remove());
                attackContainer.appendChild(contentFragment);

                // Ownership summary line (Dibs / Med Deal owners)
                try {
                    let ownershipLine = document.getElementById('tdm-ownership-line');
                    if (!ownershipLine) {
                        ownershipLine = utils.createElement('div', { id: 'tdm-ownership-line', style: { marginTop: '6px', fontSize: '0.75em', opacity: 0.9 } });
                        attackContainer.appendChild(ownershipLine);
                    }
                    const activeDib = (state.dibsData || []).find(d => d.dibsActive && String(d.opponentId) === String(opponentId));
                    const medDeal = state.medDeals?.[opponentId];
                    const dibOwner = activeDib ? (activeDib.userId === state.user.tornId ? 'You' : activeDib.username) : 'None';
                    const medOwner = (medDeal && medDeal.isMedDeal) ? (medDeal.medDealForUserId === state.user.tornId ? 'You' : (medDeal.medDealForUsername || 'Someone')) : 'None';
                    const dibColor = dibOwner === 'You' ? '#4caf50' : (dibOwner === 'None' ? '#aaa' : '#ff9800');
                    const medColor = medOwner === 'You' ? '#2196f3' : (medOwner === 'None' ? '#aaa' : '#9c27b0');
                    const html = `<span style="color:${dibColor};">Dibs: ${dibOwner}</span> | <span style="color:${medColor};">Med Deal: ${medOwner}</span>`;
                    if (ownershipLine.innerHTML !== html) ownershipLine.innerHTML = html;
                    if (activeDib && activeDib.userId !== state.user.tornId) {
                        dibsBtn.title = `Owned by ${activeDib.username}. Removing requires permission.`;
                    }
                    if (medDeal && medDeal.isMedDeal && medDeal.medDealForUserId !== state.user.tornId) {
                        const medBtn = attackContainer.querySelector('button.med-deal-btn');
                        if (medBtn) medBtn.title = `Med Deal owned by ${medDeal.medDealForUsername}`;
                    }
                } catch(_) { /* non-fatal */ }
                // --- FIX END ---

                // Display dib / med summary via existing message box system
                try { ui.showAttackPageDibMedSummary?.(opponentId); } catch(_) {}

                // Auto-remove dib on successful attack if policy requires re-dib
                try {
                    const opts = utils.getDibsStyleOptions();
                    if (opts.mustRedibAfterSuccess) {
                        // Only if I have active dib on this opponent
                        const myDib = (state.dibsData || []).find(d => d.dibsActive && d.userId === state.user.tornId && String(d.opponentId) === String(opponentId));
                        if (myDib) {
                            // Observe for Torn result title appearing
                            const selector = 'div.title___fOh2J';
                            const checkAndRemove = async (node) => {
                                try {
                                    const el = node && node.nodeType === 1 ? node.querySelector(selector) : document.querySelector(selector);
                                    const text = el?.textContent?.trim() || '';
                                    const defeated = text.startsWith('You defeated');
                                    const includesName = defeated && (text.toLowerCase().includes(String(opponentName).toLowerCase()));
                                    if (defeated && includesName) {
                                        // Fire remove with explicit reason
                                        await api.post('removeDibs', { dibsDocId: myDib.id, removedByUsername: state.user.tornUsername, factionId: state.user.factionId, removalReason: 'Dib Successfully Completed' });
                                        ui.showMessageBox(`Dibs removed: You defeated ${opponentName}.`, 'success');
                                        handlers.debouncedFetchGlobalData();
                                        if (state.script.mutationObserver) { try { state.script.mutationObserver.disconnect(); } catch(_){} }
                                    }
                                } catch(_) { /* ignore */ }
                            };
                            // Initial check in case the element already exists
                            checkAndRemove(document);
                            // Observe body for changes
                            if (state.script.mutationObserver) { try { state.script.mutationObserver.disconnect(); } catch(_){} }
                            state.script.mutationObserver = utils.registerObserver(new MutationObserver((mutations) => {
                                for (const m of mutations) {
                                    for (const n of m.addedNodes) {
                                        checkAndRemove(n);
                                    }
                                }
                            }));
                            state.script.mutationObserver.observe(document.body, { childList: true, subtree: true });
                        }
                    }
                } catch(_) { /* non-fatal */ }
            } catch (error) {
                tdmlogger('error', `[Error in attack page UI injection] ${error}`);
                attackContainer.innerHTML = '<p style="color: #ff6b6b;">Error loading attack page UI</p>';
            }
        },

        // Prominent single-line (or two-line) message summarizing active dib / med deal on attack page.
        // Uses cached snapshots only; no network calls.
        showAttackPageDibMedSummary: (explicitOpponentId = null) => {
            try {
                if (!state.page.isAttackPage) return;
                const opponentId = explicitOpponentId || new URLSearchParams(window.location.search).get('user2ID');
                if (!opponentId) return;
                const activeDib = (state.dibsData || []).find(d => d.dibsActive && String(d.opponentId) === String(opponentId));
                const medDeal = state.medDeals?.[opponentId];
                if (!activeDib && !(medDeal && medDeal.isMedDeal)) return; // Nothing to show
                const dibOwner = activeDib ? (activeDib.userId === state.user.tornId ? 'You' : (activeDib.username || 'Unknown')) : null;
                const medOwner = (medDeal && medDeal.isMedDeal) ? (medDeal.medDealForUserId === state.user.tornId ? 'You' : (medDeal.medDealForUsername || 'Unknown')) : null;
                const parts = [];
                if (dibOwner) parts.push(`Dib: ${dibOwner}`);
                if (medOwner) parts.push(`Med Deal: ${medOwner}`);
                const msg = parts.join(' | ');
                // Use info variant and short duration (3s) so it does not clutter
                ui.showMessageBox(msg, 'info', 3000);
            } catch(_) { /* silent */ }
        },

        sendAssistanceRequest: async (message, opponentId, opponentName) => {
            const _pl = utils.buildProfileLink(opponentId, opponentName);
            const opponentLink = (_pl && _pl.outerHTML) ? _pl.outerHTML : (`<a href="/profiles.php?XID=${opponentId}">${utils.sanitizePlayerName(opponentName, opponentId)}</a>`);
            const fullMessage = `TDM - ${message} ${opponentLink}`;
            const facId = state.user.factionId;
            // Copy to clipboard as fallback
            try { if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(fullMessage); } catch(_) {}

            // Or get the faction button by id
            const chatButton = document.querySelector(`#channel_panel_button\\:faction-${facId}`);
            if (storage.get('pasteMessagesToChatEnabled', true)) {
                ui.enqueueFactionChatMessage(fullMessage, facId, chatButton);
            } else {
                // User opted to only copy messages to clipboard (no auto-paste)
                ui.showTransientMessage('Message copied to clipboard (auto-paste disabled)', { type: 'info', timeout: 1400 });
            }
            
        },
        sendDibsMessage: async (opponentId, opponentName, assignedToUsername = null, customPrefix = null) => {
            try {
                // Always create/copy the dibs message to clipboard. The paste behavior
                // (automatically populating faction chat) is controlled separately by
                // the unified `pasteMessagesToChatEnabled` setting later in this function.
                // Simple cooldown to avoid duplicate messages if UI double-fires
                const now = Date.now();
                const last = state.session._lastDibsChat || { ts: 0, opponentId: null, text: '' };
                if (last.opponentId === String(opponentId) && (now - last.ts) < 3000) return;

                // Prefer a cached/stored opponent name to avoid picking up navigation anchors
                const effectiveName = opponentName || state.ui?.opponentStatusCache?.name || state.session?.userStatusCache?.[String(opponentId)]?.name || null;
                const _pl2 = utils.buildProfileLink(opponentId, effectiveName || opponentName);
                const link = (_pl2 && _pl2.outerHTML) ? _pl2.outerHTML : (`<a href="/profiles.php?XID=${opponentId}">${utils.sanitizePlayerName(effectiveName || opponentName, opponentId)}</a>`);
                const prefix = customPrefix || (assignedToUsername ? `Dibs (for ${assignedToUsername}):` : 'Dibs:');

                // Derive opponent status (Hospital/Travel/etc) and remaining time if relevant.
                // Reuse logic from opponentStatusCache where possible; fallback to cached member/user status.
                let statusFragment = '';
                try {
                    const oppIdStr = String(opponentId);
                    let canonical = '';
                    let until = 0;
                    let rawDesc = '';
                    // 1. Try opponentStatusCache if same opponent & fresh (<15s)
                    const osc = state.ui?.opponentStatusCache;
                    if (osc && osc.opponentId === oppIdStr && (now - (osc.lastFetch||0)) < 15000) {
                        rawDesc = osc.text || '';
                        // Heuristic: if we stored Hospital countdown externally keep untilEpoch
                        until = osc.untilEpoch || 0;
                        if (/hospital/i.test(rawDesc) || rawDesc === 'Hosp') canonical = 'Hospital';
                    }
                    // 2. Fallback to faction member data (already cached by other flows)
                    if (!canonical) {
                        const oppFactionId = state.lastOpponentFactionId || state?.warData?.opponentId;
                        const tf = state.tornFactionData?.[oppFactionId];
                        if (tf?.data?.members) {
                            const arr = Array.isArray(tf.data.members) ? tf.data.members : Object.values(tf.data.members);
                            const m = arr.find(x => String(x.id) === oppIdStr);
                            if (m?.status) {
                                // Check for previous status from unified tracking to help HospitalAbroad detection
                                const prevUnified = state.unifiedStatus?.[oppIdStr];
                                const rec = utils.buildUnifiedStatusV2(m, prevUnified);
                                canonical = rec?.canonical || '';
                                rawDesc = m.status.description || m.status.state || '';
                                if (canonical === 'Hospital' || canonical === 'HospitalAbroad') until = Number(m.status.until)||0;
                            }
                        }
                    }
                    // 3. Fallback to per-user cached status helper (10s TTL internally managed)
                    if (!canonical) {
                        // getUserStatus returns a promise; we'll queue a secondary update if it resolves after initial send
                        try {
                            utils.getUserStatus(oppIdStr).then(us => {
                                if (!us) return;
                                // If we already populated a fragment skip unless we had none
                                if (statusFragment) return;
                                let c2 = us.canonical || us.raw?.state || '';
                                if (!c2 || c2 === 'Okay') return;
                                let until2 = (c2 === 'Hospital' || c2 === 'HospitalAbroad') ? (us.until || 0) : 0;
                                let frag = '';
                                if (c2 === 'Hospital' || c2 === 'HospitalAbroad') {
                                    let remain2 = until2 ? (Math.floor(until2) - Math.floor(Date.now()/1000)) : 0;
                                    if (remain2 > 0 && remain2 < 3600) {
                                        const mm2 = Math.floor(remain2/60);
                                        const ss2 = (remain2%60).toString().padStart(2,'0');
                                        const hospLabel = c2 === 'HospitalAbroad' ? 'HospAbroad' : 'Hosp';
                                        frag = ` - ${hospLabel} ${mm2}:${ss2}`;
                                    } else {
                                        const hospLabel = c2 === 'HospitalAbroad' ? 'HospAbroad' : 'Hosp';
                                        frag = ` - ${hospLabel}`;
                                    }
                                } else {
                                    const shortMap = { Travel: 'Trav', Abroad: 'Abroad', Jail: 'Jail' };
                                    frag = ` - ${shortMap[c2]||c2}`;
                                }
                                // Attempt to append by editing textarea if value still matches base message
                                const facId = state.user.factionId;
                                const chatTextArea = document.querySelector(`#faction-${facId} > div.content___n3GFQ > div.root___WUd1h > textarea`);
                                if (chatTextArea && chatTextArea.value && chatTextArea.value.includes(link) && !chatTextArea.value.includes(frag.trim())) {
                                    chatTextArea.value = `${chatTextArea.value}${frag}`.trim();
                                    chatTextArea.dispatchEvent(new Event('input', { bubbles: true }));
                                }
                            }).catch(()=>{});
                        } catch(_) { /* noop */ }
                    }

                    // Format time left for Hospital/HospitalAbroad (avoid travel ETA noise for simplicity unless clearly available)
                    if (canonical === 'Hospital' || canonical === 'HospitalAbroad') {
                        let suffix = '';
                        let remain = until ? (Math.floor(until) - Math.floor(Date.now()/1000)) : 0;
                        if (remain > 0 && remain < 3600) { // cap to <1h for compactness
                            const mm = Math.floor(remain/60);
                            const ss = (remain%60).toString().padStart(2,'0');
                            suffix = `${mm}:${ss}`;
                        }
                        if (/\d+:\d{2}/.test(rawDesc)) {
                            // Already has time string
                            const hospLabel = canonical === 'HospitalAbroad' ? 'HospAbroad' : 'Hosp';
                            statusFragment = ` - ${hospLabel} ${rawDesc.match(/\d+:\d{2}/)[0]}`;
                        } else if (suffix) {
                            const hospLabel = canonical === 'HospitalAbroad' ? 'HospAbroad' : 'Hosp';
                            statusFragment = ` - ${hospLabel} ${suffix}`;
                        } else {
                            const hospLabel = canonical === 'HospitalAbroad' ? 'HospAbroad' : 'Hosp';
                            statusFragment = ` - ${hospLabel}`;
                        }
                    } else if (canonical && canonical !== 'Okay') {
                        // Short labels
                        const shortMap = { Travel: 'Trav', Abroad: 'Abroad', Jail: 'Jail' };
                        statusFragment = ` - ${shortMap[canonical]||canonical}`;
                    }
                } catch (e) { /* swallow status errors */ }

                const fullMessage = `${prefix} ${link}${statusFragment}`.trim();

                // Copy to clipboard as fallback
                try { if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(fullMessage); } catch(_) {}

                const facId = state.user.factionId;
                const chatButton = document.querySelector(`#channel_panel_button\\:faction-${facId}`);
                if (storage.get('pasteMessagesToChatEnabled', true)) {
                    ui.enqueueFactionChatMessage(fullMessage, facId, chatButton);
                    // ui.showTransientMessage('Sent to chat (and copied to clipboard)', { type: 'info', timeout: 1400 });
                } else {
                    // User opted to only copy messages to clipboard (no auto-paste)
                    ui.showTransientMessage('Message copied to clipboard (auto-paste disabled)', { type: 'info', timeout: 1400 });
                }
                state.session._lastDibsChat = { ts: now, opponentId: String(opponentId), text: fullMessage };
            } catch (_) { /* non-fatal */ }
        },
        // Send a short last-action message to faction chat (uses same enqueue/populate helpers)
        sendLastActionToChat: async (opponentId, opponentName, statusLabel = null, lastActionShort = null) => {
            try {
                if (!opponentId) return;
                const facId = state.user.factionId;
                // Build profile link
                const _pl3 = utils.buildProfileLink(opponentId, opponentName || `ID ${opponentId}`);
                const link = (_pl3 && _pl3.outerHTML) ? _pl3.outerHTML : (`<a href="/profiles.php?XID=${opponentId}">${utils.sanitizePlayerName(opponentName || `ID ${opponentId}`, opponentId)}</a>`);
                // Status and last action fragments
                const statusFrag = statusLabel ? ` - now ${statusLabel}` : '';
                const lastFrag = lastActionShort ? ` - Last Action: ${lastActionShort}` : '';
                const fullMessage = `${link}${statusFrag}${lastFrag}`.trim();

                // Copy to clipboard as fallback
                try { if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(fullMessage); } catch(_) {}

                // Try to enqueue/paste to faction chat only if user wants auto-paste.
                const chatBtn = document.querySelector(`#channel_panel_button\\:faction-${facId}`);
                if (storage.get('pasteMessagesToChatEnabled', true)) {
                    ui.enqueueFactionChatMessage(fullMessage, facId, chatBtn);
                    // ui.showTransientMessage('Sent to chat (and copied to clipboard)', { type: 'info', timeout: 1400 });
                } else {
                    // User opted to only copy messages to clipboard (no auto-paste)
                    ui.showTransientMessage('Message copied to clipboard (auto-paste disabled)', { type: 'info', timeout: 1400 });
                }
            } catch (_) { /* non-fatal */ }
        },
        // Queue a faction chat message ensuring the correct channel is open and textarea ready.
        enqueueFactionChatMessage(message, factionId, chatButton) {
            const MAX_WAIT_MS = 5000; // Increased timeout
            const CHECK_INTERVAL = 200; // Slightly longer checks
            const started = Date.now();
            let injected = false;
            const normalizedMessage = (typeof message === 'string' ? message.trim() : '');
            const textareaSelector = `#faction-${factionId} > div.content___n3GFQ > div.root___WUd1h > textarea`;
            const messageAlreadyPresent = () => {
                const chatTextArea = document.querySelector(textareaSelector);
                if (!chatTextArea) return false;
                const currentValue = (chatTextArea.value || '').trim();
                return Boolean(normalizedMessage && currentValue.includes(normalizedMessage));
            };
            let chatWasAlreadyOpen = !!document.querySelector(textareaSelector);
            const attempt = () => {
                const chatTextArea = document.querySelector(textareaSelector);
                if (messageAlreadyPresent()) {
                    injected = true;
                    return;
                }
                const correctChannel = !!document.querySelector(`#channel_panel_button\\:faction-${factionId}.active, #channel_panel_button\\:faction-${factionId}[aria-selected="true"]`);
                const ready = chatTextArea && (chatWasAlreadyOpen || correctChannel);
                if (ready) {
                    injected = true;
                    const delay = chatWasAlreadyOpen ? 500 : 900;
                    setTimeout(() => {
                        ui.populateChatMessage(message);
                    }, delay);
                    return;
                }
                if ((Date.now() - started) >= MAX_WAIT_MS) {
                    if (messageAlreadyPresent()) return;
                    if (chatTextArea) {
                        ui.populateChatMessage(message);
                        setTimeout(() => {
                            if (!messageAlreadyPresent()) {
                                ui.fallbackCopyToClipboard(message, 'Paste Manually');
                            }
                        }, 400);
                        return;
                    }
                    ui.fallbackCopyToClipboard(message, 'Paste Manually');
                    return;
                }
                setTimeout(attempt, CHECK_INTERVAL);
            };
            if (!chatWasAlreadyOpen && chatButton) {
                chatButton.click();
                setTimeout(attempt, 400);
            } else {
                attempt();
            }
        },
        populateChatMessage(message) {
            const facId = state.user.factionId;
            const chatTextArea = document.querySelector(`#faction-${facId} > div.content___n3GFQ > div.root___WUd1h > textarea`);

            if (chatTextArea) {
                // 1. Set the value of the textarea.
                // This might be read by the component during its sync process.
                chatTextArea.value = message;

                // Optional: Dispatch the 'input' event.
                // Keep this in case the component also relies on it in combination with the mutation.
                const inputEvent = new Event('input', { bubbles: true });
                chatTextArea.dispatchEvent(inputEvent);

                // 2. Trigger a DOM mutation to force the component to sync.
                // Toggling a data attribute is a reliable and non-visual way to do this.
                const dataAttributeName = 'data-userscript-synced'; // Use a distinct attribute name
                if (chatTextArea.hasAttribute(dataAttributeName)) {
                    chatTextArea.removeAttribute(dataAttributeName);
                } else {
                    chatTextArea.setAttribute(dataAttributeName, Date.now().toString()); // Add or update the attribute
                }

                // Focus the textarea to ensure it's ready for user to send
                try {
                    chatTextArea.focus();
                } catch(_) { /* ignore focus errors */ }

                tdmlogger('debug', 'Textarea value set and DOM mutation triggered.');

                ui.showMessageBox('Message added to faction chat. Click send manually.', 'success');
            } else {
                tdmlogger('warn', `Chat textbox not found, message: ${message}`);
                ui.fallbackCopyToClipboard(message, 'Chat not found');
            }
        },
        fallbackCopyToClipboard(text, reason) {
            // Robust clipboard fallback
            const perform = async () => {
                try {
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        await navigator.clipboard.writeText(text);
                        // ui.showMessageBox(`${reason}. Message Copied!`, 'info');
                        return true;
                    }
                } catch (_) { /* fallback to legacy */ }
                // Legacy method
                try {
                    const ta = document.createElement('textarea');
                    ta.style.position = 'fixed';
                    ta.style.opacity = '0';
                    ta.value = text;
                    document.body.appendChild(ta);
                    ta.focus();
                    ta.select();
                    const ok = document.execCommand('copy');
                    document.body.removeChild(ta);
                    // ui.showMessageBox(`${reason}. ${ok ? 'Message Copied!' : 'Copy failed.'}`, ok ? 'info' : 'error');
                } catch (e) {
                    ui.showMessageBox(`${reason}. Copy unavailable.`, 'error');
                }
            };
            perform();
        },
        sendRetaliationAlert: async (opponentId, opponentName) => {
            const retalOpp = state.retaliationOpportunities[opponentId];
            if (!retalOpp) return;
            const now = Math.floor(Date.now() / 1000);
            const timeRemaining = retalOpp.retaliationEndTime - now;
            let timeStr = 'expired';
            if (timeRemaining > 0) {
                const minutes = Math.floor(timeRemaining / 60);
                const seconds = Math.floor(timeRemaining % 60);
                timeStr = `${minutes}:${seconds.toString().padStart(2, '0')}`;
            }
            const _pl4 = utils.buildProfileLink(opponentId, opponentName);
            const opponentLink = (_pl4 && _pl4.outerHTML) ? _pl4.outerHTML : (`<a href="/profiles.php?XID=${opponentId}">${utils.sanitizePlayerName(opponentName, opponentId)}</a>`);
            const fullMessage = `Retal Available ${opponentLink} time left: ${timeStr} Hospitalize`;
            const facId = state.user.factionId;
            const chatButton = document.querySelector(`#channel_panel_button\\:faction-${facId}`);

            try {
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    await navigator.clipboard.writeText(fullMessage);
                    // ui.showTransientMessage('Retal message copied to clipboard', { type: 'info', timeout: 1600 });
                } else {
                    ui.fallbackCopyToClipboard(fullMessage, 'Retal message copied to clipboard');
                }
            } catch (e) {
                try { ui.fallbackCopyToClipboard(fullMessage, 'Retal message copied to clipboard'); } catch(_) {}
            }

            if (storage.get('pasteMessagesToChatEnabled', true)) {
                ui.enqueueFactionChatMessage(fullMessage, facId, chatButton);
            } else {
                ui.showTransientMessage('Message copied to clipboard (auto-paste disabled)', { type: 'info', timeout: 1400 });
            }
        },

        updateRetaliationButton: (button, opponentId, opponentName) => {
            // Check if alert buttons are globally disabled
            const alertButtonsEnabled = storage.get('alertButtonsEnabled', true);
            if (!alertButtonsEnabled) {
                // Clear any prior interval and hide the button
                if (button._retalIntervalId) {
                    try { utils.unregisterInterval(button._retalIntervalId); } catch(_) {}
                    button._retalIntervalId = null;
                }
                button.style.display = 'none';
                button.disabled = true;
                button.innerHTML = '';
                button.className = 'btn retal-btn tdm-alert-btn tdm-alert-inactive';
                return;
            }

            // Check if the alert observer is actively managing this button
            const meta = state.rankedWarChangeMeta && state.rankedWarChangeMeta[opponentId];
            const hasActiveAlert = !!(meta && (meta.activeType || meta.pendingText));
            if (hasActiveAlert) {
                // Let the alert observer handle this button - don't interfere
                return;
            }

            // Clear any prior interval tied to this button
            if (button._retalIntervalId) {
                try { utils.unregisterInterval(button._retalIntervalId); } catch(_) {}
                button._retalIntervalId = null;
            }

            const show = () => {
                button.style.display = 'inline-block';
                button.disabled = false;
                button.innerHTML = '';
                button.className = 'btn retal-btn tdm-alert-btn tdm-alert-retal';
                button.onclick = () => ui.sendRetaliationAlert(opponentId, opponentName);
            };

            const hide = () => {
                button.style.display = 'none';
                button.disabled = true;
                button.innerHTML = '';
                button.className = 'btn retal-btn tdm-alert-btn tdm-alert-inactive';
                if (button._retalIntervalId) {
                    try { utils.unregisterInterval(button._retalIntervalId); } catch(_) {}
                    button._retalIntervalId = null;
                }
            };

            const computeAndRender = () => {
                const current = state.retaliationOpportunities[opponentId];
                if (!current) {
                    hide();
                    return false;
                }
                const now = Math.floor(Date.now() / 1000);
                const timeRemaining = current.retaliationEndTime - now;
                if (timeRemaining <= 0) {
                    hide();
                    return false;
                }
                show();
                const mm = Math.floor(timeRemaining / 60);
                const ss = ('0' + (timeRemaining % 60)).slice(-2);
                button.textContent = `Retal👉${mm}:${ss}`;
                return true;
            };

            // Initial render
            const active = computeAndRender();
            if (!active) return;

            // Keep ticking; auto-hide when expired or fulfilled
            button._retalIntervalId = utils.registerInterval(setInterval(() => {
                const stillActive = computeAndRender();
                if (!stillActive) {
                    // interval cleared in hide(); just be safe
                    if (button._retalIntervalId) {
                        try { utils.unregisterInterval(button._retalIntervalId); } catch(_) {}
                        button._retalIntervalId = null;
                    }
                }
            }, 1000));
        },

        openNoteModal: (tornID, tornUsername, currentNoteContent, buttonElement) => {
            state.ui.currentNoteButtonElement = buttonElement || null;
            state.ui.currentNoteTornID = tornID;
            state.ui.currentNoteTornUsername = tornUsername;
            const parseTags = (txt) => {
                if (!txt) return { tags: [], body: '' };
                const lines = txt.split(/\n/);
                if (lines.length && /^#tags:/i.test(lines[0])) {
                    const raw = lines.shift().replace(/^#tags:/i,'').trim();
                    const tags = raw ? raw.split(/[,\s]+/).filter(Boolean).slice(0,12) : [];
                    return { tags, body: lines.join('\n') };
                }
                return { tags: [], body: txt };
            };
            const buildNoteText = (tags, body) => {
                const cleanTags = (tags||[]).filter(Boolean);
                return cleanTags.length ? `#tags: ${cleanTags.join(', ')}\n${body}`.trim() : body.trim();
            };
            const ensureModal = () => {
                if (state.ui.noteModal) return;
                const modal = utils.createElement('div', { id: 'user-note-modal', className: 'tdm-note-modal' });
                modal.innerHTML = `
                    <div class="tdm-note-modal-header">
                        <div class="tdm-note-title-wrap"><span class="tdm-note-title"></span></div>
                        <div class="tdm-note-actions">
                        <button class="tdm-note-btn tdm-note-copy" title="Copy note (Ctrl+C)">⧉</button>
                        <button class="tdm-note-btn tdm-note-clear" title="Clear note">✕</button>
                        <button class="tdm-note-btn tdm-note-close" title="Close">×</button>
                        </div>
                    </div>
                        <div class="tdm-note-tags-row">
                            <div class="tdm-note-quick-tags"></div>
                            <input type="text" class="tdm-note-tag-input" placeholder="" maxlength="24" style="display:none;margin-left:6px;" />
                            <button type="button" class="tdm-note-add-tag-btn" style="margin-left:6px;padding:4px 8px;border-radius:4px;border:1px solid #334155;background:#0f172a;color:#cbd5e1;">Add New Tag</button>
                        </div>
                        <div class="tdm-note-tags-empty" style="display:none"></div>
                    <textarea class="tdm-note-text" rows="1" placeholder="Enter note..." maxlength="5000" style="resize:vertical;min-height:1.4em;max-height:18em;"></textarea>
                    <div class="tdm-note-footer">
                        <span class="tdm-note-status">Idle</span>
                        <span class="tdm-note-char">0/5000</span>
                        <span class="tdm-note-meta"></span>
                    </div>
                    <div class="tdm-note-snapshot" style="display:none;margin:4px 0 8px;padding:6px 8px;border:1px solid #1e293b;background:#0f172a;border-radius:6px;font:11px/1.4 monospace;color:#cbd5e1"></div>
                    <div class="tdm-note-history-wrap" style="margin-top:6px;max-height:130px;overflow:auto;display:none;">
                        <div class="tdm-note-history-header" style="font-size:10px;color:#64748b;display:flex;align-items:center;gap:8px;">
                            <span>Recent Status Transitions</span>
                            <button class="tdm-note-history-refresh" title="Refresh transitions" style="background:#1f2937;border:1px solid #334155;color:#94a3b8;border-radius:4px;font-size:10px;padding:2px 6px;cursor:pointer;">↻</button>
                        </div>
                        <ul class="tdm-note-history" style="list-style:none;margin:4px 0 0;padding:0;font:11px/1.3 monospace;">
                        </ul>
                    </div>
                    
                    `;
                document.body.appendChild(modal);
                state.ui.noteModal = modal;
                state.ui.noteTextarea = modal.querySelector('.tdm-note-text');
                // Styles
                if (!document.getElementById('tdm-note-style')) {
                        const style = utils.createElement('style', { id:'tdm-note-style', textContent:`
                        .tdm-note-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:linear-gradient(145deg, rgba(15,23,42,0.96), rgba(8,15,26,0.92));color:#f8fafc;border:1px solid rgba(148,163,184,0.25);border-radius:14px;z-index:10020;box-shadow:0 22px 46px -12px rgba(15,23,42,0.75);width:520px;max-width:80vw;padding:18px 20px;font:13px/1.45 'Inter','Segoe UI',sans-serif;backdrop-filter:blur(12px);max-height:85vh;overflow-y:auto;}
                        .tdm-note-modal-header{display:flex;align-items:flex-start;justify-content:space-between;margin-bottom:12px;gap:14px;border-bottom:1px solid rgba(148,163,184,0.18);padding-bottom:12px;}
                        .tdm-note-title-wrap{display:flex;flex-direction:column;gap:4px;min-width:0;}
                        .tdm-note-title{font-weight:600;font-size:18px;letter-spacing:.02em;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#e2e8f0;}
                        .tdm-note-actions{display:flex;gap:6px;}
                        .tdm-note-btn{background:rgba(30,41,59,0.75);border:1px solid rgba(148,163,184,0.35);color:#cbd5f5;cursor:pointer;border-radius:8px;width:32px;height:32px;display:flex;align-items:center;justify-content:center;font-size:14px;padding:0;transition:all .18s ease;}
                        .tdm-note-btn:hover{background:rgba(59,130,246,0.25);border-color:rgba(59,130,246,0.65);color:#e8f2ff;}
                        .tdm-note-btn:active{transform:scale(0.96);}
                        .tdm-note-text{width:85%;background:rgba(9,15,30,0.92);border:1px solid rgba(59,130,246,0.25);color:#f1f5f9;padding:10px 12px;border-radius:10px;resize:vertical;font:13px/1.55 'JetBrains Mono',monospace;min-height:40px;box-shadow:inset 0 0 0 1px rgba(30,64,175,0.18);}
                        .tdm-note-text:focus{outline:none;border-color:rgba(96,165,250,0.9);box-shadow:0 0 0 1px rgba(96,165,250,0.8);}
                        .tdm-note-footer{display:grid;grid-template-columns:auto auto 1fr;align-items:center;gap:14px;margin-top:10px;font-size:11px;color:#cbd5f5;}
                        .tdm-note-status.saving{color:#fde68a;} .tdm-note-status.saved{color:#86efac;} .tdm-note-status.error{color:#fca5a5;}
                        .tdm-note-meta{font-family:'JetBrains Mono',monospace;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
                        .tdm-note-char{font-family:'JetBrains Mono',monospace;}
                        .tdm-note-tags-row{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0 6px;align-items:center;}
                        .tdm-note-tag{background:rgba(30,64,175,0.35);color:#e0f2fe;border:1px solid rgba(125,211,252,0.45);padding:3px 8px;font-size:11px;border-radius:999px;display:inline-flex;align-items:center;gap:6px;cursor:pointer;box-shadow:0 4px 10px rgba(15,23,42,0.3);transition:background .16s ease,transform .16s ease;}
                        .tdm-note-tag:hover{background:rgba(59,130,246,0.45);transform:translateY(-1px);}
                        .tdm-note-tag-remove{font-size:12px;line-height:1;cursor:pointer;color:#94a3b8;}
                        .tdm-note-tag-remove:hover{color:#fda4af;}
                        .tdm-note-tag-input{flex:1;min-width:140px;background:rgba(9,15,30,0.92);border:1px solid rgba(59,130,246,0.25);color:#f1f5f9;padding:6px 8px;border-radius:8px;font:12px/1.35 'JetBrains Mono',monospace;}
                        .tdm-note-tag-input:focus{outline:none;border-color:rgba(96,165,250,0.9);box-shadow:0 0 0 1px rgba(96,165,250,0.6);}
                        .tdm-note-tags-empty{font-size:11px;color:#94a3b8;margin-bottom:6px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
                        .tdm-note-tags-empty span{background:rgba(59,130,246,0.18);color:#bfdbfe;padding:1px 6px;border-radius:999px;font-family:'JetBrains Mono',monospace;}
                        .tdm-note-help{margin-top:10px;font-size:10px;color:#7f8ea3;display:flex;flex-wrap:wrap;gap:10px;}
                        .tdm-conf-badge{display:inline-block;font:10px/1.1 monospace;padding:2px 4px;border-radius:4px;margin:0 2px 0 4px;vertical-align:baseline;letter-spacing:.5px}
                        .tdm-conf-LOW{background:#374151;color:#f9fafb;border:1px solid #475569;text-decoration:underline dotted;}
                        .tdm-conf-MED{background:#92400e;color:#fff;border:1px solid #b45309;}
                        .tdm-conf-HIGH{background:#065f46;color:#d1fae5;border:1px solid #059669;}
                        .tdm-note-snapshot-line{white-space:normal;word-break:break-word;margin:0 0 2px;}
                        .tdm-note-snapshot-line span.label{color:#64748b;font-weight:600;margin-right:4px;}
                        .tdm-note-modal.shake{animation:tdmNoteShake .4s linear;}@keyframes tdmNoteShake{10%,90%{transform:translate(-50%,-50%) translateX(-1px);}20%,80%{transform:translate(-50%,-50%) translateX(2px);}30%,50%,70%{transform:translate(-50%,-50%) translateX(-4px);}40%,60%{transform:translate(-50%,-50%) translateX(4px);}}
                        @media (max-width: 600px){
                            .tdm-note-modal{width:80vw;max-width:80vw;height:40vh;max-height:40vh;padding:16px;overflow-y:auto;}
                            .tdm-note-text{max-height:96px;}
                            .tdm-note-history-wrap{max-height:72px;}
                        }
                    `});
                    document.head.appendChild(style);
                }
            };
            ensureModal();
            const modal = state.ui.noteModal; const ta = state.ui.noteTextarea; if (!modal || !ta) return;
            const statusEl = modal.querySelector('.tdm-note-status');
            const metaEl = modal.querySelector('.tdm-note-meta');
            const charEl = modal.querySelector('.tdm-note-char');
            const tagInput = modal.querySelector('.tdm-note-tag-input');
            const quickTagContainer = modal.querySelector('.tdm-note-quick-tags');
            const tagsEmpty = modal.querySelector('.tdm-note-tags-empty');
            const addTagBtn = modal.querySelector('.tdm-note-add-tag-btn');
            const titleEl = modal.querySelector('.tdm-note-title');
            const copyBtn = modal.querySelector('.tdm-note-copy');
            const clearBtn = modal.querySelector('.tdm-note-clear');
            const closeBtn = modal.querySelector('.tdm-note-close');
            const histWrap = modal.querySelector('.tdm-note-history-wrap');
            const histList = modal.querySelector('.tdm-note-history');
            const histRefresh = modal.querySelector('.tdm-note-history-refresh');
            const snapshotBox = modal.querySelector('.tdm-note-snapshot');
            titleEl.textContent = `${tornUsername} [${tornID}]`;
            
            // Tags + body separation
            const parsed = parseTags(currentNoteContent||'');
            let currentTags = parsed.tags;
            ta.value = parsed.body;
            // Snapshot compact header builder
            const fmtDur = (ms) => {
                if (!ms || ms < 0) return '0s';
                const s = Math.floor(ms/1000); const h=Math.floor(s/3600); const m=Math.floor((s%3600)/60); const sec=s%60;
                const parts=[]; if (h) parts.push(h+'h'); if (m) parts.push(m+'m'); if (!h && !m) parts.push(sec+'s'); else if (sec && parts.length<2) parts.push(sec+'s');
                return parts.slice(0,3).join(' ');
            };
            const confBadge = (c) => `<span class="tdm-conf-badge tdm-conf-${c}" aria-label="${c} confidence" title="Confidence: ${c}">${c}</span>`;
            const buildSnapshot = () => {
                if (!snapshotBox) return;
                const rec = state.unifiedStatus?.[tornID];
                if (!rec) { snapshotBox.style.display='none'; snapshotBox.innerHTML=''; return; }
                const now = Date.now();
                const history = state._activityTracking?._phaseHistory?.[tornID] || [];
                const lastTransition = history.length ? history[history.length-1] : null;
                const currentPhase = rec.canonical || rec.phase || '?';
                const prevPhase = rec.previousPhase || (lastTransition ? lastTransition.from : null);
                const started = rec.startedMs || rec.startMs || (lastTransition ? lastTransition.ts : now);
                const elapsed = fmtDur(now - started);
                const dest = rec.dest ? (utils.travel?.abbrevDest?.(rec.dest) || rec.dest) : '';
                // Arrival / until handling: include rawUntil fallback and prefer arrivalMs for ETA
                const arrMs = rec.arrivalMs || rec.etams || rec.etaMs || rec.etamsMs || (rec.rawUntil ? (Number(rec.rawUntil||0)*1000) : null);
                let etaSeg = '';
                if (arrMs && arrMs > now) {
                    if (/Hospital/i.test(currentPhase)) {
                        // For hospital-type phases show time remaining
                        etaSeg = `${fmtDur(arrMs - now)} remaining`;
                    } else {
                        etaSeg = `ETA ${new Date(arrMs).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}`;
                    }
                } else if (arrMs && arrMs <= now && now - arrMs < 300000) {
                    etaSeg = 'arrived';
                }
                const lines = [];
                const conf = rec.confidence || 'LOW';
                const parts = [currentPhase, dest? '• '+dest:'' , etaSeg? '• '+etaSeg:'' ].filter(Boolean).join(' ');
                lines.push(`<div class="tdm-note-snapshot-line"><span class="label">Current</span>${parts} ${confBadge(conf)}</div>`);
                if (lastTransition) {
                    const age = fmtDur(now - lastTransition.ts);
                    lines.push(`<div class="tdm-note-snapshot-line" style="opacity:.7"><span class="label">Changed</span>${age} ago (${lastTransition.from}→${lastTransition.to})</div>`);
                }
                // Append up to 3 recent phase history entries if available for richer snapshot
                // try {
                //     const phEntries = (state._activityTracking?._phaseHistory?.[tornID] || []).slice(-5).reverse();
                //     let added = 0;
                //     for (const e of phEntries) {
                //         if (added >= 3) break;
                //         const age = fmtDur(now - (e.ts || now));
                //         const conf = e.confTo || e.conf || '';
                //         const confDisp = conf && conf !== 'HIGH' ? ` (${conf[0]})` : '';
                //         const dest = e.dest ? ` ${utils.travel?.abbrevDest?.(e.dest) || e.dest}` : '';
                //         const etaPart = e.arrivalMs ? ` eta:${new Date(e.arrivalMs).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}` : '';
                //         lines.push(`<div class="tdm-note-snapshot-line" style="opacity:.85"><span class="label">Hist</span>${age} ago ${e.from}→${e.to}${confDisp}${dest}${etaPart}</div>`);
                //         added++;
                //     }
                // } catch(_) {}
                snapshotBox.innerHTML = lines.join('');
                snapshotBox.style.display='block';
            };
            buildSnapshot();
            // If phaseHistory for this player is not yet present in-memory (restore may be async on init),
            // attempt an on-demand hydrate from KV so the modal shows history even if background restore hasn't finished.
            (async () => {
                try {
                    const hasInMem = !!(state._activityTracking && state._activityTracking._phaseHistory && Array.isArray(state._activityTracking._phaseHistory[tornID]) && state._activityTracking._phaseHistory[tornID].length);
                    if (!hasInMem && typeof ui !== 'undefined' && ui && ui._kv && typeof ui._kv.getItem === 'function') {
                        try {
                            const key = 'tdm.phaseHistory.id_' + String(tornID);
                            const arr = await ui._kv.getItem(key);
                            if (Array.isArray(arr) && arr.length) {
                                state._activityTracking = state._activityTracking || {};
                                state._activityTracking._phaseHistory = state._activityTracking._phaseHistory || {};
                                // merge but keep most recent up to 100
                                state._activityTracking._phaseHistory[tornID] = (state._activityTracking._phaseHistory[tornID] || []).concat(arr).slice(-100);
                                if (storage.get('tdmDebugPersist', false)) tdmlogger('debug', '[NoteModal] hydrated phaseHistory for ' + tornID + ' len=' + state._activityTracking._phaseHistory[tornID].length);
                            }
                        } catch(_) { /* ignore per-key errors */ }
                    }
                } catch(_) {}
                try { buildSnapshot(); } catch(_) {}
                try { renderHistory(); } catch(_) {}
            })();
            const renderTags = () => {
                quickTagContainer.innerHTML='';
                currentTags.forEach(tag => {
                    const t = document.createElement('span'); t.className='tdm-note-tag'; t.textContent=tag;
                    const x=document.createElement('span'); x.className='tdm-note-tag-remove'; x.textContent='×'; x.title='Remove tag';
                    x.onclick=(e)=>{ e.stopPropagation(); currentTags=currentTags.filter(g=>g!==tag); renderTags(); scheduleSave(); };
                    t.appendChild(x);
                    quickTagContainer.appendChild(t);
                });
                // Quick add presets from storage (not already added)
                let presets = utils.coerceStorageString(storage.get('tdmNoteQuickTags', 'dex+,def+,str+,spd+,hosp,retal'), 'dex+,def+,str+,spd+,hosp,retal');
                const presetArr = presets.split(/[,\s]+/).filter(Boolean).slice(0,12);
                const remain = presetArr.filter(p=>!currentTags.includes(p));
                remain.slice(0,6).forEach(tag => {
                    const add = document.createElement('span'); add.className='tdm-note-tag'; add.textContent=tag; add.title='Add tag';
                    add.onclick=()=>{ currentTags.push(tag); renderTags(); scheduleSave(); };
                    quickTagContainer.appendChild(add);
                });
                if (tagsEmpty) {
                    tagsEmpty.style.display = currentTags.length ? 'none' : 'flex';
                }
            };
            renderTags();
            const updateChar = () => { charEl.textContent = `${ta.value.length}/5000`; };
            updateChar();
            // Remove autosave: only update status on edit, do not save until Save is clicked
            const scheduleEdit = () => {
                statusEl.textContent='Editing'; statusEl.className='tdm-note-status';
            };
            // Add New Tag button reveals the hidden tag input for mobile friendliness
            if (addTagBtn) {
                addTagBtn.addEventListener('click', (ev) => {
                    try { tagInput.style.display = ''; tagInput.focus(); tagInput.value = ''; } catch(_) {}
                });
            }
            tagInput.onkeydown = (e) => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    const v = (tagInput.value || '').trim();
                    if (v && !currentTags.includes(v)) { currentTags.push(v); tagInput.value = ''; renderTags(); scheduleEdit(); }
                    else { tagInput.value = ''; }
                }
            };
            ta.oninput = () => { updateChar(); scheduleEdit(); };
            // Remove markdown and keyboard shortcuts: keep only Escape to close
            ta.onkeydown = (e) => {
                if (e.key === 'Escape') { ui.closeNoteModal(); }
            };
            // Remove save on blur
            // Buttons
            closeBtn.onclick = ui.closeNoteModal;
            copyBtn.onclick = () => { try { navigator.clipboard.writeText(buildNoteText(currentTags, ta.value)); copyBtn.textContent='✔'; setTimeout(()=>copyBtn.textContent='⧉',1200);} catch(_) { copyBtn.textContent='✖'; setTimeout(()=>copyBtn.textContent='⧉',1400);} };
            clearBtn.onclick = () => { if (ta.value.trim()==='' && !currentTags.length) { modal.classList.add('shake'); setTimeout(()=>modal.classList.remove('shake'),400); return; } if (!confirm('Clear note & tags?')) return; ta.value=''; currentTags=[]; renderTags(); updateChar(); scheduleEdit(); };
            // Add Save and Cancel buttons to footer
            let footer = modal.querySelector('.tdm-note-footer');
            if (footer && !footer.querySelector('.tdm-note-save')) {
                const saveBtn = document.createElement('button');
                saveBtn.className = 'tdm-note-save tdm-note-btn';
                saveBtn.textContent = 'Save';
                saveBtn.style.marginLeft = '12px';
                saveBtn.style.minWidth = '60px';
                saveBtn.onclick = async () => { await saveAndClose(); };
                const cancelBtn = document.createElement('button');
                cancelBtn.className = 'tdm-note-cancel tdm-note-btn';
                cancelBtn.textContent = 'Cancel';
                cancelBtn.style.marginLeft = '6px';
                cancelBtn.style.minWidth = '60px';
                cancelBtn.onclick = () => { ui.closeNoteModal(); };
                footer.appendChild(saveBtn);
                footer.appendChild(cancelBtn);
            }

            async function saveAndClose() {
                try {
                    statusEl.textContent='Saving...'; statusEl.className='tdm-note-status saving';
                    const finalText = buildNoteText(currentTags, ta.value);
                    await handlers.handleSaveUserNote(state.ui.currentNoteTornID, finalText, state.ui.currentNoteButtonElement, { silent:false });
                    statusEl.textContent='Saved'; statusEl.className='tdm-note-status saved';
                    setTimeout(()=>{ ui.closeNoteModal(); }, 300);
                } catch(e){ statusEl.textContent='Error'; statusEl.className='tdm-note-status error'; }
            }
            // Render phase history timeline
            const renderHistory = () => {
                if (!histWrap || !histList) return; histList.innerHTML='';
                const ph = state._activityTracking?._phaseHistory?.[tornID];
                if (!ph || !ph.length) { histWrap.style.display='none'; return; }
                histWrap.style.display='block';
                // Work with the entries in chronological order so we can compute first/last seen
                const entries = [...ph].slice(-15); // oldest->newest
                const now = Date.now();
                const rec = state.unifiedStatus?.[tornID] || null;
                // iterate newest first for UX
                for (let i = entries.length - 1; i >= 0; i--) {
                    const tr = entries[i];
                    const li = document.createElement('li');
                    const firstSeenMs = tr.ts || 0;
                    // lastSeen is timestamp of next (newer) entry if present, else use rec.updated or now
                    const nextEntry = entries[i+1] || null;
                    const lastSeenMs = nextEntry ? (nextEntry.ts || now) : (rec?.updated || now);
                    const sinceStr = firstSeenMs ? new Date(firstSeenMs).toLocaleString([], {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'}) : '';
                    const lastSeenStr = lastSeenMs ? new Date(lastSeenMs).toLocaleString([], {hour:'2-digit', minute:'2-digit'}) : '';
                    const durationMs = Math.max(0, (lastSeenMs || now) - (firstSeenMs || now));
                    const durationStr = fmtDur(durationMs);
                    const conf = tr.confTo || tr.conf || ''; const confDisp = conf ? ` [${conf}]` : '';
                    const dest = tr.dest ? ` ${utils.travel?.abbrevDest?.(tr.dest) || tr.dest}` : '';
                    const etaPart = tr.arrivalMs ? ` • ETA ${new Date(tr.arrivalMs).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}` : '';
                    const left = `${tr.from}->${tr.to}`;
                    const sincePart = sinceStr ? ` Since: ${sinceStr}` : '';
                    const lastPart = lastSeenStr ? ` Last:${lastSeenStr}` : '';
                    li.textContent = `${left}${dest}${sincePart} Duration:${durationStr}${lastPart}${etaPart}${confDisp}`.trim();
                    li.setAttribute('aria-label', `Phase ${tr.from} to ${tr.to}. First seen ${sinceStr}. Duration ${durationStr}. Last seen ${lastSeenStr}.${conf ? ' Confidence ' + conf : ''}${dest ? ' destination ' + dest.trim() : ''}`);
                    li.style.padding='1px 0';
                    histList.appendChild(li);
                }
            };
            histRefresh && (histRefresh.onclick = (e)=>{ e.preventDefault(); renderHistory(); });
            renderHistory();
            modal.style.display='block'; ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); statusEl.textContent='Idle'; statusEl.className='tdm-note-status';
            // Keep snapshot live (lightweight)
            if (snapshotBox) {
                if (state.ui._noteSnapshotInterval) try { utils.unregisterInterval(state.ui._noteSnapshotInterval); } catch(_) {}
                state.ui._noteSnapshotInterval = utils.registerInterval(setInterval(()=>{
                    try { if (modal.style.display!=='block') { try { utils.unregisterInterval(state.ui._noteSnapshotInterval); } catch(_) {} state.ui._noteSnapshotInterval = null; return; } buildSnapshot(); } catch(_){}
                }, 5000));
            }
        },
        closeNoteModal: () => {
            if (state.ui.noteModal) state.ui.noteModal.style.display = 'none';
            state.ui.currentNoteTornID = null;
            state.ui.currentNoteTornUsername = null;
            state.ui.currentNoteButtonElement = null;
            if (state.ui._noteSnapshotInterval) { try { utils.unregisterInterval(state.ui._noteSnapshotInterval); } catch(_) {} state.ui._noteSnapshotInterval=null; }
        },
        openSetterModal: async (opponentId, opponentName, buttonElement, type) => {
            // If admin functionality is disabled (or user lacks admin rights), treat buttons as simple toggles for self
            const adminEnabled = !!state.script.canAdministerMedDeals && (storage.get('adminFunctionality', true) === true);
            if (!adminEnabled) {
                const defaultText = type === 'medDeal' ? 'Set Med Deal' : 'Dibs';
                try {
                    if (buttonElement) {
                        buttonElement.dataset.originalText = buttonElement.dataset.originalText || buttonElement.textContent;
                    }

                    const resolveHandler = (debouncedName, plainName) => {
                        if (typeof handlers[debouncedName] === 'function') return handlers[debouncedName];
                        if (typeof handlers[plainName] === 'function') return handlers[plainName];
                        return null;
                    };

                    if (type === 'medDeal') {
                        const mds = (state.medDeals || {})[opponentId];
                        const isMyMedDeal = !!(mds && mds.isMedDeal && String(mds.medDealForUserId) === String(state.user.tornId));
                        // Toggle med deal for current user (resolve debounced or plain handler)
                        const medHandler = resolveHandler('debouncedHandleMedDealToggle', 'handleMedDealToggle');
                        if (medHandler) {
                            await medHandler(
                                opponentId,
                                opponentName,
                                !isMyMedDeal,
                                state.user.tornId,
                                state.user.tornUsername,
                                buttonElement
                            );
                        } else {
                            ui.showMessageBox('Action unavailable. Please reload the page.', 'warning', 4000);
                        }
                    } else {
                        // type === 'dibs': toggle dib for current user
                        const myActive = (state.dibsData || []).find(d => d.opponentId === opponentId && d.dibsActive && String(d.userId) === String(state.user.tornId));
                        if (myActive) {
                            const remHandler = resolveHandler('debouncedRemoveDibsForTarget', 'removeDibsForTarget');
                            if (remHandler) {
                                await remHandler(opponentId, buttonElement);
                            } else {
                                ui.showMessageBox('Action unavailable. Please reload the page.', 'warning', 4000);
                            }
                        } else {
                            const addHandler = resolveHandler('debouncedDibsTarget', 'dibsTarget');
                            if (addHandler) {
                                await addHandler(opponentId, opponentName, buttonElement);
                            } else {
                                ui.showMessageBox('Action unavailable. Please reload the page.', 'warning', 4000);
                            }
                        }
                    }
                } catch (_) {
                    // noop; handlers already show messages
                } finally {
                }
                return; // do not open modal
            }
            if (buttonElement) {
                buttonElement.dataset.originalText = buttonElement.dataset.originalText || buttonElement.textContent;
            }
            state.ui.currentOpponentId = opponentId;
            state.ui.currentOpponentName = opponentName;
            state.ui.currentButtonElement = buttonElement;
            state.ui.currentSetterType = type;
            const title = type === 'medDeal' ? 'Set Med Deal for' : 'Assign Dibs for';
            const defaultText = type === 'medDeal' ? 'Set Med Deal' : 'Dibs';
            if (!state.ui.setterModal) {
                state.ui.setterModal = utils.createElement('div', { id: 'setter-modal', style: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#1a1a1a', border: '1px solid #333', borderRadius: '8px', padding: '20px', zIndex: 10002, boxShadow: '0 4px 8px rgba(0,0,0,0.5)', maxWidth: '400px', width: '90%', color: 'white' } });
                state.ui.setterModal.innerHTML = `
                    <h3 style="margin-top: 0;">${title} ${opponentName}</h3>
                    <input type="text" id="setter-search" placeholder="Search members..." style="width: calc(100% - 10px); padding: 5px; margin-bottom: 10px; background-color: #222; border: 1px solid #555; color: white; border-radius: 4px;">
                    <ul id="setter-list" style="list-style: none; padding: 0; margin: 0; max-height: 200px; overflow-y: auto; border: 1px solid #555; border-radius: 4px;"></ul>
                    <button id="cancel-setter" style="background-color: #f44336; color: white; border: none; border-radius: 4px; padding: 8px 15px; cursor: pointer; margin-top: 10px;">Cancel</button>
                `;
                document.body.appendChild(state.ui.setterModal);
                state.ui.setterSearchInput = document.getElementById('setter-search');
                state.ui.setterList = document.getElementById('setter-list');
                document.getElementById('cancel-setter').onclick = (e) => { e.preventDefault(); e.stopPropagation(); ui.closeSetterModal(); };
                state.ui.setterSearchInput.addEventListener('input', ui.filterSetterList);
            } else {
                state.ui.setterModal.querySelector('h3').textContent = `${title} ${opponentName}`;
                state.ui.setterSearchInput.value = '';
            }
            state.ui.setterList.innerHTML = '<li style="padding: 8px; text-align: center; color: #aaa;"><span class="dibs-spinner"></span> Loading...</li>';
            state.ui.setterSearchInput.disabled = true;
            try {
                // Ensure faction members are loaded (race guard). Heavy orchestrator / timeline work can delay API hydration.
                await ui._ensureFactionMembersLoaded?.();
                ui.populateSetterList();
            } catch (error) {
                tdmlogger('error', `[Error populating ${type} setter modal] ${error}`);
                state.ui.setterList.innerHTML = '<li style="padding: 8px; text-align: center; color: #f44336;">Failed to load members.</li>';
            } finally {
                state.ui.setterSearchInput.disabled = false;
            }
            state.ui.setterModal.style.display = 'block';
        },
        openDibsSetterModal: (opponentId, opponentName, buttonElement) => ui.openSetterModal(opponentId, opponentName, buttonElement, 'dibs'),
        openMedDealSetterModal: (opponentId, opponentName, buttonElement) => ui.openSetterModal(opponentId, opponentName, buttonElement, 'medDeal'),

        closeSetterModal: () => {
            if (state.ui.setterModal) state.ui.setterModal.style.display = 'none';
            const defaultText = state.ui.currentSetterType === 'medDeal' ? 'Set Med Deal' : 'Dibs';
            if (state.ui.currentButtonElement) {
                state.ui.currentButtonElement.disabled = false;
                state.ui.currentButtonElement.textContent = state.ui.currentButtonElement.dataset.originalText || defaultText;
                state.ui.currentButtonElement = null;
            }
            state.ui.currentOpponentId = null;
            state.ui.currentOpponentName = null;
            state.ui.currentSetterType = null;
            handlers.debouncedFetchGlobalData();
        },
        // Show opponent detail popup (used when clicking opponent badge) - includes Attack Page button & enriched status
        openOpponentPopup: async (opponentId, opponentName) => {
            if (!opponentId) return;
            // derive full name if not provided
            try {
                    if (!opponentName) {
                        const snap = state.rankedWarTableSnapshot && state.rankedWarTableSnapshot[opponentId];
                        if (snap && (snap.name || snap.username)) opponentName = snap.name || snap.username;
                        if (!opponentName) {
                            const mem = Array.isArray(state.factionMembers) ? state.factionMembers.find(m => String(m.id) === String(opponentId)) : null;
                            if (mem && mem.name) opponentName = mem.name;
                        }
                        if (!opponentName) {
                            const cached = state.session?.userStatusCache?.[opponentId] || {};
                            if (cached?.name) opponentName = cached.name;
                        }
                        if (!opponentName) {
                            try {
                                const anchors = Array.from(document.querySelectorAll(`a[href*="profiles.php?XID=${opponentId}"]`));
                                for (const a of anchors) {
                                    const txt = (a.textContent || '').trim();
                                    if (!txt) continue;
                                    if (/^back to profile$/i.test(txt)) continue;
                                    if (/^profile$/i.test(txt)) continue;
                                    opponentName = txt;
                                    break;
                                }
                            } catch(_) {}
                        }
                    }
            } catch(_) { /* ignore */ }
            opponentName = String(opponentName || `ID ${opponentId}`);

            // Build modal
            if (!state.ui.opponentPopup) {
                state.ui.opponentPopup = utils.createElement('div', { id: 'tdm-opponent-popup', style: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#141414', border: '1px solid #333', borderRadius: '8px', padding: '18px', zIndex: 10003, boxShadow: '0 6px 18px rgba(0,0,0,0.6)', width: 'min(520px, 94%)', color: '#fff' } });
                document.body.appendChild(state.ui.opponentPopup);
            }

            const modal = state.ui.opponentPopup;
            modal.innerHTML = '';

            const header = utils.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: '8px' } });
            const h = utils.createElement('h3', { textContent: `${opponentName}`, style: { margin: '0', fontSize: '16px' } });
            header.appendChild(h);
            modal.appendChild(header);

            const infoWrap = utils.createElement('div', { style: { marginTop: '8px', display: 'flex', gap: '10px', flexDirection: 'column' } });
            // Friendly details: activity, status, destination, last action
            const cache = state.ui.opponentStatusCache && state.ui.opponentStatusCache.opponentId === String(opponentId) ? state.ui.opponentStatusCache : null;
            let activity = cache?.activity || null;
            let statusText = cache?.text || null;
            let dest = cache?.dest || null;
            const until = cache?.untilEpoch || cache?.until || 0;
            // last action ts: try session cache
            let lastActionTs = Number(state.session?.userStatusCache?.[opponentId]?.last_action?.timestamp || state.session?.userStatusCache?.[opponentId]?.lastAction?.timestamp || 0) || 0;
            if (!lastActionTs && cache?.unified && cache.unified.last_action && cache.unified.last_action.timestamp) lastActionTs = Number(cache.unified.last_action.timestamp || 0);
            if (lastActionTs > 1e12) lastActionTs = Math.floor(lastActionTs/1000);

            // If we don't have useful info, try to fetch a fresh user status
            if ((activity === null || activity === 'Unknown' || !statusText) && utils.getUserStatus) {
                try {
                    const fresh = await utils.getUserStatus(opponentId).catch(()=>null);
                    if (fresh) {
                        // if the modal is currently showing only an "ID ####" fallback, prefer the real name from fresh
                        try {
                            const looksLikeId = String(opponentName || '').startsWith('ID ') || String(opponentName || '') === String(opponentId);
                            if (looksLikeId && fresh.name) {
                                opponentName = fresh.name;
                                if (h && h.textContent) h.textContent = opponentName;
                            }
                        } catch(_) {}
                        // Prefer last_action status for activity; fallback to other fields
                        activity = fresh.last_action?.status || fresh.activity || fresh.lastAction?.status || activity || null;
                        statusText = statusText || (fresh.raw?.state || fresh.canonical || fresh.raw?.description || fresh.description || null);
                        dest = dest || fresh.dest || fresh.city || null;
                        // Try to update lastActionTs if present in fresh
                        if (!lastActionTs) {
                            const candidateTs = Number(fresh.last_action?.timestamp || fresh.lastAction?.timestamp || 0) || 0;
                            if (candidateTs) lastActionTs = candidateTs > 1e12 ? Math.floor(candidateTs/1000) : candidateTs;
                        }
                    }
                } catch (_) { /* ignore */ }

                // Final fallback: if we still don't have an activity value, try derive it from the DOM row
                if (!activity) {
                    try {
                        const anchor = document.querySelector(`a[href*="profiles.php?XID=${opponentId}"]`);
                        const row = anchor ? (anchor.closest('li') || anchor.closest('tr')) : null;
                        if (row && typeof utils.getActivityStatusFromRow === 'function') {
                            const domActivity = utils.getActivityStatusFromRow(row);
                            if (domActivity) activity = domActivity;
                        }
                    } catch(_) { /* noop */ }
                }
            }

            // Populate details
            const list = utils.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', marginTop: '6px' } });
            list.appendChild(utils.createElement('div', { innerHTML: `<strong>Activity:</strong> ${activity || 'Unknown'}` }));
            list.appendChild(utils.createElement('div', { innerHTML: `<strong>Status:</strong> ${statusText || 'Unknown'}${dest ? ` &middot; ${dest}` : ''}` }));
            if (lastActionTs && Number.isFinite(lastActionTs) && lastActionTs > 0) {
                const el = utils.createElement('div', { innerHTML: `<strong>Last action:</strong> <span class='tdm-last-action-inline' data-last-ts='${Math.floor(lastActionTs)}'></span>` });
                list.appendChild(el);
            }

            infoWrap.appendChild(list);

            modal.appendChild(infoWrap);

            // Footer actions
            const footer = utils.createElement('div', { style: { display: 'flex', justifyContent: 'flex-end', gap: '8px', marginTop: '12px' } });
            const sendDibsBtn = utils.createElement('button', { textContent: 'Send Dibs to Chat', style: { background: '#4CAF50', color: 'white', border: 'none', padding: '8px 12px', borderRadius: '6px', cursor: 'pointer' } });
            sendDibsBtn.title = `Send dibs for ${opponentName} to faction chat`;
            sendDibsBtn.onclick = async (e) => { try { e.preventDefault(); e.stopPropagation(); await ui.sendDibsMessage(opponentId, opponentName, null); ui.showMessageBox('Sent dibs to chat (or copied to clipboard).', 'info', 2200); } catch(_) {} };

            const openAttack = utils.createElement('button', { textContent: `Open Attack Page — ${opponentName}`, style: { background: '#0b74ff', color: 'white', border: 'none', padding: '8px 12px', borderRadius: '6px', cursor: 'pointer' } });
            openAttack.title = `Open Attack Page - ${opponentName}`;
            openAttack.onclick = (e) => { e.preventDefault(); e.stopPropagation(); window.open(`https://www.torn.com/loader.php?sid=attack&user2ID=${opponentId}`, '_blank'); };

            const closeBtn = utils.createElement('button', { textContent: 'Close', style: { background: '#444', color: 'white', border: 'none', padding: '8px 12px', borderRadius: '6px', cursor: 'pointer' } });
            closeBtn.onclick = (e) => { try { state.ui.opponentPopup.style.display = 'none'; } catch(_) {} };

            // If there is an active dib for this opponent provide a quick Undib button
            try {
                const activeDib = Array.isArray(state.dibsData) ? state.dibsData.find(d => d && String(d.opponentId) === String(opponentId) && d.dibsActive) : null;
                if (activeDib) {
                    const undibBtn = utils.createElement('button', { textContent: 'Undib', style: { background: '#e53935', color: 'white', border: 'none', padding: '8px 12px', borderRadius: '6px', cursor: 'pointer' } });
                    undibBtn.title = `Remove dibs for ${opponentName}`;
                    undibBtn.onclick = async (e) => {
                        try {
                            e.preventDefault(); e.stopPropagation();
                            undibBtn.disabled = true;
                            const remover = (typeof handlers.debouncedRemoveDibsForTarget === 'function') ? handlers.debouncedRemoveDibsForTarget : (typeof handlers.removeDibsForTarget === 'function' ? handlers.removeDibsForTarget : null);
                            if (remover) await remover(opponentId, undibBtn);
                            else ui.showMessageBox('Remove handler not available', 'error');
                        } catch (err) {
                            ui.showMessageBox(`Failed to remove dibs: ${err?.message||err}`, 'error');
                        } finally {
                            try { if (state.ui.opponentPopup) state.ui.opponentPopup.style.display = 'none'; } catch(_) {}
                            try { undibBtn.disabled = false; } catch(_) {}
                        }
                    };
                    footer.appendChild(undibBtn);
                }
            } catch(_) { /* noop */ }

            footer.appendChild(sendDibsBtn);
            footer.appendChild(openAttack);
            footer.appendChild(closeBtn);
            modal.appendChild(footer);

            // Ensure last-action short label renders immediately if present
            try { ui.ensureLastActionTicker(); } catch(_) {}
            modal.style.display = 'block';
        },
        populateSetterList: () => {
            state.ui.setterList.innerHTML = '';
            const validMembers = state.factionMembers.filter(member => member.id && member.name);
            const sortedMembers = [...validMembers].sort((a, b) => {
                const aId = String(a.id);
                const bId = String(b.id);
                if (aId === state.user.tornId) return -1;
                if (bId === state.user.tornId) return 1;
                return a.name.localeCompare(b.name);
            });
            if (sortedMembers.length === 0) {
                state.ui.setterList.appendChild(utils.createElement('li', { textContent: 'No faction members found.', style: { padding: '8px', color: '#aaa' } }));
                return;
            }
            sortedMembers.forEach(member => {
                const li = utils.createElement('li', {
                    textContent: member.name,
                    dataset: { userId: member.id, username: member.name },
                    style: { padding: '8px', cursor: 'pointer', borderBottom: '1px solid #333', backgroundColor: '#2c2c2c' },
                    onmouseover: () => li.style.backgroundColor = '#444',
                    onmouseout: () => li.style.backgroundColor = '#2c2c2c',
                    onclick: async () => {
                        const btn = state.ui.currentButtonElement;
                        if (btn) {
                            btn.textContent = 'Saving...';
                            btn.disabled = true;
                            btn.className = 'btn dibs-btn btn-dibs-inactive';
                            state.ui.currentButtonElement = null; // Prevent closeSetterModal from resetting
                        }
                        if (state.ui.currentSetterType === 'medDeal') {
                            await handlers.debouncedHandleMedDealToggle(state.ui.currentOpponentId, state.ui.currentOpponentName, true, member.id, member.name, btn);
                        } else {
                            await handlers.debouncedAssignDibs(state.ui.currentOpponentId, state.ui.currentOpponentName, member.id, member.name, btn);
                        }
                        ui.closeSetterModal();
                    }
                });
                state.ui.setterList.appendChild(li);
            });
        },

        // Poll until factionMembers are available or timeout. Avoid extra API calls; just wait for hydration from existing init flow.
        _ensureFactionMembersLoaded: async (opts) => {
            const maxWaitMs = (opts && opts.maxWaitMs) || 4000;
            const pollMs = (opts && opts.pollMs) || 120;
            const start = Date.now();
            // Already loaded & non-empty
            if (Array.isArray(state.factionMembers) && state.factionMembers.length > 0) return true;
            // Create a shared waiter to prevent concurrent spinners doing redundant loops
            if (state._factionMembersWaiter) return state._factionMembersWaiter;
            state._factionMembersWaiter = (async () => {
                let lastLog = 0;
                while ((Date.now() - start) < maxWaitMs) {
                    if (Array.isArray(state.factionMembers) && state.factionMembers.length > 0) return true;
                    // Light console heartbeat every ~1s for diagnostics if still waiting
                    const now = Date.now();
                    if (now - lastLog > 1000) { tdmlogger('debug', '[SetterModal] Waiting for factionMembers...'); lastLog = now; }
                    await new Promise(r => setTimeout(r, pollMs));
                }
                return false; // timed out
            })();
            try { return await state._factionMembersWaiter; } finally { delete state._factionMembersWaiter; }
        },

        filterSetterList: () => {
            const searchTerm = state.ui.setterSearchInput.value.toLowerCase();
            const items = state.ui.setterList.querySelectorAll('li');
            items.forEach(item => {
                const username = item.dataset.username?.toLowerCase() || '';
                item.style.display = username.includes(searchTerm) ? 'block' : 'none';
            });
        },

        // Unified toast / alert manager
        // Dedupe semantics: (type + normalized message) suppressed if shown in last DEDUPE_MS window.
        // Reuse a small pooled set of DOM nodes to minimize layout / flicker.
        showMessageBox: (() => {
            const DEDUPE_MS = 5000;
            const MAX_NODES = 3;
            const recent = [];// [{ key, ts }]
            let pool = [];// recycled divs
            const containerId = 'tdm-toast-container';
            const ensureContainer = () => {
                let c = document.getElementById(containerId);
                if (!c) {
                    c = utils.createElement('div', { id: containerId, style: {
                        position: 'fixed', top: '12px', right: '12px', zIndex: 9000000005,
                        display: 'flex', flexDirection: 'column', gap: '8px', maxWidth: '320px'
                    }});
                    document.body.appendChild(c);
                }
                return c;
            };
            const pruneRecent = (now) => {
                for (let i = recent.length - 1; i >= 0; i--) if (now - recent[i].ts > DEDUPE_MS) recent.splice(i,1);
            };
            const acquireNode = () => {
                const reused = pool.pop();
                if (reused) {
                    // Reset state from prior lifecycle so click/expiry works again
                    reused._closing = false;
                    if (reused.dataset) { delete reused.dataset.key; delete reused.dataset.expire; }
                    reused.onclick = null;
                    reused.textContent = '';
                    // Reset initial animation baseline
                    reused.style.opacity = '0';
                    reused.style.transform = 'translateY(-4px)';
                    return reused;
                }
                return utils.createElement('div', { className: 'tdm-toast', style: {
                    borderRadius: '6px', padding: '8px 10px', fontSize: '12px', lineHeight: '1.3',
                    color: '#fff', boxShadow: '0 2px 6px rgba(0,0,0,.4)', opacity: '0', transform: 'translateY(-4px)',
                    transition: 'opacity .18s ease, transform .18s ease', cursor: 'pointer', userSelect: 'none'
                }});
            };
            const releaseNode = (node) => {
                if (!node) return;
                if (node.parentNode) { try { node.parentNode.removeChild(node); } catch(_){} }
                node.onclick = null;
                node._closing = false;
                if (node.dataset) { delete node.dataset.key; delete node.dataset.expire; }
                if (pool.length < MAX_NODES) pool.push(node);
            };
            return (message, type = 'info', duration = 5000, onClick = null) => {
                try {
                    const now = Date.now();
                    pruneRecent(now);
                    const normMsg = String(message || '').trim().replace(/\s+/g,' ');
                    const key = `${type}|${normMsg.toLowerCase()}`;
                    if (recent.some(r => r.key === key)) return; // suppress duplicate
                    recent.push({ key, ts: now });
                    const container = ensureContainer();
                    // Reuse existing identical active node by updating timer (extend behavior)
                    const existing = Array.from(container.children).find(ch => ch.dataset.key === key);
                    if (existing) {
                        existing.dataset.expire = String(now + duration);
                        return; // already visible
                    }
                    const node = acquireNode();
                    node.dataset.key = key;
                    node.dataset.expire = String(now + duration);
                    node.style.background = config.CSS.colors[type] || config.CSS.colors.info;
                    node.textContent = normMsg;
                    const close = (el, clicked = false) => {
                        if (!el || el._closing) return; el._closing = true;
                        el.style.opacity='0'; el.style.transform='translateY(-4px)';
                        setTimeout(()=> { releaseNode(el); }, 220);
                    };
                    node.onclick = async (e) => { e.preventDefault(); e.stopPropagation(); if (onClick) { try { await onClick(); } catch(_){} } close(node,true); };
                    container.appendChild(node);
                    requestAnimationFrame(()=> { node.style.opacity='1'; node.style.transform='translateY(0)'; });
                    // Shared sweeper (single interval) stored on container
                    if (!container._sweeper) {
                        container._sweeper = utils.registerInterval(setInterval(() => {
                            const now2 = Date.now();
                            Array.from(container.children).forEach(ch => {
                                const exp = Number(ch.dataset.expire||0);
                                if (exp && now2 >= exp) close(ch);
                                // Safety: hard cap 60s lifetime regardless of duration param
                                const born = exp ? exp - duration : now2;
                                if (now2 - born > 10000) close(ch);
                            });
                        }, 500));
                    }
                } catch(_) { /* non-fatal */ }
            };
        })(),

        // Backwards-compatible alias used by some new code paths; thin wrapper.
        showTransientMessage: (text, { type='info', timeout=2500 } = {}) => {
            try { ui.showMessageBox(text, type, timeout); } catch(_) { /* noop */ }
        },

        showConfirmationBox: (message, showCancel = true, extra = null) => {
            // extra: { thirdLabel: string, onThird: ()=>void }
            return new Promise(resolve => {
                const confirmBox = utils.createElement('div', { style: { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#1a1a1a', border: '1px solid #333', borderRadius: '8px', padding: '20px', zIndex: 10001, boxShadow: '0 4px 8px rgba(0,0,0,0.5)', maxWidth: '380px', width: '90%', color: 'white', textAlign: 'center' } });
                const messagePara = utils.createElement('p', { style: { marginBottom: '16px', fontSize: '13px', lineHeight: '1.3' }, textContent: message });
                const buttonsContainer = utils.createElement('div', { style: { display: 'flex', justifyContent: 'center', gap: '10px', flexWrap: 'wrap' } });
                const yesBtn = utils.createElement('button', { id: 'confirm-ok', style: { backgroundColor: config.CSS.colors.success, color: 'white', border: 'none', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' }, textContent: showCancel ? 'Yes' : 'OK', onclick: () => { confirmBox.remove(); resolve(true); } });
                buttonsContainer.appendChild(yesBtn);
                if (extra && extra.thirdLabel) {
                    const thirdBtn = utils.createElement('button', { id: 'confirm-third', style: { backgroundColor: '#1976d2', color: 'white', border: 'none', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' }, textContent: extra.thirdLabel, onclick: () => { confirmBox.remove(); try { extra.onThird && extra.onThird(); } catch(_) {} resolve('third'); } });
                    buttonsContainer.appendChild(thirdBtn);
                }
                if (showCancel) {
                    const noBtn = utils.createElement('button', { id: 'confirm-cancel', style: { backgroundColor: config.CSS.colors.error, color: 'white', border: 'none', borderRadius: '4px', padding: '8px 14px', cursor: 'pointer' }, textContent: 'No', onclick: () => { confirmBox.remove(); resolve(false); } });
                    buttonsContainer.appendChild(noBtn);
                }
                confirmBox.appendChild(messagePara);
                confirmBox.appendChild(buttonsContainer);
                document.body.appendChild(confirmBox);
            });
        },

        createSettingsButton: () => {
            if (document.getElementById('tdm-settings-button')) return;
            const topPageLinksList = document.querySelector('#top-page-links-list');
            if (!topPageLinksList) return;

            const settingsButton = utils.createElement('span', { id: 'tdm-settings-button', style: { marginRight: '5px', marginLeft: '10px', cursor: 'pointer', display: 'inline-block', verticalAlign: 'middle' }, innerHTML: `<span class="tdm-settings-label" style="background: linear-gradient(to bottom, #00b300, #008000); border: 2px solid #ffcc00; border-radius: 4px; box-sizing: border-box; color: #ffffff; text-shadow: 1px 1px 1px #000000; cursor: pointer; display: inline-block; font-family: 'Farfetch Basis', 'Helvetica Neue', Arial, sans-serif; font-size: 12px; font-weight: bold; line-height: 20px; height: 20px; margin: 0; padding: 0 8px; text-align: center; text-transform: none;">TreeDibs</span>`, onclick: ui.toggleSettingsPopup });
            const retalsButton = utils.createElement('span', { id: 'tdm-retals-button', style: { marginRight: '5px', marginLeft: '5px', cursor: 'pointer', display: 'inline-block', verticalAlign: 'middle' }, innerHTML: `<span style="background: linear-gradient(to bottom, #ff5722, #e64a19); border: 2px solid #ffcc00; border-radius: 4px; box-sizing: border-box; color: #ffffff; text-shadow: 1px 1px 1px #000000; cursor: pointer; display: inline-block; font-family: 'Farfetch Basis', 'Helvetica Neue', Arial, sans-serif; font-size: 12px; font-weight: bold; line-height: 20px; height: 20px; margin: 0; padding: 0 8px; text-align: center; text-transform: none;">... Retals</span>`, onclick: () => ui.showAllRetaliationsNotification() });

            if (topPageLinksList.firstChild) {
                topPageLinksList.insertBefore(retalsButton, topPageLinksList.firstChild);
                topPageLinksList.insertBefore(settingsButton, topPageLinksList.firstChild);
            } else {
                topPageLinksList.appendChild(settingsButton);
                topPageLinksList.appendChild(retalsButton);
            }
            ui.updateRetalsButtonCount(); // Call initially to set the count
            ui.updateSettingsButtonUpdateState();
        },

        updateRetalsButtonCount: () => {
            const retalsButton = document.getElementById('tdm-retals-button');
            if (!retalsButton) return;
            const retalSpan = retalsButton.querySelector('span');
            if (!retalSpan) return;

            const activeRetals = Object.values(state.retaliationOpportunities).filter(opp => opp.timeRemaining > 0).length;
            retalSpan.textContent = `${activeRetals} Retals`;
        },
        toggleSettingsPopup: async () => {
            let settingsPopup = document.getElementById('tdm-settings-popup');
            if (settingsPopup) {
                // Stop dynamic diagnostics updater when closing
                try { if (state.ui && state.ui.apiCadenceInfoIntervalId) { try { utils.unregisterInterval(state.ui.apiCadenceInfoIntervalId); } catch(_) {} state.ui.apiCadenceInfoIntervalId = null; } } catch(_) {}
                try { if (state.ui && state.ui.rwTermInfoIntervalId) { try { utils.unregisterInterval(state.ui.rwTermInfoIntervalId); } catch(_) {} state.ui.rwTermInfoIntervalId = null; } } catch(_) {}
                try {
                        if (state.uiCadenceInfoThrottle?.pending) {
                        try { utils.unregisterTimeout(state.uiCadenceInfoThrottle.pending); } catch(_) {}
                        state.uiCadenceInfoThrottle.pending = null;
                    }
                    if (state.uiCadenceInfoThrottle) state.uiCadenceInfoThrottle.lastRender = 0;
                } catch(_) { /* noop */ }
                settingsPopup.remove();
                return;
            }
            const contentTitle = document.querySelector('div.content-title.m-bottom10');
            const contentWrapper = document.querySelector('.content-wrapper');
            if (!contentTitle && !contentWrapper) return;
            settingsPopup = utils.createElement('div', { id: 'tdm-settings-popup', style: { width: '100%', marginBottom: '5px', backgroundColor: '#2c2c2c', border: '1px solid #333', borderRadius: '8px', boxShadow: '0 4px 10px rgba(0,0,0,0.5)', padding: '0', fontFamily: "'Inter', sans-serif", color: '#e0e0e0' } });
            const latestVersion = state.script.updateAvailableLatestVersion;
            const hasUpdate = latestVersion && utils.compareVersions(config.VERSION, latestVersion) < 0;
            const preferredUpdateUrl = state.script.updateAvailableLatestVersionUrl || config.GREASYFORK.pageUrl;
            const getLink = (hasUpdate && preferredUpdateUrl) ? ` <a href="${preferredUpdateUrl}" target="_blank" rel="noopener" style="color:#fff;text-decoration:underline;">Get v${latestVersion}</a>` : '';
            const header = utils.createElement('div', { style: { padding: '10px', backgroundColor: config.CSS.colors.mainColor, borderTopLeftRadius: '8px', borderTopRightRadius: '8px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }, innerHTML: `<h3 style="margin: 0; color: white; font-size: 16px;">TreeDibsMapper v${config.VERSION}${getLink ? ' ' + getLink : ''}</h3><span id="tdm-settings-close" style="cursor: pointer; font-size: 18px;">×</span>` });
            const content = utils.createElement('div', { id: 'tdm-settings-content', style: { padding: '5px' } });
            settingsPopup.appendChild(header);
            settingsPopup.appendChild(content);
            header.querySelector('#tdm-settings-close').addEventListener('click', ui.toggleSettingsPopup);

            if (contentTitle) contentTitle.parentNode.insertBefore(settingsPopup, contentTitle.nextSibling);
            else if (contentWrapper) contentWrapper.insertBefore(settingsPopup, contentWrapper.firstChild);
            else document.body.appendChild(settingsPopup);
            ui.updateSettingsContent();
            // Start dynamic diagnostics updater while panel is open (updates every second)
            try {
                if (!state.ui) state.ui = {};
                if (state.ui.apiCadenceInfoIntervalId) { try { utils.unregisterInterval(state.ui.apiCadenceInfoIntervalId); } catch(_) {} state.ui.apiCadenceInfoIntervalId = null; }
                state.ui.apiCadenceInfoIntervalId = utils.registerInterval(setInterval(() => {
                    try { if (document.getElementById('tdm-settings-popup')) ui.updateApiCadenceInfo?.(); else { try { utils.unregisterInterval(state.ui.apiCadenceInfoIntervalId); } catch(_) {} state.ui.apiCadenceInfoIntervalId = null; } } catch(_) {}
                }, 1000));
            } catch(_) { /* noop */ }
        },

        updateSettingsButtonUpdateState: () => {
            try {
                const label = document.querySelector('#tdm-settings-button .tdm-settings-label');
                if (!label) return;
                const latest = state.script.updateAvailableLatestVersion;
                const hasUpdate = latest && utils.compareVersions(config.VERSION, latest) < 0;
                // Activity Tracking indicator (lightweight)
                if (storage.get('tdmActivityTrackingEnabled', false)) {
                    label.textContent = 'TreeDibs (Tracking)';
                } else label.textContent = hasUpdate ? 'Update TDM!' : 'TreeDibs';
            } catch (_) {}
        },

        updateSettingsContent: () => {
            const perf = utils?.perf;
            perf?.start?.('ui.updateSettingsContent.total');
            let renderPhaseComplete = false;
            let bindPhaseStarted = false;
            try {
                const content = document.getElementById('tdm-settings-content');
                if (!content) {tdmlogger('debug', '[Settings UI] no tdm-settings-content'); return; }
                perf?.start?.('ui.updateSettingsContent.snapshot');
                // Read persisted collapsed state
                const collapsedKey = 'settings_collapsed';
                const collapsedState = storage.get(collapsedKey, {});
                const warData = state.warData || {};
                const warType = warData.warType || 'War Type Not Set';
                const termType = warData.termType || 'Set Term Type';
                const factionScoreCap = (warData.factionScoreCap ?? warData.scoreCap) ?? 0;
                const individualScoreCap = (warData.individualScoreCap ?? warData.scoreCap) ?? 0;
                const individualScoreType = warData.individualScoreType || warData.scoreType || 'Respect';
                const opponentFactionName = warData.opponentFactionName || state.lastOpponentFactionName;
                const opponentFactionId = warData.opponentFactionId || state.lastOpponentFactionId;
                const termedWarDisplay = warType === 'Termed War' ? 'block' : 'none';
                const warstring = warType === 'Termed War' ? `${termType} to Total ${factionScoreCap}, ${individualScoreCap} ${individualScoreType} Each` : warType;
                const ocReminderEnabled = storage.get('ocReminderEnabled', true); // Default to true
                const escapeHtml = (value) => String(value ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
                const computeApiKeyUi = () => {
                    const storedKey = getStoredCustomApiKey() || '';
                    const pdaPlaceholder = (state.script.isPDA && config.PDA_API_KEY_PLACEHOLDER && config.PDA_API_KEY_PLACEHOLDER[0] !== '#') ? config.PDA_API_KEY_PLACEHOLDER : null;
                    const source = storedKey ? 'local' : (pdaPlaceholder ? 'pda' : 'none');
                    const pdaKeyActive = (!storedKey && source === 'pda');
                    const validation = state.user.keyValidation || null;
                    // keyInfo may be cached on the user state when a key has been inspected
                    const keyInfo = state.user.keyInfoCache || null;
                    let tone = state.user.apiKeyUiMessage?.tone || null;
                    let status = 'Unverified';
                    if (validation?.isLimited) {
                        tone = tone || 'warning';
                        status = 'Limited Access Level';
                    } else if (validation?.level) {
                        tone = tone || 'success';
                        status = 'Ready';
                    } else if (pdaKeyActive) {
                        tone = tone || 'info';
                        status = 'Using PDA-provided key';
                    } else if (storedKey) {
                        tone = tone || 'error';
                        status = 'Unverified';
                    } else if (state.script.isPDA) {
                        tone = tone || 'warning';
                        status = 'PDA placeholder missing';
                    } else {
                        tone = tone || 'error';
                        status = 'No key saved';
                    }
                    let message = state.user.apiKeyUiMessage?.text || '';
                    if (!message) {
                        if (validation?.isLimited) {
                            // If we have missing scopes info, include them so users know what to fix
                            const missing = (validation.missing || []).map(s => String(s).replace('.', ' -> '));
                            message = missing.length ? `Limited key detected. Missing selections: ${missing.join(', ')}.` : 'Limited key detected.';
                        }
                        else if (validation?.level) message = 'Custom API key validated with required selections.';
                        else if (pdaKeyActive) message = 'Using PDA supplied key. Save your own key to unlock full status checks.';
                        else if (storedKey) message = 'Verify your custom Torn API key to unlock features.';
                        else if (state.script.isPDA) message = 'PDA API key placeholder not replaced. Provide a custom key.';
                        else message = 'Requires factions.basic, members, rankedwars, chain and users.basic, attacks selections.';
                    }
                    const sourceNote = (() => {
                        if (source === 'local') return 'Source: Custom key set in browser.';
                        if (source === 'pda') return 'Source: PDA provided key.';
                        return state.script.isPDA ? 'Source: PDA placeholder missing.' : 'Source: None saved yet.';
                    })();
                    // If validation indicates the key is limited, emit a compact
                    // diagnostic to help debug required selections or missing scopes.
                    // Important: do NOT log the raw API key or any PII here.
                    if (validation?.isLimited) {
                        try {
                            const access = keyInfo?.access || {};
                            const diag = {
                                reason: 'limited-diagnostic',
                                validationLevel: validation.level || null,
                                accessType: access.type || null,
                                accessLevel: access.level || null,
                                factionSelections: !!(keyInfo?.info?.selections?.faction),
                                missing: validation?.missing || [],
                                scopes: (validation?.scopes || []).slice(0, 50)
                            };
                            try { tdmlogger('warn', '[APIKEY] key marked limited — diagnostic', diag); } catch (_) { console.warn('[APIKEY] key marked limited — diagnostic', diag); }
                        } catch (e) { try { console.warn('[APIKEY] limited diagnostic failed', e?.message || e); } catch(_) {} }
                    }

                    return {
                        storedKey,
                        hasCustom: !!storedKey,
                        source,
                        sourceNote,
                        pdaKeyActive,
                        tone: tone || 'error',
                        status,
                        message
                    };
                };
                const apiKeyUi = computeApiKeyUi();
                const apiKeyStatus = apiKeyUi.status;
                const apiKeyTone = apiKeyUi.tone || 'error';
                const apiKeyMessage = apiKeyUi.message;
                const apiKeyInputValue = apiKeyUi.storedKey ? escapeHtml(apiKeyUi.storedKey) : '';
                const apiKeyMessageColor = (apiKeyTone === 'success' ? '#86efac' : apiKeyTone === 'warning' ? '#facc15' : apiKeyTone === 'info' ? '#93c5fd' : '#fca5a5');
                const apiKeyMessageHtml = escapeHtml(apiKeyMessage);
                const apiKeySourceNote = apiKeyUi.sourceNote;
                const apiKeySourceNoteHtml = apiKeySourceNote ? escapeHtml(apiKeySourceNote) : '';

                // FFScouter Key UI Logic
                const computeFFScouterKeyUi = () => {
                    const storedKey = storage.get('ffscouterApiKey', '') || '';
                    const hasKey = !!storedKey;
                    // When running inside PDA, an injected FFScouter/BSP cache may be present
                    if (state.script?.isPDA && !hasKey) {
                        return {
                            storedKey: '',
                            status: 'Using FFScouter',
                            tone: 'info',
                            message: 'No API key needed. Runs with FFScouter userscript and uses its cache.'
                        };
                    }
                    const status = hasKey ? 'Saved' : 'Not Set';
                    const tone = hasKey ? 'success' : 'error';
                    const message = hasKey ? 'FFScouter key saved.' : 'Enter your FFScouter API key to enable Fair Fight & Battle Stats estimates.';
                    return { storedKey, status, tone, message };
                };
                const ffUi = computeFFScouterKeyUi();
                const ffKeyInputValue = ffUi.storedKey ? escapeHtml(ffUi.storedKey) : '';
                const ffKeyMessageColor = (ffUi.tone === 'success' ? '#86efac' : '#fca5a5');

                // Determine admin status for settings edits
                const adminFunctionalityEnabled = storage.get('adminFunctionality', true) === true;
                const isAdmin = !!state.script.canAdministerMedDeals && adminFunctionalityEnabled;
                const factionSettings = state.script.factionSettings || {};
                const dibsStyle = (factionSettings.options && factionSettings.options.dibsStyle) || {
                    keepTillInactive: true,
                    mustRedibAfterSuccess: false,
                    allowStatuses: { Okay: true, Hospital: true, Travel: false, Abroad: false, Jail: false },
                    removeOnFly: false,
                    inactivityTimeoutSeconds: 300,
                    timeRemainingLimits: { minSecondsToDib: 0 }
                };
                const attackMode = factionSettings.options?.attackMode || factionSettings.attackMode || 'Mode Not Set';
                const showAttackMode = (warType === 'Ranked War');
                const activityCadenceMs = Number(storage.get('tdmActivityCadenceMs', 10000)) || 10000;
                const noteTagsDefault = 'dex+,def+,str+,spd+,hosp,retal';
                const noteTagsValueRaw = utils.coerceStorageString(storage.get('tdmNoteQuickTags', noteTagsDefault), noteTagsDefault);
                const noteTagsValue = (noteTagsValueRaw || noteTagsDefault).replace(/"/g, '&quot;');
                perf?.stop?.('ui.updateSettingsContent.snapshot');

                perf?.start?.('ui.updateSettingsContent.render');
                let runRankedWarPrefetch = null;
                // Inject helper styles once
                try {
                    if (!document.getElementById('tdm-mini-style')) {
                        const st = document.createElement('style');
                        st.id = 'tdm-mini-style';
                        st.textContent = `.tdm-grid-war{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px;align-items:end}`+
                            `.tdm-mini-lbl{display:block;font-size:11px;color:#ccc;margin-bottom:2px;font-weight:500}`+
                            `.tdm-mini-checkbox{font-size:12px;color:#ccc;display:flex;align-items:center;gap:4px}`+
                            `.tdm-check-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));gap:4px;margin-top:4px}`+
                            `.tdm-check-grid label{font-size:11px;display:flex;align-items:center;gap:4px;color:#ccc}`+
                            /* Termed-war input hints */
                            `.tdm-term-required{border:1px solid #facc15 !important; box-shadow:0 0 0 3px rgba(250,204,21,0.12) !important; border-radius:4px !important;}`+
                            `.tdm-term-calculated{border:1px solid #4ade80 !important; box-shadow:0 0 0 3px rgba(34,197,94,0.12) !important; border-radius:4px !important;}`+
                            `.tdm-term-error{border:1px solid #fb7185 !important; box-shadow:0 0 0 3px rgba(248,113,113,0.12) !important; border-radius:4px !important;}`+
                            `.tdm-initial-readonly{background:transparent;border:1px dashed #333;padding:4px;border-radius:4px;color:#ddd}`

                        document.head.appendChild(st);
                    }
                    if (!document.getElementById('tdm-api-key-style')) {
                        const st = document.createElement('style');
                        st.id = 'tdm-api-key-style';
                        st.textContent = `#tdm-api-key-card{transition:border-color .2s ease,box-shadow .2s ease}`+
                            `#tdm-api-key-card[data-tone="success"]{border-color:#16a34a}`+
                            `#tdm-api-key-card[data-tone="warning"]{border-color:#facc15}`+
                            `#tdm-api-key-card[data-tone="info"]{border-color:#3b82f6}`+
                            `#tdm-api-key-card[data-tone="error"]{border-color:#f87171}`+
                            `#tdm-api-key-card.tdm-api-key-highlight{box-shadow:0 0 0 2px rgba(250,204,21,0.8),0 0 14px rgba(250,204,21,0.35)}`;
                        document.head.appendChild(st);
                    }
                } catch(_) {}

            // Build new compact war / dibs layout
            content.innerHTML = `
                <div class="settings-section collapsible ${collapsedState['latest-war'] === false ? '' : 'collapsed'}" data-section="latest-war">
                    <div class="settings-header collapsible-header">RW Details: <span id="rw-warstring" style="color:#ffd600;">${warstring}</span> <span class="chevron">▾</span></div>
                    <div class="collapsible-content">
                        <div style="margin-top:4px;padding:6px;background:#222;border-radius:5px;">
                            <div class="tdm-grid-war">
                                <div id="war-type-container">
                                    <label class="tdm-mini-lbl">War Type</label>
                                    ${isAdmin ? `<select id=\"war-type-select\" class=\"settings-input\"><option value=\"\" disabled ${!warType||warType==='War Type Not Set'?'selected':''}>Not Set</option><option value=\"Termed War\" ${warType==='Termed War'?'selected':''}>Termed War</option><option value=\"Ranked War\" ${warType==='Ranked War'?'selected':''}>Ranked War</option></select>`:`<div class=\"settings-input-display\">${warType}</div>`}
                                </div>
                                <div id="term-type-container" style="display:${termedWarDisplay};">
                                    <label class="tdm-mini-lbl">Term Type</label>
                                    ${isAdmin ? `<select id=\"term-type-select\" class=\"settings-input\"><option value=\"Termed Loss\" ${termType==='Termed Loss'?'selected':''}>Termed Loss</option><option value=\"Termed Win\" ${termType==='Termed Win'?'selected':''}>Termed Win</option></select>`:`<div class=\"settings-input-display\">${termType}</div>`}
                                </div>
                                <div id="score-cap-container" style="display:${termedWarDisplay};">
                                    <label class="tdm-mini-lbl">Faction Cap</label>
                                    ${isAdmin ? `<input type=\"number\" id=\"faction-score-cap-input\" value=\"${factionScoreCap}\" min=\"0\" class=\"settings-input\" style=\"width:80px;\">`:`<div class=\"settings-input-display\">${factionScoreCap}</div>`}
                                </div>
                                <div id="individual-score-type-container" style="display:${termedWarDisplay};">
                                    <label class="tdm-mini-lbl">Indiv Type</label>
                                    ${isAdmin ? `<select id="individual-score-type-select" class="settings-input" style="width:130px;"><option value="Attacks" ${individualScoreType==='Attacks'?'selected':''}>Attacks</option><option value="Respect" ${individualScoreType==='Respect'?'selected':''}>Respect</option><option value="Respect (no chain)" ${individualScoreType==='Respect (no chain)'?'selected':''}>Respect No-Chain</option><option value="Respect (no bonus)" ${individualScoreType==='Respect (no bonus)'?'selected':''}>Respect No-Bonus</option></select>`:`<div class="settings-input-display">${individualScoreType}</div>`}
                                </div>
                                <div id="individual-score-cap-container" style="display:${termedWarDisplay};">
                                    <label class="tdm-mini-lbl">Indiv Cap</label>
                                    ${isAdmin ? `<input type="number" id="individual-score-cap-input" value="${individualScoreCap}" min="0" class="settings-input" style="width:80px;">`:`<div class="settings-input-display">${individualScoreCap}</div>`}
                                </div>
                                <!-- Row 2 inputs -->
                                <div id="opponent-score-cap-container" style="display:${termedWarDisplay};">
                                    <label class="tdm-mini-lbl">Opponent Cap</label>
                                    ${isAdmin ? `<input type="number" id="opponent-score-cap-input" value="${state.warData?.opponentScoreCap ?? ''}" min="0" class="settings-input" style="width:100px;">`:`<div class="settings-input-display">${state.warData?.opponentScoreCap ?? ''}</div>`}
                                </div>
                                <div id="initial-target-container" style="display:${termedWarDisplay};">
                                    <label class="tdm-mini-lbl">Initial Target</label>
                                    ${/* initial target is display-only (never editable) */''}
                                    <div id="initial-target-display" class="settings-input-display">${Number(state.warData?.initialTargetScore ?? ((state.lastRankWar?.war?.target ?? state.lastRankWar?.target) || 0)) || ''}</div>
                                </div>
                                <div id="target-end-container" style="display:${termedWarDisplay}; min-width:0; grid-column: span 2;">
                                    <label class="tdm-mini-lbl">Target End (UTC, hour-only)</label>
                                    ${isAdmin ? `<div style="display:flex;align-items:center;"><input type="datetime-local" step="3600" id="war-target-end-input" class="settings-input" value="${(function(){try{const t=Number(state.warData?.targetEndTime||0)||0; if(!t) return ''; const d=new Date(t*1000); const pad=(n)=>String(n).padStart(2,'0'); return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:00`; }catch(_){return ''}})()}" style="width:100%; min-width:220px; box-sizing:border-box;"><span id="war-target-end-utc-preview" style="margin-left:6px; font-size:11px; color:#aaa; white-space:nowrap;"></span></div>`:`<div class="settings-input-display" style="max-width:100%;word-break:break-word;overflow-wrap:break-word;">${state.warData?.targetEndTime ? (new Date(Number(state.warData.targetEndTime||0)*1000).toUTCString() + ' (UTC)') : ''}</div>`}
                                </div>
                                <div id="initial-score-clip">
                                
                                </div>
                                <div id="initial-score-clip-end" style="display:none"></div>
                                <div id="initial-score-clip-end2" style="display:none"></div>
                                <div id="initial-target-display" style="grid-column:span 2; margin-top:6px; color:#9ca3af; font-size:12px;" ></div>
                                <div id="initial-target-debug" style="display:none"></div>
                                <div id="initial-target-break" style="display:none"></div>
                                <div id="initial-target-extra" style="display:none"></div>
                                <div id="initial-target-traits" style="display:none"></div>
                                <div id="initial-target-final" style="display:none"></div>
                                <div id="target-end-display" style="grid-column:span 2; margin-top:4px; color:#9ca3af; font-size:12px;" ></div>
                                <div id="rw-term-info" style="margin-top:8px;font-size:12px;color:#9ca3af;display:block;width:100%;box-sizing:border-box;grid-column:1 / -1"></div>
                                <div style="grid-column:span 2;min-width:200px;">
                                    <label class="tdm-mini-lbl">Opponent</label>
                                    <div class="settings-input-display" style="white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${opponentFactionName} ${opponentFactionId?`(ID:${opponentFactionId})`:''}</div>
                                </div>
                                ${isAdmin ? `<label style="display:flex;align-items:center;gap:8px;color:#ccc;font-size:12px;margin-left:8px;"><input type="checkbox" id="war-disable-meddeals" ${state.warData?.disableMedDeals ? 'checked' : ''} /> Disable Med Deals (Only show dibs button) in Termed Wars</label>` : ''}
                                <div style="display:flex;justify-content:center;gap:6px;flex-wrap:wrap;align-self:center;">
                                    <button id="save-war-data-btn" class="settings-btn settings-btn-green" style="display:${storage.get('adminFunctionality', true)?'inline-block':'none'};" title="Persist current war configuration (term caps, opponent, score types)." aria-label="Save war data">Save War Data</button>
                                    <button id="copy-war-details-btn" class="settings-btn settings-btn-blue" title="Copy a shareable war summary to the clipboard." aria-label="Copy war details">Copy War Details</button>
                                    
                                </div>
                            </div>
                            <div style="font-size:11px;color:#888;margin-top:4px;">Set Indiv Cap = 0 for unlimited.</div>
                            <div id="attack-mode-group" style="margin-top:8px;display:${showAttackMode?'block':'none'};">
                                <div class="settings-subheader" style="margin-bottom:4px;color:#93c5fd;text-align:center;">Attack Mode</div>
                                <div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
                                    <div style="font-size:12px;">Current: <span style="color:#ffd600;">${attackMode}</span></div>
                                    ${isAdmin?`<select id=\"attack-mode-select\" class=\"settings-input\" style=\"width:120px;\" title=\"Select attack mode (Farming enforces dibs).\">${['Farming','FFA','Turtle'].map(m=>`<option value='${m}' ${attackMode===m?'selected':''}>${m}</option>`).join('')}</select><button id=\"attack-mode-save-btn\" class=\"settings-btn settings-btn-green\" title=\"Save selected attack mode.\" aria-label=\"Save attack mode\">Save</button>`:''}
                                    <div style="font-size:11px;color:#aaa;">Dibs enforced only in Farming.</div>
                                </div>
                            </div>
                            <div class="settings-subheader" style="margin-top:10px;margin-bottom:4px;color:#93c5fd;text-align:center;">Dibs Style</div>
                            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:8px;">
                                <label class="tdm-mini-checkbox"><input type="checkbox" id="dibs-keep-inactive" ${dibsStyle.keepTillInactive?'checked':''} ${isAdmin?'':'disabled'} /> Keep Until Inactive</label>
                                <label class="tdm-mini-checkbox"><input type="checkbox" id="dibs-redib-after-success" ${dibsStyle.mustRedibAfterSuccess?'checked':''} ${isAdmin?'':'disabled'} /> Require Re-dib After Success</label>
                                <label class="tdm-mini-checkbox">Inactivity (s): <input type="number" id="dibs-inactivity-seconds" min="60" step="30" value="${parseInt(dibsStyle.inactivityTimeoutSeconds||300)}" ${isAdmin?'':'disabled'} class="settings-input" style="width:80px;margin-left:4px;" /></label>
                                <label class="tdm-mini-checkbox">Max Hosp (m): <input type="number" id="dibs-max-hosp-minutes" min="0" step="1" value="${Number(dibsStyle.maxHospitalReleaseMinutes||0)}" ${isAdmin?'':'disabled'} class="settings-input" style="width:70px;margin-left:4px;" /></label>
                                <label class="tdm-mini-checkbox">Remove If Opp Travels <input type="checkbox" id="dibs-remove-on-fly" ${dibsStyle.removeOnFly?'checked':''} ${isAdmin?'':'disabled'} style="margin-left:4px;" /></label>
                                <label class="tdm-mini-checkbox">Remove If You Travel <input type="checkbox" id="dibs-remove-user-travel" ${(dibsStyle.removeWhenUserTravels?'checked':'')} ${isAdmin?'':'disabled'} style="margin-left:4px;" /></label>
                                <label class="tdm-mini-checkbox"><input type="checkbox" id="dibs-bypass-style" ${dibsStyle.bypassDibStyle ? 'checked' : ''} ${isAdmin ? '' : 'disabled'} /> Bypass Dibs Style (Admins may ignore rules)</label>
                            </div>
                            <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:12px;margin-top:10px;">
                                <div><div class="tdm-mini-lbl">Allowed Opponent Statuses</div><div class="tdm-check-grid">${['Okay','Hospital','Travel','Abroad','Jail'].map(s=>`<label><input type='checkbox' class='dibs-allow-status' data-status='${s}' ${dibsStyle.allowStatuses?.[s]?'checked':''} ${isAdmin?'':'disabled'} /> ${s}</label>`).join('')}</div></div>
                                <div><div class="tdm-mini-lbl">Allowed Opponent Activity</div><div class="tdm-check-grid">${(()=>{const dflt={Online:true,Idle:true,Offline:true};const ala=dibsStyle.allowLastActionStatuses||{};return ['Online','Idle','Offline'].map(s=>`<label><input type='checkbox' class='dibs-allow-lastaction' data-status='${s}' ${(ala[s]??dflt[s])?'checked':''} ${isAdmin?'':'disabled'} /> ${s}</label>`).join('');})()}</div></div>
                                <div><div class="tdm-mini-lbl">Allowed User Statuses</div><div class="tdm-check-grid">${(()=>{const dflt={Okay:true,Hospital:true,Travel:false,Abroad:false,Jail:false};const aus=dibsStyle.allowedUserStatuses||{};return ['Okay','Hospital','Travel','Abroad','Jail'].map(s=>`<label><input type='checkbox' class='dibs-allow-user-status' data-status='${s}' ${(aus[s]??dflt[s])?'checked':''} ${isAdmin?'':'disabled'} /> ${s}</label>`).join('');})()}</div></div>
                            </div>
                            <div style="display:flex;justify-content:center;gap:6px;flex-wrap:wrap;margin-top:8px;">
                                <button id="save-dibs-style-btn" class="settings-btn settings-btn-green" style="display:${isAdmin?'inline-block':'none'};" title="Persist dibs style rules (timeouts, allowed statuses)." aria-label="Save dibs style">Save Dibs Style</button>
                                <button id="copy-dibs-style-btn" class="settings-btn settings-btn-blue" title="Copy a summary of the current dibs style." aria-label="Copy dibs style">Copy Dibs Style</button>
                            </div>
                            ${!isAdmin?'<div style="text-align:center;color:#aaa;margin-top:4px;">Visible to all members. Only admins can edit.</div>':''}
                        </div>
                    </div>
                </div>

                <div class="settings-section settings-section-divided collapsible ${collapsedState['ranked-war-tools'] === false ? '' : 'collapsed'}" data-section="ranked-war-tools">
                    <div class="settings-header collapsible-header">Ranked War Reports<span class="chevron">▾</span></div>
                    <div class="collapsible-content" style="display:flex; gap:6px; align-items:center; flex-wrap:wrap;">
                        <select id="ranked-war-id-select" class="settings-input" style="flex-grow: 1;" title="Select a ranked war to analyze."><option value="">Loading wars...</option></select>
                        <button id="show-ranked-war-summary-btn" class="settings-btn settings-btn-green" title="Generate summary for selected ranked war." aria-label="Show ranked war summary">War Summary</button>
                        <button id="view-war-attacks-btn" class="settings-btn settings-btn-blue" title="Open detailed attacks list for selected ranked war." aria-label="View war attacks">War Attacks</button>
                    </div>
                </div>                
                <!-- ChainWatcher Section -->
                <div class="settings-section settings-section-divided collapsible ${collapsedState['chain-watcher'] === false ? '' : 'collapsed'}" data-section="chain-watcher">
                    <div class="settings-header collapsible-header">ChainWatcher: <span id="tdm-chainwatcher-header-names" style="color:#ffd600;">—</span> <span class="chevron">▾</span></div>
            <div class="collapsible-content">
                <div style="font-size:12px;color:#ccc;margin-bottom:6px;">Select current chain watchers (authoritative list stored for the faction). Selected names will appear as a badge in the UI.</div>
                <div id="tdm-chainwatcher-meta" style="font-size:11px;color:#9ca3af;margin-bottom:6px;">Last updated: —</div>
                        <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
                            <select id="tdm-chainwatcher-select" class="settings-input" multiple style="min-width:220px;max-width:420px;min-height:120px;">
                                <!-- options populated dynamically -->
                            </select>
                            <div style="display:flex;flex-direction:column;gap:6px;">
                                <button id="tdm-chainwatcher-save" class="settings-btn settings-btn-green">Save</button>
                                <button id="tdm-chainwatcher-clear" class="settings-btn">Clear</button>
                            </div>
                        </div>
                        <div style="margin-top:8px;font-size:11px;color:#888;">Selections are authoritative from the server; local UI reflects server state. Local storage key <code>chainWatchers</code> is used for quick reads.</div>
                    </div>
                </div>
                <div class="settings-section settings-section-divided collapsible ${collapsedState['api-keys'] === false ? '' : 'collapsed'}" data-section="api-keys">
                    <div class="settings-header collapsible-header">API Keys <span class="chevron">▾</span></div>
                    <div class="collapsible-content">
                        <!-- Torn API Key Card -->
                        <div id="tdm-api-key-card" class="settings-card" data-tone="${apiKeyTone}" style="margin-bottom:12px;border:1px solid #3b82f6;background:#0f172a;padding:10px;border-radius:8px;">
                            <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
                                <div>
                                    <div style="font-size:12px;color:#93c5fd;font-weight:600;">Torn API Key for TDM Access</div>
                                    <div id="tdm-api-key-status" style="font-size:11px;color:#cbd5f5;">${apiKeyStatus}</div>
                                </div>
                                <div style="display:flex;gap:6px;flex-wrap:wrap;">
                                    <!-- Revalidate removed: we auto-check key when the settings panel opens -->
                                    <button id="tdm-api-key-clear-btn" class="settings-btn settings-btn-red" title="Clear the stored custom API key and fall back to PDA or idle state.">Clear</button>
                                </div>
                            </div>
                            <div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
                                <input type="password" id="tdm-api-key-input" class="settings-input" placeholder="Enter custom Torn API key" value="${apiKeyInputValue}" style="min-width:200px;flex:1;letter-spacing:2px;" autocomplete="off" />
                                <button id="tdm-api-key-save-btn" class="settings-btn settings-btn-green">Save Key</button>
                                <button id="tdm-generate-key-btn" class="settings-btn settings-btn-blue" title="Open Torn API key creation page" aria-label="Generate custom key">Generate Custom Key</button>
                                <label style="font-size:11px;color:#9ca3af;display:flex;align-items:center;gap:4px;">
                                    <input type="checkbox" id="tdm-api-key-show" /> Show
                                </label>
                            </div>
                            <div id="tdm-api-key-message" data-tone="${apiKeyTone}" style="font-size:11px;margin-top:6px;color:${apiKeyMessageColor};">${apiKeyMessageHtml}</div>
                            <div id="tdm-api-key-source" style="font-size:10px;color:#6b7280;margin-top:6px;">${apiKeySourceNoteHtml}</div>
                        </div>

                        <!-- FFScouter API Key Card -->
                        <div id="tdm-ffscouter-key-card" class="settings-card" data-tone="${ffUi.tone}" style="margin-bottom:12px;border:1px solid #3b82f6;background:#0f172a;padding:10px;border-radius:8px;">
                            <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
                                <div>
                                    <div style="font-size:12px;color:#93c5fd;font-weight:600;">FFScouter API Key (PC Only, PDA uses other scripts key)</div>
                                    <div id="tdm-ffscouter-key-status" style="font-size:11px;color:#cbd5f5;">${ffUi.status}</div>
                                </div>
                                <div style="display:flex;gap:6px;flex-wrap:wrap;">
                                    <button id="tdm-ffscouter-key-clear-btn" class="settings-btn settings-btn-red" title="Clear the stored FFScouter API key.">Clear</button>
                                </div>
                            </div>
                            <div style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
                                <input type="password" id="tdm-ffscouter-key-input" class="settings-input" placeholder="Enter FFScouter API key" value="${ffKeyInputValue}" style="min-width:200px;flex:1;letter-spacing:2px;" autocomplete="off" />
                                <button id="tdm-ffscouter-key-save-btn" class="settings-btn settings-btn-green">Save Key</button>
                                <a href="https://ffscouter.com/" target="_blank" rel="noopener" class="settings-btn settings-btn-blue" style="text-decoration:none;line-height:24px;padding:0 12px;display:inline-block;" title="Get key at ffscouter.com">Get Key</a>
                                <label style="font-size:11px;color:#9ca3af;display:flex;align-items:center;gap:4px;">
                                    <input type="checkbox" id="tdm-ffscouter-key-show" /> Show
                                </label>
                            </div>
                            <div id="tdm-ffscouter-key-message" style="font-size:11px;margin-top:6px;color:${ffKeyMessageColor};">${ffUi.message}</div>
                        </div>
                        <!-- BSP API Key Card -->
                        <div id="tdm-bsp-key-card" class="settings-card" data-tone="${ffUi.tone}" style="margin-bottom:12px;border:1px solid #3b82f6;background:#0f172a;padding:10px;border-radius:8px;">
                            <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;">
                                <div>
                                    <div style="font-size:12px;color:#93c5fd;font-weight:600;">Battle Stats Predictor</div>
                                    <div id="tdm-ffscouter-key-status" style="font-size:11px;color:#cbd5f5;">${ffUi.status}</div>
                                </div>
                            </div>
                            <div id="tdm-bsp-key-message" style="font-size:11px;margin-top:6px;color:${ffKeyMessageColor};">No Key Needed. Uses BSP automatically when that userscript is installed and you are an active subscriber.</div>
                        </div>
                    </div>
                </div>

                <div class="settings-section settings-section-divided collapsible ${collapsedState['general-settings'] === false ? '' : 'collapsed'}" data-section="general-settings">
                    <div class="settings-header collapsible-header">General Settings <span class="chevron">▾</span></div>
                    <div class="collapsible-content">
                        <div class="settings-button-group" style="gap:4px; flex-wrap:wrap;">
                            <!-- Badges Dock (moved Core Timers & Badges into a collapsible section) -->
                            <div class="settings-section settings-subsection collapsible ${collapsedState['badges-dock'] === false ? '' : 'collapsed'}" data-section="badges-dock" style="margin-top:6px;">
                                <div class="settings-header settings-subsection collapsible-header">Badges Dock <span class="chevron">▾</span></div>
                                <div class="collapsible-content" style="display:flex; gap:12px; flex-wrap:wrap; align-items:flex-start;">
                                    <div style="display:flex; flex-direction:column; gap:6px; min-width:240px;">
                                        <div style="font-size:11px; color:#93c5fd; font-weight:600; text-align:center;">Core Timers</div>
                                        <div style="display:flex; gap:6px; flex-wrap:wrap;">
                                            <button id="chain-timer-btn" class="settings-btn ${storage.get('chainTimerEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle chain timer display." aria-label="Toggle chain timer">Chain Timer: ${storage.get('chainTimerEnabled', true) ? 'Enabled' : 'Disabled'}</button>
                                            <button id="inactivity-timer-btn" class="settings-btn ${storage.get('inactivityTimerEnabled', false) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle inactivity tracking timer." aria-label="Toggle inactivity timer">Inactivity Timer: ${storage.get('inactivityTimerEnabled', false) ? 'Enabled' : 'Disabled'}</button>
                                            <button id="opponent-status-btn" class="settings-btn ${storage.get('opponentStatusTimerEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle periodic opponent status checks." aria-label="Toggle opponent status timer">Opponent Status: ${storage.get('opponentStatusTimerEnabled', true) ? 'Enabled' : 'Disabled'}</button>
                                        </div>
                                    </div>
                                    <div style="display:flex; flex-direction:column; gap:6px; min-width:260px;">
                                        <div style="font-size:11px; color:#93c5fd; font-weight:600; text-align:center;">Badges</div>
                                        <div style="display:flex; gap:6px; flex-wrap:wrap;">
                                            <button id="api-usage-btn" class="settings-btn ${storage.get('apiUsageCounterEnabled', false) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle API usage counter badge." aria-label="Toggle API usage badge">API Counter: ${storage.get('apiUsageCounterEnabled', false) ? 'Shown' : 'Hidden'}</button>
                                            <button id="attack-mode-badge-btn" class="settings-btn ${storage.get('attackModeBadgeEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle attack mode badge in chat header (Ranked War & active)." aria-label="Toggle attack mode badge">Attack Mode Badge: ${storage.get('attackModeBadgeEnabled', true) ? 'Shown' : 'Hidden'}</button>
                                            <button id="chainwatcher-badge-btn" class="settings-btn ${storage.get('chainWatcherBadgeEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle Chain Watchers badge in the badges dock." aria-label="Toggle chain watchers badge">Chain Watchers Badge: ${storage.get('chainWatcherBadgeEnabled', true) ? 'Shown' : 'Hidden'}</button>
                                            <button id="user-score-badge-btn" class="settings-btn ${storage.get('userScoreBadgeEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle personal score badge." aria-label="Toggle user score badge">User Score Badge: ${storage.get('userScoreBadgeEnabled', true) ? 'Shown' : 'Hidden'}</button>
                                            <button id="faction-score-badge-btn" class="settings-btn ${storage.get('factionScoreBadgeEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle faction score badge." aria-label="Toggle faction score badge">Faction Score Badge: ${storage.get('factionScoreBadgeEnabled', true) ? 'Shown' : 'Hidden'}</button>
                                            <button id="dibs-deals-badge-btn" class="settings-btn ${storage.get('dibsDealsBadgeEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle dibs/deals badge." aria-label="Toggle dibs deals badge">Dibs/Deals Badge: ${storage.get('dibsDealsBadgeEnabled', true) ? 'Shown' : 'Hidden'}</button>
                                        </div>
                                    </div>
                                </div>
                            </div>
                            <div class="settings-section settings-subsection collapsible ${collapsedState['column-settings'] === false ? '' : 'collapsed'}" data-section="column-settings" style="margin-top:8px; margin-bottom:4px;">
                                <div class="settings-header settings-subsection collapsible-header">Column Settings <span class="chevron">▾</span></div>
                                <div class="collapsible-content" style="display:flex; flex-direction:column; gap:6px; text-align:center;">
                                    <div style="display:flex; justify-content:center; gap:8px; margin-bottom:6px;">
                                        <button id="reset-column-widths-btn" class="settings-btn settings-btn-red" title="Reset all column widths to defaults">Reset Column Widths</button>
                                    </div>
                                    <div class="cv-groups" style="display:flex; flex-direction:column; gap:6px; text-align:center;">
                                <div class="cv-group">
                                    <div class="mini-label" style="font-size:11px; color:#bbb; margin-bottom:2px;">Ranked War Table</div>
                                    <div id="column-visibility-rw" class="settings-button-group" style="gap:6px;"></div>
                                </div>
                                <div class="cv-group">
                                    <div class="mini-label" style="font-size:11px; color:#bbb; margin-bottom:2px;">Members List Table</div>
                                    <div id="column-visibility-ml" class="settings-button-group" style="gap:6px;"></div>
                                </div>
                                </div>
                            </div>
                        </div>
                            <div class="settings-section settings-subsection collapsible ${collapsedState['api-cadence'] === false ? '' : 'collapsed'}" data-section="api-cadence">
                                <div class="settings-header settings-subsection collapsible-header">API Cadence &amp; Polling <span class="chevron">▾</span></div>
                                <div class="collapsible-content">
                                    <div style="font-size:12px; color:#bcd; line-height:1.4; margin-bottom:2px;">
                                        Torn API cadence controls how often TreeDibsMapper pulls faction bundles. This keeps scores, dibs, and status data fresh for the whole UI. Activity Tracking (below) runs on top of these pulls and only adds extra diff processing when enabled. When the tab is inactive the cadence backs off unless the activity keep-alive option is in use.
                                    </div>
                                    <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap; justify-content:center; background:#1a1a1a; padding:8px; border-radius:6px;">
                                        <label style="display:flex; align-items:center; gap:6px; color:#ccc;">
                                            Team refresh interval (seconds):
                                            <input type="number" id="faction-bundle-refresh-seconds" class="settings-input" min="5" step="5" style="width:100px;" value="${Math.round((Number(storage.get('factionBundleRefreshMs', null)) || state.script.factionBundleRefreshMs || config.DEFAULT_FACTION_BUNDLE_REFRESH_MS)/1000)}" />
                                        </label>
                                        <button id="save-faction-bundle-refresh-btn" class="settings-btn settings-btn-green">Apply</button>
                                        <div style="flex-basis:100%; font-size:11px; color:#aaa; max-width:480px;">Your faction is always polled. The current opponent is polled while the ranked war is active; add IDs below to keep other factions in rotation.</div>
                                        <div id="tdm-last-faction-refresh" style="font-size:11px; color:#9ca3af; flex-basis:100%;">Last faction refresh: —</div>
                                        <div id="tdm-polling-status" style="font-size:11px; color:#9ca3af; flex-basis:100%;">Polling status: —</div>
                                        <div id="tdm-opponent-poll-line" style="font-size:11px; color:#9ca3af; flex-basis:100%;">Opponent polling: —</div>
                                    </div>
                                    <div style="margin-top:10px; background:#1a1a1a; padding:8px; border-radius:6px; display:flex; flex-wrap:wrap; gap:8px; align-items:flex-start;">
                                        <label style="display:flex; flex-direction:column; gap:4px; color:#ccc; font-size:12px; flex:1; min-width:240px;">
                                            Extra factions to poll (comma separated IDs):
                                            <input type="text" id="tdm-additional-factions-input" class="settings-input" value="${utils.coerceStorageString(storage.get('tdmExtraFactionPolls',''), '').replace(/"/g,'&quot;')}" placeholder="12345,67890" aria-label="Additional faction IDs" />
                                        </label>
                                        <button id="tdm-save-additional-factions-btn" class="settings-btn settings-btn-blue" title="Persist the additional faction list.">Save</button>
                                        <div id="tdm-additional-factions-status" style="font-size:11px; color:#9ca3af; align-self:center;">No extra factions configured.</div>
                                        <div id="tdm-additional-factions-summary" style="flex-basis:100%; font-size:10px; color:#777; line-height:1.3;">—</div>
                                    </div>
                                </div>
                            </div>
                            <div class="settings-section settings-subsection collapsible ${collapsedState['activity-tracking'] === false ? '' : 'collapsed'}" data-section="activity-tracking">
                                <div class="settings-header settings-subsection collapsible-header">Activity Tracking ${storage.get('tdmActivityTrackingEnabled', false) ? 'Enabled' : 'Disabled'} <span class="chevron">▾</span></div>
                                <div class="collapsible-content">
                                    <div class="activity-tracking-content" style="display:flex; flex-wrap:wrap; gap:12px; width:100%; background:#1f1f1f; padding:8px; border-radius:6px;">
                                        <label style="flex:1; min-width:200px; color:#ccc; font-size:12px;" title="Enable continuous polling of ranked war faction members to infer live status & travel phases. Disable to stop polling.">
                                            <input type="checkbox" id="tdm-activity-tracking-toggle" ${storage.get('tdmActivityTrackingEnabled', false)?'checked':''} aria-label="Toggle Activity Tracking" title="Toggle live Activity Tracking." /> Ranked War Faction Members Activity Tracking
                                        </label>
                                        <label style="flex:1; min-width:200px; color:#ccc; font-size:12px;" title="Keep activity tracking running even when this tab is unfocused or idle. Uses more CPU/network.">
                                            <input type="checkbox" id="tdm-activity-track-idle" ${storage.get('tdmActivityTrackWhileIdle', false)?'checked':''} aria-label="Track while idle" title="Keep activity tracking polling while idle." /> Track while idle (higher resource usage)
                                        </label>
                                        <label style="display:flex; align-items:center; gap:6px; color:#ccc; font-size:12px;">Activity cadence (seconds):
                                            <input type="number" id="tdm-activity-cadence-seconds" min="5" max="60" step="1" value="${Math.min(60,Math.max(5, Number(storage.get('tdmActivityCadenceMs',10000))/1000 || 10))}" class="settings-input" style="width:70px;" title="Polling interval while Activity Tracking is enabled (seconds)." />s
                                        </label>
                                        <button id="tdm-apply-activity-cadence-btn" class="settings-btn settings-btn-green" title="Apply new cadence immediately.">Apply</button>
                                        <div style="flex-basis:100%; font-size:11px; color:#888;">Runs on top of the Torn API cadence above, diffing each pull to expose live status & travel transitions. Only active while enabled. Landed phase inserted after arrival. Visible in player Notes. Disable to minimize memory & churn.</div>
                                        <div style="flex-basis:100%; margin-top:4px; display:flex; flex-wrap:wrap; gap:12px; align-items:center; background:#151515; padding:8px; border-radius:6px;">
                                            <label style="display:flex; align-items:center; gap:6px; color:#ccc; font-size:12px;" title="Upper bound for IndexedDB cache size. 0 = allow browser quota mgmt.">IDB Max Size
                                                <select id="tdm-idb-maxsize-select" class="settings-input" style="width:140px;" aria-label="IndexedDB max size (MB)" title="Limit on cached member snapshots (MB). 0 uses browser-managed quota.">
                                                    ${(()=>{const cur=Number(storage.get('tdmIdbMaxSizeMB',''))||0;const opts=[0,5,10,20,64,96];return opts.map(v=>`<option value='${v}' ${cur===v?'selected':''}>${v? v+' MB':'Auto (Browser)'}</option>`).join('');})()}
                                                </select>
                                            </label>
                                            <div id="tdm-idb-usage-line" style="font-size:11px; color:#9ca3af;">IDB Usage: —</div>
                                            <button id="tdm-flush-activity-cache-btn" class="settings-btn settings-btn-blue" title="Clear cached player states. Useful between wars to save space." aria-label="Flush activity cache">Flush Activity Cache</button>
                                            <button id="tdm-clear-idb-btn" class="settings-btn settings-btn-red" title="Delete the entire IndexedDB database (tdm-store). Frees up all space." aria-label="Clear IDB Storage">Clear IDB Storage</button>
                                            <div style="flex-basis:100%; font-size:10px; color:#666; line-height:1.3;">Cache stores states per player. Flush to reclaim memory & restart confidence ladder. Clear IDB Storage to wipe all cached data.</div>
                                        </div>
                                        <label style="flex:1; min-width:160px; color:#ccc; font-size:12px;" title="Show live metrics: transitions/min, confidence distribution, top destinations, poll timings.">
                                            <input type="checkbox" id="tdm-debug-overlay-toggle" ${storage.get('liveTrackDebugOverlayEnabled', false)?'checked':''} aria-label="Toggle debug overlay" title="Toggle tracking debug overlay." /> Debug Overlay
                                        </label>
                                    </div>
                                </div>
                            </div>
                            <div class="settings-section settings-subsection collapsible ${collapsedState['other'] === false ? '' : 'collapsed'}" data-section="other">
                                <div class="settings-header settings-subsection collapsible-header">Other<span class="chevron">▾</span></div>
                                <div class="collapsible-content">
                                    <div style="display:flex; flex-direction:column; gap:6px; min-width:260px;">
                                        <div style="font-size:11px; color:#93c5fd; font-weight:600; text-align:center;">Alerts & Messaging</div>
                                        <div style="display:flex; gap:6px; flex-wrap:wrap;">
                                            <button id="alert-buttons-toggle-btn" class="settings-btn ${storage.get('alertButtonsEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle quick alert buttons UI." aria-label="Toggle alert buttons">Alert Buttons: ${storage.get('alertButtonsEnabled', true) ? 'Shown' : 'Hidden'}</button>
                                            <!-- Message Dibs to Chat setting removed; use unified "Paste Messages to Chat" setting above -->
                                            <button id="paste-messages-chat-btn" class="settings-btn ${storage.get('pasteMessagesToChatEnabled', true) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle whether messages are automatically pasted into faction chat (when enabled) or only copied to clipboard (when disabled)." aria-label="Toggle paste messages to chat">Paste Messages to Chat: ${storage.get('pasteMessagesToChatEnabled', true) ? 'On' : 'Off'}</button>
                                            <button id="oc-reminder-btn" class="settings-btn ${ocReminderEnabled ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle organized crime (OC) reminder notifications." aria-label="Toggle OC reminder">OC Reminder: ${ocReminderEnabled ? 'Enabled' : 'Disabled'}</button>
                                        </div>
                                    </div>
                                    <br>
                                    <div style="display:flex; flex-direction:column; gap:6px; min-width:200px; align-self:flex-start;">
                                        <div style="font-size:11px; color:#93c5fd; font-weight:600; text-align:center;">Maintenance</div>
                                        <div style="display:flex; gap:6px; flex-wrap:wrap;">
                                            <button id="reset-settings-btn" class="settings-btn settings-btn-red" title="Reset all stored settings, caches, and tracking data (confirmation required)." aria-label="Reset all settings">Reset All Settings</button>
                                        </div>
                                    </div>
                                    <br>
                                </div>
                            </div>
                            ${state.script.canAdministerMedDeals ? `
                            <div class="settings-section settings-subsection collapsible ${collapsedState['Admin-Settings'] === false ? '' : 'collapsed'}" data-section="Admin-Settings">
                                <div class="settings-header settings-subsection collapsible-header">Admin Settings <span class="chevron">▾</span></div>
                                <div class="collapsible-content">
                                    <button id="admin-functionality-btn" class="column-toggle-btn ${storage.get('adminFunctionality', true) ? 'active' : 'inactive'}" title="Toggle admin functions to manage other members' dibs & deals." aria-label="Toggle admin functionality">Manage Others Dibs/Deals</button>
                                    ${state.script.canAdministerMedDeals && storage.get('adminFunctionality', true) === true ?  `
                                    <button id="view-unauthorized-attacks-btn" class="settings-btn" title="Show attacks not authorized under current dibs/attack mode rules." aria-label="View unauthorized attacks">View Unauthorized Attacks</button>
                                    <button id="tdm-adoption-btn" class="settings-btn ${storage.get('debugAdoptionInfo', false) ? 'settings-btn-green' : 'settings-btn-blue'}" title="Toggle display of adoption/use metrics overlay." aria-label="Toggle adoption info">TDM Adoption Info</button>
                                    <div style="margin-top:2px; padding:8px; border:1px solid #444; border-radius:6px;">
                                        <div style="margin-bottom:6px; color:#fca5a5; font-weight:600;">Bulk Cleanup (current enemy faction)</div>
                                        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
                                            <label><input type="checkbox" id="cleanup-notes" checked /> Notes</label>
                                            <label><input type="checkbox" id="cleanup-dibs" checked /> Dibs</label>
                                            <label><input type="checkbox" id="cleanup-meddeals" checked /> Med Deals</label>
                                            <input type="text" id="cleanup-faction-id" placeholder="Enemy faction ID (optional)" class="settings-input" style="width:160px;" />
                                            <input type="text" id="cleanup-reason" placeholder="Reason (required)" class="settings-input" style="min-width:240px;" />
                                            <button id="run-admin-cleanup-btn" class="settings-btn settings-btn-red" title="Bulk delete selected data types for enemy faction members in current war table." aria-label="Run bulk cleanup">Run Cleanup</button>
                                        </div>
                                        <div style="font-size:11px; color:#aaa; margin-top:4px;">Cleans data for opponents visible in the current Ranked War table that belong to the enemy faction. Optional override lets you specify a faction id manually.</div>
                                        <div id="cleanup-results-line" style="font-size:11px; color:#9ca3af; margin-top:4px; display:none;"></div>
                                    </div>
                                </div>
                                    ` : ''}
                            </div>
                            ` : ''}
                            <!-- Note Tag Presets Editor -->
                            <div class="settings-section settings-subsection collapsible ${collapsedState['note-tags'] === false ? '' : 'collapsed'}" data-section="note-tags" style="margin-top:12px;">
                                <div class="settings-header settings-subsection collapsible-header">Note Tag Presets <span class="chevron">▾</span></div>
                                <div class="collapsible-content">
                                    <div style="font-size:11px; color:#9ca3af;">Configure up to 12 quick-add tags (comma or space separated). Applied in note modal if not already present. Invalid or duplicate tags are skipped.</div>
                                    <input type="text" id="note-tags-input" class="settings-input" style="width:100%;" maxlength="240" value="${utils.coerceStorageString(storage.get('tdmNoteQuickTags','dex+,def+,str+,spd+,hosp,retal'), noteTagsDefault).replace(/"/g,'&quot;')}" placeholder="dex+,def+,str+,spd+,hosp,retal" title="Comma or space separated tags" aria-label="Note tag presets input" />
                                    <div id="note-tags-preview" style="display:flex; flex-wrap:wrap; gap:6px; min-height:26px;"></div>
                                    <div style="display:flex; gap:8px; flex-wrap:wrap;">
                                        <button id="note-tags-save-btn" class="settings-btn settings-btn-green" aria-label="Save note tag presets" title="Save tag presets for quick-add in note modal">Save Presets</button>
                                        <button id="note-tags-reset-btn" class="settings-btn settings-btn-blue" aria-label="Reset note tag presets" title="Reset to default tag presets">Reset Default</button>
                                        <div id="note-tags-status" style="font-size:11px; align-self:center; color:#9ca3af;">Idle</div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        <!-- Dev Section (collapsed by default) -->
                        <div class="settings-section settings-section-divided settings-subsection collapsible ${collapsedState['dev-settings'] === false ? '' : 'collapsed'}" data-section="dev-settings" style="margin-top:12px;">
                            <div class="settings-header settings-subsection collapsible-header">Dev <span class="chevron">▾</span></div>
                            <div class="collapsible-content">
                                <div class="settings-subheader" style="color:#93c5fd; text-align:center;">Developer Toggles</div>
                                <div class="settings-button-group" style="display:flex; gap:6px; flex-wrap:wrap;">
                                    <button id="verbose-row-logs-btn" class="settings-btn ${storage.get('debugRowLogs', false) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle verbose per-row console logs." aria-label="Toggle verbose row logs">Verbose Row Logs: ${storage.get('debugRowLogs', false) ? 'On' : 'Off'}</button>
                                    <button id="status-watch-btn" class="settings-btn ${storage.get('debugStatusWatch', false) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle detailed status change watch logs." aria-label="Toggle status watch logs">Status Watch Logs: ${storage.get('debugStatusWatch', false) ? 'On' : 'Off'}</button>
                                    <button id="points-parse-logs-btn" class="settings-btn ${storage.get('debugPointsParseLogs', false) ? 'settings-btn-green' : 'settings-btn-red'}" title="Toggle points parsing debug output." aria-label="Toggle points parse logs">Points Parse Logs: ${storage.get('debugPointsParseLogs', false) ? 'On' : 'Off'}</button>
                                </div>
                                <div style="margin-top:8px; display:flex; gap:8px; align-items:center; justify-content:center;">
                                    <label class="tdm-mini-lbl" style="margin:0 6px 0 0;">Log Level</label>
                                    <select id="tdm-log-level-select" class="settings-input" style="width:140px;">
                                        ${logLevels.map(l=>`<option value="${l}" ${storage.get('logLevel','warn')===l?'selected':''}>${l}</option>`).join('')}
                                    </select>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>`;
            renderPhaseComplete = true;
            perf?.stop?.('ui.updateSettingsContent.render');
            tdmlogger('debug', 'UI: Settings content rendered');
            perf?.start?.('ui.updateSettingsContent.bind');
            bindPhaseStarted = true;
            
            // API Key card handlers
            try {
                const apiKeyCard = document.getElementById('tdm-api-key-card');
                if (apiKeyCard) {
                    const apiInput = document.getElementById('tdm-api-key-input');
                    const saveBtn = document.getElementById('tdm-api-key-save-btn');
                    const clearBtn = document.getElementById('tdm-api-key-clear-btn');
                    // refreshBtn intentionally removed — we auto-check stored key when settings open
                    const refreshBtn = null;
                    const showToggle = document.getElementById('tdm-api-key-show');
                    const messageEl = document.getElementById('tdm-api-key-message');
                    const statusEl = document.getElementById('tdm-api-key-status');
                    const sourceEl = document.getElementById('tdm-api-key-source');
                    const toneColors = { success:'#86efac', warning:'#facc15', info:'#93c5fd', error:'#fca5a5' };
                    const setTone = (tone) => {
                        const t = tone || 'error';
                        apiKeyCard.dataset.tone = t;
                        if (messageEl) messageEl.style.color = toneColors[t] || toneColors.error;
                    };
                    const setStatus = (text) => {
                        if (statusEl) statusEl.textContent = text || 'Unverified';
                    };
                    const setSource = (text) => {
                        if (!sourceEl) return;
                        sourceEl.textContent = text || '';
                    };
                    const setMessage = (tone, text, reason = 'api-key-ui') => {
                        const msg = text || '';
                        const toneKey = tone || 'info';
                        setTone(toneKey);
                        if (messageEl) messageEl.textContent = msg;
                        state.user.apiKeyUiMessage = { tone: toneKey, text: msg, ts: Date.now(), reason };
                    };
                    const refreshFromState = () => {
                        const uiState = computeApiKeyUi();
                        if (apiInput && typeof uiState.storedKey === 'string') {
                            // Don't clobber unsaved user input. Only replace input if empty
                            // or it already matches the stored value (safe to sync).
                            const cur = (apiInput.value || '').trim();
                            const storedVal = String(uiState.storedKey || '').trim();
                            if (!cur || cur === storedVal) apiInput.value = storedVal;
                        }
                        setTone(uiState.tone);
                        if (messageEl) messageEl.textContent = uiState.message;
                        setStatus(uiState.status);
                        setSource(uiState.sourceNote || '');
                        if (showToggle && apiInput) {
                            apiInput.type = showToggle.checked ? 'text' : 'password';
                        }
                    };
                    // Refresh UI from state first, then attempt a silent revalidation of the stored key
                    // to keep helper text accurate when the settings panel opens.
                    refreshFromState();
                    (async () => {
                        try {
                            // Only revalidate if there is a stored or PDA-provided key; avoid toasts
                            // and protect any unsaved input value.
                            await main.revalidateStoredApiKey({ showToasts: false });
                        } catch(_) { /* ignore */ }
                        try { refreshFromState(); } catch(_) { /* noop */ }
                    })();
                    showToggle?.addEventListener('change', (e) => {
                        if (!apiInput) return;
                        apiInput.type = e.target.checked ? 'text' : 'password';
                        if (e.target.checked) apiInput.select?.();
                    });
                    saveBtn?.addEventListener('click', async () => {
                        if (!apiInput) return;
                        const value = (apiInput.value || '').trim();
                        if (!value) {
                            setMessage('error', 'Enter a Torn API key before saving.', 'api-key-empty');
                            apiInput.focus();
                            return;
                        }
                        const originalLabel = saveBtn.textContent;
                        saveBtn.disabled = true;
                        saveBtn.textContent = 'Verifying...';
                        setStatus('Verifying...');
                        setMessage('info', 'Verifying API key...', 'api-key-verifying');
                            try {
                                const verification = await main.verifyApiKey({ key: value });
                                if (!verification.ok) {
                                    // Not valid -> do not reload, show error and keep user's input intact
                                    setMessage('error', verification.message, verification.reason || 'api-key-verify-failed');
                                    setStatus('Unverified');
                                    return;
                                }

                                // If key verified but flagged as LIMITED, save it but DO NOT reload automatically.
                                // This prevents a premature reload and helps the user inspect the diagnostics.
                                if (verification.validation?.isLimited) {
                                    setMessage('warning', verification.message + ' Saved (limited access) — not reloading.', 'api-key-limited');
                                    await main.storeCustomApiKey(value, { reload: false, validation: verification.validation, keyInfo: verification.keyInfo });
                                    // Also show missing scopes/details in the helper text for clarity
                                    const missing = verification.validation?.missing || [];
                                    if (missing.length) {
                                        const friendly = missing.map(s => s.replace('.', ' -> ')).join(', ');
                                        setMessage('warning', `Limited key saved. Missing selections: ${friendly}`, 'api-key-missing-scopes');
                                    }
                                    return;
                                }

                                // Fully validated custom key — store and reload as before to reflect verified state everywhere.
                                setMessage('info', 'Custom API key verified. Saving and reloading…', 'api-key-verified-reload');
                                await main.storeCustomApiKey(value, { reload: true, validation: verification.validation, keyInfo: verification.keyInfo });
                        } catch (error) {
                            setMessage('error', error?.message || 'Failed to save API key.', 'api-key-store-error');
                        } finally {
                            if (state.user.apiKeyUiMessage?.reason !== 'stored' && saveBtn) {
                                saveBtn.disabled = false;
                                saveBtn.textContent = originalLabel;
                            }
                        }
                    });
                    clearBtn?.addEventListener('click', async () => {
                        if (!clearBtn) return;
                        const originalLabel = clearBtn.textContent;
                        clearBtn.disabled = true;
                        clearBtn.textContent = 'Clearing...';
                        try {
                            const result = await main.clearStoredApiKey({ confirm: true });
                            if (result && result.cancelled) {
                                clearBtn.disabled = false;
                                clearBtn.textContent = originalLabel;
                                refreshFromState();
                            }
                        } catch (error) {
                            setMessage('error', error?.message || 'Failed to clear API key.', 'api-key-clear-error');
                            clearBtn.disabled = false;
                            clearBtn.textContent = originalLabel;
                        }
                    });
                    // Open the Torn custom API key creator in a new tab (configurable fallback)
                    try {
                        const generateBtn = document.getElementById('tdm-generate-key-btn');
                        generateBtn?.addEventListener('click', () => {
                            const customUrl = config.customKeyUrl || 'https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=TreeDibsMapper&user=basic,profile,faction,job&faction=rankedwars,members,attacks,attacksfull,basic,chain,chains,positions,warfare,wars&torn=rankedwars,rankedwarreport';
                            try {
                                window.open(customUrl, '_blank', 'noopener');
                            } catch (err) {
                                // Fallback: surface the URL so the user can copy it manually
                                ui.showMessageBox(`Unable to open key generator. Copy this URL: ${customUrl}`, 'error');
                            }
                        });
                    } catch (_) { /* noop */ }
                    // Revalidate button removed; we revalidate silently when the panel opens.
                }
            } catch(_) {}

            // FFScouter Key Handlers
            try {
                const ffInput = document.getElementById('tdm-ffscouter-key-input');
                const ffSaveBtn = document.getElementById('tdm-ffscouter-key-save-btn');
                const ffClearBtn = document.getElementById('tdm-ffscouter-key-clear-btn');
                const ffShowToggle = document.getElementById('tdm-ffscouter-key-show');
                const ffMessageEl = document.getElementById('tdm-ffscouter-key-message');
                const ffStatusEl = document.getElementById('tdm-ffscouter-key-status');
                const ffCard = document.getElementById('tdm-ffscouter-key-card');

                const setFFMessage = (tone, text) => {
                    if (ffMessageEl) {
                        ffMessageEl.textContent = text;
                        ffMessageEl.style.color = (tone === 'success' ? '#86efac' : tone === 'info' ? '#93c5fd' : '#fca5a5');
                    }
                    if (ffCard) ffCard.dataset.tone = tone;
                    if (ffStatusEl) ffStatusEl.textContent = (tone === 'success' ? 'Saved' : 'Unverified');
                };

                ffShowToggle?.addEventListener('change', (e) => {
                    if (ffInput) ffInput.type = e.target.checked ? 'text' : 'password';
                });

                ffSaveBtn?.addEventListener('click', async () => {
                    if (!ffInput) return;
                    const val = (ffInput.value || '').trim();
                    if (!val) {
                        setFFMessage('error', 'Enter a key first.');
                        return;
                    }
                    const orig = ffSaveBtn.textContent;
                    ffSaveBtn.disabled = true;
                    ffSaveBtn.textContent = 'Verifying...';
                    setFFMessage('info', 'Verifying key with FFScouter...');
                    
                    try {
                        const res = await api.verifyFFScouterKey(val);
                        if (res.ok) {
                            storage.set('ffscouterApiKey', val);
                            setFFMessage('success', 'Key verified and saved.');
                            // Trigger an immediate fetch to populate cache
                            try {
                                const visibleIds = utils.getVisibleRankedWarFactionIds?.()?.ids || [];
                                if (visibleIds.length) api.fetchFFScouterStats(visibleIds);
                            } catch(_) {}
                        } else {
                            setFFMessage('error', res.message || 'Verification failed.');
                        }
                    } catch (e) {
                        setFFMessage('error', 'Error: ' + e.message);
                    } finally {
                        ffSaveBtn.disabled = false;
                        ffSaveBtn.textContent = orig;
                    }
                });

                ffClearBtn?.addEventListener('click', () => {
                    if (confirm('Clear FFScouter API key?')) {
                        storage.remove('ffscouterApiKey');
                        if (ffInput) ffInput.value = '';
                        setFFMessage('error', 'Key cleared.');
                        if (ffStatusEl) ffStatusEl.textContent = 'Not Set';
                    }
                });
            } catch(_) {}

            // Activity Tracking Toggle & Cadence
            try {
                const toggle = document.getElementById('tdm-activity-tracking-toggle');
                const idleToggle = document.getElementById('tdm-activity-track-idle');
                const cadenceInput = document.getElementById('tdm-activity-cadence-seconds');
                const applyStatusLine = () => {
                    // Update the header to show current status
                    const section = document.querySelector('[data-section="activity-tracking"]');
                    if (section) {
                        const header = section.querySelector('.settings-header');
                        if (header) {
                            const enabled = storage.get('tdmActivityTrackingEnabled', false);
                            header.innerHTML = `Activity Tracking ${enabled ? 'Enabled' : 'Disabled'} <span class="chevron">▾</span>`;
                        }
                    }
                };
                const applyKeepActivePreference = () => {
                    try {
                        const keepActive = utils.isActivityKeepActiveEnabled();
                        state.script.idleTrackingOverride = keepActive;
                        state.script.isWindowActive = !document.hidden;
                        if (keepActive) {
                            if (!state.script.mainRefreshIntervalId || document.hidden) {
                                main.startPolling();
                            }
                        } else if (document.hidden) {
                            if (state.script.mainRefreshIntervalId) { try { utils.unregisterInterval(state.script.mainRefreshIntervalId); } catch(_) {} state.script.mainRefreshIntervalId = null; }
                            if (state.script.activityTimeoutId) { try { utils.unregisterTimeout(state.script.activityTimeoutId); } catch(_) {} state.script.activityTimeoutId = null; }
                            if (state.script.factionBundleRefreshIntervalId) { try { utils.unregisterInterval(state.script.factionBundleRefreshIntervalId); } catch(_) {} state.script.factionBundleRefreshIntervalId = null; }
                            if (state.script.fetchWatchdogIntervalId) { try { utils.unregisterInterval(state.script.fetchWatchdogIntervalId); } catch(_) {} state.script.fetchWatchdogIntervalId = null; }
                            if (state.script.lightPingIntervalId) { try { utils.unregisterInterval(state.script.lightPingIntervalId); } catch(_) {} state.script.lightPingIntervalId = null; }
                        }
                        try { ui.updateApiCadenceInfo?.(); } catch(_) {}
                    } catch(_) { /* ignore */ }
                };
                const startTracking = () => {
                    if (!state._activityTracking) {
                        state._activityTracking = {
                            prevById: {},
                            metrics: { transitions:0, lastPoll:0, lastDiffMs:0 },
                            cadenceMs: Number(storage.get('tdmActivityCadenceMs', 10000)) || 10000,
                            lastSig: null
                        };
                    } else {
                        state._activityTracking.cadenceMs = Number(storage.get('tdmActivityCadenceMs', 10000)) || 10000;
                    }
                    handlers._initActivityTracking?.();
                    applyKeepActivePreference();
                };
                const stopTracking = () => {
                    handlers._teardownActivityTracking?.();
                    applyKeepActivePreference();
                };
                toggle?.addEventListener('change', e => {
                    const enabled = !!e.target.checked;
                    storage.set('tdmActivityTrackingEnabled', enabled);
                    if (enabled) startTracking(); else stopTracking();
                    applyStatusLine();
                    applyKeepActivePreference();
                });
                idleToggle?.addEventListener('change', e => {
                    const enabled = !!e.target.checked;
                    storage.set('tdmActivityTrackWhileIdle', enabled);
                    applyKeepActivePreference();
                });
                document.getElementById('tdm-apply-activity-cadence-btn')?.addEventListener('click', () => {
                    try {
                        let sec = Math.round(Number(cadenceInput.value)||10);
                        if (sec < 5) sec = 5; if (sec > 60) sec = 60;
                        cadenceInput.value = String(sec);
                        const ms = sec*1000;
                        storage.set('tdmActivityCadenceMs', ms);
                        if (storage.get('tdmActivityTrackingEnabled', false)) {
                            if (state._activityTracking) state._activityTracking.cadenceMs = ms;
                            handlers._updateActivityCadence?.(ms);
                        }
                        ui.showMessageBox(`Activity cadence set to ${sec}s.`, 'success');
                    } catch(err) { ui.showMessageBox('Invalid cadence.', 'error'); }
                });
                document.getElementById('tdm-flush-activity-cache-btn')?.addEventListener('click', async () => {
                    try {
                        if (!(await ui.showConfirmationBox('Flush activity cache? Confidence resets & previous states cleared.'))) return;
                        await handlers._flushActivityCache?.();
                        ui.showMessageBox('Activity cache flushed.', 'info');
                    } catch(err) { ui.showMessageBox('Flush failed','error'); }
                });
                document.getElementById('tdm-clear-idb-btn')?.addEventListener('click', async () => {
                    try {
                        if (!(await ui.showConfirmationBox('Clear entire IDB Storage? This will wipe all cached data (tdm-store).'))) return;
                        if (ui._kv && typeof ui._kv.deleteDb === 'function') {
                            const ok = await ui._kv.deleteDb();
                            if (ok) {
                                ui.showMessageBox('IDB Storage cleared.', 'success');
                                setTimeout(() => { try { ui.updateIdbUsageLine?.(); } catch(_) {} }, 500);
                            } else {
                                ui.showMessageBox('Failed to clear IDB.', 'error');
                            }
                        } else {
                            ui.showMessageBox('IDB interface not available.', 'error');
                        }
                    } catch(err) { ui.showMessageBox('Clear failed: ' + err, 'error'); }
                });
                document.getElementById('tdm-debug-overlay-toggle')?.addEventListener('change', e => {
                    const v = !!e.target.checked;
                    handlers.toggleLiveTrackDebugOverlay(v);
                });
                applyStatusLine();
                applyKeepActivePreference();
            } catch(_) {}
            tdmlogger('debug', 'UI: Settings content rendered');
            // IDB usage helpers
            if (!ui.updateIdbUsageLine) {
                ui.updateIdbUsageLine = async () => {
                    try {
                        const line = document.getElementById('tdm-idb-usage-line');
                        if (!line) return;
                        
                        let used = ui._kv ? (ui._kv._approxBytes || 0) : 0;
                        let isEstimate = false;
                        
                        // Try to get accurate origin usage
                        if (navigator.storage && navigator.storage.estimate) {
                            try {
                                const estimate = await navigator.storage.estimate();
                                if (estimate && typeof estimate.usage === 'number') {
                                    used = estimate.usage;
                                    isEstimate = true;
                                }
                            } catch(_) {}
                        }

                        const maxBytes = ui._kv ? ui._kv._maxBytes() : 0;
                        
                        let usageStr = '';
                        if (used < 1024 * 1024) {
                            usageStr = (used / 1024).toFixed(2) + ' KB';
                        } else {
                            const mb = used / 1024 / 1024;
                            usageStr = mb.toFixed(mb < 10 ? 2 : 1) + ' MB';
                        }
                        
                        const pct = maxBytes ? Math.min(100, (used / maxBytes)*100) : null;
                        const maxStr = maxBytes ? (maxBytes/1024/1024).toFixed(0) + ' MB' : 'auto';
                        
                        line.textContent = `IDB Usage: ${usageStr}${maxBytes ? ` / ${maxStr} (${pct.toFixed(1)}%)` : ` (${isEstimate ? 'Origin' : 'Est.'})`}`;
                    } catch(_) {}
                };
                setTimeout(()=>{ try { ui.updateIdbUsageLine(); } catch(_) {} }, 500);
            }
            try {
                document.getElementById('tdm-idb-maxsize-select')?.addEventListener('change', e => {
                    try {
                        const mb = Number(e.target.value)||0;
                        storage.set('tdmIdbMaxSizeMB', mb);
                        ui.showMessageBox(`IDB max size set to ${mb? mb+' MB (eviction applies)':'Auto (browser managed)'}.`, 'info');
                        ui._kv?._maybeEvict?.();
                        ui.updateIdbUsageLine?.();
                    } catch(err) { tdmlogger('error', `[Failed to apply IDB max size] ${err}`); }
                });
            } catch(_) {}
            // Re-attach all event listeners
            try { ui.updateApiCadenceInfo?.(); } catch(_) {}
            // Ensure diagnostics auto-update interval is running while settings are visible
            try {
                if (document.getElementById('tdm-settings-popup')) {
                    if (!state.ui) state.ui = {};
                    if (!state.ui.apiCadenceInfoIntervalId) {
                        state.ui.apiCadenceInfoIntervalId = utils.registerInterval(setInterval(() => {
                            try { if (document.getElementById('tdm-settings-popup')) ui.updateApiCadenceInfo?.(); else { try { utils.unregisterInterval(state.ui.apiCadenceInfoIntervalId); } catch(_) {} state.ui.apiCadenceInfoIntervalId = null; } } catch(_) {}
                        }, 1000));
                    }
                }
            } catch(_) {}
            const recomputeWarString = () => {
                const wt = document.getElementById('war-type-select')?.value || warType;
                const tt = document.getElementById('term-type-select')?.value || termType;
                const fsc = parseInt(document.getElementById('faction-score-cap-input')?.value || factionScoreCap) || 0;
                const isc = parseInt(document.getElementById('individual-score-cap-input')?.value || individualScoreCap) || 0;
                const ist = document.getElementById('individual-score-type-select')?.value || individualScoreType;
                return wt === 'Termed War' ? `${tt} to Total ${fsc}, ${isc} ${ist} Each` : (wt || 'War Type Not Set');
            };
            const applyWarString = () => {
                const span = document.getElementById('rw-warstring');
                if (span) span.textContent = recomputeWarString();
            };
            const copyTextToClipboard = async (text, label) => {
                const trimmed = (text || '').trim();
                if (!trimmed) {
                    ui.showMessageBox(`Nothing to copy for ${label.toLowerCase()}.`, 'info');
                    return;
                }
                try {
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        await navigator.clipboard.writeText(trimmed);
                        ui.showTransientMessage(`${label} copied to clipboard.`, { type: 'success', timeout: 3200 });
                    } else {
                        throw new Error('clipboard unavailable');
                    }
                } catch (_) {
                    ui.fallbackCopyToClipboard(trimmed, `${label}`);
                }
            };
            const buildWarDetailsSummary = () => {
                const formatWarStartTct = (value) => {
                    const numeric = Number(value);
                    if (!Number.isFinite(numeric) || numeric <= 0) return null;
                    const epochMs = numeric < 1e12 ? numeric * 1000 : numeric;
                    const dt = new Date(epochMs);
                    if (Number.isNaN(dt.getTime())) return null;
                    const pad = utils.pad2;
                    const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
                    return `${pad(dt.getUTCHours())}:${pad(dt.getUTCMinutes())} TCT (UTC) on ${pad(dt.getUTCDate())} ${months[dt.getUTCMonth()]} ${dt.getUTCFullYear()}`;
                };
                const resolveWarStart = () => {
                    const candidates = [];
                    try {
                        if (state?.warData) {
                            candidates.push(state.warData.warStart, state.warData.start, state.warData.warBegin);
                        }
                    } catch (_) { /* ignore */ }
                    try {
                        if (state?.lastRankWar) {
                            candidates.push(state.lastRankWar.warStart, state.lastRankWar.start, state.lastRankWar.warBegin);
                        }
                    } catch (_) { /* ignore */ }
                    try {
                        if (Array.isArray(state?.rankWars) && state.rankWars.length > 0) {
                            const latest = state.rankWars[0];
                            candidates.push(latest?.warStart, latest?.start, latest?.warBegin);
                        }
                    } catch (_) { /* ignore */ }
                    for (const candidate of candidates) {
                        const asNumber = Number(candidate);
                        if (Number.isFinite(asNumber) && asNumber > 0) return asNumber;
                    }
                    return null;
                };
                const readNumber = (id, fallback) => {
                    const input = document.getElementById(id);
                    if (!input) return Number(fallback) || 0;
                    const raw = input.value ?? input.textContent;
                    if (raw == null || String(raw).trim() === '') return 0;
                    const num = Number(raw);
                    return Number.isFinite(num) ? num : Number(fallback) || 0;
                };
                const currentWarType = document.getElementById('war-type-select')?.value || warType || 'War Type Not Set';
                const currentTermType = document.getElementById('term-type-select')?.value || termType || '';
                const currentFactionCap = currentWarType === 'Termed War' ? readNumber('faction-score-cap-input', factionScoreCap) : 0;
                const currentIndivCap = currentWarType === 'Termed War' ? readNumber('individual-score-cap-input', individualScoreCap) : 0;
                const currentIndivType = document.getElementById('individual-score-type-select')?.value || individualScoreType || 'Respect';
                const currentAttackMode = document.getElementById('attack-mode-select')?.value || attackMode || 'Mode Not Set';
                const opponentNameText = opponentFactionName || 'opponent TBD';
                const opponentLink = opponentFactionId
                    ? `<a href="/factions.php?step=profile&ID=${opponentFactionId}" class="t-blue"><b>${opponentNameText}</b></a>`
                    : opponentNameText;
                const statements = [`Our opponent for this war is ${opponentLink}.`];
                const warStartText = formatWarStartTct(resolveWarStart());
                if (warStartText) statements.push(`War started at ${warStartText}.`);
                if (currentWarType === 'Termed War') {
                    const outcome = (() => {
                        const lower = (currentTermType || '').toLowerCase();
                        if (lower.includes('win')) return 'Win';
                        if (lower.includes('loss')) return 'Lose';
                        if (lower.includes('draw')) return 'Draw';
                        return currentTermType || 'Play it out';
                    })();
                    statements.push(`This will be a <b>Termed War and we will ${outcome}</b>.`);
                    statements.push(currentFactionCap > 0 ? `Our Faction Score Cap is ${currentFactionCap}.` : 'Faction score is uncapped.');
                    statements.push(currentIndivCap > 0 ? `Individual Score Cap is ${currentIndivCap} ${currentIndivType}.` : 'Individual score is uncapped.');
                    const activeCaps = [currentFactionCap > 0, currentIndivCap > 0].filter(Boolean).length;
                    if (activeCaps === 2) {
                        statements.push('<u><b>Stop hitting once either cap is reached.</b></u>');
                    } else if (activeCaps === 1) {
                        statements.push('<u><b>Stop hitting once cap is reached.</b></u>');
                    }
                } else if (currentWarType === 'Ranked War') {
                    const modeText = currentAttackMode && currentAttackMode !== 'Mode Not Set' ? `${currentAttackMode} attack mode` : 'the current attack mode';
                    statements.push(`This is a <b>Real Ranked War</b>! We are operating in <u>${modeText}</u>; watch for leadership updates if the attack mode changes mid-war.`);
                } else {
                    statements.push('War type is not set yet; hold for leadership instructions.');
                }
                return statements.join(' ').replace(/\s+/g, ' ').trim();
            };
            const buildDibsStyleSummary = () => {
                const checked = (id, fallback) => {
                    const el = document.getElementById(id);
                    if (el) return !!el.checked;
                    return !!fallback;
                };
                const numeric = (id, fallback) => {
                    const el = document.getElementById(id);
                    if (!el) return Number(fallback) || 0;
                    const raw = el.value;
                    if (raw == null || raw === '') return 0;
                    const num = Number(raw);
                    return Number.isFinite(num) ? num : Number(fallback) || 0;
                };
                const keepInactive = checked('dibs-keep-inactive', dibsStyle.keepTillInactive);
                const inactivitySeconds = numeric('dibs-inactivity-seconds', dibsStyle.inactivityTimeoutSeconds || 0);
                const inactivityMinutes = inactivitySeconds > 0 ? Math.round(inactivitySeconds / 60) : 0;
                const mustRedib = checked('dibs-redib-after-success', dibsStyle.mustRedibAfterSuccess);
                const maxHospitalMinutes = numeric('dibs-max-hosp-minutes', dibsStyle.maxHospitalReleaseMinutes || 0);
                const removeOnFly = checked('dibs-remove-on-fly', dibsStyle.removeOnFly);
                const removeUserTravel = checked('dibs-remove-user-travel', dibsStyle.removeWhenUserTravels);
                const bypassDibStyle = checked('dibs-bypass-style', dibsStyle.bypassDibStyle);
                const allowStatusNodes = Array.from(document.querySelectorAll('.dibs-allow-status'));
                const allowStatuses = allowStatusNodes.length
                    ? allowStatusNodes.map(cb => ({ status: cb.dataset.status, allowed: cb.checked }))
                    : Object.entries(dibsStyle.allowStatuses || {}).map(([status, allowed]) => ({ status, allowed: !!allowed }));
                const allowActivityNodes = Array.from(document.querySelectorAll('.dibs-allow-lastaction'));
                const allowActivities = allowActivityNodes.length
                    ? allowActivityNodes.map(cb => ({ status: cb.dataset.status, allowed: cb.checked }))
                    : Object.entries(dibsStyle.allowLastActionStatuses || {}).map(([status, allowed]) => ({ status, allowed: !!allowed }));
                const allowUserStatusNodes = Array.from(document.querySelectorAll('.dibs-allow-user-status'));
                const userStatuses = allowUserStatusNodes.length
                    ? allowUserStatusNodes.map(cb => ({ status: cb.dataset.status, allowed: cb.checked }))
                    : Object.entries(dibsStyle.allowedUserStatuses || {}).map(([status, allowed]) => ({ status, allowed: !!allowed }));
                const sentences = [];
                const firstFragments = [];
                if (keepInactive) {
                    if (inactivityMinutes > 0) firstFragments.push(`Dib <b>stay until member is inactive for ~${inactivityMinutes} min</b>`);
                    else firstFragments.push('<b>dibs stay active until manually cleared</b>');
                } else {
                    firstFragments.push('dibs clear immediately after a successful hit');
                }
                if (mustRedib) firstFragments.push('Dibs automatically removed after successful hit');
                sentences.push(`<u><b>Dibs Style</b></u>: ${firstFragments.join('. ')}.`);
                if (maxHospitalMinutes > 0) {
                    sentences.push(`Wait until hospital release is within ${maxHospitalMinutes} minute${maxHospitalMinutes === 1 ? '' : 's'} before dibbing hospitalized targets.`);
                }
                if (removeOnFly) sentences.push('Dibs automatically remove when opponents travel.');
                if (removeUserTravel) sentences.push('Your dibs clear if you travel.');
                if (allowStatuses.length) {
                    const allowedStatusesList = allowStatuses.filter(item => item.allowed).map(item => item.status);
                    const blockedStatusesList = allowStatuses.filter(item => !item.allowed).map(item => item.status);
                    if (blockedStatusesList.length && blockedStatusesList.length < allowStatuses.length) {
                        sentences.push(`Skip targets marked ${blockedStatusesList.join(', ')}.`);
                    }
                    if (allowedStatusesList.length && allowedStatusesList.length < allowStatuses.length) {
                        sentences.push(`Allowed opponent statuses: ${allowedStatusesList.join(', ')}.`);
                    }
                }
                if (allowActivities.length) {
                    const allowedActivities = allowActivities.filter(item => item.allowed).map(item => item.status);
                    const blockedActivities = allowActivities.filter(item => !item.allowed).map(item => item.status);
                    if (blockedActivities.includes('Online')) {
                        sentences.push('<u>Don\'t Hit Online Opponents.</u>');
                    } else if (allowedActivities.length && allowedActivities.length < allowActivities.length) {
                        sentences.push(`Activity allowed: ${allowedActivities.join(', ')}.`);
                    }
                }
                if (userStatuses.length) {
                    const blockedUserStatuses = userStatuses.filter(item => !item.allowed).map(item => item.status);
                    if (blockedUserStatuses.length) {
                        const statusLabelMap = { Travel: 'Travelling' };
                        const formattedStatuses = blockedUserStatuses.map(status => statusLabelMap[status] || status);
                        sentences.push(`Can't place dibs if you are: ${formattedStatuses.join(', ')}.`);
                    }
                }
                if (bypassDibStyle) sentences.push('<b>Admin bypass enabled:</b> admins may ignore dib style rules for this faction.');
                return sentences.join(' ').replace(/\s+/g, ' ').trim();
            };
            document.getElementById('war-type-select')?.addEventListener('change', (e) => {
                const val = e.currentTarget.value;
                const isTermed = val === 'Termed War';
                document.getElementById('term-type-container').style.display = isTermed ? 'block' : 'none';
                document.getElementById('score-cap-container').style.display = isTermed ? 'block' : 'none';
                document.getElementById('individual-score-cap-container').style.display = isTermed ? 'block' : 'none';
                document.getElementById('individual-score-type-container').style.display = isTermed ? 'block' : 'none';
                // Also show the second-row termed war controls when switching
                try { document.getElementById('opponent-score-cap-container').style.display = isTermed ? 'block' : 'none'; } catch(_) {}
                try { document.getElementById('initial-target-container').style.display = isTermed ? 'block' : 'none'; } catch(_) {}
                try { document.getElementById('target-end-container').style.display = isTermed ? 'block' : 'none'; } catch(_) {}
                try { document.getElementById('initial-target-display').style.display = isTermed ? 'block' : 'none'; } catch(_) {}
                try { document.getElementById('target-end-display').style.display = isTermed ? 'block' : 'none'; } catch(_) {}
                const amg = document.getElementById('attack-mode-group');
                if (amg) amg.style.display = val === 'Ranked War' ? 'block' : 'none';
                applyWarString();
                try { updateWarTermInfo(); } catch(_) {}
            });
            document.getElementById('term-type-select')?.addEventListener('change', applyWarString);
            document.getElementById('faction-score-cap-input')?.addEventListener('input', applyWarString);
            document.getElementById('opponent-score-cap-input')?.addEventListener('input', applyWarString);
            // initial-target is display-only now; do not listen for input events
            document.getElementById('war-target-end-input')?.addEventListener('change', applyWarString);
            document.getElementById('individual-score-cap-input')?.addEventListener('input', applyWarString);
            document.getElementById('individual-score-type-select')?.addEventListener('change', applyWarString);
            // Helpers for termed-war decay math + two-of-three solver
            // Parse a datetime-local string but treat the value as UTC (skip minutes by rounding to hour if needed)
            const parseDateTimeLocalToEpochSecAssumeUTC = (str) => {
                if (!str) return 0;
                try {
                    // Treat the provided local-style string as a UTC timestamp by appending 'Z'
                    // e.g. '2025-11-27T16:00' -> '2025-11-27T16:00Z' and parse as UTC.
                    // Also normalize to hour precision by clearing minutes/seconds if present.
                    // If string includes minutes, we ignore them and use the hour component.
                    const match = String(str).match(/^(\d{4}-\d{2}-\d{2}T\d{2})(:?\d{2})?(:?\d{2})?$/);
                    let base;
                    if (match && match[1]) {
                        base = match[1]; // keep only YYYY-MM-DDTHH
                    } else {
                        // Try to support a couple of common non-ISO formats users might type
                        const sl = String(str).trim();
                        // MM/DD/YYYY HHmm or M/D/YYYY H:mm or M/D/YYYY HHmm
                        const us = sl.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{1,2})(:?)(\d{2})?$/);
                        if (us) {
                            const mm = us[1].padStart(2,'0');
                            const dd = us[2].padStart(2,'0');
                            const yyyy = us[3];
                            const hh = us[4].padStart(2,'0');
                            const mins = us[6] ? us[6].padStart(2,'0') : '00';
                            base = `${yyyy}-${mm}-${dd}T${hh}:${mins}`;
                        } else {
                            // Try YYYY-MM-DD HH:MM
                            const isoLike = sl.match(/^(\d{4}-\d{2}-\d{2})[T\s](\d{1,2})(:?)(\d{2})?$/);
                            if (isoLike) {
                                const yyyyMmDd = isoLike[1];
                                const hh = isoLike[2].padStart(2,'0');
                                const mins = isoLike[4] ? isoLike[4].padStart(2,'0') : '00';
                                base = `${yyyyMmDd}T${hh}:${mins}`;
                            } else {
                                base = str;
                            }
                        }
                    }
                    let d = new Date(base + 'Z');
                    if (!Number.isNaN(d.getTime())) return Math.floor(d.getTime() / 1000);

                    // Fallback attempts: try loose parsing of the original string
                    //  - Date.parse with appended Z
                    let parsed = Date.parse(String(str) + 'Z');
                    if (!Number.isNaN(parsed)) return Math.floor(parsed/1000);

                    //  - Try Date.parse on the raw string (some browsers accept different formats)
                    parsed = Date.parse(String(str));
                    if (!Number.isNaN(parsed)) return Math.floor(parsed/1000);

                    //  - Try replacing space with 'T' and append Z
                    const maybeT = String(str).trim().replace(' ', 'T');
                    parsed = Date.parse(maybeT + 'Z');
                    if (!Number.isNaN(parsed)) return Math.floor(parsed/1000);

                    // As a last resort, attempt to coerce an ISO-like value with minutes if the hour-only base was produced
                    if (/^\d{4}-\d{2}-\d{2}T\d{2}$/.test(base)) {
                        // append :00 minutes
                        d = new Date(base + ':00Z');
                        if (!Number.isNaN(d.getTime())) return Math.floor(d.getTime() / 1000);
                    }

                    return 0;
                } catch (_) { return 0; }
            };

            // Convert epoch seconds to an input-friendly datetime-local string but using UTC hour (minutes cleared)
            const epochSecToUtcDatetimeHourValue = (sec) => {
                if (!sec) return '';
                try {
                    const d = new Date(Number(sec) * 1000);
                    if (Number.isNaN(d.getTime())) return '';
                    const pad = utils.pad2;
                    // Hour-only UTC output (minutes set to 00)
                    return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:00`;
                } catch (_) { return ''; }
            };

            // Decay model (matches Torn Ranked War Calculator sample):
            // - No decay during first 24 hours after war start
            // - After that, once per hour, target reduces by 1% of the original initialTargetScore (linear hourly), capped at 99%
            const computeTargetAfterDecay = (initialTarget, warStartSec, atSec) => {
                initialTarget = Number(initialTarget) || 0;
                if (!initialTarget || !warStartSec || !atSec) return initialTarget;
                const now = Number(atSec);
                const startMs = Number(warStartSec) * 1000;
                const atMs = Number(now) * 1000;
                const msSinceStart = atMs - startMs;
                if (msSinceStart < 24*60*60*1000) return initialTarget; // still pre-decay
                let hoursIntoDecay = Math.floor((msSinceStart - 24*60*60*1000) / (60*60*1000));
                hoursIntoDecay = Math.max(0, hoursIntoDecay);
                const pct = Math.min(99, hoursIntoDecay * 1); // 1% per hour
                const factor = Math.max(0, 1 - pct/100);
                return initialTarget * factor;
            };

            const formatMsRelative = (ms) => {
                if (!Number.isFinite(ms)) return 'N/A';
                const abs = Math.abs(ms);
                const s = Math.floor(abs/1000); const d = Math.floor(s / (24*3600)); let r = [];
                let rem = s % (24*3600); const h = Math.floor(rem/3600); rem %= 3600; const m = Math.floor(rem/60); const sec = rem % 60;
                if (d) r.push(`${d}d`); if (h) r.push(`${h}h`); if (m) r.push(`${m}m`); if (sec) r.push(`${sec}s`);
                return r.length ? (ms<0?`-${r.join(' ')}`:r.join(' ')) : '0s';
            };

            const updateWarTermInfo = () => {
                try {
                    const infoEl = document.getElementById('rw-term-info'); if (!infoEl) return;
                    const initial = Number(state.warData?.initialTargetScore || (state.lastRankWar?.war?.target ?? state.lastRankWar?.target) || 0) || 0;
                    const rawFactionVal = document.getElementById('faction-score-cap-input')?.value ?? '';
                    const rawOpponentVal = document.getElementById('opponent-score-cap-input')?.value ?? '';
                    const rawEndVal = document.getElementById('war-target-end-input')?.value ?? '';
                    let factionCap = Number(rawFactionVal === '' ? (state.warData?.factionScoreCap || 0) : rawFactionVal) || 0;
                    let opponentCap = Number(rawOpponentVal === '' ? (state.warData?.opponentScoreCap || 0) : rawOpponentVal) || 0;
                    // Ensure non-negative
                    factionCap = Math.max(0, factionCap);
                    opponentCap = Math.max(0, opponentCap);
                    const endInput = rawEndVal || '';
                    // parse DateTime-local but we treat the value as UTC hour-only
                    // Input-provided end (explicit target end time) vs authoritative backend end
                    const endSecInput = parseDateTimeLocalToEpochSecAssumeUTC(endInput) || Number(state.warData?.targetEndTime || 0) || 0;
                    // Backend-provided end (e.g., ranked war's end timestamp) should be considered authoritative
                    const endSecApi = Number(state.lastRankWar?.war?.end || state.lastRankWar?.end || 0) || 0;
                    // Effective end we consider for display/logic: prefer backend API end when present
                    const endSec = endSecApi || endSecInput || 0;
                    // Resolve war start if available
                    const resolveStart = () => {
                        const candidates = [state.warData?.warStart, state.warData?.start, state.lastRankWar?.war?.start, state.lastRankWar?.start];
                        for (const c of candidates) {
                            const n = Number(c);
                            if (Number.isFinite(n) && n > 0) return n;
                        }
                        return 0;
                    };
                    const startSec = resolveStart();
                    const nowSec = Math.floor(Date.now()/1000);
                    // Quick-change guard: skip heavy recomputation if nothing meaningful changed
                    try {
                        if (!state.ui) state.ui = {};
                        const last = state.ui._lastWarTermInputs || null;
                        // normalize raw strings (trim) for comparison
                        // treat a literal '0' as equivalent to empty for change-detection so 0 and '' compare equal
                        const norm = (s) => {
                            if (typeof s === 'string') {
                                const t = s.trim();
                                if (t === '0') return '';
                                return t;
                            }
                            return String(s);
                        };
                        const snapshot = {
                            rawFactionVal: norm(rawFactionVal),
                            rawOpponentVal: norm(rawOpponentVal),
                            rawEndVal: norm(rawEndVal),
                            // numeric-derived values used by solver
                            factionCap: Number(factionCap) || 0,
                            opponentCap: Number(opponentCap) || 0,
                            endSec: Number(endSec) || 0,
                            startSec: Number(startSec) || 0,
                            initial: Number(initial) || 0,
                            currentTermType: (document.getElementById('term-type-select')?.value || state.warData?.termType || '')
                        };
                        // shallow-compare snapshot to last
                        let changed = false;
                        if (!last) changed = true; else {
                            for (const k of Object.keys(snapshot)) {
                                if (String(last[k]) !== String(snapshot[k])) { changed = true; break; }
                            }
                        }
                        if (!changed) return;
                        state.ui._lastWarTermInputs = snapshot;
                    } catch(_) {}

                    // Logging helper: only emit noisy debug output when explicitly enabled via state.debug?.rwTermInfo
                    // or when the snapshot meaningfully changed since last log. This prevents console spam during rapid typing.
                    const _rwLogEnabled = !!(state.debug?.rwTermInfo);
                    const _rwLogSnapshotKey = JSON.stringify({ rawFactionVal: String(rawFactionVal||''), rawOpponentVal: String(rawOpponentVal||''), rawEndVal: String(rawEndVal||''), factionCap: Number(factionCap)||0, opponentCap: Number(opponentCap)||0, endSec: Number(endSec)||0, startSec: Number(startSec)||0 });
                    if (_rwLogEnabled) {
                        try { console.debug('[updateWarTermInfo] inputs', { rawFactionVal, rawOpponentVal, rawEndVal, factionCap, opponentCap, endSec, startSec, initial, currentTermType: (document.getElementById('term-type-select')?.value || state.warData?.termType || '') }); } catch(_) {}
                        state.ui._lastWarTermInfoLogSnapshot = { key: _rwLogSnapshotKey, ts: Date.now() };
                    } else {
                        try {
                            const lastLog = state.ui._lastWarTermInfoLogSnapshot || {};
                            // Only log when the snapshot changes (or if we haven't logged before)
                            if (!lastLog.key || lastLog.key !== _rwLogSnapshotKey) {
                                try { console.debug('[updateWarTermInfo] inputs (changed)', { rawFactionVal, rawOpponentVal, rawEndVal, factionCap, opponentCap, endSec, startSec, initial, currentTermType: (document.getElementById('term-type-select')?.value || state.warData?.termType || '') }); } catch(_) {}
                                state.ui._lastWarTermInfoLogSnapshot = { key: _rwLogSnapshotKey, ts: Date.now() };
                            }
                        } catch(_) {}
                    }
                    // If the user supplied an end value string but parsing yielded 0, emit a focused debug so we can see why
                    try { if (rawEndVal && !endSec) {
                        // only emit parse-end-failed when enabled or when the rawEndVal changed since last log
                        const _pKey = String(rawEndVal||'');
                        const _last = (state.ui._lastWarTermInfoParseFailed || {});
                        if (state.debug?.rwTermInfo || !_last.key || _last.key !== _pKey) {
                            console.debug('[updateWarTermInfo] parse-end-failed', { rawEndVal });
                            state.ui._lastWarTermInfoParseFailed = { key: _pKey, ts: Date.now() };
                        }
                    } } catch(_) {}
                    let lines = [];

                    const isTermedFromSelect = (document.getElementById('war-type-select')?.value || state.warData?.warType) === 'Termed War';
                    // Update initial target display (read-only)
                    try { const it = document.getElementById('initial-target-display'); if (it) it.textContent = initial ? String(initial) : ''; if (it) it.classList.add('tdm-initial-readonly'); } catch(_) {}

                    // UI hints: mark required / calculated fields
                    try {
                        const elFac = document.getElementById('faction-score-cap-input');
                        const elOpp = document.getElementById('opponent-score-cap-input');
                        const elEnd = document.getElementById('war-target-end-input');
                        // Treat both empty string and '0' (string form) as "not manually provided" — 0 is typically the default
                        const rawTrim = utils.rawTrim;
                        const rawHasValue = utils.rawHasValue;
                        const fallbackNumIsPositive = utils.fallbackNumIsPositive;
                        const providedFac = utils.rawHasValue(rawFactionVal) || utils.fallbackNumIsPositive(state.warData?.factionScoreCap);
                        const providedOpp = utils.rawHasValue(rawOpponentVal) || utils.fallbackNumIsPositive(state.warData?.opponentScoreCap);
                        const providedEnd = utils.rawHasValue(rawEndVal) || !!(state.warData?.targetEndTime);
                        const presentCount = [providedFac, providedOpp, providedEnd].filter(Boolean).length;
                        const clearClasses = (el) => { if (!el) return; el.classList.remove('tdm-term-required'); el.classList.remove('tdm-term-calculated'); el.classList.remove('tdm-term-error'); };
                        [elFac, elOpp, elEnd].forEach(clearClasses);
                        if (presentCount < 2) {
                            // Highlight missing fields as required
                            if (!providedFac && elFac) elFac.classList.add('tdm-term-required');
                            if (!providedOpp && elOpp) elOpp.classList.add('tdm-term-required');
                            if (!providedEnd && elEnd) elEnd.classList.add('tdm-term-required');
                        } else if (presentCount === 2) {
                            // The missing one is calculated and should be marked as such
                            if (!providedFac && elFac) elFac.classList.add('tdm-term-calculated');
                            if (!providedOpp && elOpp) elOpp.classList.add('tdm-term-calculated');
                            if (!providedEnd && elEnd) elEnd.classList.add('tdm-term-calculated');
                            // Mark the provided inputs as required (the two inputs)
                            if (providedFac && elFac) elFac.classList.add('tdm-term-required');
                            if (providedOpp && elOpp) elOpp.classList.add('tdm-term-required');
                            if (providedEnd && elEnd) elEnd.classList.add('tdm-term-required');
                        }
                        // If any input was auto-filled previously, preserve the visual calculated mark for it
                        try {
                            const elFac3 = document.getElementById('faction-score-cap-input');
                            const elOpp3 = document.getElementById('opponent-score-cap-input');
                            const elEnd3 = document.getElementById('war-target-end-input');
                            if (elFac3 && elFac3.dataset && elFac3.dataset.autofilled === 'true') elFac3.classList.add('tdm-term-calculated');
                            if (elOpp3 && elOpp3.dataset && elOpp3.dataset.autofilled === 'true') elOpp3.classList.add('tdm-term-calculated');
                            if (elEnd3 && elEnd3.dataset && elEnd3.dataset.autofilled === 'true') elEnd3.classList.add('tdm-term-calculated');
                        } catch(_) {}
                        // If both caps are present, enforce term-type ordering (and show inline error if violated)
                        try {
                            const termTypeVal = (document.getElementById('term-type-select')?.value || state.warData?.termType || '').toLowerCase();
                            const elFac2 = document.getElementById('faction-score-cap-input');
                            const elOpp2 = document.getElementById('opponent-score-cap-input');
                            if (elFac2) elFac2.classList.remove('tdm-term-error');
                            if (elOpp2) elOpp2.classList.remove('tdm-term-error');
                            if ((providedFac && providedOpp) || (Number(factionCap) > 0 && Number(opponentCap) > 0)) {
                                if (termTypeVal.includes('win') && factionCap <= opponentCap) {
                                    if (elFac2) elFac2.classList.add('tdm-term-error');
                                    if (elOpp2) elOpp2.classList.add('tdm-term-error');
                                } else if (termTypeVal.includes('loss') && opponentCap <= factionCap) {
                                    if (elFac2) elFac2.classList.add('tdm-term-error');
                                    if (elOpp2) elOpp2.classList.add('tdm-term-error');
                                }
                            }
                        } catch (_) {}
                    } catch(_) {}
                    if (!startSec) {
                        // War-start is unknown — we still attempt best-effort solver autofill when the user provides an end time + one cap.
                        lines.push('<b>War Start:</b> not available (start time unknown). Decay calculations normally require a known war start; best-effort guesses will assume no decay (initial target used) where needed.');
                        if (initial) lines.push(`<b>Initial Target:</b> ${initial.toLocaleString()}`);
                        if (endSec) lines.push(`<b>Target End:</b> ${new Date(endSec*1000).toLocaleString()} (local) / ${new Date(endSec*1000).toUTCString()} (UTC)`);
                        if (factionCap || opponentCap) {
                            const diff = Math.abs(factionCap - opponentCap);
                            lines.push(`<b>Score Cap Diff:</b> ${diff || 'N/A'} — if start is unknown decay is ignored and initial target is used for estimation.`);
                        }
                        // If it's a termed war and we haven't provided two of the three key inputs (faction, opponent, end), show helper text
                        if (isTermedFromSelect && [!!factionCap, !!opponentCap, !!endSec].filter(Boolean).length < 2) {
                            lines.push(`<i>Tip: fill out any 2 of these 3 fields to generate an estimate for the third: Faction Cap, Opponent Cap, Target End (UTC hour-only).</i>`);
                        }
                        // don't return here — allow the solver below to make best-effort autofills (using initial target when start is unknown)
                        infoEl.innerHTML = lines.join('<br>');
                    }

                    // War start available: show countdown to start or active/ended status
                    const startMs = startSec * 1000;
                    const elapsedMs = Date.now() - startMs;
                    const maxMs = 123*60*60*1000; // 123 hours max per sample

                    // --- LIVE SCORES BLOCK ---
                    let liveLead = 0;
                    let liveScoresAvailable = false;
                    try {
                        const lr = state.lastRankWar || {};
                        const warObj = lr.war || lr;
                        const factions = Array.isArray(warObj.factions) ? warObj.factions : (Array.isArray(lr.factions) ? lr.factions : []);
                        if (factions && factions.length >= 2) {
                            const sorted = factions.slice().sort((x,y)=>Number(y.score||0) - Number(x.score||0));
                            const [leadF, trailF] = sorted.slice(0,2);
                            const scoreA = Number(leadF.score || 0); const scoreB = Number(trailF.score || 0);
                            liveLead = Math.abs(scoreA - scoreB);
                            liveScoresAvailable = true;
                            
                            lines.push(`<div style="margin-bottom: 6px; padding-bottom: 6px; border-bottom: 1px solid #444;">
                                <b>Live Scores:</b> <span style="color:#8bc34a">${leadF.name}: ${scoreA.toLocaleString()}</span> vs <span style="color:#ff5722">${trailF.name}: ${scoreB.toLocaleString()}</span><br>
                                <b>Current Lead:</b> <span style="color:#fff; font-weight:bold">${liveLead.toLocaleString()}</span>
                            </div>`);
                        }
                    } catch(_) {}
                    // -------------------------

                    if (elapsedMs < 0) {
                        lines.push(`<b>War starts in:</b> ${formatMsRelative(-elapsedMs)} — starts ${new Date(startMs).toLocaleString()} (local) / ${new Date(startMs).toUTCString()} (UTC)`);
                        lines.push(`<b>Initial Target:</b> ${initial.toLocaleString()}`);
                        if (endSec) {
                            // Compute final target at end
                            const final = computeTargetAfterDecay(initial, startSec, endSec);
                            lines.push(`<b>Predicted Final Target at End:</b> ${Math.round(final).toLocaleString()} (${((final/initial||0)*100).toFixed(1)}% of initial)`);
                        }
                    } else if (endSec > 0 && endSec <= nowSec) {
                        // War *has* ended per backend (ranked war canonical end)
                        const endMs = endSec * 1000;
                        const durMs = endMs - startMs;
                        // Try to show results from lastRankWar if available
                        try {
                            const lr = state.lastRankWar || {};
                            const warObj = lr.war || lr;
                            const factions = Array.isArray(warObj.factions) ? warObj.factions : (Array.isArray(lr.factions) ? lr.factions : []);
                            const winnerId = Number(warObj.winner || lr.winner || 0) || 0;
                            if (factions && factions.length > 0) {
                                const left = factions.map(f => `${f.name}: ${Number(f.score||0).toLocaleString()}`).join(' — ');
                                lines.push(`<b>War Status:</b> Ended — duration ${formatMsRelative(durMs)} (started ${new Date(startMs).toLocaleString()}, ended ${new Date(endMs).toLocaleString()}).`);
                                lines.push(`<b>Result:</b> ${left}${winnerId ? ` — winner: ${factions.find(ff => String(ff.id)===String(winnerId))?.name || winnerId}` : ''}`);
                            } else {
                                lines.push(`<b>War Status:</b> Ended — duration ${formatMsRelative(durMs)} (started ${new Date(startMs).toLocaleString()}, ended ${new Date(endMs).toLocaleString()}).`);
                            }
                        } catch(_) {
                            lines.push(`<b>War Status:</b> Ended — ended at ${new Date(endSec*1000).toLocaleString()} / ${new Date(endSec*1000).toUTCString()}.`);
                        }
                    } else {
                        // Active
                        const currentTarget = computeTargetAfterDecay(initial, startSec, nowSec);
                        const distToTarget = Math.max(0, Math.round(currentTarget) - liveLead);

                        lines.push(`<b>War Status:</b> <span style="color:#4caf50">Active</span> — elapsed ${formatMsRelative(elapsedMs)} (started ${new Date(startMs).toLocaleString()}).`);
                        lines.push(`<b>Targets:</b> Initial: ${initial.toLocaleString()} — <span style="color:#2196f3">Current: ${Math.round(currentTarget).toLocaleString()}</span>`);
                        
                        if (liveScoresAvailable) {
                             if (liveLead >= currentTarget) {
                                 lines.push(`<span style="color:#4caf50"><b>Target Met!</b> Current lead exceeds target by ${(liveLead - currentTarget).toLocaleString()}. War should end.</span>`);
                             } else {
                                 lines.push(`<b>Distance to Target:</b> ${distToTarget.toLocaleString()} (Lead needs to increase or target needs to decay)`);
                             }
                        }

                        // Next decay drop info
                        if (elapsedMs < 24*60*60*1000) {
                            const msUntilDecay = 24*60*60*1000 - elapsedMs;
                            lines.push(`<b>Decay:</b> Not started — will begin in ${formatMsRelative(msUntilDecay)} (at ${new Date(startMs + 24*60*60*1000).toLocaleString()})`);
                        } else {
                            const hoursSinceDecay = Math.floor((elapsedMs - 24*60*60*1000) / (60*60*1000));
                            const pct = Math.min(99, hoursSinceDecay * 1);
                            const nextDropMs = (60*60*1000) - ((elapsedMs - 24*60*60*1000) % (60*60*1000));
                            lines.push(`<b>Decay:</b> In effect — ${pct}% total applied. Next hourly drop in ${formatMsRelative(nextDropMs)}.`);
                        }
                        
                        // Always show Live Estimator if scores are available, regardless of whether an end time is specified
                        if (liveScoresAvailable) {
                            try {
                                // If lead already >= current target then war should end; otherwise estimate when decay causes target <= lead
                                if (liveLead >= Math.round(currentTarget)) {
                                    lines.push(`<b>Live Estimator:</b> Current lead (${liveLead.toLocaleString()}) already exceeds current target (${Math.round(currentTarget).toLocaleString()}) — war would be expected to end now.`);
                                } else {
                                    // Solve for hoursIntoDecay needed where initial*(1 - h/100) <= lead => h >= 100*(1 - lead/initial)
                                    const neededPct = Math.max(0, 1 - (liveLead / initial));
                                    const hoursNeededTotal = Math.ceil(neededPct * 100);
                                    // Hours into decay = hoursNeededTotal; but some hours already elapsed
                                    const hoursSinceDecay = Math.max(0, Math.floor((elapsedMs - 24*60*60*1000)/(60*60*1000)));
                                    const hoursRemaining = Math.max(0, hoursNeededTotal - hoursSinceDecay);
                                    const estEndMs = startMs + 24*60*60*1000 + (hoursNeededTotal * 60*60*1000);
                                    
                                    if (hoursNeededTotal === 0) {
                                        lines.push(`<b>Live Estimator:</b> Lead (${liveLead.toLocaleString()}) implies war could end shortly (no further decay required).`);
                                    } else {
                                        lines.push(`<b>Live Estimator (Decay to Score Diff ${liveLead.toLocaleString()}):</b> In ${formatMsRelative(estEndMs - Date.now())} (at ${new Date(estEndMs).toUTCString()})`);
                                    }
                                }
                            } catch(_) {}
                        }

                        // If there's an authoritative API end time (ranked war ended/declared end), show final target at end
                        if (endSecApi) {
                            const finalAtEnd = computeTargetAfterDecay(initial, startSec, endSecApi);
                            lines.push(`<b>Target at Official End:</b> ${Math.round(finalAtEnd).toLocaleString()} (end ${new Date(endSecApi*1000).toLocaleString()})`);
                        } else if (endSecInput) {
                            // User-specified end
                            const finalAtEnd = computeTargetAfterDecay(initial, startSec, endSecInput);
                            lines.push(`<b>Target at Specified End:</b> ${Math.round(finalAtEnd).toLocaleString()} (end ${new Date(endSecInput*1000).toLocaleString()})`);
                        }
                    }

                    // Two-of-three solver: only for Termed Wars. If the war is Ranked, use live-ranked estimators instead.
                    // If the war is Ranked but we still have Termed-style fields in warData (legacy), do NOT run the Termed solver.
                    // For Ranked wars we'll compute separate live estimations below using `state.lastRankWar`.
                    // Guard: if the user is currently editing one of the three inputs we *must not* run the
                    // autofill solver — only run solver when the user has left the field (blur / Enter)
                    try {
                        const activeId = (document.activeElement && document.activeElement.id) ? document.activeElement.id : '';
                        const editingField = (state.ui && state.ui._warTermEditing) || ['faction-score-cap-input', 'opponent-score-cap-input', 'war-target-end-input'].includes(activeId);
                        if (editingField) {
                            // Skip solver while user is typing; ensure the UI status text still updates
                            try { const solverSnap = JSON.stringify({ presentCount: [utils.rawHasValue(rawOpponentVal), utils.rawHasValue(rawFactionVal), utils.rawHasValue(rawEndVal)].filter(Boolean).length, factionCap: Number(factionCap)||0, opponentCap: Number(opponentCap)||0, endSec: Number(endSec)||0, startSec: Number(startSec)||0, termType: String((document.getElementById('term-type-select')?.value || state.warData?.termType || '')||'') });
                                const _lastSolver = state.ui._lastWarTermInfoSolverSnap || {};
                                if (state.debug?.rwTermInfo || !_lastSolver.key || _lastSolver.key !== solverSnap) {
                                    // indicate we skipped solver due to editing
                                    console.debug('[updateWarTermInfo] solver skipped because user is editing', { activeId, presentCount: [utils.rawHasValue(rawOpponentVal), utils.rawHasValue(rawFactionVal), utils.rawHasValue(rawEndVal)].filter(Boolean).length });
                                    state.ui._lastWarTermInfoSolverSnap = { key: solverSnap, ts: Date.now() };
                                }
                            } catch(_) {}
                        } else {
                        // term type matters (Termed Loss vs Termed Win semantics)
                        const currentTermType = (document.getElementById('term-type-select')?.value || state.warData?.termType || '').toLowerCase();
                        const isLoss = currentTermType.includes('loss');
                        // Use the normalized rawHasValue helper here too, so '0' behaves like empty
                        const presentCount = [utils.rawHasValue(rawOpponentVal), utils.rawHasValue(rawFactionVal), utils.rawHasValue(rawEndVal)].filter(Boolean).length;
                        try {
                            // similar duplicate-suppression for solver state: either enabled via debug flag or only logged when changed
                            const solverSnap = JSON.stringify({ presentCount, factionCap: Number(factionCap)||0, opponentCap: Number(opponentCap)||0, endSec: Number(endSec)||0, startSec: Number(startSec)||0, termType: String(currentTermType||'') });
                            const _lastSolver = state.ui._lastWarTermInfoSolverSnap || {};
                            if (state.debug?.rwTermInfo || !_lastSolver.key || _lastSolver.key !== solverSnap) {
                                console.debug('[updateWarTermInfo] solver state', { presentCount, providedFlags: { faction: utils.rawHasValue(rawFactionVal), opponent: utils.rawHasValue(rawOpponentVal), end: utils.rawHasValue(rawEndVal) }, numericCaps: { factionCap, opponentCap }, endSec, startSec, initial, termType: currentTermType });
                                state.ui._lastWarTermInfoSolverSnap = { key: solverSnap, ts: Date.now() };
                            }
                        } catch(_) {}
                        let didAutoFill = false;
                        const setInput = (id, val) => { try { const el = document.getElementById(id); if (el) { el.value = String(val); el.dataset.autofilled = 'true'; el.classList.remove('tdm-term-required'); el.classList.remove('tdm-term-error'); el.classList.add('tdm-term-calculated'); didAutoFill = true; /* do NOT dispatch an input event to avoid re-entrancy */ } } catch(_) {} };
                        // Only run the Termed solver when this is a Termed War
                        if (isTermedFromSelect) {
                        // If endTime + factionCap -> compute opponentCap if the opponent input
                        // has not been provided by the user. We DO NOT overwrite a user-supplied
                        // value for opponent cap.
                        // Only compute when opponent raw value is not present.
                        if (endSec && factionCap && !utils.rawHasValue(rawOpponentVal)) {
                            const finalT = Math.round(computeTargetAfterDecay(initial, startSec, endSec));
                            // For Termed Loss: final = opponent - faction -> opponent = faction + final
                            // For Termed Win: final = faction - opponent -> opponent = faction - final
                            const guessedOppCap = isLoss ? Math.max(0, Math.round(factionCap + finalT)) : Math.max(0, Math.round(factionCap - finalT));
                            lines.push(`<b>Solver:</b> Given end time + faction cap, opponent cap ≈ ${guessedOppCap} (final target ≈ ${finalT}).`);
                            // apply computed value and refresh UI state
                            setInput('opponent-score-cap-input', guessedOppCap);
                            try {
                                // debug: ensure the input element value was set (helps identify race/DOM issues)
                                const debugEl = document.getElementById('opponent-score-cap-input');
                                if (debugEl && debugEl.value !== String(guessedOppCap)) {
                                    // Mismatch here often indicates a race/another handler overwrote us — keep this log but amount-limited
                                    const tkey = JSON.stringify({ guessedOppCap: guessedOppCap, value: debugEl.value });
                                    const lastMiss = state.ui._lastWarTermInfoMismatch || {};
                                    if (state.debug?.rwTermInfo || !lastMiss.key || lastMiss.key !== tkey) {
                                        try { console.debug('[updateWarTermInfo] guessedOppCap set but input value did not match', { guessedOppCap, value: debugEl.value }); } catch(_) {}
                                        state.ui._lastWarTermInfoMismatch = { key: tkey, ts: Date.now() };
                                    }
                                }
                            } catch(_) {}
                            /* scheduled later if needed (didAutoFill) to avoid re-entrancy */
                        }
                        // If endTime + opponentCap -> compute factionCap only when faction
                        // input was not provided by the user. Avoid overwriting a user value.
                        if (endSec && opponentCap && !utils.rawHasValue(rawFactionVal)) {
                            const finalT = Math.round(computeTargetAfterDecay(initial, startSec, endSec));
                            // For Termed Loss: final = opponent - faction -> faction = opponent - final
                            // For Termed Win: final = faction - opponent -> faction = opponent + final
                            const guessedFacCap = isLoss ? Math.max(0, Math.round(opponentCap - finalT)) : Math.max(0, Math.round(opponentCap + finalT));
                            lines.push(`<b>Solver:</b> Given end time + opponent cap, faction cap ≈ ${guessedFacCap} (final target ≈ ${finalT}).`);
                            // apply computed value and refresh UI state
                            setInput('faction-score-cap-input', guessedFacCap);
                        }
                        // If both caps provided, compute needed decay hours to reach target difference and the end time
                        if (isTermedFromSelect && factionCap && opponentCap && initial) {
                            // For Termed Loss/Win, desired final is opponent - faction (loss) or faction - opponent (win)
                            const desiredFinal = isLoss ? Math.max(0, opponentCap - factionCap) : Math.max(0, factionCap - opponentCap);
                            if (desiredFinal >= initial) {
                                lines.push(`<b>Solver:</b> Desired final (${desiredFinal}) ≥ initial (${initial}) — no decay required (end would be immediate or earlier).`);
                            } else {
                                // percent reduction required
                                const reduction = 1 - (desiredFinal / initial);
                                const hoursNeeded = Math.ceil(reduction * 100); // 1% per hour
                                if (!startSec) {
                                    lines.push(`<b>Solver:</b> Cannot estimate end time from both caps because war start is unknown.`);
                                } else {
                                    const endCandidateMs = (startSec * 1000) + 24*60*60*1000 + (hoursNeeded * 60*60*1000);
                                    lines.push(`<b>Solver:</b> To reach final target ${desiredFinal}, decay needs ~${hoursNeeded}h after 24h. Estimated end: ${new Date(endCandidateMs).toLocaleString()} / ${new Date(endCandidateMs).toUTCString()}`);
                                    // Auto-fill target end only when the user did not provide an end value.
                                    // If the user supplied an explicit Target End, do not overwrite it.
                                    if (!utils.rawHasValue(rawEndVal)) {
                                        setInput('war-target-end-input', epochSecToUtcDatetimeHourValue(Math.round(endCandidateMs/1000)));
                                    }
                                }
                                
                            }
                        }
                        // End Termed solver section
                        } // end if (isTermedFromSelect)

                        // If this is a Ranked War then add a live-ranked estimator (based on lastRankWar data)
                        if (!isTermedFromSelect && (((document.getElementById('war-type-select')?.value || state.warData?.warType) === 'Ranked War') || (state.lastRankWar && state.lastRankWar.id))) {
                            try {
                                const lr = state.lastRankWar || {};
                                // only estimate if war is active (no end or end in future)
                                const lrEnd = Number(lr.end || (lr.war && lr.war.end) || 0) || 0;
                                const lrStart = Number(lr.start || (lr.war && lr.war.start) || 0) || startSec || 0;
                                if (lrStart) {
                                    const factions = Array.isArray(lr.factions) ? lr.factions : (lr.war && Array.isArray(lr.war.factions) ? lr.war.factions : []);
                                    if (factions.length >= 2) {
                                        const sorted = factions.slice().sort((x,y)=>Number(y.score||0)-Number(x.score||0));
                                        const [lead, trail] = sorted.slice(0,2);
                                        const leadScore = Number(lead.score||0), trailScore = Number(trail.score||0);
                                        const liveLead = Math.abs(leadScore - trailScore);
                                        // Use lr.target when available, otherwise initial
                                        const targetAtStart = Number(lr.target || initial || 0) || 0;
                                        // Current target using decay model
                                        const curTarget = computeTargetAfterDecay(initial, startSec, nowSec);
                                        // If end exists in the past, war ended — no estimator, but show final target
                                        if (lrEnd && lrEnd <= nowSec) {
                                            // ended — nothing to estimate
                                        } else {
                                            // Estimate when liveLead >= decayed target
                                            if (leadScore >= curTarget) {
                                                lines.push(`<b>Ranked Estimator:</b> Current live lead (${liveLead.toLocaleString()}) already meets current target (${Math.round(curTarget).toLocaleString()}).`);
                                            } else {
                                                const neededPct = Math.max(0, 1 - (liveLead / (initial || targetAtStart || 1)));
                                                const hoursNeeded = Math.ceil(neededPct * 100);
                                                const decayStartMs = startMs + (24*60*60*1000);
                                                const estEndMs = decayStartMs + (hoursNeeded * 60*60*1000);
                                                const hoursSinceDecay = Math.max(0, Math.floor((elapsedMs - 24*60*60*1000) / (60*60*1000)));
                                                const hoursRemaining = Math.max(0, hoursNeeded - hoursSinceDecay);
                                                lines.push(`<b>Ranked Estimator:</b> If scores hold, lead ${liveLead.toLocaleString()} meets decayed target after ~${hoursNeeded}h of decay (${hoursRemaining}h left). Estimated end: ${new Date(estEndMs).toLocaleString()} / ${new Date(estEndMs).toUTCString()}`);
                                            }
                                        }
                                    }
                                }
                            } catch (_) {}
                        }

                        // If we autofilled any inputs above, schedule a single follow-up refresh
                        if (didAutoFill) {
                            try { setTimeout(() => { try { updateWarTermInfo(); applyWarString(); } catch(_) {} }, 0); } catch(_) {}
                        }

                        // For Termed wars we also present a live-score based estimate using lastRankWar scores
                        try {
                            if (isTermedFromSelect && state.lastRankWar && state.lastRankWar.war) {
                                const lr = state.lastRankWar.war || state.lastRankWar;
                                const factions = Array.isArray(lr.factions) ? lr.factions : [];
                                if (factions.length >= 2 && initial && startSec) {
                                    const sorted = factions.slice().sort((x,y) => Number(y.score||0) - Number(x.score||0));
                                    const [leadFaction, trailFaction] = sorted.slice(0,2);
                                    const liveLead = Math.abs(Number(leadFaction.score||0) - Number(trailFaction.score||0));
                                    lines.push(`<b>Live Scores:</b> ${leadFaction.name || leadFaction.id}: ${Number(leadFaction.score||0).toLocaleString()} — ${trailFaction.name || trailFaction.id}: ${Number(trailFaction.score||0).toLocaleString()} (lead ${liveLead.toLocaleString()})`);

                                    // If two caps provided in UI, show differences between entered caps and live scores
                                    if ((factionCap || opponentCap)) {
                                        const enteredFacName = (state.warData?.factionName || (factions[0] && factions[0].name) || 'Faction');
                                        const enteredOppName = (state.warData?.opponentFactionName || (factions[1] && factions[1].name) || 'Opponent');
                                        lines.push(`<b>Entered Caps (warData):</b> ${enteredFacName}: ${Number(factionCap).toLocaleString()} — ${enteredOppName}: ${Number(opponentCap).toLocaleString()}`);
                                        try {
                                            const diffFacVsLive = (Number(factionCap) || 0) - Number(leadFaction.score||0);
                                            const diffOppVsLive = (Number(opponentCap) || 0) - Number(trailFaction.score||0);
                                            lines.push(`<b>Diff (entered - live):</b> ${enteredFacName}: ${diffFacVsLive>=0?'+':''}${diffFacVsLive.toLocaleString()} — ${enteredOppName}: ${diffOppVsLive>=0?'+':''}${diffOppVsLive.toLocaleString()}`);
                                        } catch(_) {}
                                    }

                                    // Estimate end from live scores: find when decayed target <= live lead
                                    const currentTarget = Math.round(computeTargetAfterDecay(initial, startSec, nowSec));
                                    if (liveLead >= currentTarget) {
                                        lines.push(`<b>Live-based Estimator:</b> Current lead (${liveLead.toLocaleString()}) already meets current target (${currentTarget.toLocaleString()}) — war would be expected to end now if scores hold.`);
                                    } else {
                                        const neededPctLive = Math.max(0, 1 - (liveLead / initial));
                                        const hoursNeededLiveTotal = Math.ceil(neededPctLive * 100);
                                        const hoursSinceDecay = Math.max(0, Math.floor((elapsedMs - 24*60*60*1000) / (60*60*1000)));
                                        const hoursRemainingLive = Math.max(0, hoursNeededLiveTotal - hoursSinceDecay);
                                        const estEndLiveMs = startMs + 24*60*60*1000 + (hoursNeededLiveTotal * 60*60*1000);
                                        lines.push(`<b>Live-based Estimator:</b> If current scores hold, lead ${liveLead.toLocaleString()} will meet decayed target after ~${hoursNeededLiveTotal}h of decay (${hoursRemainingLive}h left). Estimated end: ${new Date(estEndLiveMs).toLocaleString()} / ${new Date(estEndLiveMs).toUTCString()}`);
                                    }
                                }
                            }
                        } catch(_) {}
                    
                    }
                    } catch (_) { /* ignore solver error */ }

                    // If this is a termed war and user hasn't provided at least 2 of the 3 inputs, offer the quick helper hint
                    if (isTermedFromSelect && [!!factionCap, !!opponentCap, !!endSec].filter(Boolean).length < 2) {
                        lines.push(`<i>Tip: fill out any 2 of these 3 fields to generate an estimate for the third: Faction Cap, Opponent Cap, Target End.</i>`);
                    }

                    infoEl.innerHTML = lines.join('<br>');
                } catch (err) { /* non-fatal — don't stop settings build */ }
            };

                    // Wire realtime updates for the term info area
                    // Debounced update helper so fast typing (eg. 4-digit caps) doesn't recalc every keystroke
                    try {
                        if (!state.ui) state.ui = {};
                        state.ui._rwTermInfoDebounceTimeout = state.ui._rwTermInfoDebounceTimeout || null;
                        const debouncedUpdateWarTermInfo = () => {
                            try { if (state.ui._rwTermInfoDebounceTimeout) utils.unregisterTimeout(state.ui._rwTermInfoDebounceTimeout); } catch(_) {}
                            state.ui._rwTermInfoDebounceTimeout = utils.registerTimeout(setTimeout(() => {
                                try { updateWarTermInfo(); } catch(_) {}
                                state.ui._rwTermInfoDebounceTimeout = null;
                            }, 600));
                        };
                        // Also expose for outside callers (tests / other codepaths)
                        state.ui.debouncedUpdateWarTermInfo = debouncedUpdateWarTermInfo;
                    } catch(_) {}
            // initial-target is display-only now; do not listen for input events
            // Debounced handlers: wait until user pauses typing to trigger solver
            const _winTermImmediate = () => { try { if (state.ui && state.ui._rwTermInfoDebounceTimeout) { utils.unregisterTimeout(state.ui._rwTermInfoDebounceTimeout); state.ui._rwTermInfoDebounceTimeout = null; } if (!state.ui) state.ui = {}; state.ui._lastWarTermInputs = null; state.ui._lastWarTermInfoSolverSnap = null; updateWarTermInfo(); } catch(_) {} };
            // Only clear autofill marker while typing; do NOT trigger solver until user leaves field
            document.getElementById('opponent-score-cap-input')?.addEventListener('input', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = true; } catch(_) {} ; /* no auto-solve on input */ });
            document.getElementById('opponent-score-cap-input')?.addEventListener('blur', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; _winTermImmediate(); });
            document.getElementById('opponent-score-cap-input')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; _winTermImmediate(); } });
            // Only clear autofill marker while typing; do NOT trigger solver until user leaves field
            document.getElementById('faction-score-cap-input')?.addEventListener('input', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = true; } catch(_) {} ; /* no auto-solve on input */ });
            document.getElementById('faction-score-cap-input')?.addEventListener('blur', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; _winTermImmediate(); });
            document.getElementById('faction-score-cap-input')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; _winTermImmediate(); } });
            // target end is a date/time input; use both input & change to be responsive to typing or picker selection
            // For the date/time picker we clear autofill when the user edits, but we only
            // run the solver when they exit the control (blur) or press Enter.
            document.getElementById('war-target-end-input')?.addEventListener('input', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = true; } catch(_) {} ; /* no auto-solve on input */ });
            document.getElementById('war-target-end-input')?.addEventListener('change', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; /* no auto-solve on change */ });
            document.getElementById('war-target-end-input')?.addEventListener('blur', (e) => { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; _winTermImmediate(); });
            document.getElementById('war-target-end-input')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { try { e.currentTarget.removeAttribute('data-autofilled'); if (!state.ui) state.ui = {}; state.ui._warTermEditing = false; } catch(_) {} ; _winTermImmediate(); } });

            // Run a first pass to populate the info area and keep it live-updating while panel is open
            try {
                if (!state.ui) state.ui = {};
                state.ui._lastWarTermInputs = null;
                state.ui._lastWarTermInfoLogSnapshot = null;
            } catch(_) {}
            try { updateWarTermInfo(); } catch(_) {}
            try {
                if (!state.ui) state.ui = {};
                if (state.ui.rwTermInfoIntervalId) { try { utils.unregisterInterval(state.ui.rwTermInfoIntervalId); } catch(_) {} state.ui.rwTermInfoIntervalId = null; }
                state.ui.rwTermInfoIntervalId = utils.registerInterval(setInterval(() => {
                    try { if (document.getElementById('tdm-settings-popup')) updateWarTermInfo(); else { try { utils.unregisterInterval(state.ui.rwTermInfoIntervalId); } catch(_) {} state.ui.rwTermInfoIntervalId = null; } } catch(_) {}
                }, 1000));
            } catch(_) {}

            document.getElementById('save-war-data-btn')?.addEventListener('click', async (e) => {
                const warDataToSave = { ...state.warData };
                warDataToSave.warType = document.getElementById('war-type-select').value;
                if (warDataToSave.warType === 'Termed War') {
                    warDataToSave.termType = document.getElementById('term-type-select').value;
                    warDataToSave.factionScoreCap = parseInt(document.getElementById('faction-score-cap-input').value) || 0;
                    // New fields: opponent cap, initial target, target end time
                    try { warDataToSave.opponentScoreCap = parseInt(document.getElementById('opponent-score-cap-input')?.value) || 0; } catch(_) { warDataToSave.opponentScoreCap = warDataToSave.opponentScoreCap || 0; }
                    try { warDataToSave.initialTargetScore = Number(state.warData?.initialTargetScore || (state.lastRankWar?.war?.target ?? state.lastRankWar?.target) || 0) || 0; } catch(_) { warDataToSave.initialTargetScore = warDataToSave.initialTargetScore || 0; }
                    try { const dt = document.getElementById('war-target-end-input')?.value || ''; if (dt) warDataToSave.targetEndTime = parseDateTimeLocalToEpochSecAssumeUTC(dt) || warDataToSave.targetEndTime || 0; } catch(_) {}
                    warDataToSave.individualScoreCap = parseInt(document.getElementById('individual-score-cap-input').value) || 0;
                    warDataToSave.individualScoreType = document.getElementById('individual-score-type-select').value;
                    // Backward-compat fields
                    warDataToSave.scoreCap = warDataToSave.factionScoreCap;
                    warDataToSave.scoreType = warDataToSave.individualScoreType;
                }
                else {
                    // Switching away from Termed War: clear Termed-specific fields so Ranked War
                    // doesn't keep stale/invalid data.
                    try {
                        delete warDataToSave.termType;
                        delete warDataToSave.factionScoreCap;
                        delete warDataToSave.opponentScoreCap;
                        delete warDataToSave.initialTargetScore;
                        delete warDataToSave.targetEndTime;
                        delete warDataToSave.individualScoreCap;
                        delete warDataToSave.individualScoreType;
                        // Backwards-compat / aliases
                        delete warDataToSave.scoreCap;
                        delete warDataToSave.scoreType;
                        // Optional Term-specific flag
                        delete warDataToSave.disableMedDeals;
                    } catch(_) {}
                }
                // Optional: disable med deals during this war (only show dibs button)
                try { warDataToSave.disableMedDeals = !!document.getElementById('war-disable-meddeals')?.checked; } catch(_) { /* ignore */ }
                // Validation: for Termed War, ensure we have two-of-three inputs (faction cap, opponent cap, target end)
                if (warDataToSave.warType === 'Termed War') {
                    try {
                        const rawFac = document.getElementById('faction-score-cap-input')?.value?.trim() || '';
                        const rawOpp = document.getElementById('opponent-score-cap-input')?.value?.trim() || '';
                        const rawEnd = document.getElementById('war-target-end-input')?.value?.trim() || '';
                        const initial = warDataToSave.initialTargetScore || Number(state.warData?.initialTargetScore || (state.lastRankWar?.war?.target ?? state.lastRankWar?.target) || 0) || 0;
                        const present = [rawFac !== '', rawOpp !== '', rawEnd !== ''].filter(Boolean).length;
                        if (present < 2) {
                            try { const infoEl = document.getElementById('rw-term-info'); if (infoEl) { infoEl.innerHTML = `<b style="color:#f87171">Please provide at least <u>TWO</u> of these fields: Faction Cap, Opponent Cap, or Target End Time (UTC, hour-only) so the solver can estimate the third.</b>`; } } catch(_) {}
                            return;
                        }
                        // Resolve war start if available (needed to estimate end)
                        const resolveStart = () => {
                            const candidates = [state.warData?.warStart, state.warData?.start, state.lastRankWar?.war?.start, state.lastRankWar?.start];
                            for (const c of candidates) {
                                const n = Number(c);
                                if (Number.isFinite(n) && n > 0) return n;
                            }
                            return 0;
                        };
                        const startSec = resolveStart();
                        const currentTermType = (document.getElementById('term-type-select')?.value || warDataToSave.termType || '').toLowerCase();
                        const isLoss = currentTermType.includes('loss');
                        // Compute missing third value
                        const fac = rawFac !== '' ? (parseInt(rawFac) || 0) : (warDataToSave.factionScoreCap || 0);
                        const opp = rawOpp !== '' ? (parseInt(rawOpp) || 0) : (warDataToSave.opponentScoreCap || 0);
                        const endVal = rawEnd !== '' ? parseDateTimeLocalToEpochSecAssumeUTC(rawEnd) || 0 : (warDataToSave.targetEndTime || 0);
                        // Helper: compute final target at end
                        const finalFromEnd = (endSec) => Math.round(computeTargetAfterDecay(initial, startSec, endSec));
                        if (rawEnd !== '' && rawFac !== '' && rawOpp === '') {
                            const finalT = finalFromEnd(endVal);
                            const guessedOpp = isLoss ? Math.max(0, Math.round(fac + finalT)) : Math.max(0, Math.round(fac - finalT));
                            warDataToSave.opponentScoreCap = guessedOpp;
                        } else if (rawEnd !== '' && rawOpp !== '' && rawFac === '') {
                            const finalT = finalFromEnd(endVal);
                            const guessedFac = isLoss ? Math.max(0, Math.round(opp - finalT)) : Math.max(0, Math.round(opp + finalT));
                            warDataToSave.factionScoreCap = guessedFac;
                        } else if (rawFac !== '' && rawOpp !== '' && rawEnd === '') {
                            // Need start time to estimate end
                            if (!startSec) {
                                try { const infoEl = document.getElementById('rw-term-info'); if (infoEl) { infoEl.innerHTML = `<b style="color:#f87171">Cannot estimate end time without a known war start time. Add an end time or ensure war start is available.</b>`; } } catch(_) {}
                                return;
                            }
                            const desiredFinal = isLoss ? Math.max(0, opp - fac) : Math.max(0, fac - opp);
                            if (desiredFinal >= initial) {
                                // end is effectively immediate (start)
                                warDataToSave.targetEndTime = Math.floor(Date.now() / 1000);
                            } else {
                                const reduction = 1 - (desiredFinal / initial);
                                const hoursNeeded = Math.ceil(reduction * 100);
                                const endCandidateMs = (startSec * 1000) + 24*60*60*1000 + (hoursNeeded * 60*60*1000);
                                warDataToSave.targetEndTime = Math.round(endCandidateMs / 1000);
                            }
                        }
                        // Final validation: ensure non-negative and term-type ordering is satisfied before saving
                        try {
                            const finalFac = Number(warDataToSave.factionScoreCap || 0);
                            const finalOpp = Number(warDataToSave.opponentScoreCap || 0);
                            // clamp
                            warDataToSave.factionScoreCap = Math.max(0, finalFac);
                            warDataToSave.opponentScoreCap = Math.max(0, finalOpp);
                            const infoEl = document.getElementById('rw-term-info');
                            const termTypeVal2 = (document.getElementById('term-type-select')?.value || warDataToSave.termType || '').toLowerCase();
                            if (termTypeVal2.includes('win') && warDataToSave.factionScoreCap <= warDataToSave.opponentScoreCap) {
                                if (infoEl) infoEl.innerHTML = `<b style="color:#fb7185">Error: Termed Win requires Faction Cap &gt; Opponent Cap.</b>`;
                                return;
                            }
                            if (termTypeVal2.includes('loss') && warDataToSave.opponentScoreCap <= warDataToSave.factionScoreCap) {
                                if (infoEl) infoEl.innerHTML = `<b style="color:#fb7185">Error: Termed Loss requires Opponent Cap &gt; Faction Cap.</b>`;
                                return;
                            }
                        } catch (_) {}
                    } catch (err) {
                        tdmlogger('warn', `[save-war-data] solver validation failed: ${err?.message || err}`);
                    }
                }

                await handlers.debouncedSetFactionWarData(warDataToSave, e.currentTarget);
            });
            document.getElementById('copy-war-details-btn')?.addEventListener('click', async () => {
                const summary = buildWarDetailsSummary();
                await copyTextToClipboard(summary, 'War details');
            });
            const rwButtonsContainer = document.getElementById('column-visibility-rw');
            const mlButtonsContainer = document.getElementById('column-visibility-ml');
            if (rwButtonsContainer || mlButtonsContainer) {
                    const rankedWarColumns = [
                        { key: 'lvl', label: 'Level' },
                        { key: 'members', label: 'Member' },
                        { key: 'points', label: 'Points' },
                        { key: 'status', label: 'Status' },
                        { key: 'attack', label: 'Attack' },
                        { key: 'factionIcon', label: 'Faction Icon' }
                    ];
                const membersListColumns = [
                    { key: 'lvl', label: 'Level' },
                    { key: 'member', label: 'Member' },
                    { key: 'memberIcons', label: 'Member Icons' },
                    { key: 'position', label: 'Position' },
                    { key: 'days', label: 'Days' },
                    { key: 'status', label: 'Status' },
                    { key: 'factionIcon', label: 'Faction Icon' },
                    { key: 'dibsDeals', label: 'Dibs/Deals' },
                    { key: 'notes', label: 'Notes' }
                ];
                // Ranked War Buttons (toggle + width control)
                if (rwButtonsContainer) {
                    let deferredRWFactionWrapper = null;
                    rankedWarColumns.forEach(col => {
                        if (!rwButtonsContainer.querySelector(`button[data-column="${col.key}"][data-table="rankedWar"]`)) {
                            const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY);
                            const active = vis.rankedWar?.[col.key] !== false ? 'active' : 'inactive';
                            const widths = storage.get('columnWidths', config.DEFAULT_COLUMN_WIDTHS);
                            const curW = (widths && widths.rankedWar && typeof widths.rankedWar[col.key] === 'number') ? widths.rankedWar[col.key] : (config.DEFAULT_COLUMN_WIDTHS.rankedWar[col.key] || 6);
                            const wrapper = utils.createElement('div', { className: 'column-control', style: { display: 'flex', gap: '6px', alignItems: 'center' } });
                            const button = utils.createElement('button', {
                                className: `column-toggle-btn ${active}`,
                                dataset: { column: col.key, table: 'rankedWar' },
                                textContent: col.label,
                                onclick: () => {
                                    const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY) || {};
                                    if (!vis.rankedWar) vis.rankedWar = {};
                                    // Toggle against the effective current value (stored value if present, otherwise default)
                                    const cur = (typeof vis.rankedWar[col.key] !== 'undefined')
                                        ? vis.rankedWar[col.key]
                                        : (config.DEFAULT_COLUMN_VISIBILITY?.rankedWar?.[col.key] ?? true);
                                    vis.rankedWar[col.key] = !cur;
                                    storage.set('columnVisibility', vis);
                                    ui.updateColumnVisibilityStyles();
                                    ui.updateSettingsContent();
                                }
                            });
                            // don't offer width adjustment for small icon columns (factionIcon)
                            let widthInput;
                            if (col.key !== 'factionIcon') {
                                widthInput = utils.createElement('input', { type: 'number', className: 'settings-input-display column-width-input', dataset: { column: col.key, table: 'rankedWar' }, min: '1', max: '99', step: '1', value: String(curW), title: 'Column width percentage' });
                                widthInput.style.width = '64px';
                                widthInput.addEventListener('change', (e) => {
                                try {
                                    const v = Math.max(1, Math.min(99, Math.round(Number(e.target.value) || 0)));
                                    const w = storage.get('columnWidths', config.DEFAULT_COLUMN_WIDTHS) || {};
                                    if (!w.rankedWar) w.rankedWar = {};
                                    w.rankedWar[col.key] = v;
                                    storage.set('columnWidths', w);
                                    ui.updateColumnVisibilityStyles();
                                    ui.updateSettingsContent();
                                } catch(_) {}
                                });
                            }
                            wrapper.appendChild(button);
                            if (widthInput) {
                                wrapper.appendChild(widthInput);
                                wrapper.appendChild(utils.createElement('div', { textContent: '%', style: { color: '#ccc', fontSize: '0.85em' } }));
                            }
                            if (col.key === 'factionIcon') {
                                // Defer appending factionIcon toggle - it should appear below the other controls
                                deferredRWFactionWrapper = wrapper;
                            } else {
                                rwButtonsContainer.appendChild(wrapper);
                            }
                        }
                    });
                    if (deferredRWFactionWrapper) rwButtonsContainer.appendChild(deferredRWFactionWrapper);
                }
                // Members List Buttons (toggle + width control)
                if (mlButtonsContainer) {
                    let deferredMLFactionWrapper = null;
                    membersListColumns.forEach(col => {
                        if (!mlButtonsContainer.querySelector(`button[data-column="${col.key}"][data-table="membersList"]`)) {
                            const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY);
                            const active = vis.membersList?.[col.key] !== false ? 'active' : 'inactive';
                            const widths = storage.get('columnWidths', config.DEFAULT_COLUMN_WIDTHS);
                            const curW = (widths && widths.membersList && typeof widths.membersList[col.key] === 'number') ? widths.membersList[col.key] : (config.DEFAULT_COLUMN_WIDTHS.membersList[col.key] || 8);
                            const wrapper = utils.createElement('div', { className: 'column-control', style: { display: 'flex', gap: '6px', alignItems: 'center' } });
                            const button = utils.createElement('button', {
                                className: `column-toggle-btn ${active}`,
                                dataset: { column: col.key, table: 'membersList' },
                                textContent: col.label,
                                onclick: () => {
                                    const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY) || {};
                                    if (!vis.membersList) vis.membersList = {};
                                    // Toggle relative to stored value or the default if not present
                                    const cur = (typeof vis.membersList[col.key] !== 'undefined')
                                        ? vis.membersList[col.key]
                                        : (config.DEFAULT_COLUMN_VISIBILITY?.membersList?.[col.key] ?? true);
                                    vis.membersList[col.key] = !cur;
                                    storage.set('columnVisibility', vis);
                                    ui.updateColumnVisibilityStyles();
                                    ui.updateSettingsContent();
                                }
                            });
                            // don't offer width adjustment for small icon columns (factionIcon)
                            let widthInput;
                            if (col.key !== 'factionIcon') {
                                widthInput = utils.createElement('input', { type: 'number', className: 'settings-input-display column-width-input', dataset: { column: col.key, table: 'membersList' }, min: '1', max: '99', step: '1', value: String(curW), title: 'Column width percentage' });
                                widthInput.style.width = '64px';
                                widthInput.addEventListener('change', (e) => {
                                try {
                                    const v = Math.max(1, Math.min(99, Math.round(Number(e.target.value) || 0)));
                                    const w = storage.get('columnWidths', config.DEFAULT_COLUMN_WIDTHS) || {};
                                    if (!w.membersList) w.membersList = {};
                                    w.membersList[col.key] = v;
                                    storage.set('columnWidths', w);
                                    ui.updateColumnVisibilityStyles();
                                    ui.updateSettingsContent();
                                } catch(_) {}
                                });
                            }
                            wrapper.appendChild(button);
                            if (widthInput) {
                                wrapper.appendChild(widthInput);
                                wrapper.appendChild(utils.createElement('div', { textContent: '%', style: { color: '#ccc', fontSize: '0.85em' } }));
                            }
                            if (col.key === 'factionIcon') {
                                // Defer appending factionIcon toggle - place below other members controls
                                deferredMLFactionWrapper = wrapper;
                            } else {
                                mlButtonsContainer.appendChild(wrapper);
                            }
                        }
                    });
                    if (deferredMLFactionWrapper) mlButtonsContainer.appendChild(deferredMLFactionWrapper);
                }
            }
            document.getElementById('reset-column-widths-btn')?.addEventListener('click', async () => {
                const ok = await ui.showConfirmationBox('Reset all column widths back to defaults?');
                if (!ok) return;
                try {
                    storage.set('columnWidths', config.DEFAULT_COLUMN_WIDTHS);
                    ui.updateColumnVisibilityStyles();
                    ui.updateSettingsContent();
                    ui.showMessageBox('Column widths reset to defaults.', 'info');
                } catch (e) { ui.showMessageBox('Failed to reset column widths.', 'error'); }
            });
            // Add recommended presets for PC and PDA after the reset button
            try {
                const resetBtn = document.getElementById('reset-column-widths-btn');
                if (resetBtn && !document.getElementById('apply-recommendation-pc')) {
                    const pcBtn = utils.createElement('button', { id: 'apply-recommendation-pc', className: 'settings-btn', style: { marginLeft: '8px' }, textContent: 'PC Rec', title: 'Apply recommended settings for PC (desktop) interfaces' });
                    const pdaBtn = utils.createElement('button', { id: 'apply-recommendation-pda', className: 'settings-btn', style: { marginLeft: '6px' }, textContent: 'PDA Rec', title: 'Apply recommended settings for PDA (mobile) interfaces' });
                    resetBtn.insertAdjacentElement('afterend', pdaBtn);
                    resetBtn.insertAdjacentElement('afterend', pcBtn);

                    pcBtn.addEventListener('click', async () => {
                        if (!(await ui.showConfirmationBox('Apply PC recommended column visibility and widths? This will overwrite current settings.'))) return;
                        try {
                            storage.set('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY);
                            storage.set('columnWidths', config.DEFAULT_COLUMN_WIDTHS);
                            ui.updateColumnVisibilityStyles();
                            ui.updateSettingsContent();
                            ui.showMessageBox('Applied PC recommended column settings.', 'success');
                        } catch (e) { ui.showMessageBox('Failed to apply PC recommended settings.', 'error'); }
                    });

                    pdaBtn.addEventListener('click', async () => {
                        if (!(await ui.showConfirmationBox('Apply PDA recommended column visibility and widths? This will overwrite current settings.'))) return;
                        try {
                            storage.set('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY_PDA);
                            storage.set('columnWidths', config.DEFAULT_COLUMN_WIDTHS_PDA);
                            ui.updateColumnVisibilityStyles();
                            ui.updateSettingsContent();
                            ui.showMessageBox('Applied PDA recommended column settings.', 'success');
                        } catch (e) { ui.showMessageBox('Failed to apply PDA recommended settings.', 'error'); }
                    });
                }
            } catch(_) { /* noop */ }
            document.getElementById('admin-functionality-btn')?.addEventListener('click', () => {
                storage.set('adminFunctionality', !storage.get('adminFunctionality', true));
                ui.updateSettingsContent();
            });
            // Timeline (legacy) handlers removed – replaced by unified activity tracking
            document.getElementById('save-faction-bundle-refresh-btn')?.addEventListener('click', () => {
                try {
                    const sec = Math.max(5, Math.round(Number(document.getElementById('faction-bundle-refresh-seconds').value || '0')));
                    const ms = sec * 1000;
                    storage.set('factionBundleRefreshMs', ms);
                    state.script.factionBundleRefreshMs = ms;
                    // Restart decoupled interval to apply new cadence
                    try {
                        if (state.script.factionBundleRefreshIntervalId) try { utils.unregisterInterval(state.script.factionBundleRefreshIntervalId); } catch(_) {}
                        state.script.factionBundleRefreshIntervalId = utils.registerInterval(setInterval(() => {
                            try { if ((state.script.isWindowActive !== false) || utils.isActivityKeepActiveEnabled()) api.refreshFactionBundles?.().catch(() => {}); } catch(_) {}
                        }, ms));
                    } catch(_) {}
                    ui.showMessageBox(`Faction bundle refresh set to ${sec}s.`);
                    ui.updateApiCadenceInfo?.({ force: true });
                } catch(_) { ui.showMessageBox('Invalid refresh interval.'); }
            });
            document.getElementById('tdm-save-additional-factions-btn')?.addEventListener('click', () => {
                const input = document.getElementById('tdm-additional-factions-input');
                if (!input) return;
                try {
                    const rawValue = String(input.value || '');
                    const meta = {};
                    const parsed = utils.parseFactionIdList(rawValue, meta);
                    const serialized = parsed.join(',');
                    if (serialized) storage.set('tdmExtraFactionPolls', serialized); else storage.remove('tdmExtraFactionPolls');
                    state.script.additionalFactionPolls = parsed;
                    input.value = serialized;
                    ui.updateApiCadenceInfo?.({ force: true });
                    try { api.refreshFactionBundles?.({ force: true, source: 'settings-extra-factions' }).catch(() => {}); } catch(_) { /* noop */ }

                    const notes = [];
                    if (meta.duplicateCount > 0) {
                        notes.push(`Removed ${meta.duplicateCount} duplicate id${meta.duplicateCount === 1 ? '' : 's'}.`);
                    }
                    if (meta.hadInvalid) {
                        const bad = Array.isArray(meta.invalidTokens) ? meta.invalidTokens.filter(Boolean) : [];
                        if (bad.length) {
                            notes.push(`Ignored invalid entries (${bad.join(', ')}). Only numbers and commas are allowed.`);
                        } else {
                            notes.push('Ignored invalid entries. Only numbers and commas are allowed.');
                        }
                    }
                    if (parsed.length) {
                        notes.push(`Extra faction polling saved (${parsed.length} id${parsed.length === 1 ? '' : 's'}).`);
                    } else {
                        notes.push('Extra faction polling cleared.');
                    }
                    const level = meta.hadInvalid ? 'warning' : 'info';
                    ui.showMessageBox(notes.join(' '), level);
                } catch(_) {
                    ui.showMessageBox('Failed to save extra faction list.', 'error');
                }
            });
            // Legacy purge buttons removed; replaced by single Flush Activity Cache
            document.getElementById('points-parse-logs-btn')?.addEventListener('click', () => {
                const cur = !!storage.get('debugPointsParseLogs', false);
                storage.set('debugPointsParseLogs', !cur);
                state.debug = state.debug || {}; state.debug.pointsParseLogs = !cur;
                ui.updateSettingsContent();
            });
            // Wire Dev: Log level selector (persisted)
            try {
                const logSelect = document.getElementById('tdm-log-level-select');
                if (logSelect) {
                    try { logSelect.value = storage.get('logLevel', 'warn') || 'warn'; } catch(_) {}
                    logSelect.addEventListener('change', (e) => {
                        try {
                            const v = e.target.value;
                            storage.set('logLevel', v);
                            ui.showMessageBox(`Log level set to ${v}`, 'info');
                        } catch(_) {}
                    });
                }
            } catch(_) {}
            document.getElementById('view-unauthorized-attacks-btn')?.addEventListener('click', () => ui.showUnauthorizedAttacksModal());
            document.getElementById('tdm-adoption-btn')?.addEventListener('click', () => {
                const cur = !!storage.get('debugAdoptionInfo', false);
                storage.set('debugAdoptionInfo', !cur);
                state.debug = state.debug || {}; state.debug.adoptionInfo = !cur;
                // Show info modal if turning on
                if (!cur) ui.showAdoptionInfo?.();
                ui.updateSettingsContent();
            });
            document.getElementById('attack-mode-save-btn')?.addEventListener('click', async (e) => {
                e?.preventDefault?.();
                const select = document.getElementById('attack-mode-select');
                if (!select) return;
                const next = select.value;
                const cur = (state.script.factionSettings?.options?.attackMode || state.script.factionSettings?.attackMode || 'Farming');
                if (next === cur) { ui.showMessageBox('Attack mode unchanged.', 'info'); return; }
                if (!(await ui.showConfirmationBox(`Set faction attack mode to ${next}?`))) return;
                const btnStart = document.getElementById('attack-mode-save-btn');
                if (btnStart) { btnStart.disabled = true; btnStart.textContent = 'Saving...'; }
                try {
                    const res = await api.post('updateAttackMode', { factionId: state.user.factionId, attackMode: next });
                    if (res?.settings) state.script.factionSettings = res.settings; else if (res?.attackMode) {
                        const fs = state.script.factionSettings || {}; state.script.factionSettings = { ...fs, options: { ...(fs.options||{}), attackMode: res.attackMode } };
                    }
                    // Normalize for readers that use either field
                    try {
                        const fsn = state.script.factionSettings || {};
                        state.script.factionSettings = { ...fsn, options: { ...(fsn.options||{}), attackMode: next }, attackMode: next };
                    } catch(_) { /* noop */ }
                    ui.showMessageBox(`Attack mode set to ${next}.`, 'success');
                    ui.ensureAttackModeBadge();
                    // Sync the header select if present
                    const headerSel = document.getElementById('tdm-attack-mode-select');
                    if (headerSel) headerSel.value = next;
                    ui.updateAllPages?.();
                } catch (err) {
                    ui.showMessageBox(`Failed to set mode: ${err.message || 'Unknown error'}`, 'error');
                } finally {
                    const btnEnd = document.getElementById('attack-mode-save-btn');
                    if (btnEnd) { btnEnd.disabled = false; btnEnd.textContent = 'Save'; }
                    ui.updateSettingsContent();
                }
            });
            // Unified status debug toggle
            document.getElementById('tdm-debug-unified-status')?.addEventListener('change', (e) => {
                const on = !!e.target.checked;
                storage.set('debugUnifiedStatus', on);
                ui.showMessageBox(`Unified status debug ${on ? 'enabled' : 'disabled'}.`, 'info');
            });
            // Disable legacy segments toggle (immediate effect; purging left to user)
            document.getElementById('tdm-disable-legacy-status')?.addEventListener('change', (e) => {
                const on = !!e.target.checked;
                storage.set('disableLegacyStatusSegments', on);
                ui.showMessageBox(`Legacy status segments ${on ? 'disabled' : 'enabled'}.`, 'info');
            });
            document.getElementById('run-admin-cleanup-btn')?.addEventListener('click', async (e) => {
                const reason = (document.getElementById('cleanup-reason')?.value || '').trim();
                if (!reason) { ui.showMessageBox('Reason is required.', 'error'); return; }
                const types = [];
                if (document.getElementById('cleanup-notes')?.checked) types.push('notes');
                if (document.getElementById('cleanup-dibs')?.checked) types.push('dibs');
                if (document.getElementById('cleanup-meddeals')?.checked) types.push('medDeals');
                if (types.length === 0) { ui.showMessageBox('Select at least one data type.', 'error'); return; }
                const factionOverrideRaw = (document.getElementById('cleanup-faction-id')?.value || '').trim();
                let factionOverride = null;
                if (factionOverrideRaw) {
                    if (!/^[0-9]+$/.test(factionOverrideRaw)) { ui.showMessageBox('Faction ID must be numeric.', 'error'); return; }
                    factionOverride = factionOverrideRaw;
                }
                // Opponent ID enumeration removed: backend interprets empty targetOpponentIds as FULL cleanup.
                const opponentIds = []; // always full-faction scope
                // Build friendly type list (human readable) and confirmation text.
                const humanMap = { notes: 'Notes', dibs: 'Dibs', medDeals: 'Med Deals' };
                const selectedTypes = types.slice();
                const typesHuman = selectedTypes.map(t => humanMap[t] || t).join(', ');
                const isAllTypes = selectedTypes.length === 3;
                const targetText = factionOverride ? `opponents in faction ${factionOverride}` : 'ALL opponents';
                let confirmText;
                if (isAllTypes && !factionOverride) {
                    confirmText = `Confirm FULL cleanup for ${targetText}? This cannot be undone.`;
                } else if (isAllTypes && factionOverride) {
                    confirmText = `Confirm cleanup of ALL data types for ${targetText}? This cannot be undone.`;
                } else {
                    confirmText = `Confirm cleanup of ${typesHuman} for ${targetText}? This cannot be undone.`;
                }
                const ok = await ui.showConfirmationBox(confirmText);
                if (!ok) return;
                const btn = (e && e.currentTarget) ? e.currentTarget : document.getElementById('run-admin-cleanup-btn');
                if (btn) { btn.disabled = true; btn.textContent = 'Cleaning...'; }
                try {
                    const res = await api.post('adminCleanupFactionData', {
                        factionId: state.user.factionId,
                        targetOpponentIds: opponentIds,
                        targetOpponentFactionId: factionOverride || null,
                        types,
                        reason,
                        debug: true // temporary: enable backend verbose logging; remove or gate by UI toggle later
                    });
                    const msg = `Cleanup done. Notes Deleted: ${res?.notesDeleted||0} | Dibs Deactivated: ${res?.dibsDeactivated||0} | Med Deals Unset: ${res?.medDealsRemoved||0}`;
                    tdmlogger('info', msg);
                    ui.showMessageBox(msg, 'success');
                    const line = document.getElementById('cleanup-results-line');
                    if (line) { line.textContent = msg; line.style.display = 'block'; }
                    handlers.debouncedFetchGlobalData?.();
                } catch (err) {
                    ui.showMessageBox(`Cleanup failed: ${err.message || 'Unknown error'}`, 'error');
                    const line = document.getElementById('cleanup-results-line');
                    if (line) { line.textContent = `Cleanup failed.`; line.style.display = 'block'; }
                } finally {
                    if (btn) { btn.disabled = false; btn.textContent = 'Run Cleanup'; }
                }
            });
            // Collapsible toggles: attach per-header listeners for reliable toggling
            try {
                const container = document.getElementById('tdm-settings-content');
                if (container) {
                    // Remove any delegated handler we might have set previously
                    try { if (window._tdm_settings_click_handler && typeof window._tdm_settings_click_handler === 'function') { container.removeEventListener('click', window._tdm_settings_click_handler); } } catch(_) {}
                    window._tdm_settings_click_handler = null;
                    // Attach a click handler to each header. Remove prior handlers if present to avoid duplicates.
                    const headers = container.querySelectorAll('.collapsible-header');
                    headers.forEach(h => {
                        try {
                            if (h._tdm_click_handler) h.removeEventListener('click', h._tdm_click_handler);
                        } catch(_) {}
                        const handler = (e) => {
                            try {
                                // Ignore clicks that originate on interactive controls inside the header
                                if (e.target && e.target.closest && e.target.closest('input,button,a,select,textarea,label')) return;
                                const sec = h.closest('.collapsible');
                                if (!sec) return;
                                const isCollapsed = sec.classList.toggle('collapsed');
                                const key = sec.getAttribute('data-section');
                                const stateMap = storage.get('settings_collapsed', {});
                                stateMap[key] = isCollapsed;
                                storage.set('settings_collapsed', stateMap);
                            } catch(_) {}
                        };
                        h._tdm_click_handler = handler;
                        h.addEventListener('click', handler);
                    });
                }
            } catch(_) {}
            // Attach event listeners
            document.getElementById('chain-timer-btn')?.addEventListener('click', () => {
                const cur = storage.get('chainTimerEnabled', true);
                storage.set('chainTimerEnabled', !cur);
                if (!cur) ui.ensureChainTimer(); else ui.removeChainTimer();
                ui.updateSettingsContent();
            });
            document.getElementById('inactivity-timer-btn')?.addEventListener('click', () => {
                const cur = storage.get('inactivityTimerEnabled', false);
                storage.set('inactivityTimerEnabled', !cur);
                if (!cur) ui.ensureInactivityTimer(); else ui.removeInactivityTimer();
                ui.updateSettingsContent();
            });
            document.getElementById('opponent-status-btn')?.addEventListener('click', () => {
                const cur = storage.get('opponentStatusTimerEnabled', true);
                storage.set('opponentStatusTimerEnabled', !cur);
                if (!cur) ui.ensureOpponentStatus(); else ui.removeOpponentStatus();
                ui.updateSettingsContent();
            });
            document.getElementById('api-usage-btn')?.addEventListener('click', () => {
                const cur = storage.get('apiUsageCounterEnabled', false);
                storage.set('apiUsageCounterEnabled', !cur);
                if (!cur) ui.ensureApiUsageBadge();
                if (handlers?.debouncedUpdateApiUsageBadge) { handlers.debouncedUpdateApiUsageBadge(); } else { ui.updateApiUsageBadge(); }
                ui.updateSettingsContent();
            });
            document.getElementById('attack-mode-badge-btn')?.addEventListener('click', () => {
                const cur = storage.get('attackModeBadgeEnabled', true);
                storage.set('attackModeBadgeEnabled', !cur);
                if (!cur) ui.ensureAttackModeBadge(); else ui.removeAttackModeBadge?.();
                ui.updateSettingsContent();
            });
            document.getElementById('chainwatcher-badge-btn')?.addEventListener('click', () => {
                const cur = storage.get('chainWatcherBadgeEnabled', true);
                storage.set('chainWatcherBadgeEnabled', !cur);
                if (!cur) ui.ensureChainWatcherBadge(); else ui.removeChainWatcherBadge?.();
                ui.updateSettingsContent();
            });
            document.getElementById('alert-buttons-toggle-btn')?.addEventListener('click', () => {
                const cur = storage.get('alertButtonsEnabled', true);
                storage.set('alertButtonsEnabled', !cur);
                // Observer will respect this; force a UI refresh
                ui.updateAllPages?.();
                ui.updateSettingsContent();
            });
            document.getElementById('user-score-badge-btn')?.addEventListener('click', () => {
                const cur = storage.get('userScoreBadgeEnabled', true);
                storage.set('userScoreBadgeEnabled', !cur);
                if (!cur) ui.ensureUserScoreBadge(); else ui.removeUserScoreBadge?.();
                ui.updateUserScoreBadge?.();
                ui.updateSettingsContent();
            });
            // Removed: refreshing shimmer toggle and related CSS injection
            document.getElementById('faction-score-badge-btn')?.addEventListener('click', () => {
                const cur = storage.get('factionScoreBadgeEnabled', true);
                storage.set('factionScoreBadgeEnabled', !cur);
                if (!cur) ui.ensureFactionScoreBadge(); else ui.removeFactionScoreBadge?.();
                ui.updateFactionScoreBadge?.();
                ui.updateSettingsContent();
            });
            document.getElementById('dibs-deals-badge-btn')?.addEventListener('click', () => {
                const cur = storage.get('dibsDealsBadgeEnabled', true);
                storage.set('dibsDealsBadgeEnabled', !cur);
                if (!cur) ui.ensureDibsDealsBadge(); else ui.removeDibsDealsBadge?.();
                ui.updateDibsDealsBadge?.();
                ui.updateSettingsContent();
            });
            document.getElementById('paste-messages-chat-btn')?.addEventListener('click', () => {
                const cur = storage.get('pasteMessagesToChatEnabled', true);
                storage.set('pasteMessagesToChatEnabled', !cur);
                ui.updateSettingsContent();
            });
            document.getElementById('oc-reminder-btn')?.addEventListener('click', () => {
                const currentState = storage.get('ocReminderEnabled', true);
                storage.set('ocReminderEnabled', !currentState);
                ui.updateSettingsContent(); // Re-render settings
            });
            document.getElementById('verbose-row-logs-btn')?.addEventListener('click', () => {
                const current = storage.get('debugRowLogs', false);
                storage.set('debugRowLogs', !current);
                if (!state.debug) state.debug = {};
                state.debug.rowLogs = !current;
                // Small toast to indicate effect without flooding console
                ui.showMessageBox(`Verbose Row Logs ${!current ? 'Enabled' : 'Disabled'}`, !current ? 'success' : 'info', 2000);
                ui.updateSettingsContent();
            });
            document.getElementById('status-watch-btn')?.addEventListener('click', () => {
                const current = storage.get('debugStatusWatch', false);
                storage.set('debugStatusWatch', !current);
                if (!state.debug) state.debug = {};
                state.debug.statusWatch = !current;
                ui.showMessageBox(`Status Watch Logs ${!current ? 'Enabled' : 'Disabled'}`,'success',1500);
                ui.updateSettingsContent();
            });
            document.getElementById('reset-api-counters-btn')?.addEventListener('click', async () => {
                if (!(await ui.showConfirmationBox('Reset API counters for this tab/session?'))) return;
                try {
                    sessionStorage.removeItem('api_calls');
                    sessionStorage.removeItem('api_calls_client');
                    sessionStorage.removeItem('api_calls_backend');
                } catch(_) {}
                try {
                    state.session.apiCalls = 0;
                    state.session.apiCallsClient = 0;
                    state.session.apiCallsBackend = 0;
                } catch(_) {}
                if (handlers?.debouncedUpdateApiUsageBadge) { handlers.debouncedUpdateApiUsageBadge(); } else { ui.updateApiUsageBadge?.(); }
                ui.showMessageBox('API counters reset.', 'success', 1500);
            });
            document.getElementById('reset-settings-btn')?.addEventListener('click', async () => {
                // Custom confirmation with Normal / Factory / Cancel buttons
                const modal = ui.showMessageBox ? null : null; // placeholder to satisfy linter if showMessageBox tree-shaken
                const wrapper = document.createElement('div');
                wrapper.style.padding='4px 2px';
                wrapper.innerHTML = `
                <div style="font-size:12px;line-height:1.5;color:#e2e8f0;max-width:480px;">
                    <strong>Reset TreeDibsMapper Data</strong><br><br>
                    <div style="margin-bottom:6px;">
                    <span style="color:#93c5fd;">Normal Reset</span> – Purges caches, history, IndexedDB, unified status. Preserves API key, column settings.
                    </div>
                    <div style="margin-bottom:6px;">
                    <span style="color:#fca5a5;">Factory Reset</span> – Everything above <em>plus</em> removes API key, IndexedDB, and all TDM localStorage. Script reload acts like first install.
                    </div>
                    <div style="margin-bottom:8px;opacity:.8;">Choose reset scope:</div>
                    <div style="display:flex;gap:8px;flex-wrap:wrap;">
                    <button id="tdm-reset-normal" class="settings-btn" style="flex:1;min-width:120px;">Normal Reset</button>
                    <button id="tdm-reset-factory" class="settings-btn settings-btn-red" style="flex:1;min-width:120px;">Factory Reset</button>
                    <button id="tdm-reset-cancel" class="settings-btn" style="flex:1;min-width:90px;">Cancel</button>
                    </div>
                </div>`;
                const host = document.createElement('div');
                host.style.position='fixed'; host.style.top='50%'; host.style.left='50%'; host.style.transform='translate(-50%,-50%)';
                host.style.background='#111827'; host.style.border='1px solid #1f2937'; host.style.padding='14px 16px'; host.style.borderRadius='8px'; host.style.zIndex='10050'; host.style.boxShadow='0 10px 24px -4px rgba(0,0,0,.55)';
                host.appendChild(wrapper);
                document.body.appendChild(host);
                const closeHost = ()=>{ try { host.remove(); } catch(_) {} };
                const run = async (factory=false) => {
                    closeHost();
                    try { await handlers.performHardReset({ factory }); } catch(e) { tdmlogger('error', '[Reset] error', e); }
                    setTimeout(()=>location.reload(), 1200);
                };
                host.querySelector('#tdm-reset-normal')?.addEventListener('click', ()=> run(false));
                host.querySelector('#tdm-reset-factory')?.addEventListener('click', ()=> run(true));
                host.querySelector('#tdm-reset-cancel')?.addEventListener('click', closeHost);
            });
            document.getElementById('tdm-adoption-btn')?.addEventListener('click', ui.showTDMAdoptionModal);

            document.getElementById('copy-dibs-style-btn')?.addEventListener('click', async () => {
                const summary = buildDibsStyleSummary();
                await copyTextToClipboard(summary, 'Dibs style');
            });

            // Dibs Style save
            const saveDibsBtn = document.getElementById('save-dibs-style-btn');
            if (saveDibsBtn) {
                saveDibsBtn.addEventListener('click', async (e) => {
                    const btn = e.currentTarget;
                    btn.disabled = true; btn.innerHTML = '<span class="dibs-spinner"></span> Saving...';
                    try {
                        const allowStatuses = {};
                        document.querySelectorAll('.dibs-allow-status').forEach(cb => {
                            allowStatuses[cb.dataset.status] = cb.checked;
                        });
                        const allowLastActionStatuses = {};
                        document.querySelectorAll('.dibs-allow-lastaction').forEach(cb => {
                            allowLastActionStatuses[cb.dataset.status] = cb.checked;
                        });
                        const allowedUserStatuses = {};
                        document.querySelectorAll('.dibs-allow-user-status').forEach(cb => {
                            allowedUserStatuses[cb.dataset.status] = cb.checked;
                        });
                        const maxHospMinsRaw = document.getElementById('dibs-max-hosp-minutes')?.value;
                        const maxHospMins = maxHospMinsRaw === '' ? 0 : Math.max(0, parseInt(maxHospMinsRaw));
                        const payload = {
                            options: {
                                dibsStyle: {
                                    keepTillInactive: document.getElementById('dibs-keep-inactive').checked,
                                    mustRedibAfterSuccess: document.getElementById('dibs-redib-after-success').checked,
                                    bypassDibStyle: document.getElementById('dibs-bypass-style')?.checked === true,
                                    removeOnFly: document.getElementById('dibs-remove-on-fly').checked,
                                    removeWhenUserTravels: document.getElementById('dibs-remove-user-travel')?.checked || false,
                                    inactivityTimeoutSeconds: parseInt(document.getElementById('dibs-inactivity-seconds').value) || 300,
                                    maxHospitalReleaseMinutes: maxHospMins,
                                    allowStatuses,
                                    allowLastActionStatuses,
                                    allowedUserStatuses
                                }
                            }
                        };
                        const res = await api.post('updateFactionSettings', { factionId: state.user.factionId, ...payload });
                        state.script.factionSettings = res?.settings || state.script.factionSettings;
                        ui.showMessageBox('Dibs Style saved.', 'success');
                        // Immediately reflect new rules in UI without page reload
                        ui.updateAllPages();
                        handlers.debouncedFetchGlobalData?.();
                    } catch (err) {
                        ui.showMessageBox(`Failed to save Dibs Style: ${err.message || 'Unknown error'}`, 'error');
                    } finally {
                        btn.disabled = false; btn.textContent = 'Save Dibs Style';
                        ui.updateSettingsContent();
                    }
                });
            }
            
            const rankedWarSelect = document.getElementById('ranked-war-id-select');
            const showRankedWarSummaryBtn = document.getElementById('show-ranked-war-summary-btn');
            const viewWarAttacksBtn = document.getElementById('view-war-attacks-btn');
            // Note tag presets elements
            const noteTagsInput = document.getElementById('note-tags-input');
            const noteTagsPreview = document.getElementById('note-tags-preview');
            const noteTagsSaveBtn = document.getElementById('note-tags-save-btn');
            const noteTagsResetBtn = document.getElementById('note-tags-reset-btn');
            const noteTagsStatus = document.getElementById('note-tags-status');

            const renderNoteTagsPreview = () => {
                if (!noteTagsPreview || !noteTagsInput) return;
                noteTagsPreview.innerHTML='';
                const raw = (noteTagsInput.value||'').trim();
                const tags = raw.split(/[\s,]+/).filter(Boolean).map(t=>t.slice(0,24)).map(t=>t.replace(/[^a-z0-9_\-]/gi,''));
                const uniq=[]; const seen=new Set();
                for (const t of tags) { if (!t) continue; const l=t.toLowerCase(); if (seen.has(l)) continue; seen.add(l); uniq.push(t); if (uniq.length>=12) break; }
                if (uniq.length===0) { noteTagsPreview.appendChild(utils.createElement('div',{ style:{fontSize:'11px',color:'#666'}, textContent:'No valid tags'})); }
                uniq.forEach(t=>{ noteTagsPreview.appendChild(utils.createElement('span',{ textContent:'#'+t, style:{ background:'#333', padding:'2px 6px', borderRadius:'4px', fontSize:'11px', color:'#ddd' }})); });
            };
            if (noteTagsInput) { noteTagsInput.addEventListener('input', utils.debounce(renderNoteTagsPreview, 120)); renderNoteTagsPreview(); }
            const saveNoteTags = () => {
                if (!noteTagsInput || !noteTagsStatus) return;
                const raw = (noteTagsInput.value||'').trim();
                const tags = raw.split(/[\s,]+/).filter(Boolean).map(t=>t.slice(0,24)).map(t=>t.replace(/[^a-z0-9_\-]/gi,''));
                const uniq=[]; const seen=new Set();
                for (const t of tags) { if (!t) continue; const l=t.toLowerCase(); if (seen.has(l)) continue; seen.add(l); uniq.push(t); if (uniq.length>=12) break; }
                storage.set('tdmNoteQuickTags', uniq.join(','));
                noteTagsStatus.textContent = 'Saved'; noteTagsStatus.style.color = '#10b981';
                setTimeout(()=>{ noteTagsStatus.textContent='Idle'; noteTagsStatus.style.color='#9ca3af'; }, 1800);
                renderNoteTagsPreview();
            };
            noteTagsSaveBtn?.addEventListener('click', saveNoteTags);
            noteTagsResetBtn?.addEventListener('click', ()=>{ if (!noteTagsInput) return; noteTagsInput.value='dex+,def+,str+,spd+,hosp,retal'; saveNoteTags(); });

            // ChainWatcher settings wiring
            const chainSelect = document.getElementById('tdm-chainwatcher-select');
            const chainSaveBtn = document.getElementById('tdm-chainwatcher-save');
            const chainClearBtn = document.getElementById('tdm-chainwatcher-clear');
            const isTouchDevice = () => ('ontouchstart' in window) || navigator.maxTouchPoints > 0 || window.matchMedia && window.matchMedia('(pointer:coarse)').matches;

            const renderChainCheckboxList = (members, preselectedIds) => {
                // Create a scrollable list of checkboxes for mobile-friendly selection
                let container = document.getElementById('tdm-chainwatcher-checkbox-list');
                if (!container) {
                    container = document.createElement('div');
                    container.id = 'tdm-chainwatcher-checkbox-list';
                    container.style.maxHeight = '160px';
                    container.style.overflowY = 'auto';
                    container.style.padding = '6px';
                    container.style.border = '1px solid #444';
                    container.style.background = '#111';
                    container.style.marginTop = '6px';
                    chainSelect.parentElement?.appendChild(container);
                }
                container.innerHTML = '';
                for (const m of members) {
                    const row = document.createElement('label');
                    row.style.display = 'block';
                    row.style.padding = '6px';
                    row.style.cursor = 'pointer';
                    row.style.color = '#ddd';
                    const cb = document.createElement('input');
                    cb.type = 'checkbox';
                    cb.value = String(m.id);
                    // store canonical member name on the input so we don't accidentally include status suffixes
                    try { cb.dataset.memberName = String(m.name || ''); } catch(_) {}
                    cb.style.marginRight = '8px';
                    if (preselectedIds.has(String(m.id))) cb.checked = true;
                    // Name span
                        const nameSpan = document.createElement('span');
                        nameSpan.className = 'tdm-chainwatcher-name';
                        nameSpan.textContent = `${m.name} [${m.id}]`;
                        nameSpan.style.marginRight = '6px';
                        nameSpan.dataset.memberId = String(m.id);
                    // Status span (dynamic)
                        const statusSpan = document.createElement('span');
                        statusSpan.className = 'tdm-chainwatcher-status tdm-last-action-inline';
                        statusSpan.dataset.memberId = String(m.id);
                        // Start without textual status; coloring is applied to nameSpan instead
                        statusSpan.textContent = '';

                        row.appendChild(cb);
                        row.appendChild(nameSpan);
                        row.appendChild(statusSpan);
                        // Apply initial coloring to the name span based on last_action.status
                        try { utils.addLastActionStatusColor && utils.addLastActionStatusColor(nameSpan, m); } catch(_) {}
                    container.appendChild(row);
                }
                return container;
            };

            // Retry counter to handle cases where factionMembers aren't loaded yet
            let _tdm_chainPopulateRetries = 0;
            const populateChainSelect = () => {
                if (!chainSelect) return;
                chainSelect.innerHTML = '';
                // Use factionMembers if available
                const members = Array.isArray(state.factionMembers) ? state.factionMembers : [];
                // Sort alphabetically (case-insensitive), but keep current user on top
                const sorted = [...members].filter(m => m && m.id && m.name).sort((a, b) => {
                    const idA = String(a.id);
                    const idB = String(b.id);
                    if (idA === String(state.user.tornId)) return -1;
                    if (idB === String(state.user.tornId)) return 1;
                    return (a.name || '').toLowerCase().localeCompare((b.name || '').toLowerCase());
                });

                // If we have no members yet (fresh reset/cache cleared), try to fetch and retry a few times
                if ((!Array.isArray(members) || members.length === 0) && _tdm_chainPopulateRetries < 5) {
                    _tdm_chainPopulateRetries++;
                    // Try to trigger a fetch of faction members if available
                    try { if (handlers && typeof handlers.fetchFactionMembers === 'function') handlers.fetchFactionMembers(); } catch(_) {}
                    // Schedule a retry after a short delay to allow fetch to populate state.factionMembers
                    setTimeout(() => { try { populateChainSelect(); } catch(_) {} }, 700 + (_tdm_chainPopulateRetries * 200));
                    // Clear any existing checkbox list to avoid showing an empty box
                    const existingBox = document.getElementById('tdm-chainwatcher-checkbox-list'); if (existingBox) existingBox.remove();
                    // Keep native select hidden until we populate
                    chainSelect.style.display = 'none';
                    return;
                }

                // Build options for desktop multi-select
                for (const m of sorted) {
                    const opt = document.createElement('option');
                    opt.value = String(m.id);
                    try { opt.dataset.memberName = String(m.name || ''); } catch(_) {}
                    const statusText = (m && m.last_action && m.last_action.status) ? String(m.last_action.status) : '';
                    opt.textContent = statusText ? `${m.name} [${m.id}] - ${statusText}` : `${m.name} [${m.id}]`;
                    chainSelect.appendChild(opt);
                }

                // Load stored values (authoritative server will overwrite on next fetch)
                let preselected = new Set();
                try {
                    const stored = storage.get('chainWatchers', []);
                    if (Array.isArray(stored) && stored.length) preselected = new Set(stored.map(s => String(s.id || s)));
                } catch(_) { preselected = new Set(); }

                // Always render the checkbox list for easier multi-selection on all devices
                // Remove existing checkbox list if any
                const existing = document.getElementById('tdm-chainwatcher-checkbox-list');
                if (existing) existing.remove();
                renderChainCheckboxList(sorted, preselected);
                // Hide the native select to avoid confusion
                chainSelect.style.display = 'none';
            };
            // Initial populate
            populateChainSelect();
                // Also repopulate when factionMembers update and refresh displayed statuses
                try {
                    handlers.addObserver && handlers.addObserver('factionMembers', populateChainSelect);
                    handlers.addObserver && handlers.addObserver('factionMembers', ui.updateChainWatcherBadge);
                    handlers.addObserver && handlers.addObserver('factionMembers', ui.updateChainWatcherDisplayedStatuses);
                } catch(_) {}

            chainSaveBtn?.addEventListener('click', async () => {
                if (!chainSelect) return;
                // Collect selections from either checkbox list (mobile) or native multi-select
                let selected = [];
                const checkboxContainer = document.getElementById('tdm-chainwatcher-checkbox-list');
                if (checkboxContainer) {
                    const checks = checkboxContainer.querySelectorAll('input[type="checkbox"]');
                    for (const cb of checks) if (cb.checked) {
                        const name = cb.dataset.memberName || cb.parentElement?.querySelector('.tdm-chainwatcher-name')?.textContent?.split(' [')[0]?.trim() || String(cb.value);
                        selected.push({ id: String(cb.value), name });
                    }
                } else {
                    selected = Array.from(chainSelect.selectedOptions || []).map(o => {
                        const name = o.dataset.memberName || (o.textContent || '').split(' - ')[0].split(' [')[0].trim();
                        return { id: String(o.value), name };
                    });
                }
                if (selected.length > 3) { ui.showMessageBox('Select at most 3 watchers.', 'error'); return; }
                // Persist to backend so selection is shared across faction
                try {
                    const res = await api.post('setChainWatchers', { factionId: state.user.factionId, watchers: selected });
                    // Accept either shape: { ok: true, watchers, meta } OR { status:'success', data: { watchers, meta } }
                    let remoteWatchers = null; let remoteMeta = null; let ok = false;
                    if (res && (res.ok === true || res.status === 'success')) {
                        ok = true;
                        if (res.data && res.data.watchers) remoteWatchers = res.data.watchers;
                        if (res.data && ('meta' in res.data)) remoteMeta = res.data.meta;
                        if (!remoteWatchers && Array.isArray(res.watchers)) remoteWatchers = res.watchers;
                        if (!remoteMeta && ('meta' in res)) remoteMeta = res.meta;
                    }
                    if (ok && Array.isArray(remoteWatchers)) {
                        storage.set('chainWatchers', remoteWatchers);
                        try { storage.set('chainWatchers_meta', remoteMeta || null); } catch(_) {}
                        ui.ensureChainWatcherBadge();
                        ui.updateChainWatcherMeta && ui.updateChainWatcherMeta(remoteMeta || null);
                        ui.showMessageBox('ChainWatcher saved for faction.', 'success');
                        // Update in-modal selections to match authoritative list
                        populateChainSelect();
                    } else {
                        ui.showMessageBox('Unexpected backend response; please refresh to see authoritative state.', 'warn');
                    }
                } catch (err) {
                    ui.showMessageBox('Failed to save on server; please try again.', 'error');
                }
            });
            chainClearBtn?.addEventListener('click', async () => {
                try {
                    const res = await api.post('setChainWatchers', { factionId: state.user.factionId, watchers: [] });
                    // Accept both shapes
                    const ok = !!(res && (res.ok === true || res.status === 'success'));
                    if (ok) {
                        storage.set('chainWatchers', []);
                        try { storage.set('chainWatchers_meta', null); } catch(_) {}
                        // Unselect options and remove checkboxes
                        if (chainSelect) Array.from(chainSelect.options).forEach(o=>o.selected=false);
                        const existing = document.getElementById('tdm-chainwatcher-checkbox-list'); if (existing) existing.querySelectorAll('input[type="checkbox"]').forEach(cb=>cb.checked=false);
                        ui.updateChainWatcherBadge();
                        ui.updateChainWatcherMeta && ui.updateChainWatcherMeta(null);
                        ui.showMessageBox('ChainWatcher cleared.', 'info');
                    } else {
                        ui.showMessageBox('Failed to clear ChainWatcher on server.', 'error');
                    }
                } catch(e) {
                    ui.showMessageBox('Failed to clear ChainWatcher on server.', 'error');
                }
            });

            // Ensure badge exists if stored selections present
            try { ui.ensureChainWatcherBadge(); } catch(_) {}

            if (rankedWarSelect && showRankedWarSummaryBtn && viewWarAttacksBtn) {
                showRankedWarSummaryBtn.disabled = true;
                viewWarAttacksBtn.disabled = true;
                // Dev button for forcing backend war attacks refresh (only show when debug enabled)
                try {
                    if (state.debug?.apiLogs && !document.getElementById('force-backend-war-attacks-refresh-btn')) {
                        const devBtn = utils.createElement('button', {
                            id: 'force-backend-war-attacks-refresh-btn',
                            className: 'settings-btn settings-btn-yellow',
                            textContent: 'Force Backend Attacks Refresh',
                            title: 'Force backend to pull latest ranked war attacks manifest and chunks, then refresh local cache.'
                        });
                        devBtn.addEventListener('click', async () => {
                            const warId = rankedWarSelect.value;
                            if (!warId) { ui.showMessageBox('Select a war first.', 'error'); return; }
                            devBtn.disabled = true; const old = devBtn.textContent; devBtn.textContent = 'Refreshing...';
                            try {
                                const ok = await api.forceBackendWarAttacksRefresh(warId, state.user.factionId);
                                if (ok) {
                                    ui.showTransientMessage('Backend refresh triggered; refetching manifest in 2s...', { type: 'success' });
                                    setTimeout(async ()=>{
                                        try { await api.fetchWarManifestV2(warId, state.user.factionId, { force: true }); await api.assembleAttacksFromV2(warId, state.user.factionId, { forceWindowBootstrap: false }); } catch(_) {}
                                    }, 2000);
                                } else {
                                    ui.showTransientMessage('Backend refresh callable unavailable.', { type: 'error' });
                                }
                            } catch(e){ ui.showMessageBox('Refresh failed: '+(e.message||e), 'error'); }
                            finally { devBtn.disabled=false; devBtn.textContent=old; }
                        });
                        // Insert near existing war buttons
                        viewWarAttacksBtn.parentElement?.insertBefore(devBtn, viewWarAttacksBtn.nextSibling);
                        // (Removed) escalate truncated attacks button
                    }
                } catch(_) { /* noop */ }

                const prefetchRankedWars = async () => {
                    perf?.start?.('ui.updateSettingsContent.loadRankedWars');
                    try {
                        showRankedWarSummaryBtn.disabled = true;
                        viewWarAttacksBtn.disabled = true;
                        rankedWarSelect.innerHTML = '<option value="">Loading...</option>';

                        let rankedWars = Array.isArray(state.rankWars) ? state.rankWars : [];
                        if (rankedWars.length === 0) {
                            // Try direct Torn fetch if cache is empty (respects 1-hour init fetch, but as a fallback)
                            try {
                                const warsUrl = `https://api.torn.com/v2/faction/rankedwars?key=${state.user.actualTornApiKey}&comment=TDM_FEgRW2&timestamp=${Math.floor(Date.now()/1000)}`;
                                const warsResp = await fetch(warsUrl);
                                const warsData = await warsResp.json();
                                utils.incrementClientApiCalls(1);
                                if (!warsData.error) {
                                    const list = Array.isArray(warsData.rankedwars) ? warsData.rankedwars : (Array.isArray(warsData) ? warsData : []);
                                    rankedWars = list;
                                    storage.updateStateAndStorage('rankWars', rankedWars);
                                }
                            } catch (_) { /* ignore */ }
                        }
                        if (Array.isArray(rankedWars) && rankedWars.length > 0) {
                            rankedWarSelect.innerHTML = '';
                            rankedWars.forEach(war => {
                                if (war && war.id && war.factions) {
                                    const opponentFaction = Object.values(war.factions).find(f => f.id !== parseInt(state.user.factionId));
                                    const opponentName = opponentFaction ? opponentFaction.name : 'Unknown';
                                    const option = utils.createElement('option', { value: war.id, textContent: `${war.id} - ${opponentName}` });
                                    rankedWarSelect.appendChild(option);
                                }
                            });
                            showRankedWarSummaryBtn.disabled = false;
                            viewWarAttacksBtn.disabled = false;
                        } else {
                            rankedWarSelect.innerHTML = '<option value="">No ranked wars found</option>';
                        }
                    } catch (error) {
                        tdmlogger('error', `[Error loading ranked wars into settings panel] ${error}`);
                        rankedWarSelect.innerHTML = '<option value="">Error loading wars</option>';
                    } finally {
                        perf?.stop?.('ui.updateSettingsContent.loadRankedWars');
                    }
                };
                runRankedWarPrefetch = prefetchRankedWars;
                prefetchRankedWars();

                showRankedWarSummaryBtn.addEventListener('click', async () => {
                    const selectedWarId = rankedWarSelect.value;
                    if (!selectedWarId) { ui.showMessageBox('Please select a ranked war first', 'error'); return; }

                    showRankedWarSummaryBtn.disabled = true;
                    showRankedWarSummaryBtn.innerHTML = '<span class="dibs-spinner"></span>';

                    try {
                        // Use the freshest source (local vs server) - getRankedWarSummaryFreshest handles smart fallback
                        const summary = await api.getRankedWarSummaryFreshest(selectedWarId, state.user.factionId);
                        ui.showRankedWarSummaryModal(summary, selectedWarId);
                    } catch (error) {
                        ui.showMessageBox(`Error fetching war summary: ${error.message || 'Unknown error'}`, 'error');
                        tdmlogger('error', `[War Summary Error] ${error}`);
                    } finally {
                        showRankedWarSummaryBtn.disabled = false;
                        showRankedWarSummaryBtn.textContent = 'War Summary';
                    }
                });

                viewWarAttacksBtn.addEventListener('click', async () => {
                    const selectedWarId = rankedWarSelect.value;
                    if (!selectedWarId) { ui.showMessageBox('Please select a ranked war first', 'error'); return; }

                    viewWarAttacksBtn.disabled = true;
                    viewWarAttacksBtn.innerHTML = '<span class="dibs-spinner"></span>';
                    try {
                        // First, tell the backend to update the raw attack logs from the API
                        await api.post('updateRankedWarAttacks', { rankedWarId: selectedWarId, factionId: state.user.factionId });
                        // Then, show the modal which reads that raw data from Firestore
                        ui.showCurrentWarAttacksModal(selectedWarId);
                    } catch (error) {
                        ui.showMessageBox(`Error preparing war attacks: ${error.message || 'Unknown error'}`, 'error');
                        tdmlogger('error', `[War Attacks Error] ${error}`);
                    } finally {
                        viewWarAttacksBtn.disabled = false;
                        viewWarAttacksBtn.textContent = 'War Attacks';
                    }
                });
            }
            // Sanitize native tooltips on touch devices to prevent sticky titles (PDA)
            try { ui._sanitizeTouchTooltips(content); } catch(_) {}
            } catch (error) {
                tdmlogger('error', '[Settings UI] updateSettingsContent failed', error);
                try {
                    const fallbackContent = document.getElementById('tdm-settings-content');
                    if (fallbackContent) {
                        fallbackContent.innerHTML = `<div style="padding:12px;background:#1f2937;border:1px solid #ef4444;border-radius:6px;color:#f87171;font-size:12px;">Failed to render settings panel. Check console for details.</div>`;
                    }
                } catch(_) {/* noop */}
            } finally {
                if (!renderPhaseComplete) perf?.stop?.('ui.updateSettingsContent.render');
                if (bindPhaseStarted) perf?.stop?.('ui.updateSettingsContent.bind');
                perf?.stop?.('ui.updateSettingsContent.total');
                tdmlogger('info', `[Perf] ui.updateSettingsContent: ${perf?.toString?.() || 'no perf data'}`);
            }
        },

        applyGeneralStyles: () => {
            if (document.getElementById('dibs-general-styles')) return;
            const styleTag = utils.createElement('style', {
                type: 'text/css',
                id: 'dibs-general-styles',
                textContent: `
                    /* userInfoWrap honorWrap*/
                    .honorWrap___BHau4, .members-cont a a > div  { margin-left: 1px !important; margin-right: 1px !important; }
                    /* status cells */
                    .table-body .table-cell .status, .members-list .status { line-height: 1.2 !important; padding: 2px 4px !important; margin: 1px !important; }
                    /* Level Cell Indicators */
                    .tdm-level-cell { position: relative; }
                    .tdm-level-indicator { position: absolute; top: 1px; right: 1px; font-size: 7px; opacity: 0.6; line-height: 1; pointer-events: none; font-weight: normal; color: inherit; }

                    /* Hide FF Scouter V2 columns to avoid redundancy */
                    .ff-scouter-ff-visible, .ff-scouter-est-visible, .ff-scouter-ff-hidden, .ff-scouter-est-hidden { display: none !important; }

                    /* Compact last-action tag in status column */
                    /* Hide legacy FF Scouter last-action row to prevent layout shifts; inline renderer handles display */
                    li[data-last-action] .last-action-row{display:none !important;}
                    /* Force 386px for ranked war tables */
                    .d .f-war-list.war-new .faction-war .tab-menu-cont{min-width:385px;}

                    /* Inline last-action placed within our subrow */
                    .tdm-last-action-inline{font-size:10px;line-height:1.1;opacity:.8;margin-top:0;margin-left:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
                    .tdm-fav-heart{color:#94a3b8;font-size:18px;line-height:1;margin-right:6px;cursor:pointer;background:transparent;border:none;padding:0;transition:color .12s ease,transform .12s ease;}
                    .tdm-fav-heart:hover{color:#fcd34d;transform:scale(1.05);}
                    .tdm-fav-heart--active{color:rgba(240, 4, 4, 0.98);}
                    .tdm-favorite-row{background:rgba(250,204,21,0.08)!important;}
                    .tdm-travel-eta{font-size:10px;line-height:1.1;opacity:.85;margin-left:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
                    .tdm-travel-eta.tdm-travel-conf{font-weight:600;opacity:.95;}
                    .tdm-travel-eta.tdm-travel-lowconf{opacity:.6;}
                    /* Travel icon next to last-action */
                    .tdm-la-online{color:#82C91E;}
                    .tdm-la-idle{color:#f59e0b;}
                    .tdm-la-offline{color:#9ca3af;}

                    /* Layout and icon styles for our dibs/notes subrow */
                    .dibs-notes-subrow{display:flex;align-items:center;gap:6px;margin-top:2px;max-width:100%;flex-wrap:wrap;overflow:visible;}
                    .dibs-notes-subrow .note-button.btn{background:transparent !important;border:none !important;padding:0 2px !important;min-width:auto !important;width:24px;height:20px;display:inline-flex;align-items:center;justify-content:center;}
                    .dibs-notes-subrow .note-button.btn svg{width:18px;height:18px;stroke:${config.CSS.colors.noteInactive}; background-color: #7ae7f3a4;}
                    .dibs-notes-subrow .note-button.btn.inactive-note-button svg{stroke:${config.CSS.colors.noteInactive}; background-color: #7ae7f3a4;}
                    .dibs-notes-subrow .note-button.btn.inactive-note-button:hover svg{stroke:${config.CSS.colors.noteInactiveHover}; background-color: #7ae7f3a4;}
                    .dibs-notes-subrow .note-button.btn.active-note-button svg{stroke:${config.CSS.colors.noteActive};background-color: #019cf6af;}
                    .dibs-notes-subrow .note-button.btn.active-note-button:hover svg{stroke:${config.CSS.colors.noteActiveHover};background-color: #0078bdaf;}

                    /* --- Main Controls Container --- */
                    .tdm-dibs-deals-container, .tdm-notes-container {
                        min-width: 48px;
                        /* allow the emitted percent-based widths to control layout; avoid fixed max width */
                        max-width: none;
                        padding: 2px !important;
                        display: flex;
                        gap: 2px;
                        flex-direction: row;
                    }
                    /* Make the notes column fill the space on our own faction page (when dibsDeals container is hidden) */
                    .f-war-list .table-body > li:has(.tdm-dibs-deals-container[style*="display: none"]) .tdm-notes-container,
                    .f-war-list .table-body > li:has(.tdm-dibs-deals-container[style*="display: none"]) .notes-cell {
                        width: 100%;
                    }
                    /* Header for the controls column (split into dibs, med-deal, and notes) */
                    #col-header-member-index { min-width: 3%; max-width: 3%; }
                    #col-header-dibs-deals, #col-header-notes {
                        min-width: 48px;
                        max-width: none;
                    }

                    /* --- Individual Cell Styling (Dibs/Notes) --- */
                    .dibs-cell, .notes-cell {
                        flex: 1; /* Make cells share space equally */
                        display: flex;
                        flex-direction: column;
                        gap: 1px; /* MODIFIED: Reduced gap for tighter fit */
                        min-width: 0; /* Important for flex-shrinking */
                    }

                    /* Early Leave Highlight */
                    .tdm-early-leave {
                        background-color: rgba(255, 255, 0, 0.2) !important;
                        border: 1px solid rgba(255, 255, 0, 0.5) !important;
                    }

                    /* --- Button Styling --- */
                    .tdm-dibs-deals-container .btn, .tdm-notes-container .btn {
                        flex: 1; /* Make buttons share vertical space */
                        min-height: 0; /* Allows buttons to shrink */
                        padding: 0 4px !important; /* MODIFIED: Removed vertical padding */
                        font-size: 0.8em !important;
                        white-space: nowrap;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        line-height: 1.1; /* MODIFIED: Reduced line height */
                        border-radius: 3px; /* Added for consistency */
                    }
                    .tdm-dibs-deals-container .med-deal-button {
                        white-space: normal; /* Allow med deal text to wrap */
                    }
                    .tdm-dibs-deals-container .dibs-cell:has(.med-deal-button[style*="display: none"]) .dibs-button {
                        flex: 2; /* Make dibs button fill the whole cell if no med deal */
                        height: 100%;
                    }

                    /* --- Other Styles --- */
                    .dibs-spinner { border: 2px solid rgba(255, 255, 255, 0.3); border-top: 2px solid #fff; border-radius: 50%; width: 12px; height: 12px; animation: spin 1s linear infinite; display: inline-block; vertical-align: middle; }
                    @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
                    .message-box-on-top { position: fixed; top: 10px; left: 50%; transform: translateX(-50%); background-color: rgba(0, 0, 0, 0.8); color: white; border-radius: 4px; z-index: 10000; padding: 5px 10px; font-family: 'Inter', sans-serif; font-size: 14px; display: flex; align-items: center; cursor: pointer; }
                    .dibs-cell, .notes-cell, #col-header-dibs-deals, #col-header-notes, .notes-container { display: flex; flex-direction: column; justify-content: center; align-items: stretch; gap: 0px; padding: 0px !important; box-sizing: border-box; height: 100% !important; }
                    .dibs-cell button, .dibs-cell .dibs-button, .dibs-cell .btn-med-deal, #col-header-dibs-deals button, #col-header-notes button, .notes-cell button, .notes-container button, .notes-container .btn {
                        display: flex !important;
                        flex: 1 1 auto !important;
                        width: 100% !important;
                        height: auto !important;
                        margin: 0 !important;
                        box-sizing: border-box !important;
                        font-size: 0.85em !important;
                        align-items: center !important;
                        justify-content: center !important;
                    }
                    .dibs-cell .dibs-button:only-child, .dibs-cell-full .btn-med-deal:only-child, .inactive-note-button:only-child, .active-note-button:only-child, .btn-med-deal-inactive:only-child, .notes-cell .btn:only-child, .notes-cell .button:only-child, .notes-container .btn:only-child, .notes-container .button:only-child { flex: 1 1 100% !important; margin: 0 !important; width: 100% !important; height: 100% !important; }


                    /* Button Colors */
                    .btn-dibs-inactive { background-color: ${config.CSS.colors.dibsInactive} !important; color: #fff !important; }
                    .btn-dibs-inactive:hover { background-color: ${config.CSS.colors.dibsInactiveHover} !important; }
                    /* Dibs Disabled (policy) */
                    .btn-dibs-disabled { background-color: #5a5a5a !important; color: #cfcfcf !important; cursor: not-allowed !important; border: 1px solid #777 !important; }
                    .btn-dibs-disabled:hover { background-color: #505050 !important; color: #e0e0e0 !important; }
                    .btn-dibs-success-you { background-color: ${config.CSS.colors.dibsSuccess} !important; color: #fff !important; }
                    .btn-dibs-success-you:hover { background-color: ${config.CSS.colors.dibsSuccessHover} !important; }
                    .btn-dibs-success-other { background-color: ${config.CSS.colors.dibsOther} !important; color: #fff !important; }
                    .btn-dibs-success-other:hover { background-color: ${config.CSS.colors.dibsOtherHover} !important; }
                    .inactive-note-button, .note-button { background-color: ${config.CSS.colors.noteInactive} !important; color: #fff !important; }
                    .inactive-note-button:hover, .note-button:hover { background-color: ${config.CSS.colors.noteInactiveHover} !important; }
                    .active-note-button { background-color: ${config.CSS.colors.noteActive} !important; color: #fff !important; }
                    .active-note-button:hover { background-color: ${config.CSS.colors.noteActiveHover} !important; }
                    .btn-med-deal-inactive, .btn-med-deal-default { background-color: ${config.CSS.colors.medDealInactive} !important; color: #fff !important; }
                    .btn-med-deal-inactive:hover, .btn-med-deal-default:hover { background-color: ${config.CSS.colors.medDealInactiveHover} !important; }
                    .btn-med-deal-set { background-color: ${config.CSS.colors.medDealSet} !important; color: #fff !important; }
                    .btn-med-deal-set:hover { background-color: ${config.CSS.colors.medDealSetHover} !important; }
                    .btn-med-deal-mine { background-color: ${config.CSS.colors.medDealMine} !important; color: #fff !important; }
                    .btn-med-deal-mine:hover { background-color: ${config.CSS.colors.medDealMineHover} !important; }
                    .req-assist-button { background-color: ${config.CSS.colors.assistButton} !important; color: #fff !important; }
                    .req-assist-button:hover { background-color: ${config.CSS.colors.assistButtonHover} !important; }
                    .btn-retal-inactive { background-color: #555555 !important; color: #ccc !important; }
                    .btn-retal-inactive:hover { background-color: #444444 !important; color: #ddd !important; }
                    /* Standardized alert button base */
                    .tdm-alert-btn { min-width: 36px; max-width: 90px; max-height: 30px; font-size: 0.75em; padding: 1px 1px; border-radius: 3px; margin-left: 0; margin-right: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; display:flex; align-items:center; justify-content:center; flex: 0 1 auto; }
                    /* Variants */
                    .tdm-alert-retal { background-color: #ff5722 !important; color: #fff !important; }
                    .tdm-alert-retal:hover { background-color: #e64a19 !important; color: #fff !important; }
                    .tdm-alert-green { background-color: #3cba54 !important; color: #fff !important; } /* Idle/Offline -> Online */
                    .tdm-alert-yellow { background-color: #f4c20d !important; color: #000 !important; } /* Online -> Idle or misc */
                    .tdm-alert-grey { background-color: #6b7280b1 !important; color: #fff !important; } /* Offline-related or Okay->Hosp */
                    .tdm-alert-red { background-color: #ef4444d6 !important; color: #fff !important; } /* Travel->Abroad/Okay, Hosp->Okay */
                    .tdm-alert-inactive { background-color: #55555587 !important; color: #ccc !important; }
                    .btn-retal-expired { background-color: #795548 !important; color: #fff !important; }
                    .btn-retal-expired:hover { background-color: #5d4037 !important; color: #fff !important; }
                    .dibs-notes-subrow { width: 100% !important; flex-basis:100%; order:100; display:flex; gap:4px; margin-top:1px; margin-bottom:1px; justify-content:flex-start; background:transparent; border:none; box-sizing:border-box; position:relative; max-width:100%; flex-wrap:wrap; overflow:visible; }
                    .members-list > li, .members-cont > li { flex-wrap: wrap !important; width: inherit !important; }
                    .dibs-notes-subrow .btn { min-width: 70px; max-width: 70px; max-height: 30px; font-size: 0.75em; padding: 1px 1px; border-radius: 3px; margin-left: 0; margin-right: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
                    /* Keep retal alert visible on narrow viewports */
                    .tdm-retal-container{flex:0 0 auto;display:flex;justify-content:flex-end;align-items:center;gap:4px;max-width:100%;box-sizing:border-box;min-width:0;}
                    .dibs-notes-subrow .retal-btn{min-width:28px !important;max-width:90px !important;padding:0 6px !important;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 0 auto;}

                    /* Settings Panel Styles (Compacted) */
                    .settings-section { margin-bottom: 4px; width: 100%; }
                    .settings-section-divided { padding-top: 6px; border-top: 1px solid #374151; display: flex; gap: 6px; justify-content: center; align-items: center; flex-wrap: wrap; width: 100%; }
                    .settings-header { font-size: 12px; font-weight: 600; margin: 0 0 6px 0; text-align: center; color: #93c5fd; background-color: #1a1a1a; padding: 4px; border-radius: 4px; width: 100%; user-select: none; }
                    /* Make subsection headers slightly lighter for visual separation */
                    .settings-subsection > .settings-header { color: #5089daff !important; }
                    .settings-subheader { font-size: 10px; font-weight: 600; }
                    .settings-button-group { display: flex; flex-wrap: wrap; gap: 4px; justify-content: center; width: 100%; }
                    .settings-input, .settings-input-display { width: 100%; padding: 4px; background-color: #333; color: white; border: 1px solid #555; border-radius: 4px; box-sizing: border-box; }
                    .settings-input-display { padding: 6px 4px; }
                    .settings-btn { background-color: #4b5563; color: #eee; border: 1px solid #6b7280; padding: 5px 10px; font-size: 0.85em; border-radius: 5px; cursor: pointer; transition: background-color 0.2s; }
                    .settings-btn:hover { background-color: #606d7a; }
                    .settings-btn-green { background-color: #4CAF50; border-color: #4CAF50; }
                    .settings-btn-green:hover { background-color: #45a049; }
                    .settings-btn-red { background-color: #f44336; border-color: #f44336; }
                    .settings-btn-red:hover { background-color: #e53935; }
                    .war-type-controls { display: flex; gap: 8px; justify-content: center; margin-bottom: 15px; }
                    .war-type-controls .settings-btn { flex: 1; }
                    .column-control { display:flex; gap:6px; align-items:center; }
                    .column-width-input { padding: 4px; border-radius: 4px; border: 1px solid #555; background: #222; color: #fff; font-size: 0.85em; text-align: right; box-sizing: border-box; }
                    .column-toggle-btn { padding: 4px 12px; border: 2px solid #555; border-radius: 6px; background: #2c2c2c; color: #ccc; cursor: pointer; transition: all 0.3s ease; font-size: 0.85em; font-weight: 500; min-width: 92px; text-align: center; line-height:1.2; }
                    .column-toggle-btn.active { background: #4CAF50; border-color: #4CAF50; color: white; }
                    .column-toggle-btn.active:hover { background: #45a049; border-color: #45a049; color: white; }
                    .column-toggle-btn.inactive { background: #f44336; border-color: #f44336; color: white; }

                    /* Collapsible sections */
                    .collapsible .collapsible-header { cursor: pointer; position: relative; }
                    .collapsible .chevron { float: right; font-size: 12px; opacity: 0.8; }
                    .collapsible.collapsed .collapsible-content { display: none !important; }
                    .collapsible.collapsed .chevron { transform: rotate(-90deg); }
                    .collapsible.settings-section-divided { flex-direction: column; align-items: stretch; }
                    .collapsible.settings-section-divided > .collapsible-header,
                    .collapsible.settings-section-divided > .collapsible-content { width: 100%; }

                    /* Text halo for timers */
                    .tdm-text-halo, .tdm-text-halo a { 
                        text-shadow: 
                            -1px -1px 0 #000,
                            1px -1px 0 #000,
                            -1px 1px 0 #000,
                            1px 1px 0 #000,
                            0 0 3px #000;
                    }
                    .tdm-halo-link { color: inherit; text-decoration: underline; cursor: pointer; }
                    /* Thin red border highlight for score increases */
                          .tdm-score-bump { outline: 2px solid rgba(239,68,68,0.9) !important; outline-offset: -2px; transition: outline-color 0.6s ease; }
                          .tdm-score-bump.fade { outline-color: rgba(239,68,68,0.0) !important; }
                          /* When the red burst expires it transitions to an orange persistent highlight.
                              The orange stay lasts longer (controlled in JS) and will be cleared on status change/hospital. */
                          .tdm-score-bump-orange { outline: 2px solid rgba(255,165,0,0.95) !important; outline-offset: -2px; transition: outline-color 0.6s ease; }
                          /* Very short click feedback for dibs buttons */
                          .tdm-dibs-clicked { box-shadow: 0 0 10px 4px rgba(255,180,80,0.9) !important; transition: box-shadow 160ms ease; }
                `
            });
            document.head.appendChild(styleTag);
            // Global delegated click visual feedback for dibs buttons (short glow)
            try {
                document.addEventListener('click', (evt) => {
                    try {
                        const btn = evt.target && evt.target.closest && evt.target.closest('.dibs-btn');
                        if (!btn) return;
                        btn.classList.add('tdm-dibs-clicked');
                        setTimeout(() => { try { btn.classList.remove('tdm-dibs-clicked'); } catch(_) {} }, 220);
                    } catch(_) { /* noop */ }
                });
            } catch(_) { /* noop */ }
        },

        updateColumnVisibilityStyles: () => {
            let styleTag = document.getElementById('dibs-column-visibility-dynamic-style');
            if (!styleTag) {
                styleTag = utils.createElement('style', { id: 'dibs-column-visibility-dynamic-style' });
                document.head.appendChild(styleTag);
            }
            let css = '';
            // Ensure our dibs/notes headers & cells show torn vertical dividers where appropriate
            try {
                const hdrSelectors = ['.tab-menu-cont .members-list #col-header-dibs-deals', '.tab-menu-cont .members-list #col-header-notes', '.f-war-list.members-list #col-header-dibs-deals', '.f-war-list.members-list #col-header-notes', '#faction-war #col-header-dibs', '#faction-war #col-header-notes', '.f-war-list .table-header #col-header-dibs', '.f-war-list .table-header #col-header-notes'];
                document.querySelectorAll(hdrSelectors.join(', ')).forEach(el => { try { el.classList.add('torn-divider','divider-vertical'); } catch(_){} });
                const cellSelectors = ['.tab-menu-cont .members-list .tdm-dibs-deals-container', '.tab-menu-cont .members-list .tdm-notes-container', '.f-war-list.members-list .tdm-dibs-deals-container', '.f-war-list.members-list .tdm-notes-container', '.f-war-list .tdm-dibs-deals-container', '.f-war-list .tdm-notes-container'];
                document.querySelectorAll(cellSelectors.join(', ')).forEach(el => { try { el.classList.add('torn-divider','divider-vertical'); } catch(_){} });
            } catch(_) { /* noop */ }
            // Update column visibility for both tables using table-specific keys
            const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY);
            // Members List selectors
            const membersSelectors = {
                // Members list: prefer f-war-list.members-list (explicit members-list variant)
                lvl: [
                    '.f-war-list.members-list .table-header .lvl.torn-divider.divider-vertical', '.f-war-list.members-list .table-body .lvl',
                    '.f-war-list.members-list .lvl', '.f-war-list.members-list .table-body .lvl'
                    
                ],
                memberIcons: [
                    '.f-war-list.members-list .table-header .member-icons.torn-divider.divider-vertical', '.f-war-list.members-list .table-body .member-icons'
                ],
                position: ['.f-war-list.members-list .table-header .position', '.f-war-list.members-list .table-body .position', '.f-war-list.members-list .table-header .position', '.f-war-list.members-list .table-body .position'],
                days: ['.f-war-list.members-list .table-header .days', '.f-war-list.members-list .table-body .days', '.f-war-list.members-list .table-header .days', '.f-war-list.members-list .table-body .days'],
                factionIcon: ['.f-war-list.members-list .factionWrap___GhZMa'],
                // Target header li and direct per-row table-cell elements to ensure header/row widths match
                dibsDeals: [
                    '.f-war-list.members-list .table-header #col-header-dibs-deals',
                    '.f-war-list.members-list .table-header > li#col-header-dibs-deals',
                    '.f-war-list.members-list .table-body > li > .tdm-dibs-deals-container',
                    '.f-war-list.members-list .table-body > li .tdm-dibs-deals-container',
                    '.f-war-list.members-list .table-body .table-cell .tdm-dibs-deals-container',
                    '.f-war-list.members-list .table-body .dibs-cell',
                    '.f-war-list.members-list .table-body .med-deal-button'
                ],
                memberIndex: ['.f-war-list.members-list .table-header #col-header-member-index', '.f-war-list.members-list .table-body .tt-member-index', '.f-war-list.members-list .table-header #col-header-member-index', '.f-war-list.members-list .table-body .tt-member-index'],
                member: ['.f-war-list.members-list .table-header .member', '.f-war-list.members-list .table-body .member', '.f-war-list.members-list .table-header .member', '.f-war-list.members-list .table-body .member'],
                statusHeader: ['.f-war-list.members-list .table-header .status'],
                statusBody: ['.f-war-list.members-list .table-body .status'],
                notes: [
                    '.f-war-list.members-list .table-header #col-header-notes',
                    '.f-war-list.members-list .table-header > li#col-header-notes',
                    '.f-war-list.members-list .table-body > li > .tdm-notes-container',
                    '.f-war-list.members-list .table-body > li .tdm-notes-container',
                    '.f-war-list.members-list .table-body .notes-cell'
                ]
            };
            // Ranked War selectors
            const rankedWarSelectors = {
                lvl: ['.tab-menu-cont .level', '.white-grad.c-pointer .level'],
                members: ['.tab-menu-cont .member','.white-grad.c-pointer .member'],
                points: ['.tab-menu-cont .points','.white-grad.c-pointer .points'],
                status: ['.tab-menu-cont .status','.white-grad.c-pointer .status'],
                attack: ['.tab-menu-cont .attack', '.white-grad.c-pointer .attack'],
                factionIcon: ['.tab-menu-cont .factionWrap___GhZMa']
            };

            // Hide columns for Members List
            for (const colName in membersSelectors) {
                // treat header/body variants as the same logical column for visibility settings
                const baseCol = String(colName).replace(/(?:Header|Body)$/, '');
                if (vis.membersList?.[baseCol] === false) {
                    const selectors = membersSelectors[colName];
                    if (selectors) css += `${selectors.join(', ')} { display: none !important; }\n`;
                }
            }
                // Special: hide dibsDeals entirely on our own faction page (members list)
                try {
                    if (state.page?.isMyFactionPage) {
                        const ds = membersSelectors.dibsDeals || [];
                        if (ds.length) css += `${ds.join(', ')} { display: none !important; }\n`;
                    }
                } catch(_) {}

                // Special: if both dibsDeals and notes are hidden, remove combined header cell if present
            try {
                const dibsHidden = vis.membersList?.dibsDeals === false;
                const notesHidden = vis.membersList?.notes === false;
                if (dibsHidden && notesHidden) {
                    // hide combined dibs/notes header + controls for members-list variants only
                    css += `.tab-menu-cont .members-list .table-header #col-header-dibs-notes, .f-war-list.members-list .table-header #col-header-dibs-notes, .tab-menu-cont .members-list .table-body .tdm-controls-container, .f-war-list.members-list .table-body .tdm-controls-container, .tab-menu-cont .members-list .table-body .dibs-cell, .f-war-list.members-list .table-body .dibs-cell, .tab-menu-cont .members-list .table-body .notes-cell, .f-war-list.members-list .table-body .notes-cell { display: none !important; }\n`;
                } else if (notesHidden) {
                    // If notes hidden but our members rows have only notes (no dibs/med deal), collapse empty space by shrinking container
                    css += `.tab-menu-cont .members-list .table-body .tdm-controls-container:empty, .f-war-list.members-list .table-body .tdm-controls-container:empty, .tab-menu-cont .members-list .table-body .tdm-controls-container:not(:has(.dibs-cell,.med-deal-button)):not(:has(.note-button)), .f-war-list.members-list .table-body .tdm-controls-container:not(:has(.dibs-cell,.med-deal-button)):not(:has(.note-button)) { display:none !important; }\n`;
                }
            } catch(_) { /* noop */ }
            // Special: if dibs, medDeals and notes are ALL hidden, remove the whole controls container
            try {
                const dibsDealsHidden = vis.membersList?.dibsDeals === false;
                const notesHidden = vis.membersList?.notes === false;
                if (dibsDealsHidden && notesHidden) {
                    css += `.tab-menu-cont .members-list .table-header #col-header-dibs-deals, .f-war-list.members-list .table-header #col-header-dibs-deals, .tab-menu-cont .members-list .table-header > li#col-header-dibs-deals, .f-war-list.members-list .table-header > li#col-header-dibs-deals, .tab-menu-cont .members-list .table-header #col-header-notes, .f-war-list.members-list .table-header #col-header-notes, .tab-menu-cont .members-list .table-header > li#col-header-notes, .f-war-list.members-list .table-header > li#col-header-notes, .tab-menu-cont .members-list .table-body > li > .tdm-dibs-deals-container, .f-war-list.members-list .table-body > li > .tdm-dibs-deals-container, .tab-menu-cont .members-list .table-body > li .tdm-dibs-deals-container, .f-war-list.members-list .table-body > li .tdm-dibs-deals-container, .tab-menu-cont .members-list .table-body .tdm-dibs-deals-container, .f-war-list.members-list .table-body .tdm-dibs-deals-container, .tab-menu-cont .members-list .table-body > li > .tdm-notes-container, .f-war-list.members-list .table-body > li > .tdm-notes-container, .tab-menu-cont .members-list .table-body > li .tdm-notes-container, .f-war-list.members-list .table-body > li .tdm-notes-container, .tab-menu-cont .members-list .table-body .tdm-notes-container, .f-war-list.members-list .table-body .tdm-notes-container, .tab-menu-cont .members-list .table-body .dibs-cell, .f-war-list.members-list .table-body .dibs-cell, .tab-menu-cont .members-list .table-body .notes-cell, .f-war-list.members-list .table-body .notes-cell, .tab-menu-cont .members-list .table-body .med-deal-button, .f-war-list.members-list .table-body .med-deal-button { display: none !important; }`;
                } else if (notesHidden) {
                    // If notes hidden but our faction rows only have notes (no dibs/med deal), collapse empty space by shrinking containers
                    css += `.tab-menu-cont .members-list .table-body > li > .tdm-notes-container:empty, .f-war-list.members-list .table-body > li > .tdm-notes-container:empty, .tab-menu-cont .members-list .table-body > li .tdm-notes-container:empty, .f-war-list.members-list .table-body > li .tdm-notes-container:empty, .tab-menu-cont .members-list .table-body .tdm-notes-container:empty, .f-war-list.members-list .table-body .tdm-notes-container:empty, .tab-menu-cont .members-list .table-body > li > .tdm-notes-container:not(:has(.note-button)), .f-war-list.members-list .table-body > li > .tdm-notes-container:not(:has(.note-button)) { display:none !important; }`;
                }
            } catch(_) { /* noop */ }
            try {
                const widths = storage.get('columnWidths', config.DEFAULT_COLUMN_WIDTHS) || {};
                const mw = widths.membersList || {};
                for (const colName in membersSelectors) {
                    // treat header/body variants as the same logical column when reading configured widths
                    const baseCol = String(colName).replace(/(?:Header|Body)$/, '');
                    // Special handling: if the memberIndex rows are not present/visible on small devices,
                    // don't apply explicit widths for that column — avoids header-only width when rows collapse.
                    if (baseCol === 'memberIndex') {
                        try {
                            const selArr = (membersSelectors[colName] || []).filter(s => s.includes('.table-body') || s.includes('.tt-member-index'));
                            let foundVisible = false;
                            for (const s of selArr) {
                                const nodes = Array.from(document.querySelectorAll(s));
                                if (nodes.some(n => n && n.offsetParent !== null && window.getComputedStyle(n).display !== 'none')) { foundVisible = true; break; }
                            }
                            if (!foundVisible) continue; // skip width application when rows are not visible (small screens)
                        } catch(_) { /* ignore and continue with width */ }
                    }
                    // Don't apply explicit widths to icon-only columns like factionIcon
                    if (baseCol === 'factionIcon') continue;
                    const w = mw[baseCol];
                    if (typeof w === 'number' && w > 0) {
                        const selectors = membersSelectors[colName] || [];
                        // For the members list we want percent widths applied to the outer container/header
                        // but inner cells (.dibs-cell / .notes-cell) should be 100% so inner contents fill the outer container.
                        const outerSelectors = selectors.filter(s => !s.includes('.dibs-cell') && !s.includes('.notes-cell') && !s.includes('.med-deal-button'));
                        const innerSelectors = selectors.filter(s => s.includes('.dibs-cell') || s.includes('.notes-cell') || s.includes('.med-deal-button'));
                        if (outerSelectors.length) css += `${outerSelectors.join(', ')} { flex: 0 0 ${w}% !important; width: ${w}% !important; max-width: ${w}% !important; }`;
                        if (innerSelectors.length) css += `${innerSelectors.join(', ')} { flex: 1 1 auto !important; width: 100% !important; max-width: none !important; }`;
                    }
                }
            } catch(_) {}

            // Hide columns for Ranked War
            for (const colName in rankedWarSelectors) {
                if (vis.rankedWar?.[colName] === false) {
                    const selectors = rankedWarSelectors[colName];
                    if (selectors) css += `${selectors.join(', ')} { display: none !important; }\n`;
                }
            }
            // Apply explicit widths (percent) for Ranked War cols if configured
            try {
                const widths = storage.get('columnWidths', config.DEFAULT_COLUMN_WIDTHS) || {};
                const rw = widths.rankedWar || {};
                for (const colName in rankedWarSelectors) {
                    // Don't apply explicit widths to icon-only columns like factionIcon
                    if (colName === 'factionIcon') continue;
                    const w = rw[colName];
                    if (typeof w === 'number' && w > 0) {
                        const selectors = rankedWarSelectors[colName] || [];
                        if (selectors.length) {
                            // Some ranked-war cells contain inner containers (icons, dibs/notes controls).
                            // Apply percent widths to the outer/header containers, and force inner cells to fill 100%.
                            const outerSelectors = selectors.filter(s => !s.includes('.dibs-cell') && !s.includes('.notes-cell') && !s.includes('.med-deal-button') && !s.includes('.member-icons') && !s.includes('membersCol'));
                            const innerSelectors = selectors.filter(s => s.includes('.dibs-cell') || s.includes('.notes-cell') || s.includes('.med-deal-button') || s.includes('.member-icons') || s.includes('membersCol'));
                            if (outerSelectors.length) css += `${outerSelectors.join(', ')} { flex: 0 0 ${w}% !important; width: ${w}% !important; max-width: ${w}% !important; }
`;
                            if (innerSelectors.length) css += `${innerSelectors.join(', ')} { flex: 1 1 auto !important; width: 100% !important; max-width: none !important; }
`;
                        }
                    }
                }
            } catch(_) {}

            // Ensure Status column header is vertically centered (fix alignment introduced by DOM mutations)
            try {
                css += `
/* TDM: center the Status header TEXT only, keep sort icon placement intact */
.f-war-list.members-list .table-header .status,
.tab-menu-cont .members-list .table-header .status,
#react-root-faction-info .f-war-list.members-list .table-header .status {
    display: block !important;
}
.f-war-list.members-list .table-header .status > *:not([class*="sortIcon"]),
.tab-menu-cont .members-list .table-header .status > *:not([class*="sortIcon"]),
#react-root-faction-info .f-war-list.members-list .table-header .status > *:not([class*="sortIcon"]) {
    display: flex !important;
    align-items: center !important;
    justify-content: center !important;
}

/* TDM: Notes button — single-line with ellipsis, top-left aligned visual start, avoid vertical clipping */
.f-war-list.members-list .tdm-notes-container .note-button,
.tab-menu-cont .members-list .tdm-notes-container .note-button,
#react-root-faction-info .f-war-list.members-list .tdm-notes-container .note-button {
    display: block !important;
    white-space: nowrap !important;
    overflow: hidden !important;
    text-overflow: ellipsis !important;
    text-align: left !important;
    padding-top: 2px !important;
    padding-bottom: 2px !important;
    line-height: 1.1 !important;
    height: auto !important;
    min-height: 0 !important;
    max-height: none !important;
}
`;
            } catch(_) {}

            styleTag.textContent = css;
        },

        showCurrentWarAttacksModal: async function(warId) {
            const { modal, controls, tableWrap, footer, setLoading, clearLoading, setError } = ui.createReportModal({ id: 'current-war-attacks-modal', title: `War Attacks (ID: ${warId})` });
            setLoading('Loading war attacks...');
            try {
                let allAttacks = await api.getRankedWarAttacksSmart(warId, state.user.factionId, { onDemand: true }) || [];
                if ((!allAttacks || allAttacks.length === 0) && state.rankedWarAttacksCache?.[warId]?.attacks?.length) {
                    tdmlogger('warn', `[WarAttacks] Smart fetch empty, using cached fallback: ${state.rankedWarAttacksCache[warId].attacks.length}`);
                    allAttacks = state.rankedWarAttacksCache[warId].attacks;
                }
                const normalizeAttack = (a) => {
                    if (!a || typeof a !== 'object') return null;
                    const attackerId = a.attacker?.id ?? a.attackerId ?? a.attacker_id ?? a.attacker;
                    const defenderId = a.defender?.id ?? a.defenderId ?? a.defender_id ?? a.defender;
                    const attackerName = a.attacker?.name ?? a.attackerName ?? a.attacker_name ?? '';
                    const defenderName = a.defender?.name ?? a.defenderName ?? a.defender_name ?? '';
                    const attackerFactionId = a.attacker?.faction?.id ?? a.attackerFactionId ?? a.attacker_faction ?? a.attackerFaction ?? null;
                    const defenderFactionId = a.defender?.faction?.id ?? a.defenderFactionId ?? a.defender_faction ?? a.defenderFaction ?? null;
                    const ended = Number(a.ended || a.end || a.finish || a.timestamp || 0) || 0;
                    const started = Number(a.started || a.start || a.begin || ended || 0) || ended;
                    // Expose common numeric/boolean fields used by backend: respect_gain, respect_gain_no_bonus, respect_loss, chain, time_since_last_attack, chain_saver, modifiers
                    const respect_gain = Number(a.respect_gain || a.respectGain || a.respect_gain_no_bonus || 0) || 0;
                    const respect_gain_no_bonus = Number(a.respect_gain_no_bonus || a.respectGainNoBonus || 0) || 0;
                    const respect_loss = Number(a.respect_loss || a.respectLoss || 0) || 0;
                    const chain = Number(a.modifiers?.chain || a.chain || 1) || 1;
                    const chain_gap = Number(a.time_since_last_attack || a.chain_gap || a.chainGap || 0) || 0;
                    const chain_saver = !!(a.chain_saver || a.chainSaver || a.chain_saver_flag);
                    const outside = Number(a?.modifiers?.war ?? a.outside ?? 0) !== 2;
                    const overseas = (a.modifiers?.overseas || a.overseas || 1) > 1;
                    const retaliation = (a.modifiers?.retaliation || a.retaliation || 1) > 1;
                    const attackCode = a.code || a.attackCode || a.attack_id || a.attackId || '';
                    return {
                        // keep original payload for any other fields
                        __raw: a,
                        attackId: a.attackId || a.id || a.attack_id || a.attackSeq || a.seq || attackCode || null,
                        attacker: { id: attackerId, name: attackerName, faction: { id: attackerFactionId } },
                        defender: { id: defenderId, name: defenderName, faction: { id: defenderFactionId } },
                        ended, started,
                        respect_gain, respect_gain_no_bonus, respect_loss,
                        chain, chain_gap, chain_saver,
                        outside, overseas, retaliation,
                        code: attackCode,
                        result: a.result || a.outcome || a.type || '',
                        modifiers: a.modifiers || {},
                        // preserve arbitrary backend-provided fields in top-level for dynamic columns
                        ...a
                    };
                };
                allAttacks = allAttacks.map(normalizeAttack).filter(a => a && a.attacker?.id && a.defender?.id);
                if (!allAttacks.length) {
                    clearLoading();
                    controls.appendChild(utils.createElement('div', { style: { color: '#ccc' }, textContent: 'No attacks found (final file maybe not published or normalization empty).' }));
                    tdmlogger('warn', `[WarAttacks] No normalized attacks. Raw cache entry: ${state.rankedWarAttacksCache?.[warId]}`);
                    return;
                }
                // Determine opponent faction id for color-coding
                const ourFactionId = String(state.user?.factionId || '');
                let opponentFactionId = '';
                try {
                    const warObj = utils.getWarById(warId) || state.lastRankWar || {};
                    const candidates = [];
                    if (warObj.faction1) candidates.push(warObj.faction1);
                    if (warObj.faction2) candidates.push(warObj.faction2);
                    if (warObj.opponent) candidates.push(warObj.opponent);
                    if (warObj.enemy) candidates.push(warObj.enemy);
                    // Each candidate may be object or id
                    for (const c of candidates) {
                        const fid = String(c?.faction_id || c?.id || c?.factionId || (typeof c === 'number' ? c : ''));
                        if (fid && fid !== ourFactionId) { opponentFactionId = fid; break; }
                    }
                    // Fallback: scan attacks for a faction id not ours
                    if (!opponentFactionId) {
                        for (const a of allAttacks) {
                            const fidA = String(a.attacker?.faction?.id || '');
                            const fidD = String(a.defender?.faction?.id || '');
                            if (fidA && fidA !== ourFactionId) { opponentFactionId = fidA; break; }
                            if (fidD && fidD !== ourFactionId) { opponentFactionId = fidD; break; }
                        }
                    }
                    // Dev button for forcing backend war summary rebuild (when debug enabled)
                    if (state.debug?.apiLogs && !document.getElementById('force-full-war-summary-rebuild-btn')) {
                        const rebuildBtn = utils.createElement('button', {
                            id: 'force-full-war-summary-rebuild-btn',
                            className: 'settings-btn settings-btn-yellow',
                            textContent: 'Force Full Summary Rebuild',
                            title: 'Trigger backend function triggerRankedWarSummaryRebuild to rebuild summary.'
                        });
                        rebuildBtn.addEventListener('click', async () => {
                            const selWarId = rankedWarSelect.value || state.lastRankWar?.id;
                            if (!selWarId) { ui.showMessageBox('Select a war first.', 'error'); return; }
                            const oldTxt = rebuildBtn.textContent; rebuildBtn.disabled = true; rebuildBtn.textContent = 'Rebuilding...';
                            try {
                                const ok = await api.forceFullWarSummaryRebuild(selWarId, state.user.factionId);
                                if (ok) {
                                    ui.showTransientMessage('Summary rebuild triggered.', { type: 'success' });
                                    setTimeout(async ()=>{ try { await api.fetchWarManifestV2(selWarId, state.user.factionId, { force: true }); await api.assembleAttacksFromV2(selWarId, state.user.factionId, { forceWindowBootstrap: false }); } catch(_) {} }, 2500);
                                } else {
                                    ui.showTransientMessage('Rebuild callable unavailable.', { type: 'error' });
                                }
                            } catch(e){ ui.showMessageBox('Rebuild failed: '+(e.message||e), 'error'); }
                            finally { rebuildBtn.disabled=false; rebuildBtn.textContent=oldTxt; }
                        });
                        viewWarAttacksBtn.parentElement?.insertBefore(rebuildBtn, viewWarAttacksBtn.nextSibling);
                    }
                } catch(_) { /* noop */ }

                // Inject styles once for faction highlighting
                try {
                    if (!document.getElementById('tdm-war-attack-color-css')) {
                        const st = document.createElement('style');
                        st.id = 'tdm-war-attack-color-css';
                        st.textContent = `.tdm-war-our{color:#4caf50 !important;font-weight:600;} .tdm-war-opp{color:#ff5252 !important;font-weight:600;} .tdm-war-our.t-blue,.tdm-war-opp.t-blue{color:inherit;}`; // base t-blue overridden by explicit color due to !important
                        document.head.appendChild(st);
                    }
                } catch(_) { /* noop */ }

                // Diagnostics removed (was used for snapshot/window debugging)

                const factionClass = (fid) => {
                    const s = String(fid||'');
                    if (s && s === ourFactionId) return 'tdm-war-our';
                    if (s && s === opponentFactionId) return 'tdm-war-opp';
                    return '';
                };
                // Pagination & sorting state
                let currentPage = 1, attacksPerPage = 50, sortKey = 'attackTime', sortAsc = false;
                // Dynamic filter state: array of { fieldKey, op, value }
                let dynamicFilters = [];
                const uniqueAttackers = [...new Set(allAttacks.map(a => a.attacker?.name).filter(Boolean))].sort();
                const uniqueDefenders = [...new Set(allAttacks.map(a => a.defender?.name).filter(Boolean))].sort();
                const controlsBar = utils.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: '10px', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' } });
                const left = utils.createElement('div'); const mid = utils.createElement('div');
                const perPageInput = utils.createElement('input', { id: 'attacks-per-page', type: 'number', value: String(attacksPerPage), min: '1', max: '5000', className: 'settings-input', style: { width: '60px' } });

                // Filter definitions: key, label, accessor, optional choices for dropdown
                const filterDefs = [
                    { key: 'attacker.name', label: 'Attacker', accessor: (a) => String(a.attacker?.name || ''), choices: uniqueAttackers },
                    { key: 'attacker.id', label: 'AttackerId', accessor: (a) => a.attacker?.id || '' },
                    { key: 'defender.name', label: 'Defender', accessor: (a) => String(a.defender?.name || ''), choices: uniqueDefenders },
                    { key: 'defender.id', label: 'DefenderId', accessor: (a) => a.defender?.id || '' },
                    { key: 'result', label: 'Result', accessor: (a) => String(a.result || '') },
                    { key: 'direction', label: 'Direction', accessor: (a) => String(a.direction || '') },
                    // Treat an attack as ranked if explicit flag is set OR modifiers.war==2 (Torn war modifier)
                    { key: 'is_ranked_war', label: 'Ranked', accessor: (a) => !!(a.is_ranked_war || a.isRankedWar || Number(a?.modifiers?.war || 0) === 2) },
                    { key: 'is_stealthed', label: 'Stealthed', accessor: (a) => !!(a.is_stealthed || a.isStealthed) },
                    { key: 'chain', label: 'Chain', accessor: (a) => (typeof a.chain !== 'undefined' ? a.chain : (a.modifiers?.chain || '')) },
                    { key: 'respect_gain', label: 'RespectGain', accessor: (a) => Number(a.respect_gain || 0) },
                    { key: 'code', label: 'LogCode', accessor: (a) => String(a.code || '') },
                    { key: 'attackTime', label: 'AttackTime', accessor: (a) => Number(a.ended || a.started || 0) }
                ];

                const makeFieldSelect = (selectedKey) => {
                    const sel = utils.createElement('select', { className: 'settings-input', style: { width: '160px' } });
                    sel.appendChild(utils.createElement('option', { value: '', textContent: '-- Field --' }));
                    filterDefs.forEach(def => sel.appendChild(utils.createElement('option', { value: def.key, textContent: def.label, selected: def.key === selectedKey })));
                    // Ensure the select's displayed value matches the requested selectedKey (some environments need explicit assignment)
                    try { sel.value = selectedKey || ''; } catch(_) {}
                    return sel;
                };

                const filterRowsContainer = utils.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: '6px', minWidth: '320px' } });
                const addFilterBtn = utils.createElement('button', { className: 'settings-btn', textContent: 'Add filter', title: 'Add a filter condition' });
                controlsBar.appendChild(left); controlsBar.appendChild(mid); controls.appendChild(controlsBar);
                left.appendChild(utils.createElement('label', { htmlFor: 'attacks-per-page', textContent: 'Records per page: ' })); left.appendChild(perPageInput);
                mid.appendChild(filterRowsContainer); mid.appendChild(addFilterBtn);

                const operators = ['=','!=','>','<','>=','<=','contains'];
                const booleanFields = ['is_ranked_war', 'is_stealthed'];

                const evaluateFilter = (attack, f) => {
                    try {
                        const def = filterDefs.find(d => d.key === f.fieldKey);
                        if (!def) return true;
                        let lv = def.accessor(attack);
                        const rv = f.value;
                        const op = f.op;
                        if (op === 'contains') {
                            return String(lv || '').toLowerCase().includes(String(rv || '').toLowerCase());
                        }
                        // Numeric comparisons
                        if (['>','<','>=','<='].includes(op)) {
                            const Ln = Number(lv || 0);
                            const Rn = Number(rv || 0);
                            if (op === '>') return Ln > Rn;
                            if (op === '<') return Ln < Rn;
                            if (op === '>=') return Ln >= Rn;
                            if (op === '<=') return Ln <= Rn;
                        }
                        // Equality (handle booleans specially)
                        if (typeof lv === 'boolean') {
                            const rvb = (String(rv).toLowerCase() === 'true' || String(rv).toLowerCase() === 'yes');
                            return op === '=' ? lv === rvb : lv !== rvb;
                        }
                        // Default equality string compare (case-insensitive)
                        if (op === '=' || op === '!=') {
                            const res = String(lv == null ? '' : String(lv)).toLowerCase() === String(rv == null ? '' : String(rv)).toLowerCase();
                            return op === '=' ? res : !res;
                        }
                    } catch(_) { return true; }
                    return true;
                };

                const renderFilters = () => {
                    filterRowsContainer.innerHTML = '';
                    dynamicFilters.forEach((f) => {
                        const idx = dynamicFilters.indexOf(f);
                        const row = utils.createElement('div', { style: { display: 'flex', gap: '6px', alignItems: 'center' } });
                        const fieldSel = makeFieldSelect(f.fieldKey || '');
                        const opSel = utils.createElement('select', { className: 'settings-input', style: { width: '90px' } });
                        operators.forEach(op => opSel.appendChild(utils.createElement('option', { value: op, textContent: op, selected: op === f.op })));
                        try { opSel.value = f.op || '='; } catch(_) {}
                        let valInput;
                        const def = filterDefs.find(d => d.key === f.fieldKey);
                        if (def && Array.isArray(def.choices) && def.choices.length) {
                            valInput = utils.createElement('select', { className: 'settings-input', style: { width: '160px' } });
                            valInput.appendChild(utils.createElement('option', { value: '', textContent: '-- any --' }));
                            def.choices.forEach(c => valInput.appendChild(utils.createElement('option', { value: c, textContent: c, selected: String(c) === String(f.value) })));
                        } else if (def && booleanFields.includes(def.key)) {
                            // Boolean fields: provide a Yes/No dropdown
                            valInput = utils.createElement('select', { className: 'settings-input', style: { width: '160px' } });
                            valInput.appendChild(utils.createElement('option', { value: '', textContent: '-- any --' }));
                            valInput.appendChild(utils.createElement('option', { value: 'Yes', textContent: 'Yes', selected: String(f.value) === 'Yes' }));
                            valInput.appendChild(utils.createElement('option', { value: 'No', textContent: 'No', selected: String(f.value) === 'No' }));
                        } else {
                            valInput = utils.createElement('input', { className: 'settings-input', type: 'text', value: f.value || '', style: { width: '160px' } });
                        }
                        const removeBtn = utils.createElement('button', { className: 'settings-btn settings-btn-red', textContent: 'Remove' });
                        // Wire events using current index lookup
                        fieldSel.addEventListener('change', (e) => {
                            const i = dynamicFilters.indexOf(f);
                            if (i === -1) return;
                            const newKey = e.target.value;
                            const oldVal = dynamicFilters[i].value;
                            dynamicFilters[i].fieldKey = newKey;
                            // Coerce/clear stored value when switching to a field with a different choice set or boolean type
                            const newDef = filterDefs.find(d => d.key === newKey);
                            if (newDef) {
                                if (Array.isArray(newDef.choices) && newDef.choices.length) {
                                    if (!newDef.choices.includes(oldVal)) dynamicFilters[i].value = '';
                                } else if (booleanFields.includes(newDef.key)) {
                                    // Normalize oldVal to Yes/No if possible
                                    const nv = String(oldVal || '').toLowerCase();
                                    if (nv === 'true' || nv === 'yes' || nv === '1') dynamicFilters[i].value = 'Yes';
                                    else if (nv === 'false' || nv === 'no' || nv === '0') dynamicFilters[i].value = 'No';
                                    else dynamicFilters[i].value = '';
                                }
                            }
                            // Re-render to refresh value control type
                            renderFilters();
                            currentPage = 1; renderAll();
                        });
                        opSel.addEventListener('change', (e) => { const i = dynamicFilters.indexOf(f); if (i === -1) return; dynamicFilters[i].op = e.target.value; currentPage = 1; renderAll(); });
                        valInput.addEventListener('change', (e) => { const i = dynamicFilters.indexOf(f); if (i === -1) return; dynamicFilters[i].value = e.target.value; currentPage = 1; renderAll(); });
                        removeBtn.addEventListener('click', () => { const i = dynamicFilters.indexOf(f); if (i === -1) return; dynamicFilters.splice(i, 1); renderFilters(); currentPage = 1; renderAll(); });
                        row.appendChild(fieldSel); row.appendChild(opSel); row.appendChild(valInput); row.appendChild(removeBtn);
                        filterRowsContainer.appendChild(row);
                    });
                };

                addFilterBtn.addEventListener('click', () => { dynamicFilters.push({ fieldKey: '', op: '=', value: '' }); renderFilters(); });

                // Initialize empty filters UI
                renderFilters();
                // computeRows: filter -> sort -> paginate -> map to row objects for rendering
                const computeRows = () => {
                    const filteredAttacks = allAttacks.filter(a => {
                        // apply all dynamic filters (AND semantics)
                        if (Array.isArray(dynamicFilters) && dynamicFilters.length) {
                            for (const f of dynamicFilters) {
                                if (!evaluateFilter(a, f)) return false;
                            }
                        }
                        return true;
                    });
                    // Sort accessor map for common sort keys. Defaults to numeric attack time.
                    const accessor = (k) => {
                        if (!k || k === 'attackTime') return (a) => Number(a.ended || a.started || 0) || 0;
                        if (k === 'attacker') return (a) => String(a.attacker?.name || '').toLowerCase();
                        if (k === 'defender') return (a) => String(a.defender?.name || '').toLowerCase();
                        if (k === 'result') return (a) => String(a.result || '').toLowerCase();
                        if (k === 'resDelta' || k === 'respect_gain') return (a) => Number(a.respect_gain || a.respect_gain_no_bonus || 0) || 0;
                        if (k === 'respect_gain_no_bonus') return (a) => Number(a.respect_gain_no_bonus || 0) || 0;
                        if (k === 'respect_loss') return (a) => Number(a.respect_loss || 0) || 0;
                        if (k === 'chain' || k === 'chain_val') return (a) => Number(a.modifiers?.chain || a.chain || 1) || 1;
                        if (k === 'chain_gap' || k === 'time_since_last_attack') return (a) => Number(a.time_since_last_attack || a.chain_gap || 0) || 0;
                        // Fallback: try to read raw field
                        return (a) => {
                            const v = a[k];
                            if (v == null) return '';
                            if (typeof v === 'number') return v;
                            return String(v).toLowerCase();
                        };
                    };
                    const acc = accessor(sortKey);
                    const sorted = filteredAttacks.slice().sort((a, b) => {
                        try {
                            const A = acc(a); const B = acc(b);
                            if (A === B) return 0;
                            // numeric compare when both numbers
                            if (typeof A === 'number' && typeof B === 'number') return sortAsc ? (A - B) : (B - A);
                            return sortAsc ? (A > B ? 1 : -1) : (A < B ? 1 : -1);
                        } catch(_) { return 0; }
                    });
                    const totalPages = Math.max(1, Math.ceil(sorted.length / attacksPerPage));
                    currentPage = Math.max(1, Math.min(currentPage, totalPages));
                    const pageAttacks = sorted.slice((currentPage - 1) * attacksPerPage, (currentPage - 1) * attacksPerPage + attacksPerPage);
                    const rows = pageAttacks.map(a => {
                        const attackerFaction = a.attacker?.faction?.id?.toString();
                        const defenderFaction = a.defender?.faction?.id?.toString();
                        // Time: local time from ended (seconds -> ms)
                        const timeLocal = (a.ended || a.started) ? new Date((a.ended || a.started) * 1000).toLocaleString() : '';
                        const attackerLink = utils.createElement('a', { href: `/profiles.php?XID=${a.attacker?.id}`, textContent: a.attacker?.name || a.attacker?.id || '', className: `t-blue ${factionClass(attackerFaction)}` });
                        const defenderLink = utils.createElement('a', { href: `/profiles.php?XID=${a.defender?.id}`, textContent: a.defender?.name || a.defender?.id || '', className: `t-blue ${factionClass(defenderFaction)}` });
                        const modifiersStr = a.modifiers ? JSON.stringify(a.modifiers) : '';
                        
                        // New fields
                        const attackerFactionName = a.attacker?.faction?.name || a.attackerFactionName || a.attacker_faction_name || '';
                        const defenderFactionName = a.defender?.faction?.name || a.defenderFactionName || a.defender_faction_name || '';
                        const attackerStatus = a.attacker_status || '';
                        const attackerActivity = a.attacker_activity || '';
                        const attackerLastAction = a.attacker_last_action_ts ? new Date(a.attacker_last_action_ts * 1000).toLocaleString() : '';
                        const attackerLADiff = a.attacker_la_diff != null ? a.attacker_la_diff : '';
                        const defenderStatus = a.defender_status || '';
                        const defenderActivity = a.defender_activity || '';
                        const defenderLastAction = a.defender_last_action_ts ? new Date(a.defender_last_action_ts * 1000).toLocaleString() : '';
                        const defenderLADiff = a.defender_la_diff != null ? a.defender_la_diff : '';
                        return {
                            time: timeLocal,
                            log: a.code ? utils.createElement('a', { href: `https://www.torn.com/loader.php?sid=attackLog&ID=${a.code}`, target: '_blank', textContent: 'view' }) : '',
                            attacker: attackerLink,
                            attacker_faction: attackerFactionName,
                            attacker_status: attackerStatus,
                            attacker_activity: attackerActivity,
                            attacker_last_action: attackerLastAction,
                            attacker_la_diff: attackerLADiff,
                            defender: defenderLink,
                            defender_faction: defenderFactionName,
                            defender_status: defenderStatus,
                            defender_activity: defenderActivity,
                            defender_last_action: defenderLastAction,
                            defender_la_diff: defenderLADiff,
                            result: a.result || '',
                            direction: a.direction || '',
                            // Treat as ranked war when Torn API flag present or war modifier indicates a war context
                            is_ranked_war: !!(a.is_ranked_war || a.isRankedWar || a.isRankedWar === true || a.is_ranked_war === true || Number(a?.modifiers?.war || 0) === 2),
                            is_stealthed: !!(a.is_stealthed || a.isStealthed || a.is_stealthed === true),
                            chain: (typeof a.chain !== 'undefined' ? a.chain : (a.modifiers?.chain || '')),
                            chain_saver: a.chain_saver ? 'Yes' : '',
                            time_since_last_attack: a.time_since_last_attack || a.timeSinceLastAttack || '',
                            respect_gain: Number(a.respect_gain || 0).toFixed(2),
                            respect_gain_no_bonus: Number(a.respect_gain_no_bonus || a.respect_gain_no_bonus || 0).toFixed(2),
                            respect_gain_no_chain: Number(a.respect_gain_no_chain || 0).toFixed(2),
                            modifiers: modifiersStr,
                            __attack: a
                        };
                    });
                    return { rows, total: filteredAttacks.length, totalPages };
                };
                // Fixed columns in the exact order requested by user
                // Order: time (local ended), log (code), attacker.name (link colored), defender (link colored), result, direction,
                // is_ranked_war, is_stealthed, chain, chain_saver, time_since_last_attack, respect_gain, respect_gain_no_bonus, respect_gain_no_chain, modifiers
                const allColumns = [
                    { key: 'time', label: 'Time' },
                    { key: 'log', label: 'Log', align: 'center' },
                    { key: 'attacker', label: 'Attacker' },
                    { key: 'attacker_faction', label: 'AtkFaction' },
                    { key: 'attacker_status', label: 'AtkStatus' },
                    { key: 'defender', label: 'Defender' },
                    { key: 'defender_faction', label: 'DefFaction' },
                    { key: 'defender_status', label: 'DefStatus' },
                    { key: 'result', label: 'Result' },
                    { key: 'direction', label: 'Direction', align: 'center' },
                    { key: 'is_ranked_war', label: 'RankedWar', align: 'center' },
                    { key: 'is_stealthed', label: 'Stealthed', align: 'center' },
                    { key: 'chain', label: 'Chain', align: 'center' },
                    { key: 'chain_saver', label: 'ChainSaver', align: 'center' },
                    { key: 'time_since_last_attack', label: 'ChainCountdown(sec)', align: 'center' },
                    { key: 'respect_gain', label: 'Respect', align: 'center' },
                    { key: 'respect_gain_no_bonus', label: 'RespectNoBonus', align: 'center' },
                    { key: 'respect_gain_no_chain', label: 'RespectNoChain', align: 'center' },
                    { key: 'attacker_activity', label: 'AtkActivity' },
                    { key: 'attacker_last_action', label: 'AtkLastAction' },
                    { key: 'attacker_la_diff', label: 'AtkLADiff' },
                    { key: 'defender_activity', label: 'DefActivity' },
                    { key: 'defender_last_action', label: 'DefLastAction' },
                    { key: 'defender_la_diff', label: 'DefLADiff' },
                    { key: 'modifiers', label: 'Modifiers' }
                ];

                // Column visibility state
                const visibleColumnKeys = new Set(allColumns.map(c => c.key));
                
                // Create Column Toggler
                const columnToggleWrap = utils.createElement('div', { style: { position: 'relative', display: 'inline-block', marginLeft: '10px' } });
                const columnToggleBtn = utils.createElement('button', { className: 'settings-btn', textContent: 'Columns \u25BC', style: { padding: '2px 8px', fontSize: '12px' } });
                const columnDropdown = utils.createElement('div', { 
                    style: { 
                        display: 'none', position: 'absolute', top: '100%', right: '0', 
                        backgroundColor: '#222', border: '1px solid #444', padding: '10px', 
                        zIndex: '1000', maxHeight: '300px', overflowY: 'auto', minWidth: '200px',
                        boxShadow: '0 4px 8px rgba(0,0,0,0.5)', borderRadius: '4px'
                    } 
                });
                
                allColumns.forEach(col => {
                    const label = utils.createElement('label', { style: { display: 'block', marginBottom: '4px', cursor: 'pointer', color: '#ddd', fontSize: '12px', userSelect: 'none' } });
                    const cb = utils.createElement('input', { type: 'checkbox', style: { marginRight: '6px' } });
                    cb.checked = visibleColumnKeys.has(col.key);
                    cb.onchange = () => {
                        if (cb.checked) visibleColumnKeys.add(col.key); else visibleColumnKeys.delete(col.key);
                        renderAll();
                    };
                    label.appendChild(cb);
                    label.appendChild(document.createTextNode(col.label));
                    columnDropdown.appendChild(label);
                });

                columnToggleBtn.onclick = (e) => {
                    e.stopPropagation();
                    columnDropdown.style.display = columnDropdown.style.display === 'none' ? 'block' : 'none';
                };
                const closeDropdownHandler = (e) => {
                    if (!columnToggleWrap.contains(e.target)) columnDropdown.style.display = 'none';
                };
                document.addEventListener('click', closeDropdownHandler);
                columnToggleWrap.appendChild(columnDropdown);
                columnToggleWrap.appendChild(columnToggleBtn);

                clearLoading();
                let paginationBar = utils.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '10px' } });
                const renderAll = () => {
                    const { rows, total, totalPages } = computeRows();
                    try { console.debug('[WarAttacks] renderAll', { currentPage, attacksPerPage, total, totalPages, sortKey, sortAsc, dynamicFilters }); } catch(_) {}
                    
                    const visibleColumns = allColumns.filter(c => visibleColumnKeys.has(c.key));

                    tableWrap.innerHTML = '';
                    ui.renderReportTable(tableWrap, { columns: visibleColumns, rows, tableId: 'war-attacks-table', manualSort: true, defaultSort: { key: sortKey, asc: sortAsc }, onSortChange: (k, asc) => { sortKey = k; sortAsc = asc; currentPage = 1; renderAll(); } });
                    paginationBar.innerHTML = '';
                    // Previous button (no stale closure on totalPages)
                    const prevBtn = utils.createElement('button', { className: 'settings-btn', textContent: 'Previous' });
                    // Ensure button semantics
                    try { prevBtn.type = 'button'; } catch(_) {}
                    const prevHandler = () => { try { console.debug('[WarAttacks] Prev clicked', { currentPage, totalPages }); currentPage = Math.max(1, Number(currentPage) - 1); renderAll(); } catch(e){ console.error('[WarAttacks][Prev] handler error', e); } };
                    prevBtn.addEventListener('click', prevHandler);
                    // Fallback for environments that replace event listeners
                    prevBtn.onclick = prevHandler;
                    prevBtn.disabled = (currentPage === 1);
                    prevBtn.style.cursor = prevBtn.disabled ? 'not-allowed' : 'pointer';
                    prevBtn.style.pointerEvents = 'auto';
                    prevBtn.tabIndex = 0;
                    paginationBar.appendChild(prevBtn);
                    // Middle status + export
                    const statusWrap = utils.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: '8px' } });
                    statusWrap.appendChild(utils.createElement('span', { textContent: `Page ${currentPage} of ${totalPages} (${total} total)` }));
                    const exportBtn = utils.createElement('button', { className: 'settings-btn', textContent: 'Export CSV', onclick: () => {
                        try {
                            // Build full (filtered, sorted) list using same accessor used by computeRows
                            const filtered = allAttacks.filter(a => {
                                if (Array.isArray(dynamicFilters) && dynamicFilters.length) {
                                    for (const f of dynamicFilters) { if (!evaluateFilter(a, f)) return false; }
                                }
                                return true;
                            });
                            const accessor = (k) => {
                                if (!k || k === 'attackTime' || k === 'time') return (a) => Number(a.ended || a.started || 0) || 0;
                                if (k === 'attacker') return (a) => String(a.attacker?.name || '').toLowerCase();
                                if (k === 'defender') return (a) => String(a.defender?.name || '').toLowerCase();
                                if (k === 'result') return (a) => String(a.result || '').toLowerCase();
                                if (k === 'resDelta' || k === 'respect_gain') return (a) => Number(a.respect_gain || a.respect_gain_no_bonus || 0) || 0;
                                if (k === 'respect_gain_no_bonus') return (a) => Number(a.respect_gain_no_bonus || 0) || 0;
                                if (k === 'respect_loss') return (a) => Number(a.respect_loss || 0) || 0;
                                if (k === 'chain' || k === 'chain_val') return (a) => Number(a.modifiers?.chain || a.chain || 1) || 1;
                                if (k === 'chain_gap' || k === 'time_since_last_attack') return (a) => Number(a.time_since_last_attack || a.chain_gap || 0) || 0;
                                return (a) => {
                                    const v = a[k];
                                    if (v == null) return '';
                                    if (typeof v === 'number') return v;
                                    return String(v).toLowerCase();
                                };
                            };
                            const acc = accessor(sortKey);
                            const sorted = filtered.slice().sort((a, b) => {
                                try {
                                    const A = acc(a); const B = acc(b);
                                    if (A === B) return 0;
                                    if (typeof A === 'number' && typeof B === 'number') return sortAsc ? (A - B) : (B - A);
                                    return sortAsc ? (A > B ? 1 : -1) : (A < B ? 1 : -1);
                                } catch(_) { return 0; }
                            });
                            // CSV header in exact display order (expand attacker/defender to id+name for export)
                            const header = ['Time','Log','AttackerId','Attacker','AtkFaction','AtkStatus','DefenderId','Defender','DefFaction','DefStatus','Result','Direction','is_ranked_war','is_stealthed','Chain','ChainSaver','TimeSinceLastAttack','RespectGain','RespectGainNoBonus','RespectGainNoChain','AtkActivity','AtkLastAction','AtkLADiff','DefActivity','DefLastAction','DefLADiff','Modifiers'];
                            const csvLines = [header.join(',')];
                            const csvEscape = (v) => {
                                if (v === null || typeof v === 'undefined') return '';
                                const s = String(v);
                                return /[",\n]/.test(s) ? '"' + s.replace(/"/g,'""') + '"' : s;
                            };
                            for (const a of sorted) {
                                const endedIso = (a.ended || a.started) ? new Date((a.ended||a.started)*1000).toISOString() : '';
                                const modifiersStr = a.modifiers ? JSON.stringify(a.modifiers) : '';
                                const row = [
                                    endedIso,
                                    a.code || '',
                                    a.attacker?.id || '',
                                    a.attacker?.name || '',
                                    a.attacker?.faction?.name || a.attackerFactionName || a.attacker_faction_name || '',
                                    a.attacker_status || '',
                                    a.attacker_activity || '',
                                    a.attacker_last_action_ts ? new Date(a.attacker_last_action_ts * 1000).toISOString() : '',
                                    a.attacker_la_diff != null ? a.attacker_la_diff : '',
                                    a.defender?.id || '',
                                    a.defender?.name || '',
                                    a.defender?.faction?.name || a.defenderFactionName || a.defender_faction_name || '',
                                    a.defender_status || '',
                                    a.defender_activity || '',
                                    a.defender_last_action_ts ? new Date(a.defender_last_action_ts * 1000).toISOString() : '',
                                    a.defender_la_diff != null ? a.defender_la_diff : '',
                                    a.result || '',
                                    a.direction || '',
                                    a.is_ranked_war ? 'Yes' : (a.isRankedWar ? 'Yes' : ''),
                                    a.is_stealthed ? 'Yes' : (a.isStealthed ? 'Yes' : ''),
                                    a.chain || '',
                                    a.chain_saver ? 'Yes' : '',
                                    a.time_since_last_attack || '',
                                    Number(a.respect_gain || 0).toFixed(2),
                                    Number(a.respect_gain_no_bonus || 0).toFixed(2),
                                    Number(a.respect_gain_no_chain || 0).toFixed(2),
                                    modifiersStr
                                ].map(csvEscape);
                                csvLines.push(row.join(','));
                            }
                            const blob = new Blob([csvLines.join('\n')], { type: 'text/csv;charset=utf-8;' });
                            const url = URL.createObjectURL(blob);
                            const aEl = document.createElement('a');
                            aEl.href = url;
                            aEl.download = `war_${warId || 'unknown'}_attacks_${Date.now()}.csv`;
                            document.body.appendChild(aEl);
                            aEl.click();
                            setTimeout(()=>{ URL.revokeObjectURL(url); aEl.remove(); }, 2500);
                        } catch(err) {
                            tdmlogger('warn', '[WarAttacks][ExportCSV] Failed', err);
                            ui.showTransientMessage('CSV export failed', { type: 'error' });
                        }
                    } });
                    statusWrap.appendChild(exportBtn);
                    statusWrap.appendChild(columnToggleWrap);
                    paginationBar.appendChild(statusWrap);
                    // Next button (simplified logic)
                    const nextBtn = utils.createElement('button', { className: 'settings-btn', textContent: 'Next' });
                    try { nextBtn.type = 'button'; } catch(_) {}
                    const nextHandler = () => { try { console.debug('[WarAttacks] Next clicked', { currentPage, totalPages }); currentPage = Math.min(Number(totalPages), Number(currentPage) + 1); renderAll(); } catch(e){ console.error('[WarAttacks][Next] handler error', e); } };
                    nextBtn.addEventListener('click', nextHandler);
                    nextBtn.onclick = nextHandler;
                    nextBtn.disabled = (currentPage >= totalPages);
                    nextBtn.style.cursor = nextBtn.disabled ? 'not-allowed' : 'pointer';
                    nextBtn.style.pointerEvents = 'auto';
                    nextBtn.tabIndex = 0;
                    paginationBar.appendChild(nextBtn);
                };
                controls.appendChild(paginationBar);
                perPageInput.addEventListener('change', (e)=>{ const v=parseInt(e.target.value)||1; attacksPerPage=Math.max(1,Math.min(5000,v)); currentPage=1; renderAll(); });
                renderAll();
                if (!footer.querySelector('#close-attacks-btn')) footer.appendChild(utils.createElement('button', { id: 'close-attacks-btn', className: 'settings-btn settings-btn-red', textContent: 'Close', style: { width: '100%' }, onclick: () => { modal.style.display = 'none'; } }));
            } catch(e) {
                setError(`Error loading attacks: ${e.message}`);
            }
        },
        
        showUnauthorizedAttacksModal: async function() {
            if (!state.unauthorizedAttacks || !state.unauthorizedAttacks.length) {
                await handlers.fetchUnauthorizedAttacks();
            }
            const { modal, controls, tableWrap, footer } = ui.createReportModal({ id: 'unauthorized-attacks-modal', title: 'Unauthorized Attacks', maxWidth: '820px' });
            controls.innerHTML = '';
            tableWrap.innerHTML = '';

            const attacks = Array.isArray(state.unauthorizedAttacks) ? state.unauthorizedAttacks.slice() : [];
            if (!attacks.length) {
                controls.appendChild(utils.createElement('div', { style: { textAlign: 'center', color: '#ccc', margin: '12px 0' }, textContent: 'No unauthorized attacks recorded.' }));
            } else {
                const rows = attacks.map((attack) => {
                    const attackTime = Number(attack.attackTime || attack.timestamp || attack.ended || 0);
                    const attackerId = String(attack.attackerUserId || attack.attackerId || attack.attacker || attack.attacker_id || '').trim();
                    const defenderId = String(attack.defenderUserId || attack.defenderId || attack.defender || attack.defender_id || '').trim();
                    const attackerNameRaw = (attack.attackerUsername || attack.attackerName || '').trim();
                    const defenderNameRaw = (attack.defenderUsername || attack.defenderName || '').trim();
                    return {
                        attackTime,
                        attackerId,
                        attackerName: attackerNameRaw || (attackerId ? `ID ${attackerId}` : 'Unknown'),
                        defenderId,
                        defenderName: defenderNameRaw || (defenderId ? `ID ${defenderId}` : 'Unknown'),
                        violation: attack.violationType || attack.reason || 'Policy violation',
                        result: attack.result || attack.outcome || ''
                    };
                });

                controls.appendChild(utils.createElement('div', {
                    style: { color: '#9ca3af', fontSize: '12px', marginBottom: '8px' },
                    textContent: `${rows.length} unauthorized attack${rows.length === 1 ? '' : 's'} recorded.`
                }));

                const columns = [
                    {
                        key: 'attackTime',
                        label: 'Time',
                        render: (row) => row.attackTime ? new Date(row.attackTime * 1000).toLocaleString() : '—',
                        sortValue: (row) => row.attackTime || 0
                    },
                    {
                        key: 'attackerName',
                        label: 'Attacker',
                        render: (row) => {
                            if (!row.attackerId) return row.attackerName;
                            return utils.createElement('a', {
                                href: `/profiles.php?XID=${row.attackerId}`,
                                textContent: row.attackerName,
                                style: { color: config.CSS.colors.error, textDecoration: 'underline', fontWeight: 'bold' }
                            });
                        },
                        sortValue: (row) => (row.attackerName || '').toLowerCase()
                    },
                    {
                        key: 'defenderName',
                        label: 'Defender',
                        render: (row) => {
                            if (!row.defenderId) return row.defenderName;
                            return utils.createElement('a', {
                                href: `/profiles.php?XID=${row.defenderId}`,
                                textContent: row.defenderName,
                                style: { color: '#ffffff', textDecoration: 'underline', fontWeight: 'bold' }
                            });
                        },
                        sortValue: (row) => (row.defenderName || '').toLowerCase()
                    },
                    {
                        key: 'violation',
                        label: 'Violation',
                        sortValue: (row) => (row.violation || '').toLowerCase()
                    },
                    {
                        key: 'result',
                        label: 'Result',
                        render: (row) => row.result || '—',
                        sortValue: (row) => (row.result || '').toLowerCase()
                    }
                ];

                ui.renderReportTable(tableWrap, { columns, rows, defaultSort: { key: 'attackTime', asc: false }, tableId: 'unauthorized-attacks-table' });
            }

            if (!footer.querySelector('#unauthorized-attacks-close-btn')) {
                footer.appendChild(utils.createElement('button', {
                    id: 'unauthorized-attacks-close-btn',
                    className: 'settings-btn settings-btn-red',
                    textContent: 'Close',
                    style: { width: '100%' },
                    onclick: () => { modal.style.display = 'none'; }
                }));
            }
        },
        showRankedWarSummaryModal: function(initialSummaryData, rankedWarId) {
            const { modal, header, controls, tableWrap, footer } = ui.createReportModal({ id: 'ranked-war-summary-modal', title: `Ranked War Summary (ID: ${rankedWarId})` });
            let summaryData = [];
            try {
                if (Array.isArray(initialSummaryData)) summaryData = initialSummaryData.slice();
                else if (initialSummaryData && Array.isArray(initialSummaryData.items)) {
                    summaryData = initialSummaryData.items.slice();
                    try { state.rankedWarLastSummaryMeta = state.rankedWarLastSummaryMeta || {}; state.rankedWarLastSummaryMeta.scoreBleed = initialSummaryData.scoreBleed || null; } catch(_) {}
                }
            } catch (_) { summaryData = Array.isArray(initialSummaryData) ? initialSummaryData.slice() : []; }
            const factionId = state.user.factionId;

            const formatAge = (ts) => {
                if (!ts) return '—';
                const ageMs = Date.now() - ts;
                if (ageMs < 0) return 'just now';
                const mins = Math.floor(ageMs / 60000);
                if (mins < 1) return '<1m ago';
                if (mins < 60) return `${mins}m ago`;
                const hrs = Math.floor(mins / 60);
                if (hrs < 24) return `${hrs}h ${mins % 60}m ago`;
                const days = Math.floor(hrs / 24);
                return `${days}d ${hrs % 24}h ago`;
            };

            const getSummaryFreshnessTs = () => {
                try {
                    const meta = state.rankedWarLastSummaryMeta || {};
                    const lm = meta.lastModified ? Date.parse(meta.lastModified) : 0;
                    // Fallback to max lastTs in rows
                    const maxLocal = summaryData.reduce((m,r)=>Math.max(m, Number(r.lastTs||0)*1000),0);
                    return Math.max(lm||0, maxLocal||0);
                } catch(_) { return 0; }
            };

            const renderEmpty = (reason = 'No summary data available for this war.') => {
                controls.innerHTML = '';
                tableWrap.innerHTML = '';
                controls.appendChild(utils.createElement('div', { style: { color: config.CSS.colors.error, marginBottom: '10px', textAlign:'center' }, textContent: reason }));
                const closeBtn = utils.createElement('button', { className: 'settings-btn settings-btn-red', textContent: 'Close', onclick: () => { modal.style.display = 'none'; }, style: { width: '100%' } });
                if (!footer.querySelector('#war-summary-close-btn')) footer.appendChild(closeBtn);
            };

            let activeFactionId = null;
            const buildFactionGroups = () => {
                const factionGroups = {};
                summaryData.forEach(attacker => {
                    const factionId = attacker.attackerFaction || attacker.attackerFactionId || 'Unknown';
                    const factionName = attacker.attackerFactionName || 'Unknown Faction';
                    if (!factionGroups[factionId]) {
                        factionGroups[factionId] = {
                            name: factionName,
                            attackers: [],
                            totalAttacks: 0,
                            totalAttacksScoring: 0,
                            totalFailedAttacks: 0,
                            totalRespect: 0,
                            totalRespectNoChain: 0,
                            totalRespectNoBonus: 0,
                            totalRespectLost: 0,
                            totalChainSavers: 0,
                            totalAssists: 0,
                            totalOutside: 0,
                            totalOverseas: 0,
                            totalRetaliations: 0
                        };
                    }
                    factionGroups[factionId].attackers.push(attacker);
                    factionGroups[factionId].totalAttacks += attacker.totalAttacks || 0;
                    factionGroups[factionId].totalAttacksScoring += attacker.totalAttacksScoring || 0;
                    factionGroups[factionId].totalFailedAttacks += attacker.failedAttackCount || 0;
                    factionGroups[factionId].totalRespect += attacker.totalRespectGain || 0;
                    factionGroups[factionId].totalRespectNoChain += attacker.totalRespectGainNoChain || 0;
                    factionGroups[factionId].totalRespectNoBonus += attacker.totalRespectGainNoBonus || 0;
                    factionGroups[factionId].totalRespectLost += attacker.totalRespectLoss || 0;
                    factionGroups[factionId].totalChainSavers += attacker.chainSaverCount || 0;
                    factionGroups[factionId].totalAssists += attacker.assistCount || 0;
                    factionGroups[factionId].totalOutside += attacker.outsideCount || 0;
                    factionGroups[factionId].totalOverseas += attacker.overseasCount || 0;
                    factionGroups[factionId].totalRetaliations += attacker.retaliationCount || 0;
                });
                return factionGroups;
            };

            // Show score-bleed summary when available
            try {
                const sb = state.rankedWarLastSummaryMeta?.scoreBleed || null;
                if (sb) {
                    const el = utils.createElement('div', { style: { marginBottom: '6px', color: config.CSS.colors.warning, textAlign: 'center', fontSize: '12px' }, textContent: `Score Bleed: ${sb.count || 0} offline-hits, ${sb.respect || 0} total respect` });
                    controls.appendChild(el);
                }
            } catch(_) {}

            const allColumns = [
                { key: 'attackerName', label: 'Name', render: (r) => { const a=document.createElement('a'); a.href=`/profiles.php?XID=${r.attackerId}`; a.textContent=r.attackerName||`ID ${r.attackerId}`; a.style.color=config.CSS.colors.success; a.style.textDecoration='underline'; return a; }, sortValue: (r) => (r.attackerName||'').toLowerCase() },
                { key: 'totalAttacks', label: 'Attacks', align: 'center', sortValue: (r) => Number(r.totalAttacks)||0 },
                { key: 'totalAttacksScoring', label: 'Scoring', align: 'center', sortValue: (r) => Number(r.totalAttacksScoring)||0 },
                { key: 'failedAttackCount', label: 'Failed', align: 'center', sortValue: (r) => Number(r.failedAttackCount)||0 },
                { key: 'totalRespectGain', label: 'Respect', align: 'center', render: (r)=> (Number(r.totalRespectGain||0)).toFixed(2), sortValue: (r)=>Number(r.totalRespectGain)||0 },
                { key: 'totalRespectGainNoChain', label: 'Respect (No Chain)', align: 'center', render: (r)=> (Number(r.totalRespectGainNoChain||0)).toFixed(2), sortValue: (r)=>Number(r.totalRespectGainNoChain)||0 },
                { key: 'totalRespectGainNoBonus', label: 'Respect (No Bonus)', align: 'center', render: (r)=> (Number(r.totalRespectGainNoBonus||0)).toFixed(2), sortValue: (r)=>Number(r.totalRespectGainNoBonus)||0 },
                { key: 'totalRespectLoss', label: 'Respect Lost', align: 'center', render: (r)=> (Number(r.totalRespectLoss||0)).toFixed(2), sortValue: (r)=>Number(r.totalRespectLoss)||0 },
                { key: 'averageRespectGain', label: 'Avg Respect', align: 'center', render: (r)=> (Number(r.averageRespectGain||0)).toFixed(2), sortValue: (r)=>Number(r.averageRespectGain)||0 },
                { key: 'averageRespectGainNoChain', label: 'Avg Respect (No Chain)', align: 'center', render: (r)=> (Number(r.averageRespectGainNoChain||0)).toFixed(2), sortValue: (r)=>Number(r.averageRespectGainNoChain)||0 },
                { key: 'averageRespectGainNoBonus', label: 'Avg Respect (No Bonus)', align: 'center', render: (r)=> (Number(r.averageRespectGainNoBonus||0)).toFixed(2), sortValue: (r)=>Number(r.averageRespectGainNoBonus)||0 },
                { key: 'chainSaverCount', label: 'Chain Savers', align: 'center', sortValue: (r)=>Number(r.chainSaverCount)||0 },
                { key: 'averageTimeSinceLastAttack', label: 'Avg Chain Gap (s)', align: 'center', render: (r)=> r.averageTimeSinceLastAttack > 0 ? (Number(r.averageTimeSinceLastAttack||0)).toFixed(1) : '', sortValue: (r)=>Number(r.averageTimeSinceLastAttack)||0 },
                { 
                    key: 'resultCounts', 
                    label: 'Results', 
                    align: 'center', 
                    render: (r) => { 
                        if (!r.resultCounts) return '';
                        const counts = r.resultCounts;
                        const mainResults = ['Mugged', 'Attacked', 'Hospitalized', 'Arrested', 'Bounty'].filter(result => counts[result] > 0);
                        const otherResults = Object.keys(counts).filter(result => !['Mugged', 'Attacked', 'Hospitalized', 'Arrested', 'Bounty'].includes(result) && counts[result] > 0);
                        const parts = [...mainResults.map(r => `${r}:${counts[r]}`), ...otherResults.map(r => `${r}:${counts[r]}`)];
                        const text = parts.join(', ');
                        const span = document.createElement('span');
                        span.textContent = text.length > 20 ? text.substring(0, 20) + '...' : text;
                        span.title = parts.join(' | ');
                        span.style.cursor = 'help';
                        return span;
                    }, 
                    sortValue: (r) => Object.values(r.resultCounts || {}).reduce((sum, count) => sum + count, 0) 
                },
                { key: 'assistCount', label: 'Assists', align: 'center', sortValue: (r)=>Number(r.assistCount)||0 },
                { key: 'outsideCount', label: 'Outside', align: 'center', sortValue: (r)=>Number(r.outsideCount)||0 },
                { key: 'overseasCount', label: 'Overseas', align: 'center', sortValue: (r)=>Number(r.overseasCount)||0 },
                { key: 'retaliationCount', label: 'Retals', align: 'center', sortValue: (r)=>Number(r.retaliationCount)||0 },
                { key: 'averageModifiers.fair_fight', label: 'FF', align: 'center', render: (r)=> (Number(r.averageModifiers?.fair_fight||0)).toFixed(2), sortValue: (r)=>Number(r.averageModifiers?.fair_fight)||0 }
            ];

            // Column visibility state
            const visibleColumnKeys = new Set(allColumns.map(c => c.key));
            
            // Create Column Toggler
            const columnToggleWrap = utils.createElement('div', { style: { position: 'relative', display: 'inline-block', marginLeft: '10px', float: 'right' } });
            const columnToggleBtn = utils.createElement('button', { className: 'settings-btn', textContent: 'Columns \u25BC', style: { padding: '2px 8px', fontSize: '12px' } });
            const columnDropdown = utils.createElement('div', { 
                style: { 
                    display: 'none', position: 'absolute', top: '100%', right: '0', 
                    backgroundColor: '#222', border: '1px solid #444', padding: '10px', 
                    zIndex: '1000', maxHeight: '300px', overflowY: 'auto', minWidth: '200px',
                    boxShadow: '0 4px 8px rgba(0,0,0,0.5)', borderRadius: '4px'
                } 
            });
            
            allColumns.forEach(col => {
                const label = utils.createElement('label', { style: { display: 'block', marginBottom: '4px', cursor: 'pointer', color: '#ddd', fontSize: '12px', userSelect: 'none' } });
                const cb = utils.createElement('input', { type: 'checkbox', style: { marginRight: '6px' } });
                cb.checked = visibleColumnKeys.has(col.key);
                cb.onchange = () => {
                    if (cb.checked) visibleColumnKeys.add(col.key); else visibleColumnKeys.delete(col.key);
                    if (currentFactionGroups) renderFactionTable(currentFactionGroups);
                };
                label.appendChild(cb);
                label.appendChild(document.createTextNode(col.label));
                columnDropdown.appendChild(label);
            });

            columnToggleBtn.onclick = (e) => {
                e.stopPropagation();
                columnDropdown.style.display = columnDropdown.style.display === 'none' ? 'block' : 'none';
            };
            const closeDropdownHandler = (e) => {
                if (!columnToggleWrap.contains(e.target)) columnDropdown.style.display = 'none';
            };
            document.addEventListener('click', closeDropdownHandler);
            columnToggleWrap.appendChild(columnDropdown);
            columnToggleWrap.appendChild(columnToggleBtn);

            let tabsContainer = null;
            let totalsWrap = null;
            let currentTable = null;
            let currentFactionGroups = null;

            const renderFactionTable = (factionGroups) => {
                currentFactionGroups = factionGroups;
                const f = factionGroups[activeFactionId];
                if (!f) return;
                
                const visibleColumns = allColumns.filter(c => visibleColumnKeys.has(c.key));

                totalsWrap.innerHTML = '';
                totalsWrap.appendChild(utils.createElement('span', { style: { marginRight:'5px' }, innerHTML: `<strong>Total Attacks:</strong> ${f.totalAttacks}` }));
                totalsWrap.appendChild(utils.createElement('span', { style: { marginRight:'5px' }, innerHTML: `<strong>Total Respect:</strong> ${f.totalRespect.toFixed(2)}` }));
                totalsWrap.appendChild(utils.createElement('span', { style: { marginRight:'5px' }, innerHTML: `<strong>Total Respect Lost:</strong> ${(f.totalRespectLost||0).toFixed(2)}` }));
                totalsWrap.appendChild(utils.createElement('span', { innerHTML: `<strong>Total Assists:</strong> ${f.totalAssists}` }));
                currentTable = ui.renderReportTable(tableWrap, { columns: visibleColumns, rows: f.attackers, defaultSort: { key:'totalAttacks', asc:false } });
            };

            const buildTabs = (factionGroups) => {
                tabsContainer = utils.createElement('div', { id:'faction-tabs-container', style:{ display:'flex', marginBottom:'10px', borderBottom:'1px solid #444', gap:'6px', flexWrap:'wrap' } });
                const factionIds = Object.keys(factionGroups);
                if (!activeFactionId) activeFactionId = factionIds[0];
                factionIds.forEach(fid => {
                    const isActive = fid === activeFactionId;
                    const btn = utils.createElement('button', {
                        'data-faction-id': fid,
                        className: `faction-tab ${isActive ? 'active-tab':''}`,
                        textContent: `${factionGroups[fid].name} (${factionGroups[fid].attackers.length})`,
                        style: { padding:'6px 8px', backgroundColor: isActive?config.CSS.colors.success:'#555', color:'white', border:'none', borderTopLeftRadius:'4px', borderTopRightRadius:'4px', cursor:'pointer' },
                        onclick: (e)=>{ activeFactionId = fid; tabsContainer.querySelectorAll('.faction-tab').forEach(b=>{ b.style.backgroundColor='#555'; b.classList.remove('active-tab'); }); e.currentTarget.style.backgroundColor = config.CSS.colors.success; e.currentTarget.classList.add('active-tab'); renderFactionTable(factionGroups); }
                    });
                    tabsContainer.appendChild(btn);
                });
                controls.appendChild(tabsContainer);
            };

            const renderMetaBar = () => {
                const wrap = utils.createElement('div', { style:{ display:'flex', flexWrap:'wrap', gap:'8px', alignItems:'center', justifyContent:'space-between', marginBottom:'8px', fontSize:'12px', background:'#181818', padding:'6px 8px', borderRadius:'6px' } });
                const src = state.rankedWarLastSummarySource || 'unknown';
                const meta = state.rankedWarLastSummaryMeta || {};
                const attacksSrc = state.rankedWarLastAttacksSource || 'unknown';
                const freshnessTs = getSummaryFreshnessTs();
                const ageStr = formatAge(freshnessTs);
                const warActive = !!utils.isWarActive(rankedWarId);
                const stale = warActive && freshnessTs && (Date.now() - freshnessTs > 5*60*1000);
                const sourceLine = utils.createElement('div', { style:{ display:'flex', flexDirection:'column', gap:'2px', flex:'1 1 260px' } });
                sourceLine.appendChild(utils.createElement('div', { innerHTML: `<strong>Summary Source:</strong> ${src}${meta.count?` (${meta.count})`:''}` }));
                sourceLine.appendChild(utils.createElement('div', { innerHTML: `<strong>Attacks Source:</strong> ${attacksSrc}` }));
                sourceLine.appendChild(utils.createElement('div', { innerHTML: `<strong>Freshness:</strong> ${ageStr}${stale?` <span style='color:#f59e0b;'>(stale)</span>`:''}` }));
                wrap.appendChild(sourceLine);
                const btnWrap = utils.createElement('div', { style:{ display:'flex', gap:'6px', alignItems:'center' } });
                const refreshBtn = utils.createElement('button', { id:'war-summary-refresh-btn', className:'settings-btn settings-btn-blue', textContent:'Refresh', title:'Re-fetch summary choosing freshest (local vs storage).', onclick: ()=>doRefresh(false, refreshBtn) });
                const hardBtn = utils.createElement('button', { id:'war-summary-hard-refresh-btn', className:'settings-btn', textContent:'Hard Refresh', title:'Force manifest/attacks pull then rebuild summary locally.', style:{ background:'#666' }, onclick: ()=>doRefresh(true, hardBtn) });
                const exportBtn = utils.createElement('button', { id:'war-summary-export-btn', className:'settings-btn settings-btn-green', textContent:'Export CSV', title:'Download CSV for current faction tab.', onclick: exportCurrentFaction });
                btnWrap.appendChild(refreshBtn); btnWrap.appendChild(hardBtn);
                btnWrap.appendChild(exportBtn);
                wrap.appendChild(btnWrap);
                controls.appendChild(wrap);
            };

            let refreshing = false;
            const doRefresh = async (force, btn) => {
                if (refreshing) return; refreshing = true;
                const original = btn.textContent; btn.disabled = true; btn.innerHTML = '<span class="dibs-spinner"></span>';
                try {
                    // Pull attacks (onDemand). Force manifest optionally
                    try { await api.getRankedWarAttacksSmart(rankedWarId, factionId, { onDemand: true, forceManifest: !!force }); } catch(_) {}
                    let fresh = await api.getRankedWarSummaryFreshest(rankedWarId, factionId).catch(()=>[]);
                    if ((!fresh || fresh.length === 0) && state.rankedWarAttacksCache?.[String(rankedWarId)]?.attacks?.length) {
                        // Fallback to local aggregation if server/remote summary empty
                        try { fresh = await api.getRankedWarSummaryLocal(rankedWarId, factionId); } catch(_) {}
                    }
                    if (Array.isArray(fresh)) summaryData = fresh.slice();
                    renderAll();
                } catch(e) {
                    controls.appendChild(utils.createElement('div', { style:{ color:config.CSS.colors.error, fontSize:'11px' }, textContent:`Refresh failed: ${e.message}` }));
                } finally {
                    btn.disabled = false; btn.textContent = original; refreshing = false;
                }
            };

            // CSV Export (current active faction)
            const exportCurrentFaction = () => {
                try {
                    const factionGroups = buildFactionGroups();
                    const grp = factionGroups[activeFactionId];
                    if (!grp || !Array.isArray(grp.attackers) || grp.attackers.length === 0) {
                        ui.showMessageBox('No data to export for current faction tab.', 'error');
                        return;
                    }
                    const rows = grp.attackers;
                    const headers = [
                        'attackerId','attackerName','attackerFactionId','attackerFactionName','totalAttacks','wins','losses','stalemates','totalRespectGain','totalRespectGainNoChain','totalRespectLoss','averageRespectGain','averageRespectGainNoChain','assistCount','outsideCount','overseasCount','retaliationCount','avgFairFight','lastTs'
                    ];
                    const csvEscape = (v) => {
                        if (v === null || typeof v === 'undefined') return '';
                        const s = String(v);
                        return /[",\n]/.test(s) ? '"' + s.replace(/"/g,'""') + '"' : s;
                    };
                    const lines = [];
                    lines.push(headers.join(','));
                    for (const r of rows) {
                        const line = [
                            r.attackerId,
                            r.attackerName,
                            r.attackerFactionId || '',
                            r.attackerFactionName || '',
                            r.totalAttacks || 0,
                            r.wins || 0,
                            r.losses || 0,
                            r.stalemates || 0,
                            (Number(r.totalRespectGain || 0)).toFixed(2),
                            (Number(r.totalRespectGainNoChain || 0)).toFixed(2),
                            (Number(r.totalRespectLoss || 0)).toFixed(2),
                            (Number(r.averageRespectGain || 0)).toFixed(2),
                            (Number(r.averageRespectGainNoChain || 0)).toFixed(2),
                            r.assistCount || 0,
                            r.outsideCount || 0,
                            r.overseasCount || 0,
                            r.retaliationCount || 0,
                            (Number(r.averageModifiers?.fair_fight || 0)).toFixed(2),
                            r.lastTs || 0
                        ].map(csvEscape).join(',');
                        lines.push(line);
                    }
                    const blob = new Blob([lines.join('\n')], { type: 'text/csv;charset=utf-8;' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    const safeFaction = (grp.name || 'faction').replace(/[^a-z0-9_-]+/gi,'_');
                    a.href = url;
                    a.download = `war_${rankedWarId}_${safeFaction}_${Date.now()}.csv`;
                    document.body.appendChild(a);
                    a.click();
                    setTimeout(()=>{ URL.revokeObjectURL(url); a.remove(); }, 2000);
                } catch(e) {
                    tdmlogger('error', `[CSV export failed] ${e}`);
                    ui.showMessageBox('CSV export failed: ' + (e.message || 'Unknown error'), 'error');
                }
            };

            const renderAll = () => {
                controls.innerHTML = '';
                tableWrap.innerHTML = '';
                if (!summaryData || summaryData.length === 0) { renderEmpty('No summary data (try Refresh).'); return; }
                renderMetaBar();
                controls.appendChild(columnToggleWrap);
                // name-resolution removed: no background enqueue
                const factionGroups = buildFactionGroups();
                if (!Object.keys(factionGroups).length) { renderEmpty('No grouped summary data.'); return; }
                totalsWrap = utils.createElement('div', { style:{ marginBottom:'6px', textAlign:'right', fontSize:'0.9em' } });
                controls.appendChild(totalsWrap);
                buildTabs(factionGroups);
                renderFactionTable(factionGroups);
                // Footer button (only once)
                if (!footer.querySelector('#war-summary-close-btn')) {
                    footer.appendChild(utils.createElement('button', { id:'war-summary-close-btn', className:'settings-btn settings-btn-red', textContent:'Close', style:{ width:'100%' }, onclick: ()=>{ modal.style.display='none'; } }));
                }
            };

            renderAll();
            // name-resolution event handler removed (no background resolution exists)
        },

        showAllRetaliationsNotification: function() {
        
            const opportunities = state.retaliationOpportunities;
            const activeOpportunities = Object.values(opportunities || {}).filter(opp => opp.timeRemaining > -60);

            if (activeOpportunities.length === 0) {
                ui.showMessageBox('No active retaliation opportunities available', 'info');
                return;
            }

            // Clean up any previous popups and timers
            let existingPopup = document.getElementById('tdm-retals-popup');
            if (existingPopup) existingPopup.remove();
            state.ui.retalTimerIntervals.forEach(id => { try { utils.unregisterInterval(id); } catch(_) {} });
            state.ui.retalTimerIntervals = [];

            const popupContent = utils.createElement('div', {
                style: {
                    position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#2c2c2c', border: '1px solid #333', borderRadius: '8px',
                    boxShadow: '0 4px 10px rgba(0,0,0,0.5)', padding: '15px', color: 'white', zIndex: 10001, maxWidth: '350px'
                }
            });

            const header = utils.createElement('h3', { textContent: 'Active Retaliation Opportunities', style: { marginTop: '0', marginBottom: '5px', textAlign: 'center' } });
            const list = utils.createElement('ul', { style: { listStyle: 'none', padding: '0', margin: '0', maxHeight: '300px', overflowY: 'auto' } });

            activeOpportunities.forEach(opp => {
                const timeLeftSpan = utils.createElement('span', { style: { color: '#ffcc00' } });
                const alertButton = utils.createElement('button', {
                    textContent: 'Send Alert',
                    style: { backgroundColor: '#ff5722', color: 'white', border: 'none', borderRadius: '4px', padding: '4px 8px', cursor: 'pointer', fontSize: '12px' },
                    onclick: () => ui.sendRetaliationAlert(opp.attackerId, opp.attackerName)
                });
                const listItem = utils.createElement('li', {
                    style: { marginBottom: '15px', padding: '10px', backgroundColor: '#1a1a1a', textAlign: 'center', borderRadius: '5px' }
                }, [
                    utils.createElement('div', {
                        innerHTML: `<a href="/profiles.php?XID=${opp.attackerId}" style="color:#ff6b6b;font-weight:bold;">${opp.attackerName}</a>
                                <span> attacked </span><a href="/profiles.php?XID=${opp.defenderId}" style="color:#ffffff;font-weight:bold;">${opp.defenderName}</a><span> - </span>`
                    }, [ timeLeftSpan ]), // Append the span element here
                    utils.createElement('div', { style: { marginTop: '8px' } }, [alertButton])
                ]);

                // --- COUNTDOWN TIMER LOGIC ---
                const updateCountdown = () => {
                    const timeRemaining = opp.retaliationEndTime - (Date.now() / 1000);
                    if (timeRemaining > 0) {
                        const minutes = Math.floor(timeRemaining / 60);
                        const seconds = Math.floor(timeRemaining % 60);
                        timeLeftSpan.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
                    } else {
                        timeLeftSpan.textContent = `Expired`;
                        timeLeftSpan.style.color = '#aaa';
                        alertButton.disabled = true;
                        alertButton.style.backgroundColor = '#777';
                        // Stop the timer after it expires
                        utils.unregisterInterval(intervalId);
                        // Remove the item 60 seconds after expiring
                        setTimeout(() => {
                            if (listItem.parentNode) {
                                listItem.parentNode.removeChild(listItem);
                                // If list is empty, close the popup
                                if (list.children.length === 0 && document.getElementById('tdm-retals-popup')) {
                                    document.getElementById('tdm-retals-popup').remove();
                                }
                            }
                        }, 60000);
                    }
                };
                
                updateCountdown(); // Initial call to set the time immediately
                const intervalId = utils.registerInterval(setInterval(updateCountdown, 1000));
                state.ui.retalTimerIntervals.push(intervalId);
                // --- END TIMER LOGIC ---
                
                list.appendChild(listItem);
            });
            
            const dismissButton = utils.createElement('button', {
                textContent: 'Dismiss',
                style: { backgroundColor: '#f44336', color: 'white', border: 'none', borderRadius: '4px', padding: '8px 20px', cursor: 'pointer', display: 'block', margin: '15px auto 0', fontSize: '14px' },
                onclick: (e) => {
                    e.currentTarget.closest('#tdm-retals-popup').remove();
                    // Clear all timers when the user dismisses the popup
                    state.ui.retalTimerIntervals.forEach(id => { try { utils.unregisterInterval(id); } catch(_) {} });
                    state.ui.retalTimerIntervals = [];
                }
            });

            // Only add the list if there are opportunities to show
            if (list.children.length > 0) {
                popupContent.appendChild(header);
                popupContent.appendChild(list);
            } else {
                popupContent.appendChild(utils.createElement('p', {textContent: 'No active retaliation opportunities.', style: {textAlign: 'center'}}));
            }
            
            popupContent.appendChild(dismissButton);
            const notification = utils.createElement('div', { id: 'tdm-retals-popup' }, [popupContent]);
            document.body.appendChild(notification);
        },
        showTDMAdoptionModal: async function() {
            const { modal, controls, tableWrap, footer, setLoading, clearLoading, setError } = ui.createReportModal({ id: 'tdm-adoption-modal', title: 'Faction TDM Adoption' });
            setLoading('Loading adoption stats...');

            // Helper: safe Firestore Timestamp/string/number -> Date or null
            const toSafeDate = (val) => {
                try {
                    if (!val) return null;
                    if (val instanceof Date) return isNaN(val.getTime()) ? null : val;
                    if (typeof val?.toMillis === 'function') return new Date(val.toMillis());
                    if (typeof val?._seconds === 'number') return new Date(val._seconds * 1000);
                    if (typeof val?.seconds === 'number') return new Date(val.seconds * 1000);
                    if (typeof val === 'number') return new Date(val);
                    if (typeof val === 'string') {
                        const d = new Date(val);
                        return isNaN(d.getTime()) ? null : d;
                    }
                } catch (_) { /* ignore */ }
                return null;
            };

            let tdmUsers = [];
            try {
                const apiUsers = await api.get('getTDMUsersByFaction', { factionId: state.user.factionId });
                tdmUsers = Array.isArray(apiUsers) ? apiUsers : [];
                tdmUsers = tdmUsers.map(u => ({ ...u, lastVerified: toSafeDate(u.lastVerified) }));
                tdmUsers = tdmUsers.filter(u => u.position !== 'Resting in Elysian Fields' && u.name !== 'Wunda');
            } catch (e) {
                tdmlogger('error', `[Error fetching TDM users] ${e}`);
                setError('Error fetching TDM user data.');
                return;
            }

            const members = Array.isArray(state.factionMembers) ? state.factionMembers : [];
            const merged = members.map(m => {
                // pick most recent record for this member (if any)
                const recs = tdmUsers.filter(u => String(u.tornId) === String(m.id));
                let mostRecent = null;
                if (recs.length > 0) {
                    mostRecent = recs.reduce((latest, current) => {
                        const a = toSafeDate(latest?.lastVerified);
                        const b = toSafeDate(current?.lastVerified);
                        return (b && (!a || b > a)) ? current : latest;
                    });
                }
                const lastVerified = toSafeDate(mostRecent?.lastVerified);
                return {
                    id: m.id,
                    name: m.name,
                    level: m.level,
                    days: m.days_in_faction,
                    position: m.position,
                    isTDM: !!mostRecent,
                    tdmVersion: mostRecent?.version || '',
                    last_action: new Date(Number(m.last_action?.timestamp || 0) * 1000) || '',
                    lastVerified: lastVerified && lastVerified.getTime() > 0 ? lastVerified : ''
                };
            });

            const adoptedCount = merged.filter(m => m.isTDM).length;
            const totalCount = merged.length;
            const percent = totalCount ? Math.round((adoptedCount / totalCount) * 100) : 0;

            clearLoading();

            // Progress summary
            const progressWrap = utils.createElement('div', { style: { marginBottom: '16px' } });
            progressWrap.appendChild(utils.createElement('div', { style: { fontSize: '1.1em' }, textContent: `${adoptedCount} of ${totalCount} members have installed TDM (${percent}%)` }));
            const bar = utils.createElement('div', { style: { background: '#333', borderRadius: '6px', height: '22px', width: '100%', marginTop: '8px', position: 'relative' } });
            bar.appendChild(utils.createElement('div', { style: { background: config.CSS.colors.success, height: '100%', borderRadius: '6px', width: `${percent}%`, transition: 'width 0.5s' } }));
            bar.appendChild(utils.createElement('div', { style: { position: 'absolute', left: '50%', top: '0', transform: 'translateX(-50%)', color: 'white', fontWeight: 'bold', lineHeight: '22px' }, textContent: `${percent}%` }));
            progressWrap.appendChild(bar);
            controls.appendChild(progressWrap);

            // Table
            const columns = [
                { key: 'name', label: 'Name', render: (m) => {
                    const a = utils.buildProfileLink(m.id, m.name || `ID ${m.id}`);
                    a.style.color = config.CSS.colors.success; a.style.textDecoration = 'underline';
                    return a;
                }, sortValue: (m) => (m.name || '').toLowerCase() },
                { key: 'level', label: 'Lvl', align: 'center', sortValue: (m) => Number(m.level) || 0 },
                { key: 'position', label: 'Position' },
                { key: 'days', label: 'Days', align: 'center', sortValue: (m) => Number(m.days) || 0 },
                { key: 'isTDM', label: 'TDM?', align: 'center', render: (m) => (m.isTDM ? '✅' : ''), sortValue: (m) => (m.isTDM ? 1 : 0) },
                { key: 'tdmVersion', label: 'Version' },
                { key: 'last_action', label: 'Last Action', render: (m) => (m.last_action ? m.last_action.toLocaleDateString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : ''), sortValue: (m) => (m.last_action instanceof Date ? m.last_action.getTime() : 0) },
                { key: 'lastVerified', label: 'Last Verified', render: (m) => (m.lastVerified ? m.lastVerified.toLocaleDateString(undefined, { year: 'numeric', month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' }) : ''), sortValue: (m) => (m.lastVerified instanceof Date ? m.lastVerified.getTime() : 0) }
            ];

            ui.renderReportTable(tableWrap, { columns, rows: merged, defaultSort: { key: 'lastVerified', asc: false }, tableId: 'tdm-adoption-table' });

            // Footer note and dismiss
            footer.textContent = 'TDM = TreeDibsMapper userscript installed and verified with backend.';
            if (!modal.querySelector('#tdm-adoption-dismiss')) {
                const dismissBtn = utils.createElement('button', {
                    id: 'tdm-adoption-dismiss',
                    className: 'settings-btn settings-btn-red',
                    style: { marginTop: '8px', display: 'block', width: '100%' },
                    textContent: 'Dismiss',
                    onclick: () => { modal.style.display = 'none'; }
                });
                modal.appendChild(dismissBtn);
            }
        },
        openSettingsToApiKeySection: async ({ highlight = false, focusInput = false } = {}) => {
            try {
                const existed = document.getElementById('tdm-settings-popup');
                if (!existed) {
                    await ui.toggleSettingsPopup();
                } else {
                    ui.updateSettingsContent?.();
                }
                const content = document.getElementById('tdm-settings-content');
                if (!content) return;
                const section = content.querySelector('[data-section="api-keys"]');
                if (!section) return;
                if (section.classList.contains('collapsed')) {
                    section.classList.remove('collapsed');
                }
                const apiCard = content.querySelector('#tdm-api-key-card');
                if (highlight && apiCard) {
                    apiCard.dataset.highlighted = 'true';
                    apiCard.classList.add('tdm-api-key-highlight');
                    setTimeout(() => {
                        apiCard?.classList?.remove('tdm-api-key-highlight');
                        if (apiCard?.dataset) delete apiCard.dataset.highlighted;
                    }, 2500);
                }
                if (focusInput) {
                    const input = document.getElementById('tdm-api-key-input');
                    if (input) {
                        input.focus();
                        input.select?.();
                    }
                }
                apiCard?.scrollIntoView({ behavior: 'smooth', block: 'center' });
            } catch(_) {}
        }
    };

    state.events.on('script:admin-permissions-updated', () => {
        try {
            if (document.getElementById('tdm-settings-popup')) ui.updateSettingsContent?.();
        } catch (_) {}
        try {
            if (state.page.isFactionPage && state.dom.factionListContainer) ui.updateFactionPageUI(state.dom.factionListContainer);
        } catch (_) {}
        try {
            if (state.page.isAttackPage && typeof ui.injectAttackPageUI === 'function') {
                const maybePromise = ui.injectAttackPageUI();
                if (maybePromise && typeof maybePromise.catch === 'function') maybePromise.catch(() => {});
            }
        } catch (_) {}
    });

    //======================================================================
    // 6. EVENT HANDLERS & CORE LOGIC
    //======================================================================
    const handlers = {
        // Hard reset: purge legacy keys, indexedDB (async), in-memory tracking, and mark a guard so tracking doesn't auto-start mid-reset.
        performHardReset: async (opts={}) => {
            try {
                if (state._hardResetInProgress) return;
                state._hardResetInProgress = true;
                const factory = !!opts.factory;
                tdmlogger('info', '[Reset] Initiating hard reset');
                // Best-effort clear of long-lived timers/observers/listeners to avoid cross-reset leaks
                try { utils.cleanupAllResources(); } catch(_) {}
                // 1. Stop activity tracking loop if running
                try { handlers._teardownActivityTracking(); } catch(_) {}
                // 2. Clear unified status & phase history caches
                state.unifiedStatus = {};
                if (state._activityTracking) {
                    delete state._activityTracking._phaseHistory;
                    delete state._activityTracking._transitionLog;
                }
                // 4. Clear new structured keys (notes cache etc.) except user settings & api key (unless factory)
                // Normal reset: keep tdm.user.customApiKey, clear other tdm.* keys.
                // Factory reset: clear everything.
                const preserve = factory ? new Set() : new Set(['tdm.user.customApiKey','tdm.columnVisibility', 'tdm.columnWidths']);
                try {
                    const toRemove=[];
                    for (let i=0;i<localStorage.length;i++) {
                        const k = localStorage.key(i); if (!k) continue; if (preserve.has(k)) continue; if (k.startsWith('tdm')) toRemove.push(k);
                    }
                    toRemove.forEach(k=>{ try { localStorage.removeItem(k); } catch(_) {} });
                } catch(_) {}
                // 5. IndexedDB purge (best-effort) - known DB name(s) and tdm-store (kv)
                // Only performed on factory reset.
                let idbDeleted = false;
                if (factory) {
                    try {
                        // Delete tdm-store (kv) via ui._kv if available
                        if (typeof ui !== 'undefined' && ui._kv && typeof ui._kv.deleteDb === 'function') {
                            const ok = await ui._kv.deleteDb();
                            if (ok) idbDeleted = true;
                        }
                        const dbNames = ['TDM_DB','TDM_CACHE','tdm-store'];
                        for (const dbName of dbNames) {
                            await new Promise(res=>{ const req = indexedDB.deleteDatabase(dbName); req.onsuccess=()=>{ idbDeleted=true; res(); }; req.onerror=()=>res(); req.onblocked=()=>res(); });
                        }
                    } catch(_) { /* ignore */ }
                }
                // 6. Session markers for post-reset verification path
                sessionStorage.setItem('post_reset_check','1');
                if (idbDeleted) sessionStorage.setItem('post_reset_idb_deleted','1');
                // 7. Clear UI artifacts
                try { document.querySelectorAll('.tdm-travel-eta').forEach(el=>el.remove()); } catch(_) {}
                try {
                    const ov=document.getElementById('tdm-live-track-overlay');
                    if (ov) ov.remove();
                    ui.ensureDebugOverlayContainer?.({ passive: true, skipShow: true });
                } catch(_) {}
                // 8. Provide feedback (overlay style flash)
                try { alert(factory ? 'TreeDibsMapper: Factory reset complete. Reload to start fresh.' : 'TreeDibsMapper: Hard reset completed. Reload to reinitialize.'); } catch(_) {}
            } finally {
                state._hardResetInProgress = false;
            }
        },
        toggleLiveTrackDebugOverlay: (force) => {
            try {
                const current = !!storage.get('liveTrackDebugOverlayEnabled', false);
                const next = typeof force === 'boolean' ? force : !current;
                storage.set('liveTrackDebugOverlayEnabled', next);
                ui.ensureDebugOverlayContainer?.({ passive: true, skipShow: true });
                if (next) {
                    handlers._renderLiveTrackDebugOverlay?.();
                } else {
                    const overlay = document.getElementById('tdm-live-track-overlay');
                    if (overlay) overlay.remove();
                    state.ui.debugOverlayMinimizeEl = null;
                }
                const checkbox = document.getElementById('tdm-debug-overlay-toggle');
                if (checkbox) checkbox.checked = next;
            } catch (_) { /* non-fatal */ }
        },
        // Activity tracking helper methods (migrated from legacy live tracking)
        _didStateChange: (prev, curr) => {
            if (!prev) return true; if (!curr) return true;
            if (prev.canonical !== curr.canonical) return true;
            if (prev.dest !== curr.dest) return true;
            if (prev.arrivalMs !== curr.arrivalMs) return true;
            if (prev.startMs !== curr.startMs) return true;
            return false;
        },
        _maybeInjectLanded: (prev, curr) => {
            if (!prev || !curr) return null;
            const p = prev.canonical; const c = curr.canonical;
            if ((p === 'Travel' || p === 'Abroad' || p === 'Returning') && (c === 'Okay' || c === 'Idle')) {
                return { ...curr, canonical: 'Landed', transient: true, expiresAt: Date.now()+config.LANDED_TTL_MS };
            }
            return null;
        },
        // Per-player debounced phaseHistory persistence utilities
        _phaseHistoryWriteTimers: {},
        _writePhaseHistoryToKV: function(playerId){
            try {
                if (!storage.get('tdmActivityTrackingEnabled', false)) return Promise.resolve();
                if (!ui || !ui._kv) return Promise.resolve();
                if (!state._activityTracking || !state._activityTracking._phaseHistory) return Promise.resolve();
                const arr = state._activityTracking._phaseHistory[playerId] || [];
                const toSave = Array.isArray(arr) ? arr.slice(-100) : [];
                return ui._kv.setItem('tdm.phaseHistory.id_' + playerId, toSave).then(()=>{
                    if (storage.get('tdmDebugPersist', false)) tdmlogger('debug','[KV Persist] wrote phaseHistory for ' + playerId + ' len=' + toSave.length);
                }).catch(()=>{});
            } catch(e){ return Promise.resolve(); }
        },
        _schedulePhaseHistoryWrite: function(playerId){
            try {
                if (!playerId) return;
                if (this._phaseHistoryWriteTimers[playerId]) try { utils.unregisterTimeout(this._phaseHistoryWriteTimers[playerId]); } catch(_) {}
                this._phaseHistoryWriteTimers[playerId] = utils.registerTimeout(setTimeout(()=>{
                    delete this._phaseHistoryWriteTimers[playerId];
                    try { this._writePhaseHistoryToKV(playerId); } catch(_) {}
                }, 1000));
            } catch(e){}
        },
        _applyActivityTransitions: (transitions = []) => {
            if (!transitions || !transitions.length) return;
            const now = Date.now();
            state.unifiedStatus = state.unifiedStatus || {};

            // process incoming transitions (accept items of shape {id, state})
            const prevMap = {};
            for (const t of transitions) {
                const id = t.id || (t.playerId || t.pid);
                const incoming = t.state || t;
                if (!id || !incoming) continue;

                // Skip already-expired transients
                if (incoming.transient && incoming.expiresAt && incoming.expiresAt < now) continue;

                const existing = state.unifiedStatus[id] || {};
                prevMap[id] = existing;

                // Merge (preserve existing higher-confidence)
                const merged = { ...existing, ...incoming };
                const map = { LOW: 1, MED: 2, HIGH: 3 };
                if (existing.confidence && merged.confidence) {
                    if ((map[merged.confidence] || 0) < (map[existing.confidence] || 0)) merged.confidence = existing.confidence;
                }

                merged.updated = now;
                state.unifiedStatus[id] = merged;
            }

            // persist snapshot (debounced elsewhere)
            try { scheduleUnifiedStatusSnapshotSave(); } catch (_) {}

            // Minimal UI refresh across known row containers (ranked war + members lists)
            try {
                const selector = '.f-war-list .table-body .table-row, .members-cont .members-list li, #faction-war .table-body .table-row';
                const rows = document.querySelectorAll(selector);
                rows.forEach(row => {
                    try {
                        const link = row.querySelector('a[href*="profiles.php?XID="]');
                        if (!link) return; const pid = (link.href.match(/XID=(\d+)/) || [])[1]; if (!pid) return;
                        const rec = state.unifiedStatus[pid]; if (!rec) return;

                        const statusCell = row.querySelector('.tdm-status-cell') || row.querySelector('.status') || row.lastElementChild;
                        if (!statusCell) return;

                        const prevDataPhase = statusCell.dataset.tdmPhase;
                        const prevDataConf = statusCell.dataset.tdmConf;

                        // Early-leave detection: compare previous canonical status (from prevMap) to new
                        const prevRec = (typeof prevMap !== 'undefined' && prevMap[pid]) ? prevMap[pid] : null;
                        if (prevRec) {
                            const wasHospital = /hospital/i.test(String(prevRec.canonical || prevRec.rawState || prevRec.status || ''));
                            const nowOk = /okay/i.test(String(rec.canonical || rec.rawState || rec.status || ''));
                            // compute previous until ms if available
                            let prevUntilMs = 0;
                            try {
                                if (prevRec.rawUntil) prevUntilMs = (prevRec.rawUntil < 1000000000000 ? prevRec.rawUntil*1000 : prevRec.rawUntil);
                                else if (prevRec.arrivalMs) prevUntilMs = prevRec.arrivalMs;
                                else if (prevRec.until) prevUntilMs = (prevRec.until < 1000000000000 ? prevRec.until*1000 : prevRec.until);
                            } catch(_) { prevUntilMs = 0; }
                            if (wasHospital && nowOk && prevUntilMs && prevUntilMs > Date.now() + 30000) {
                                statusCell.classList.add('tdm-early-leave');
                                setTimeout(() => statusCell.classList.remove('tdm-early-leave'), 60000);
                            }
                            if (/hospital/i.test(String(rec.canonical || rec.rawState || rec.status || ''))) {
                                statusCell.classList.remove('tdm-early-leave');
                            }
                        }

                        // Format status via shared helper if available, fallback to canonical label
                        const meta = state.rankedWarChangeMeta ? state.rankedWarChangeMeta[pid] : null;
                        let label = rec.canonical || rec.phase || rec.status || '';
                        let remainingText = '';
                        let sortVal = '';
                        let subRank = 0;
                        if (ui && ui._formatStatus) {
                            try {
                                const out = ui._formatStatus(rec, meta) || {};
                                label = out.label || label;
                                remainingText = out.remainingText || '';
                                sortVal = out.sortVal || '';
                                subRank = out.subRank || 0;
                            } catch (_) {}
                        }

                        let disp = label + (remainingText || '');
                        
                        const currRendered = (statusCell.firstElementChild && statusCell.firstElementChild.textContent) || (statusCell.textContent || '');
                        if (currRendered !== disp || statusCell.dataset.tdmPhase !== (rec.canonical || rec.phase) || statusCell.dataset.tdmConf !== (rec.confidence || '')) {
                            // Update visible text when rendered display differs, or phase/confidence changed.
                            if (statusCell.firstElementChild) statusCell.firstElementChild.textContent = disp; else statusCell.textContent = disp;
                            statusCell.dataset.tdmPhase = rec.canonical || rec.phase || '';
                            statusCell.dataset.tdmConf = rec.confidence || '';
                        }

                        if (sortVal) statusCell.dataset.sortValue = sortVal; else delete statusCell.dataset.sortValue;
                        if (subRank > 0) statusCell.dataset.subRank = subRank; else delete statusCell.dataset.subRank;

                        // Special visual for Landed transients
                        const landed = ((rec.canonical||'').toLowerCase() === 'landed');
                        if (landed) {
                            statusCell.style.opacity = '0.85'; statusCell.style.transition = 'opacity 0.6s';
                            setTimeout(()=>{ try { if ((statusCell.textContent||'').includes('Landed')) statusCell.style.opacity = '1'; } catch(_){} }, 4800);
                        } else { statusCell.style.opacity = '1'; }
                    } catch(_) {}
                });
            } catch(_) {}

            // Notify listeners
            try { window.dispatchEvent(new CustomEvent('tdm:unifiedStatusUpdated', { detail: { ts: Date.now(), count: transitions.length } })); } catch(_) {}
        },
        _escalateConfidence: (prev, curr) => {
            // Basic heuristic: first observation -> LOW, consecutive identical with supporting fields -> MED, stable across 2+ ticks or with travel timing alignment -> HIGH.
            if (!curr) return curr;
            const ladder = ['LOW','MED','HIGH'];
            const map = { LOW:1, MED:2, HIGH:3 };
            const prevLevel = prev?.confidence || 'LOW';
            let next = prevLevel;
            // Determine base criteria
            const samePhase = prev && prev.canonical === curr.canonical;
            const isTravel = curr.canonical === 'Travel';
            const hasTiming = isTravel && curr.arrivalMs && curr.startMs;
            if (!prev) {
                next = 'LOW';
            } else if (samePhase) {
                if (prevLevel==='LOW') next = 'MED';
                else if (prevLevel==='MED') {
                    if (hasTiming) next = 'HIGH';
                }
            } else {
                const pCanon = prev.canonical; const cCanon = curr.canonical;
                const travelPair = (a,b) => (a==='Travel' && b==='Returning') || (a==='Returning' && b==='Travel');
                if (travelPair(pCanon, cCanon)) {
                    // Preserve momentum when bouncing between Travel and Returning (landing grace) states
                    next = prevLevel;
                    if (map[next] < map.MED && curr.dest) next = 'MED';
                    if (map[next] < map.HIGH && curr.arrivalMs && prev.arrivalMs && Math.abs((curr.arrivalMs||0)-(prev.arrivalMs||0))<=2000) next = 'HIGH';
                } else if (cCanon==='Landed' && (pCanon==='Travel'||pCanon==='Returning'||pCanon==='Abroad') && prevLevel==='HIGH') {
                    next='HIGH';
                } else {
                    next='LOW';
                }
            }
            // Witness escalation: if curr.witnessCount (future) or supporting evidence flagged
            if (curr.witness && map[next] < map.HIGH) next='HIGH';
            return { ...curr, confidence: next };
        },
        _expireTransients: () => {
            const unified = state.unifiedStatus||{}; const now = Date.now();
            let changed=false;
            for (const [id, rec] of Object.entries(unified)) {
                if (rec.canonical==='Landed' && rec.expiresAt && rec.expiresAt < now) {
                    // Replace with stable fallback (Okay) if still showing Landed
                    unified[id] = { ...rec, canonical:'Okay', confidence: rec.confidence||'MED' };
                    changed=true;
                }
            }
            if (changed) {
                state.unifiedStatus = unified;
                try { window.dispatchEvent(new CustomEvent('tdm:unifiedStatusUpdated', { detail: { ts: now, pruned:true } })); } catch(_) {}
            }
        },
        _renderLiveTrackDebugOverlay: () => {
            try {
                if (!storage.get('liveTrackDebugOverlayEnabled', false)) { ui.ensureDebugOverlayStyles?.(false); return; }
                const at = state._activityTracking; if (!at) { ui.ensureDebugOverlayStyles?.(false); return; }
                const el = ui.ensureDebugOverlayContainer({ passive: true });
                ui.ensureDebugOverlayContainer?.({ passive: true });
                const body = el._innerBodyRef || el; // backward safety
                const unified = state.unifiedStatus||{};
                const total = Object.keys(unified).length;
                const phCounts = { Trav:0, Abroad:0, Ret:0, Hosp:0, Jail:0, Landed:0 };
                const confCounts = { LOW:0, MED:0, HIGH:0 };
                const travelConf = { LOW:0, MED:0, HIGH:0 };
                let landedTransients=0;
                const destCounts = {};
                for (const rec of Object.values(unified)) {
                    const canon = rec?.canonical;
                    switch(canon){
                        case 'Travel': phCounts.Trav++; break;
                        case 'Abroad': phCounts.Abroad++; break;
                        case 'Returning': phCounts.Ret++; break;
                        case 'Hospital': phCounts.Hosp++; break;
                        case 'Jail': phCounts.Jail++; break;
                        case 'Landed': phCounts.Landed++; break;
                    }
                    if (rec?.confidence && confCounts[rec.confidence]!==undefined) confCounts[rec.confidence]++;
                    const canonPhase = (rec && (rec.canonical || rec.phase)) || '';
                    if (canonPhase === 'Travel' || canonPhase === 'Returning' || canonPhase === 'Abroad') {
                        travelConf[rec.confidence||'LOW'] = (travelConf[rec.confidence||'LOW']||0)+1;
                        if (rec.dest) destCounts[rec.dest] = (destCounts[rec.dest]||0)+1;
                    }
                    if (rec && rec.canonical === 'Landed' && rec.expiresAt && rec.expiresAt > Date.now()) { landedTransients++; }
                }
                // derive percentages
                const travelTotal = Object.values(travelConf).reduce((a,b)=>a+b,0);
                const pct = (n)=> travelTotal? ((n/travelTotal*100).toFixed(0)+'%'):'0%';
                // transitions per minute & avg diff
                const metrics = state._activityTracking.metrics || {};
                const now = Date.now();
                const windowMs = 5*60*1000; // 5m rolling window of transition timestamps if tracked
                state._activityTracking._recentTransitions = state._activityTracking._recentTransitions || [];
                // Prune old
                state._activityTracking._recentTransitions = state._activityTracking._recentTransitions.filter(t=> now - t < windowMs);
                const tpm = state._activityTracking._recentTransitions.length / (windowMs/60000);
                // Maintain rolling avg of diff times
                state._activityTracking._diffSamples = state._activityTracking._diffSamples || [];
                state._activityTracking._diffSamples.push(metrics.lastDiffMs||0);
                if (state._activityTracking._diffSamples.length>60) state._activityTracking._diffSamples.shift();
                const avgDiff = state._activityTracking._diffSamples.reduce((a,b)=>a+b,0)/(state._activityTracking._diffSamples.length||1);
                // Top destinations
                const topDests = Object.entries(destCounts).sort((a,b)=>b[1]-a[1]).slice(0,4).map(([d,c])=>`${d}:${c}`).join(' ');
                const totalTicks = metrics.totalTicks || 0;
                const signatureSkips = (metrics.signatureSkips != null ? metrics.signatureSkips : (metrics.skippedTicks || 0)) || 0;
                const skipRate = totalTicks ? ((signatureSkips / totalTicks) * 100) : 0;
                const throttleInf = metrics.bundleSkipsInFlight || 0;
                const throttleRecent = metrics.bundleSkipsRecent || 0;
                const fetchHits = metrics.fetchHits || 0;
                const fetchErrors = metrics.fetchErrors || 0;
                const lastFetch = metrics.lastFetchReason || 'n/a';
                const lastOutcome = metrics.lastTickOutcome || 'n/a';
                const skipped = signatureSkips;
                const lines = [];
                lines.push('ActivityTrack');
                lines.push(`members: ${total}`);
                lines.push(`counts: T:${phCounts.Trav} Ab:${phCounts.Abroad} R:${phCounts.Ret} Ld:${phCounts.Landed} H:${phCounts.Hosp} J:${phCounts.Jail}`);
                lines.push(`conf all: L:${confCounts.LOW} M:${confCounts.MED} H:${confCounts.HIGH}`);
                lines.push(`travel conf: H:${travelConf.HIGH||0}(${pct(travelConf.HIGH||0)}) M:${travelConf.MED||0}(${pct(travelConf.MED||0)}) L:${travelConf.LOW||0}(${pct(travelConf.LOW||0)})`);
                lines.push(`landed transients: ${landedTransients}`);
                // Validation stats (if available)
                if (metrics.validationLast) {
                    const v = metrics.validationLast;
                    lines.push(`validate: norm=${v.normalized} prunedEta=${v.etaPruned} malformed=${v.malformed}`);
                }
                // Drift and timing averages
                const avg = (arr)=> arr && arr.length ? (arr.reduce((a,b)=>a+b,0)/arr.length) : 0;
                const avgDrift = avg(metrics.driftSamples||[]);
                const avgTick = avg(state._activityTracking.metrics.tickSamples||[]);
                // Compute p95 & max for tick durations (rolling window)
                let p95='0.0', maxTick='0.0';
                try {
                    const samples = (state._activityTracking.metrics.tickSamples||[]).slice().sort((a,b)=>a-b);
                    if (samples.length) {
                        const idx = Math.min(samples.length-1, Math.floor(samples.length*0.95));
                        p95 = samples[idx].toFixed(1);
                        maxTick = samples[samples.length-1].toFixed(1);
                    }
                } catch(_) {}
                lines.push(`poll: last=${new Date(metrics.lastPoll).toLocaleTimeString()} tick=${(metrics.lastDiffMs||0).toFixed(1)}ms (api:${(metrics.lastApiMs||0).toFixed(1)} build:${(metrics.lastBuildMs||0).toFixed(1)} apply:${(metrics.lastApplyMs||0).toFixed(1)}) avg=${avgTick.toFixed(1)}ms p95=${p95}ms max=${maxTick}ms`);
                const driftWarnThreshold = (at.cadenceMs||10000) + 500;
                const driftLine = `rate: tpm=${tpm.toFixed(2)} skipped=${skipped}(${skipRate.toFixed(1)}%) drift=${(metrics.lastDriftMs||0).toFixed(1)}ms avgDrift=${avgDrift.toFixed(1)}ms`;
                if ((metrics.lastDriftMs||0) > driftWarnThreshold) {
                    lines.push(`<span style="color:#f87171" title="Drift exceeded cadence+500ms; consider reducing work per tick or increasing cadence.">${driftLine}</span>`);
                } else {
                    lines.push(driftLine);
                }
                const tickLine = `ticks: total=${totalTicks} sigSkip=${signatureSkips} throttle(if/rec)=${throttleInf}/${throttleRecent} fetches=${fetchHits}${fetchErrors ? ' err='+fetchErrors : ''}`;
                lines.push(tickLine);
                if (totalTicks && (signatureSkips/totalTicks) > 0.8) {
                    lines.push('<span style="color:#f59e0b">note: sigSkip grows when roster phases stay unchanged; fetch cadence shown above.</span>');
                }
                lines.push(`last tick: outcome=${lastOutcome} fetch=${lastFetch}`);
                if (topDests) lines.push(`top dest: ${topDests}`);
                // Confidence promotion counts (since page load)
                if (metrics.confPromos) {
                    const cp = metrics.confPromos;
                    lines.push(`promos: L->M:${cp.L2M||0} M->H:${cp.M2H||0}`);
                }
                // Diagnostics counters from singleton (script reinjection, ensures, badge updates)
                try {
                    if (window.__TDM_SINGLETON__) {
                        const diag = window.__TDM_SINGLETON__;
                        lines.push(`diag: reloads=${diag.reloads} overlayEns=${diag.overlayEnsures} dockEns=${diag.badgeDockEnsures} dibsDealsUpd=${diag.dibsDealsUpdates}`);
                    }
                } catch(_) {}
                // name-resolution queue processing removed
                if (metrics.warNameRes) {
                    const wn = metrics.warNameRes;
                    lines.push(`war names: pend:${(wn.queue&&wn.queue.length)||0} infl:${wn.inFlight||0} res:${wn.resolved||0} fail:${wn.failed||0}`);
                }
                // Cadence & store sizing
                const cadenceMs = at.cadenceMs || 10000;
                // Approximate unified status serialized size (in KB) - lightweight, skip if too frequent
                if (!metrics._lastSizeSample || (now - metrics._lastSizeSample) > 15000) {
                    try {
                        metrics._lastSizeSample = now;
                        metrics._lastUnifiedJsonLen = JSON.stringify(unified).length;
                    } catch(_) { metrics._lastUnifiedJsonLen = 0; }
                }
                const jsonLen = metrics._lastUnifiedJsonLen || 0;
                const approxKB = jsonLen ? (jsonLen/1024).toFixed(jsonLen>10240?1:2) : '0';
                lines.push(`cadence: ${(cadenceMs/1000).toFixed(1)}s store≈${approxKB}KB`);
                // Memory stats (browser support dependent)
                try {
                    state._activityTracking.metrics.memorySamples = state._activityTracking.metrics.memorySamples || [];
                    let memLine='';
                    if (performance && performance.memory) {
                        const { usedJSHeapSize, totalJSHeapSize } = performance.memory; // bytes
                        const usedMB = (usedJSHeapSize/1048576).toFixed(1);
                        const totalMB = (totalJSHeapSize/1048576).toFixed(1);
                        state._activityTracking.metrics.memorySamples.push(usedJSHeapSize);
                        if (state._activityTracking.metrics.memorySamples.length>60) state._activityTracking.metrics.memorySamples.shift();
                        const avgUsed = state._activityTracking.metrics.memorySamples.reduce((a,b)=>a+b,0)/state._activityTracking.metrics.memorySamples.length;
                        memLine = `mem: ${usedMB}/${totalMB}MB avg ${(avgUsed/1048576).toFixed(1)}MB`;
                    } else if (!metrics._memUnsupportedLogged) {
                        metrics._memUnsupportedLogged = true;
                        memLine = 'mem: n/a';
                    }
                    if (memLine) lines.push(memLine);
                } catch(_) { /* ignore memory */ }
                // Recent transition mini-log (last 5)
                if (state._activityTracking._transitionLog && state._activityTracking._transitionLog.length) {
                    const recent = state._activityTracking._transitionLog.slice(-5).reverse();
                    const nowTs = Date.now();
                    lines.push('recent:');
                    for (const tr of recent) {
                        const age = ((nowTs - tr.ts)/1000).toFixed(0);
                        lines.push(` ${tr.id}:${tr.from}->${tr.to} (${age}s)`);
                    }
                }
                const uiMetrics = state.metrics?.uiRankedWarUi;
                if (uiMetrics) {
                    const totalCalls = uiMetrics.total || 0;
                    const perMin = uiMetrics.perMinute || 0;
                    lines.push(`rw ui: total=${totalCalls} rpm=${perMin}`);
                    if (Array.isArray(uiMetrics.history) && uiMetrics.history.length) {
                        const hist = uiMetrics.history.slice(-45).map(ts => Math.max(0, Math.round((now - ts) / 1000)));
                        lines.push(`rw last45s: ${hist.join(',')}`);
                    }
                }
                const applyStats = state.metrics?.fetchApply;
                if (applyStats && Object.keys(applyStats).length) {
                    const top = Object.entries(applyStats)
                        .sort((a, b) => ((b[1]?.lastMs || 0) - (a[1]?.lastMs || 0)))
                        .slice(0, 3);
                    if (top.length) {
                        lines.push('fgd apply (last/avg/max ms):');
                        for (const [key, stat] of top) {
                            if (!stat) continue;
                            const avg = stat.totalMs && stat.runs ? (stat.totalMs / stat.runs) : 0;
                            const last = typeof stat.lastMs === 'number' ? stat.lastMs.toFixed(1) : '0.0';
                            const avgVal = Number.isFinite(avg) ? avg.toFixed(1) : '0.0';
                            const max = typeof stat.maxMs === 'number' ? stat.maxMs.toFixed(1) : '0.0';
                            lines.push(` ${key}: ${last}/${avgVal}/${max}`);
                        }
                    }
                }
                const storageStats = state.metrics?.storageWrites;
                if (storageStats && Object.keys(storageStats).length) {
                    const recent = Object.entries(storageStats)
                        .sort((a, b) => ((b[1]?.lastMs || 0) - (a[1]?.lastMs || 0)))
                        .slice(0, 2);
                    if (recent.length) {
                        lines.push('storage set (last ms / size):');
                        for (const [key, stat] of recent) {
                            if (!stat) continue;
                            const sizeKb = stat.lastBytes ? (stat.lastBytes / 1024).toFixed(stat.lastBytes > 2048 ? 1 : 2) : '0.00';
                            const stringify = typeof stat.lastStringifyMs === 'number' && stat.lastStringifyMs > 0 ? ` s=${stat.lastStringifyMs.toFixed(1)}` : '';
                            const last = typeof stat.lastMs === 'number' ? stat.lastMs.toFixed(1) : '0.0';
                            lines.push(` ${key}: ${last}ms / ${sizeKb}KB${stringify}`);
                        }
                    }
                }
                body.innerHTML = lines.join('<br>');
            } catch(_) { /* ignore */ }
        },
        // Scan ranked war summary rows for missing attacker names and enqueue for resolution
        // NOTE: name resolution (profile page scraping) has been disabled due to TOS/privacy concerns.
        // These stubs preserve the metrics shape but perform no network requests.
        _scanWarSummaryForMissingNames: (rows) => {
            try {
                if (!Array.isArray(rows) || !rows.length) return;
                const metrics = state._activityTracking.metrics || (state._activityTracking.metrics = {});
                // Ensure structure exists but do not enqueue or fetch profile pages
                metrics.warNameRes = metrics.warNameRes || { queue: [], attempts: {}, inFlight: 0, lastAttempt: 0, resolved: 0, failed: 0 };
                return;
            } catch(_) { /* noop */ }
        },
        // Name-fetching removed: return a resolved failure immediately
        _fetchProfileName: (playerId) => {
            return Promise.resolve({ ok: false });
        },
        // Process name resolution queue removed — keep metrics but avoid any network activity
        _processWarNameResolutionQueue: async () => {
            try {
                const metrics = state._activityTracking.metrics || (state._activityTracking.metrics = {});
                metrics.warNameRes = metrics.warNameRes || { queue: [], attempts: {}, inFlight: 0, lastAttempt: 0, resolved: 0, failed: 0 };
                return;
            } catch(_) { /* noop */ }
        },
        fetchGlobalData: async (opts = {}) => {
            const { force = false, focus = null } = opts || {};
            if (document.hidden && !force) {
                if (state.debug.cadence || state.debug.apiLogs) {
                    try { tdmlogger('debug', '[Fetch] skip: document hidden and force flag not set'); } catch(_) {}
                }
                return;
            }
            if (api._shouldBailDueToIpRateLimit('fetchGlobalData')) {
                if (state.debug.cadence || state.debug.apiLogs) {
                    try { tdmlogger('debug', '[Fetch] skip: backend IP rate limit active'); } catch(_) {}
                }
                return;
            }
            if (state.debug.cadence || state.debug.apiLogs) { try { tdmlogger('debug', `[Fetch] fetchGlobalData begin opts= ${opts}`); } catch(_) {} }
            utils.perf.start('fetchGlobalData');
            const measureApply = async (name, fn) => {
                const hasPerf = typeof performance !== 'undefined' && typeof performance.now === 'function';
                const start = hasPerf ? performance.now() : Date.now();
                try {
                    return await fn();
                } finally {
                    const end = hasPerf ? performance.now() : Date.now();
                    const duration = Math.max(0, end - start);
                    try {
                        const metricsRoot = state.metrics || (state.metrics = {});
                        const applyStats = metricsRoot.fetchApply || (metricsRoot.fetchApply = {});
                        const entry = applyStats[name] || (applyStats[name] = { runs: 0, totalMs: 0, maxMs: 0, lastMs: 0 });
                        entry.runs += 1;
                        entry.lastMs = duration;
                        entry.totalMs += duration;
                        if (duration > entry.maxMs) entry.maxMs = duration;
                        entry.lastAt = Date.now();
                    } catch(_) { /* metrics collection is best-effort */ }
                }
            };
            // Re-entrancy/throttle guard (PDA can trigger rapid duplicate calls)
            const nowMsFG = Date.now();
            if (!force && state.script._lastGlobalFetch && (nowMsFG - state.script._lastGlobalFetch) < config.MIN_GLOBAL_FETCH_INTERVAL_MS) {
                if (state.debug.cadence || state.debug.apiLogs) { try { tdmlogger('debug', '[Fetch] throttled: last fetch too recent'); } catch(_) {} }
                utils.perf.stop('fetchGlobalData');
                return;
            }
            state.script._lastGlobalFetch = nowMsFG;
            if (!state.user.tornId) {
                tdmlogger('warn', '[TDM] fetchGlobalData: User context missing.');
                if (state.debug.cadence || state.debug.apiLogs) { try { tdmlogger('info', '[Fetch] abort: missing user context'); } catch(_) {} }
                utils.perf.stop('fetchGlobalData');
                return;
            }

            try {
                // Call new backend endpoint
                const originalClientTimestamps = (state.dataTimestamps && typeof state.dataTimestamps === 'object') ? state.dataTimestamps : {};
                const clientTimestamps = {};
                try {
                    for (const key of Object.keys(originalClientTimestamps)) {
                        clientTimestamps[key] = originalClientTimestamps[key];
                    }
                    // Inject warData timestamp for freshness check
                    if (state.warData && state.warData.lastUpdated) {
                        clientTimestamps.warData = state.warData.lastUpdated;
                    }
                    
                    // Sanity check: if we claim to have data (via timestamp) but local state is empty, drop the timestamp to force refetch
                    // This fixes the "chicken-and-egg" issue after a hard reset where timestamps might persist but data is gone.
                    if (clientTimestamps.warData && (!state.warData || Object.keys(state.warData).length === 0)) {
                        delete clientTimestamps.warData;
                    }
                    if (clientTimestamps.dibsData && (!state.dibsData || !Array.isArray(state.dibsData) || state.dibsData.length === 0)) {
                        delete clientTimestamps.dibsData;
                    }
                    if (clientTimestamps.medDeals && (!state.medDeals || Object.keys(state.medDeals).length === 0)) {
                        delete clientTimestamps.medDeals;
                    }
                } catch(_) {}
                const pendingTracker = typeof handlers._getPendingDibsTracker === 'function' ? handlers._getPendingDibsTracker(false) : null;
                const hasPendingDibs = !!(pendingTracker && pendingTracker.ids && pendingTracker.ids.size);
                const localDibsMissingButFingerprint = (!Array.isArray(state.dibsData) || state.dibsData.length === 0) && !!state._fingerprints?.dibs;
                const shouldForceDibsPayload = (focus === 'dibs') || hasPendingDibs || localDibsMissingButFingerprint;
                // Precompute dynamic inputs and time them separately
                // TODO figure out if visibleOpponentIds and clientNoteTimestamps is cheapest method for db queries
                utils.perf.start('fetchGlobalData.compute.visibleOpponentIds');
                const _visibleOpponentIds = utils.getVisibleOpponentIds();
                utils.perf.stop('fetchGlobalData.compute.visibleOpponentIds');

                utils.perf.start('fetchGlobalData.compute.clientNoteTimestamps');
                const _clientNoteTimestamps = utils.getClientNoteTimestamps();
                utils.perf.stop('fetchGlobalData.compute.clientNoteTimestamps');

                utils.perf.start('fetchGlobalData.api.getGlobalDataForUser');
                // Heartbeat logic: if user has an active dib and passive heartbeat enabled, override lastActivityTime to "now"
                let effectiveLastActivity = state.script.lastActivityTime;
                const clientFingerprints = {
                    dibs: state._fingerprints?.dibs || null,
                    medDeals: state._fingerprints?.medDeals || null
                };
                if (shouldForceDibsPayload) {
                    if (typeof clientTimestamps.dibs !== 'undefined') delete clientTimestamps.dibs;
                    clientFingerprints.dibs = null;
                    if (state.debug?.apiLogs) {
                        tdmlogger('debug', '[dibs] forcing payload refresh', {
                            focus,
                            hasPendingDibs,
                            localDibsMissingButFingerprint,
                            originalClientTimestamp: originalClientTimestamps?.dibs
                        });
                    }
                }
                const globalData = await api.post('getGlobalDataForUser', {
                    tornId: state.user.tornId,
                    factionId: state.user.factionId,
                    clientTimestamps,
                    clientFingerprints,
                    lastActivityTime: effectiveLastActivity,
                    visibleOpponentIds: _visibleOpponentIds,
                    clientNoteTimestamps: _clientNoteTimestamps,
                    // Pass latest warId if we have one cached from local rankedwars fetch
                    warId: state?.lastRankWar?.id || null,
                    warType: state?.warData?.warType || null
                });
                utils.perf.stop('fetchGlobalData.api.getGlobalDataForUser');
                const clientApiMs = utils.perf.getLast('fetchGlobalData.api.getGlobalDataForUser');
                if (clientApiMs >= 3000 && globalData && globalData.timings) {
                    try {
                        // Avoid [object Object] in some consoles (e.g., PDA)
                        const pretty = JSON.stringify(globalData.timings);
                        tdmlogger('info', `[Perf][backend timings] ${pretty}`);
                    } catch (_) {
                        tdmlogger('info', `[Perf][backend timings] ${globalData.timings}`);
                    }
                    // Slow report: highlight only expensive subtasks to keep logs concise
                    try {
                        const t = globalData.timings || {};
                        const importantKeys = new Set([
                            'verifyUserMs',
                            'getFactionSettingsMs',
                            'getMasterTimestampsMs',
                            'parallelFetchMs',
                            'totalHandlerMs'
                        ]);
                        const entries = Object.entries(t).filter(([k, v]) =>
                            typeof v === 'number' && (k.startsWith('pf_') || importantKeys.has(k))
                        );
                        const SLOW_THRESHOLD = 300; // ms per-subtask
                        const slow = entries
                            .filter(([_, v]) => v >= SLOW_THRESHOLD)
                            .sort((a, b) => b[1] - a[1])
                            .map(([k, v]) => `${k}:${v.toFixed(0)}ms`);
                        // Compute rough overhead (client - backend), when backend timings are available
                        let overhead = null;
                        if (typeof t.totalHandlerMs === 'number' && t.totalHandlerMs > 0) {
                            const delta = clientApiMs - t.totalHandlerMs;
                            if (isFinite(delta)) overhead = Math.max(0, Math.round(delta));
                        }
                        const overheadStr = overhead !== null ? `, overhead≈${overhead}ms` : '';
                        const report = `SlowReport client=${clientApiMs.toFixed(0)}ms${overheadStr}${slow.length ? ' | ' + slow.join(', ') : ' | no hot subtasks ≥300ms'}`;
                        tdmlogger('warn', `[Perf] ${report}`);
                    } catch (_) { /* ignore formatting issues */ }
                }
                // Persist warStatus meta (phase, nextPollHintSec, updatedAt) if provided so cadence can piggyback on global fetches
                try {
                    // Merge backend-provided chainWatcher list into state for UI convenience
                    try {
                            if (globalData && globalData.firebase && globalData.firebase.chainWatcher) {
                                // Server returns { watchers, meta }
                                const srv = globalData.firebase.chainWatcher;
                                const watchers = Array.isArray(srv.watchers) ? srv.watchers.map(c => ({ id: String(c.id || c), name: c.name || c.username || c.displayName || '' })).filter(Boolean) : [];
                                // Always overwrite local storage with authoritative server list
                                storage.set('chainWatchers', watchers || []);
                                // Persist meta for UI display
                                const meta = srv.meta || null;
                                try { storage.set('chainWatchers_meta', meta); } catch(_) {}
                                state.chainWatcher = srv.watchers || [];
                                
                                // Update timestamp so future polls can skip if unchanged
                                if (globalData.masterTimestamps && globalData.masterTimestamps.chainWatcher) {
                                    state.dataTimestamps.chainWatcher = globalData.masterTimestamps.chainWatcher;
                                    storage.set('dataTimestamps', state.dataTimestamps);
                                }

                                // Refresh select options if present and apply selections
                                try {
                                    const sel = document.getElementById('tdm-chainwatcher-select');
                                    if (sel && typeof sel === 'object') {
                                        // Unselect all, then select those present in watchers
                                        const vals = new Set((watchers||[]).map(w=>String(w.id)));
                                        for (const opt of sel.options) opt.selected = vals.has(opt.value);
                                    }
                                } catch(_) {}
                                ui.updateChainWatcherMeta && ui.updateChainWatcherMeta(meta || null);
                            }
                    } catch(_) {}
                    const wsMeta = globalData?.meta?.warStatus;
                    if (wsMeta && (wsMeta.phase || wsMeta.nextPollHintSec)) {
                        const updatedAtMs = (() => {
                            const u = wsMeta.updatedAt;
                            if (!u) return Date.now();
                            if (typeof u === 'number') return u;
                            if (u && typeof u.toMillis === 'function') return u.toMillis();
                            if (u && u._seconds) return (u._seconds * 1000) + Math.floor((u._nanoseconds||0)/1e6);
                            return Date.now();
                        })();
                        // Mirror structure of getWarStatus cache: { data: {...status fields...}, fetchedAt }
                        state._warStatusCache = state._warStatusCache || {};
                        const existingPhase = state._warStatusCache?.data?.phase;
                        const phaseChanged = existingPhase && wsMeta.phase && existingPhase !== wsMeta.phase;
                        state._warStatusCache.data = {
                            ...(state._warStatusCache.data || {}),
                            phase: wsMeta.phase,
                            nextPollHintSec: wsMeta.nextPollHintSec,
                            lastAttackAgeSec: (typeof wsMeta.lastAttackAgeSec === 'number' ? wsMeta.lastAttackAgeSec : state._warStatusCache.data?.lastAttackAgeSec),
                            // Provide a synthetic lastAttackStarted estimate if absent so interval heuristics (age buckets) can still function.
                            lastAttackStarted: (() => {
                                if (state._warStatusCache?.data?.lastAttackStarted) return state._warStatusCache.data.lastAttackStarted;
                                if (typeof wsMeta.lastAttackAgeSec === 'number') return Math.floor(Date.now()/1000) - wsMeta.lastAttackAgeSec;
                                return state._warStatusCache?.data?.lastAttackStarted || null;
                            })()
                        };
                        state._warStatusCache.fetchedAt = updatedAtMs;
                        state.script.lastWarStatusFetchMs = updatedAtMs;
                        if (phaseChanged && state.debug.cadence) {
                            tdmlogger('debug', `[Cadence] warStatus phase changed via global fetch -> ${existingPhase} -> ${wsMeta.phase}`);
                        }
                    }
                    // Handle userScore from meta if present (Enhancement #1)
                    if (globalData?.meta?.userScore) {
                        state.userScore = globalData.meta.userScore;
                        try {
                            tdmlogger('debug', `[GlobalData] Received userScore: ${JSON.stringify(state.userScore)}`);
                            storage.set('userScore', state.userScore);
                        } catch(_) {}
                        // Update the badge if the UI function exists
                        if (typeof ui.updateUserScoreBadge === 'function') {
                            ui.updateUserScoreBadge();
                        }
                    } else {
                        try { tdmlogger('debug', `[GlobalData] No userScore in meta.`); } catch(_) {}
                    }
                } catch(_) { /* non-fatal */ }
                
                const TRACKED_COLLECTIONS = [
                    'dibs',
                    'userNotes',
                    'medDeals',
                    'rankedWars',
                    'rankedWars_attacks',
                    'rankedWars_summary',
                    'unauthorizedAttacks',
                    'attackerActivity',
                    'warData'
                ];
                // Check if collections have changed
                // Removed TornAPICalls_rankedwars handling; ranked wars list is handled client-side
                if (utils.isCollectionChanged(clientTimestamps, globalData.masterTimestamps, 'rankedWars')) {
                    await measureApply('rankedWars', async () => {
                        utils.perf.start('fetchGlobalData.apply.rankedWars');
                        // Backend provides warData when changed; rankedWars list is managed client-side now
                        tdmlogger('debug', `[rankedwars][warData][Master] ${globalData.tornApi.warData}`);
                        tdmlogger('debug', `[rankedwars][warData][Client] ${state.warData}`);
                        const incomingWarData = globalData?.tornApi?.warData;
                        let normalizedWarData;
                        if (incomingWarData && typeof incomingWarData === 'object' && !Array.isArray(incomingWarData)) {
                            normalizedWarData = { ...incomingWarData };
                        } else if (incomingWarData === null) {
                            normalizedWarData = { warType: 'War Type Not Set' };
                        } else if (state.warData && typeof state.warData === 'object') {
                            normalizedWarData = { ...state.warData };
                        } else {
                            normalizedWarData = { warType: 'War Type Not Set' };
                        }
                        // If the lastRankWar differs from the warId stored in incoming warData or we have a new lastRankWar,
                        // ensure we reset per-war saved state for the new war so old settings don't carry forward.
                        const appliedWarData = { ...normalizedWarData };
                        try {
                            const currentLastWarId = state.lastRankWar?.id || null;
                            const incomingWarId = (typeof appliedWarData.warId !== 'undefined') ? appliedWarData.warId : (appliedWarData?.war?.id || appliedWarData?.id || null);
                            if (currentLastWarId && incomingWarId && String(currentLastWarId) !== String(incomingWarId)) {
                                // Backend warData doesn't match the currently known lastRankWar — reset to defaults for new war
                                const defaultInitial = (state.lastRankWar?.war && Number(state.lastRankWar.war.target)) || Number(state.lastRankWar?.target) || 0;
                                const newWarData = Object.assign({}, { warType: 'War Type Not Set', warId: currentLastWarId, initialTargetScore: defaultInitial });
                                // Keep opponent info if incoming provided or derive from lastRankWar factions
                                try {
                                    if (state.lastRankWar && state.lastRankWar.factions) {
                                        const opp = Object.values(state.lastRankWar.factions).find(f => String(f.id) !== String(state.user.factionId));
                                        if (opp) {
                                            newWarData.opponentFactionId = opp.id;
                                            newWarData.opponentFactionName = opp.name;
                                        }
                                    }
                                } catch(_) {}
                                storage.updateStateAndStorage('warData', newWarData);
                                // New war detected: invalidate lightweight userScore cache so we don't show stale scores
                                try {
                                    state.userScore = null;
                                    if (storage && typeof storage.remove === 'function') storage.remove('userScore');
                                } catch(_) {}
                                try { ui.updateUserScoreBadge?.(); } catch(_) {}
                                try { ui.ensureAttackModeBadge?.(); } catch(_) {}
                            } else {
                                // If warId matches (or not provable), accept normalized warData
                                storage.updateStateAndStorage('warData', appliedWarData);
                                try { ui.ensureAttackModeBadge?.(); } catch(_) {}
                            }
                        } catch (e) {
                            // If anything goes wrong, fallback to applying the incoming warData to avoid blocking
                            storage.updateStateAndStorage('warData', appliedWarData);
                            try { ui.ensureAttackModeBadge?.(); } catch(_) {}
                        }
                        state.dataTimestamps.rankedWars = globalData.masterTimestamps.rankedWars;
                        storage.set('dataTimestamps', state.dataTimestamps); 
                        utils.perf.stop('fetchGlobalData.apply.rankedWars');
                    });
                }

                const dibsCollectionChanged = utils.isCollectionChanged(clientTimestamps, globalData.masterTimestamps, 'dibs');
                const isForcedDibsRefresh = focus === 'dibs';
                const shouldProcessDibs = dibsCollectionChanged || isForcedDibsRefresh || localDibsMissingButFingerprint;
                if (shouldProcessDibs && !dibsCollectionChanged && !isForcedDibsRefresh) {
                     try { tdmlogger('debug', `[dibs] Processing triggered by localDibsMissingButFingerprint (fingerprint: ${state._fingerprints?.dibs})`); } catch(_) {}
                }
                if (!shouldProcessDibs && (state.debug?.cadence || state.debug?.apiLogs)) {
                    tdmlogger('debug', '[dibs] collection skip', { changed: dibsCollectionChanged, focus, force, clientTs: originalClientTimestamps?.dibs, masterTs: globalData.masterTimestamps?.dibs });
                }
                if (shouldProcessDibs) {
                    await measureApply('dibs', async () => {
                        utils.perf.start('fetchGlobalData.apply.dibs');
                        // tdmlogger('debug', `[dibs][Master] ${globalData.firebase.dibsData}`);
                        // tdmlogger('debug', `[dibs][Client] ${state.dibsData}`);
                        const incomingFp = globalData?.meta?.warStatus?.dibsFingerprint || globalData?.statusDoc?.dibsFingerprint || globalData?.meta?.dibsFingerprint || null;
                        try { state._fingerprints = state._fingerprints || {}; } catch(_) {}
                        const prevFp = state._fingerprints?.dibs || null;
                        const rawDibs = Array.isArray(globalData.firebase?.dibsData) ? globalData.firebase.dibsData : null;
                        const computedFp = rawDibs ? utils.computeDibsFingerprint(rawDibs) : null;
                        const cacheFriendlyDibs = rawDibs ? rawDibs.slice() : (Array.isArray(state.dibsData) ? state.dibsData.slice() : []);
                        const fingerprintToStore = incomingFp || computedFp || null;
                        if (state.debug?.apiLogs) {
                            tdmlogger('debug', '[dibs] fetched payload', {
                                rawLength: rawDibs ? rawDibs.length : null,
                                incomingFp,
                                prevFp,
                                computedFp,
                                masterTimestamp: globalData.masterTimestamps?.dibs
                            });
                        }
                        const isFocusedDibsRefresh = focus === 'dibs';
                        let shouldApply = true;
                        let dibsApplyReason = 'always apply (simplified)';
                        const masterTimestamp = globalData.masterTimestamps?.dibs;
                        const clientTimestamp = originalClientTimestamps?.dibs;
                        const currentClientLength = Array.isArray(state.dibsData) ? state.dibsData.length : null;

                        // [Refactor] Removed "fingerprint unchanged" skip logic. 
                        // We always apply server data to ensure consistency and fix stale/shifting UI issues.
                        
                        if (shouldApply) {
                            // [Refactor] Removed "Merge pending optimistic dibs" logic.
                            // Server data is now authoritative. Optimistic updates are for immediate UI feedback only
                            // and will be overwritten by the next fetch to ensure a single source of truth.

                            try {
                                if (state._mutate?.setDibsData) {
                                    state._mutate.setDibsData(cacheFriendlyDibs, { fingerprint: fingerprintToStore, source: 'fetchGlobalData' });
                                } else {
                                    storage.updateStateAndStorage('dibsData', cacheFriendlyDibs);
                                    if (fingerprintToStore) {
                                        state._fingerprints = state._fingerprints || {};
                                        state._fingerprints.dibs = fingerprintToStore;
                                        try { storage.set('fingerprints', state._fingerprints); } catch(_) {}
                                    }
                                }
                            } catch (_) {
                                storage.updateStateAndStorage('dibsData', cacheFriendlyDibs);
                                if (fingerprintToStore) {
                                    state._fingerprints = state._fingerprints || {};
                                    state._fingerprints.dibs = fingerprintToStore;
                                    try { storage.set('fingerprints', state._fingerprints); } catch(_) {}
                                }
                            }
                            ui.updateAllPages();
                            // Attack page inline dib/med message refresh only on change
                            try { ui.showAttackPageDibMedSummary?.(); } catch(_) {}
                        }
                        if (!shouldApply || state.debug?.apiLogs) {
                            tdmlogger('info', '[dibs] apply decision', {
                                shouldApply,
                                reason: dibsApplyReason,
                                masterTimestamp,
                                clientTimestamp,
                                incomingFp,
                                prevFp,
                                computedFp,
                                fingerprintToStore,
                                rawLength: rawDibs ? rawDibs.length : null,
                                clientLength: currentClientLength
                            });
                        }
                        // Always advance timestamp to acknowledge server change (even if fingerprint same)
                        state.dataTimestamps.dibs = globalData.masterTimestamps.dibs;
                        storage.set('dataTimestamps', state.dataTimestamps);
                        // [Refactor] Removed pending resolution call.
                        // handlers._resolvePendingDibsAfterApply(cacheFriendlyDibs);
                        utils.perf.stop('fetchGlobalData.apply.dibs');
                    });
                }
                
                // Check if we should process med deals (collection changed OR client empty but backend has data)
                const collectionChanged = utils.isCollectionChanged(clientTimestamps, globalData.masterTimestamps, 'medDeals');
                const clientEmpty = Object.keys(state.medDeals || {}).length === 0;
                const backendHasData = Array.isArray(globalData.firebase?.medDeals) && globalData.firebase.medDeals.length > 0;
                const shouldProcessMedDeals = collectionChanged || (clientEmpty && backendHasData);
                
                // Debug logging for med deals decision (when debug enabled)
                if (state.debug?.apiLogs) {
                    tdmlogger('debug', '[medDeals] Processing decision after cache clear', {
                        collectionChanged,
                        clientEmpty,
                        backendHasData,
                        shouldProcessMedDeals
                    });
                }
                
                if (shouldProcessMedDeals) {
                    await measureApply('medDeals', async () => {
                        utils.perf.start('fetchGlobalData.apply.medDeals');
                        tdmlogger('debug', `[medDeals][Master] ${globalData.firebase.medDeals}`);
                        tdmlogger('debug', `[medDeals][Client] ${state.medDeals}`);
                        
                        // Enhanced debugging for med deals issue
                        if (state.debug?.apiLogs) {
                            tdmlogger('debug', '[medDeals] Raw backend response', {
                                firebaseSection: globalData.firebase,
                                medDealsRaw: globalData.firebase?.medDeals,
                                medDealsType: typeof globalData.firebase?.medDeals,
                                medDealsIsArray: Array.isArray(globalData.firebase?.medDeals),
                                globalDataKeys: Object.keys(globalData || {}),
                                warType: state.warData?.warType,
                                masterTimestamps: globalData.masterTimestamps?.medDeals
                            });
                        }
                        const incomingFp = globalData?.meta?.warStatus?.medDealsFingerprint || globalData?.statusDoc?.medDealsFingerprint || globalData?.meta?.medDealsFingerprint || null;
                        try { state._fingerprints = state._fingerprints || {}; } catch(_) {}
                        const prevFp = state._fingerprints?.medDeals || null;
                        
                        // Debug logging for med deals processing
                        if (state.debug?.apiLogs) {
                            tdmlogger('debug', '[medDeals] Processing details', {
                                hasData: Array.isArray(globalData.firebase.medDeals),
                                dataLength: Array.isArray(globalData.firebase.medDeals) ? globalData.firebase.medDeals.length : 'N/A',
                                incomingFp,
                                prevFp,
                                warStatusFp: globalData?.meta?.warStatus?.medDealsFingerprint,
                                statusDocFp: globalData?.statusDoc?.medDealsFingerprint,
                                metaFp: globalData?.meta?.medDealsFingerprint
                            });
                        }
                        
                        let applyDeals = true;
                        let applyReason = 'always apply (simplified)';
                        
                        // [Refactor] Removed complex fingerprint skipping logic.
                        // Always apply server data to ensure consistency.
                        
                        if (applyDeals && Array.isArray(globalData.firebase.medDeals)) {
                            const incomingArr = globalData.firebase.medDeals;
                            const nextMap = {};
                            for (const status of incomingArr) {
                                if (status && status.id != null) nextMap[status.id] = status;
                            }
                            
                            // Use backend fingerprint if available, otherwise compute frontend fingerprint
                            let fingerprintToUse = incomingFp;
                            if (!fingerprintToUse) {
                                try {
                                    fingerprintToUse = utils.computeMedDealsFingerprint(nextMap);
                                    if (state.debug?.apiLogs) tdmlogger('debug', '[medDeals] computed frontend fingerprint:', fingerprintToUse);
                                } catch(_) {}
                            }
                            
                            setMedDeals(nextMap, { fingerprint: fingerprintToUse });
                            try { ui.showAttackPageDibMedSummary?.(); } catch(_) {}
                        } else if (!Array.isArray(globalData.firebase.medDeals)) {
                            if (state.debug?.apiLogs) {
                                tdmlogger('debug', '[medDeals] Backend omitted medDeals array; preserving existing state', {
                                    medDealsValue: globalData.firebase?.medDeals,
                                    medDealsType: typeof globalData.firebase?.medDeals,
                                    hasFirebaseSection: !!globalData.firebase,
                                    firebaseKeys: globalData.firebase ? Object.keys(globalData.firebase) : 'no firebase section'
                                });
                            }
                        } else if (collectionChanged && clientEmpty && !backendHasData) {
                            // Special case: Collection changed + client empty + backend empty = likely race condition
                            // Schedule a retry after a short delay to let backend initialize
                            if (state.debug?.apiLogs) tdmlogger('debug', '[medDeals] Detected race condition - scheduling retry');
                            setTimeout(() => {
                                if (Object.keys(state.medDeals || {}).length === 0) {
                                    if (state.debug?.apiLogs) tdmlogger('debug', '[medDeals] Retrying fetch after race condition');
                                    handlers.fetchGlobalData({ force: true, focus: 'dibs' });
                                }
                            }, 2000);
                        }
                        
                        // Debug: Check final med deals state (when debug enabled)
                        if (state.debug?.apiLogs) {
                            const finalMedDealsCount = Object.keys(state.medDeals || {}).length;
                            tdmlogger('debug', '[medDeals] Final state after processing', {
                                finalMedDealsCount,
                                wasProcessed: shouldProcessMedDeals,
                                hadBackendData: backendHasData
                            });
                        }
                        // Always advance timestamp regardless of whether we applied new map
                        state.dataTimestamps.medDeals = globalData.masterTimestamps.medDeals;
                        storage.set('dataTimestamps', state.dataTimestamps);
                        utils.perf.stop('fetchGlobalData.apply.medDeals');
                    });
                }
                // Always apply retaliation opportunities from backend (10s cached server-side)
                await measureApply('retaliationOpportunities', async () => {
                    try {
                        const retals = Array.isArray(globalData?.tornApi?.retaliationOpportunities)
                            ? globalData.tornApi.retaliationOpportunities : [];
                        if (retals) {
                            const map = {};
                            const nowSec = Math.floor(Date.now() / 1000);
                            for (const r of retals) {
                                if (!r || r.attackerId == null) continue;
                                // Keep slight grace if server marks expired within a few seconds
                                const tr = Number(r.timeRemaining || 0);
                                const expired = !!r.expired && tr <= 0;
                                if (expired) continue;
                                map[String(r.attackerId)] = {
                                    attackerId: Number(r.attackerId),
                                    attackerName: r.attackerName,
                                    defenderId: r.defenderId,
                                    defenderName: r.defenderName,
                                    retaliationEndTime: Number(r.retaliationEndTime || 0),
                                    timeRemaining: tr > 0 ? tr : Math.max(0, Number(r.retaliationEndTime || 0) - nowSec),
                                    expired: false
                                };
                                // Stamp a short "Retal Done" meta when backend indicates fulfillment
                                if (r.fulfilled) {
                                    const id = String(r.attackerId);
                                    const nowMs = Date.now();
                                    state.rankedWarChangeMeta = state.rankedWarChangeMeta || {};
                                    const prevMeta = state.rankedWarChangeMeta[id];
                                    const withinTtl = prevMeta && prevMeta.activeType === 'retalDone' && (nowMs - (prevMeta.ts || 0) < 60000);
                                    const moreImportant = prevMeta && (prevMeta.activeType === 'retal' || prevMeta.activeType === 'status' || prevMeta.activeType === 'activity');
                                    if (!withinTtl && !moreImportant) {
                                        state.rankedWarChangeMeta[id] = { activeType: 'retalDone', pendingText: 'Retal Done', ts: nowMs };
                                    }
                                }
                            }
                            storage.updateStateAndStorage('retaliationOpportunities', map);
                            try { ui.updateRetalsButtonCount?.(); } catch(_) {}
                        }
                    } catch(_) { /* non-fatal */ }
                });
                if (utils.isCollectionChanged(clientTimestamps, globalData.masterTimestamps, 'userNotes')) {
                    await measureApply('userNotes', async () => {
                        utils.perf.start('fetchGlobalData.apply.userNotes');
                        // Merge delta results
                        const delta = Array.isArray(globalData.firebase.userNotesDelta) ? globalData.firebase.userNotesDelta : [];
                        const missing = Array.isArray(globalData.firebase.userNotesMissing) ? globalData.firebase.userNotesMissing : [];
                        if (delta.length > 0 || missing.length > 0) {
                            const merged = { ...(state.userNotes || {}) };
                            for (const note of delta) {
                                if (!note || !note.id) continue;
                                merged[note.id] = note;
                            }
                            for (const id of missing) {
                                if (id in merged) delete merged[id];
                            }
                            storage.updateStateAndStorage('userNotes', merged);
                            state.dataTimestamps.userNotes = globalData.masterTimestamps.userNotes;
                            storage.set('dataTimestamps', state.dataTimestamps);
                        }
                        utils.perf.stop('fetchGlobalData.apply.userNotes');
                    });
                }

                
                if (utils.isCollectionChanged(clientTimestamps, globalData.masterTimestamps, 'dibsNotifications')) {
                    await measureApply('dibsNotifications', async () => {
                        utils.perf.start('fetchGlobalData.apply.dibsNotifications');
                        tdmlogger('debug', `[dibsNotifications][Master] ${globalData.firebase.dibsNotifications}`);
                        tdmlogger('debug', `[dibsNotifications][ClientTs] ${state.dataTimestamps.dibsNotifications}`);
                        // Initialize / load seen map (id -> firstSeenSec)
                        if (!state._seenDibsNotificationIds) {
                            const persisted = storage.get('seenDibsNotificationIds', {});
                            state._seenDibsNotificationIds = (persisted && typeof persisted === 'object') ? persisted : {};
                        }
                        const nowSec = Math.floor(Date.now() / 1000);
                        const SEEN_TTL_SEC = 6 * 60 * 60; // 6h retention window
                        try {
                            for (const id of Object.keys(state._seenDibsNotificationIds)) {
                                if ((nowSec - (state._seenDibsNotificationIds[id] || 0)) > SEEN_TTL_SEC) delete state._seenDibsNotificationIds[id];
                            }
                        } catch(_) {}
                        if (Array.isArray(globalData.firebase.dibsNotifications) && globalData.firebase.dibsNotifications.length > 0) {
                            const raw = globalData.firebase.dibsNotifications.slice();
                            raw.sort((a, b) => (b.createdAt?._seconds || 0) - (a.createdAt?._seconds || 0));
                            const displayedOpponentEventWindow = {}; // opponentId -> lastShownSec
                            const DEDUPE_WINDOW_SEC = 15; // suppress rapid duplicates
                            const myTornId = String(state.user?.tornId || '');
                            raw.forEach(notification => {
                                if (!notification || !notification.id) return;
                                const createdAtSec = notification.createdAt?._seconds || 0;
                                // Skip if already seen
                                if (state._seenDibsNotificationIds[notification.id]) return;
                                // Skip if action initiated by current user
                                if (String(notification.removedByUserId || '') === myTornId) return;

                                // Skip notification for "Replaced by new dibs" (auto-acknowledge)
                                if (notification.removalReason === 'Replaced by new dibs') {
                                    state._seenDibsNotificationIds[notification.id] = nowSec;
                                    storage.set('seenDibsNotificationIds', state._seenDibsNotificationIds);
                                    api.post('markDibsNotificationAsRead', { notificationId: notification.id }).catch(() => {});
                                    return;
                                }

                                // Dedupe per opponent in short window
                                const oppId = String(notification.opponentId || '');
                                if (oppId) {
                                    const lastShown = displayedOpponentEventWindow[oppId] || 0;
                                    if (createdAtSec - lastShown < DEDUPE_WINDOW_SEC) return;
                                    displayedOpponentEventWindow[oppId] = createdAtSec;
                                }
                                const message = `Your dibs on ${notification.opponentName} was removed. Reason: ${notification.removalReason}. Click to dismiss.`;
                                let isMarked = false;
                                const markAsRead = async () => {
                                    // Local guard for this closure
                                    if (isMarked) return;
                                    // Shared seen map guard - avoid duplicate server calls across tabs/closures
                                    try {
                                        if (state._seenDibsNotificationIds && state._seenDibsNotificationIds[notification.id]) {
                                            isMarked = true;
                                            return;
                                        }
                                    } catch(_) { /* ignore */ }
                                    isMarked = true;
                                    try { await api.post('markDibsNotificationAsRead', { notificationId: notification.id }); } catch(_) {}
                                    state._seenDibsNotificationIds[notification.id] = nowSec;
                                    storage.set('seenDibsNotificationIds', state._seenDibsNotificationIds);
                                };
                                // Keep toast visible 60s but attempt to mark read earlier (20s) if still unseen
                                ui.showMessageBox(message, 'warning', 60000, markAsRead);
                                setTimeout(() => { try { if (!state._seenDibsNotificationIds || !state._seenDibsNotificationIds[notification.id]) markAsRead(); } catch(_) {} }, 10000);
                                // Best-effort: force a focused dibs refresh so UI reflects removal immediately
                                try { setTimeout(() => { try { handlers.fetchGlobalDataForced && handlers.fetchGlobalDataForced('dibs'); } catch(_) {} }, 300); } catch(_) {}
                            });
                            storage.set('seenDibsNotificationIds', state._seenDibsNotificationIds);
                        }
                        state.dataTimestamps.dibsNotifications = globalData.masterTimestamps.dibsNotifications;
                        storage.set('dataTimestamps', state.dataTimestamps);
                        utils.perf.stop('fetchGlobalData.apply.dibsNotifications');
                    });
                }
                // Removed Firestore rankedWars_summary fallback; storage artifacts & local aggregation are authoritative now.

                // Score cap check
                utils.perf.start('fetchGlobalData.scoreCap');
                await handlers.checkTermedWarScoreCap();
                utils.perf.stop('fetchGlobalData.scoreCap');

                if (!focus || focus === 'all') {
                    utils.perf.start('fetchGlobalData.ui.updateAllPages');
                    ui.updateAllPages();
                    utils.perf.stop('fetchGlobalData.ui.updateAllPages');
                    try { ui.showAttackPageDibMedSummary?.(); } catch(_) {}
                } else if (focus === 'dibs') {
                    // Minimal UI patch: only re-render dibs dependent areas
                    try { ui.updateFactionPageUI?.(document); } catch(_) {}
                    try { ui.processRankedWarTables?.().catch(e => {
                        try { tdmlogger('error', `[processRankedWarTables] failed: ${e}`); } catch(_) {}
                    }); } catch(_) {}
                    try { ui.showAttackPageDibMedSummary?.(); } catch(_) {}
                }
                // Run enforcement pass (auto-removals) after UI updates
                utils.perf.start('fetchGlobalData.enforcePolicies');
                handlers.enforceDibsPolicies?.();
                utils.perf.stop('fetchGlobalData.enforcePolicies');

            } catch (error) {
                tdmlogger('error', `Error fetching global data: ${error}`);
                if (state.debug.cadence || state.debug.apiLogs) { try { tdmlogger('debug', `[Fetch] error: ${error?.message || String(error)}`); } catch(_) {} }
            }
            utils.perf.stop('fetchGlobalData');
            if (state.debug.cadence || state.debug.apiLogs) { try { tdmlogger('debug', `[TDM][Fetch] fetchGlobalData end totalMs=${utils.perf.getLast('fetchGlobalData')}`); } catch(_) {} }
        },
        fetchGlobalDataForced: async (focus = null) => handlers.fetchGlobalData({ force: true, focus }),
        fetchUnauthorizedAttacks: async () => {
            try {
                const response = await api.get('getUnauthorizedAttacks', { factionId: state.user.factionId });
                // Persist to state/storage so UI modals render results immediately
                const list = Array.isArray(response) ? response : [];
                storage.updateStateAndStorage('unauthorizedAttacks', list);
                return list;
            } catch (error) {
                if (error?.message?.includes('FAILED_PRECONDITION')) {
                    storage.updateStateAndStorage('unauthorizedAttacks', []);
                    return [];
                }
                tdmlogger('warn', `Non-critical error fetching unauthorized attacks: ${error.message || 'Unknown error'}`);
                storage.updateStateAndStorage('unauthorizedAttacks', []);
                return [];
            }
        },
        checkAndDisplayDibsNotifications: async () => {
            if (!state.user.tornId) return;
            try {
                let notifications = await api.get('getDibsNotifications', { factionId: state.user.factionId });
                if (Array.isArray(notifications) && notifications.length > 0) {
                    notifications.sort((a, b) => (b.createdAt?._seconds || 0) - (a.createdAt?._seconds || 0));
                    notifications.forEach(notification => {
                        const message = `Your dibs on ${notification.opponentName} was removed. Reason: ${notification.removalReason}. Click to dismiss.`;
                        let isMarked = false;
                        const markAsRead = async () => {
                            if (isMarked) return;
                            try {
                                if (state._seenDibsNotificationIds && state._seenDibsNotificationIds[notification.id]) {
                                    isMarked = true;
                                    return;
                                }
                            } catch(_) { /* ignore */ }
                            isMarked = true;
                            await api.post('markDibsNotificationAsRead', { notificationId: notification.id, factionId: state.user.factionId });
                            // Persist seen so other closures/tabs will short-circuit
                            try { state._seenDibsNotificationIds = state._seenDibsNotificationIds || {}; state._seenDibsNotificationIds[notification.id] = Math.floor(Date.now()/1000); storage.set('seenDibsNotificationIds', state._seenDibsNotificationIds); } catch(_) {}
                        };
                        ui.showMessageBox(message, 'warning', 60000, markAsRead);
                        setTimeout(() => { try { if (!state._seenDibsNotificationIds || !state._seenDibsNotificationIds[notification.id]) markAsRead(); } catch(_) {} }, 10000);
                    });
                }
            } catch (error) {
                tdmlogger('error', `Error fetching dibs notifications: ${error}`);
            }
        },
        dibsTarget: async (opponentId, opponentName, buttonElement) => {
            const originalText = buttonElement.textContent;
            try {
                const opts = utils.getDibsStyleOptions();
                const [oppStat, meStat] = await Promise.all([
                    utils.getUserStatus(opponentId),
                    utils.getUserStatus(null)
                ]);
                const myCanon = meStat.canonical;
                if (opts.allowedUserStatuses && opts.allowedUserStatuses[myCanon] === false) {
                    throw new Error(`Your status (${myCanon}) is not allowed to place dibs by faction policy.`);
                }
                const canonOpp = oppStat.canonical;
                if (opts.allowStatuses && opts.allowStatuses[canonOpp] === false) {
                    throw new Error(`Target status (${canonOpp}) is not allowed by faction policy.`);
                }
                // New: last_action.status gate (Online/Idle/Offline)
                const act = String(oppStat.activity || '').trim();
                if (opts.allowLastActionStatuses && act && opts.allowLastActionStatuses[act] === false) {
                    throw new Error(`Target activity (${act}) is not allowed by faction policy.`);
                }
                // If configured: limit dibbing a Hospital opponent to those with release time under N minutes
                if (canonOpp === 'Hospital') {
                    const limitMin = Number(opts.maxHospitalReleaseMinutes || 0);
                    if (limitMin > 0) {
                        const remaining = Math.max(0, (oppStat.until || 0) - Math.floor(Date.now() / 1000));
                        if (remaining > limitMin * 60) {
                            throw new Error(`Target is hospitalized too long (${Math.ceil(remaining/60)}m). Policy allows < ${limitMin}m.`);
                        }
                    }
                }
                // Derive opponent faction id directly from status cache (preferred) then snapshot/warData fallbacks
                let opponentFactionId = null;
                try {
                    const entry = state.session?.userStatusCache?.[opponentId];
                    if (entry && entry.factionId) opponentFactionId = String(entry.factionId);
                } catch(_) {}
                if (!opponentFactionId) {
                    try {
                        const snap = state.rankedWarTableSnapshot && state.rankedWarTableSnapshot[opponentId];
                        if (snap?.factionId) opponentFactionId = String(snap.factionId);
                    } catch(_) {}
                }
                if (!opponentFactionId) {
                    try { if (oppStat?.factionId) opponentFactionId = String(oppStat.factionId); } catch(_) {}
                }
                if (!opponentFactionId) {
                    try { if (state.warData?.opponentFactionId) opponentFactionId = String(state.warData.opponentFactionId); } catch(_) {}
                }

                // Immediate feedback: Set button to "Saving..." state
                if (buttonElement) {
                    try {
                        buttonElement.textContent = 'Saving...';
                        buttonElement.disabled = true;
                        buttonElement.className = 'btn dibs-btn btn-dibs-inactive'; // Keep neutral style while saving
                    } catch(_) {}
                }

                const resp = await api.post('dibsTarget', { userid: state.user.tornId, username: state.user.tornUsername, opponentId, opponentname: opponentName, warType: state.warData.warType, factionId: state.user.factionId, userStatusAtDib: myCanon, opponentStatusAtDib: canonOpp, opponentFactionId });
                // Backend returns an object. When a new dib is created it includes an `id`.
                // If no `id` is present the backend may have returned a non-error informational
                // message (for example: "You already have dibs on X."). Handle that gracefully
                // instead of always showing a success toast.
                const createdId = resp?.id;
                const serverMsg = (resp && resp.message) ? String(resp.message) : '';
                let needsDibsRefresh = false;
                if (createdId) {
                    ui.showMessageBox(`Successfully dibbed ${opponentName}!`, 'success');
                    
                    if (resp.dibsData && Array.isArray(resp.dibsData)) {
                        // Use reactive setter to ensure UI updates and fingerprint consistency
                        if (state._mutate?.setDibsData) {
                            const fp = utils.computeDibsFingerprint ? utils.computeDibsFingerprint(resp.dibsData) : null;
                            state._mutate.setDibsData(resp.dibsData, { fingerprint: fp, source: 'dibsTarget' });
                        } else {
                            storage.updateStateAndStorage('dibsData', resp.dibsData);
                        }
                        // Mark recent-active window so activityTick treats this user as active
                        try {
                            const pingMs = Number(config.ATTACKER_ACTIVITY_PING_MS) || 60000;
                            state._recentlyHadActiveDibsUntil = Date.now() + (pingMs * 2);
                        } catch(_) {}
                    } else {
                        // Optimistic local state update so immediate removal works without waiting for global refresh
                        try {
                            const previouslyActive = [];
                            // Mark any existing active dibs by me as inactive locally (they were server-updated)
                            for (const d of state.dibsData) {
                                if (d.userId === String(state.user.tornId) && d.dibsActive) {
                                    if (String(d.opponentId) !== String(opponentId)) {
                                        previouslyActive.push({ id: String(d.opponentId), name: d.opponentname || d.opponentName || '' });
                                    }
                                    d.dibsActive = false; // local optimistic
                                }
                            }
                            // Push new dib record
                            const optimistic = {
                                id: createdId,
                                factionId: String(state.user.factionId),
                                userId: String(state.user.tornId),
                                username: state.user.tornUsername,
                                opponentId: String(opponentId),
                                opponentname: opponentName,
                                dibbedAt: { _seconds: Math.floor(Date.now()/1000) },
                                lastActionTimestamp: { _seconds: Math.floor(Date.now()/1000) },
                                dibsActive: true,
                                warType: state.warData.warType || 'unknown',
                                userStatusAtDib: myCanon,
                                opponentStatusAtDib: canonOpp,
                                opponentFactionId: opponentFactionId || null
                            };
                            state.dibsData.push(optimistic);
                            // [Refactor] Removed pending tracker call. Server data is authoritative.
                            // handlers._trackPendingDibsId(createdId);
                            
                            // Use reactive setter to persist & emit
                            try { state._mutate.setDibsData(state.dibsData.slice(), { source: 'optimistic-dib' }); } catch(_) { storage.set('dibsData', state.dibsData); }
                            // Mark recent-active window so activityTick treats this user as active
                            try {
                                const pingMs = Number(config.ATTACKER_ACTIVITY_PING_MS) || 60000;
                                state._recentlyHadActiveDibsUntil = Date.now() + (pingMs * 2);
                            } catch(_) {}
                            if (previouslyActive.length && utils.updateDibsButton) {
                                previouslyActive.forEach(({ id, name }) => {
                                    try {
                                        const btns = document.querySelectorAll(`.dibs-btn[data-opponent-id='${id}']`);
                                    btns.forEach(btn => utils.updateDibsButton(btn, id, name || btn?.dataset?.opponentName || opponentName));
                                } catch(_) { /* ignore */ }
                            });
                            }
                        } catch (e) {
                            tdmlogger('warn', `[dibsTarget][optimistic] failed: ${e}`);
                        }
                    }
                } else if (serverMsg.toLowerCase().includes('already have dibs')) {
                    // Informational: user already owns dibs. Reactivate locally if needed.
                    ui.showMessageBox(`You already have dibs on ${opponentName}.`, 'info');
                    try {
                        const existing = state.dibsData.find(d => String(d.opponentId) === String(opponentId) && String(d.userId) === String(state.user.tornId));
                        if (existing) {
                            existing.dibsActive = true;
                            try { const pingMs = Number(config.ATTACKER_ACTIVITY_PING_MS) || 60000; state._recentlyHadActiveDibsUntil = Date.now() + (pingMs * 2); } catch(_) {}
                            // Invalidate fingerprint to force re-sync
                            if (state._fingerprints) state._fingerprints.dibs = null;
                            try { state._mutate.setDibsData(state.dibsData.slice(), { source: 'optimistic-reactivate' }); } catch(_) { storage.set('dibsData', state.dibsData); }
                        }
                    } catch(_) {}
                } else {
                    // Unknown non-error response: show server message if present, otherwise show generic success
                    ui.showMessageBox(serverMsg || `Successfully dibbed ${opponentName}!`, 'success');
                    // If success but no ID, assume it worked and try to reactivate local state if possible
                    try {
                        const existing = state.dibsData.find(d => String(d.opponentId) === String(opponentId) && String(d.userId) === String(state.user.tornId));
                        if (existing) {
                            existing.dibsActive = true;
                            try { const pingMs = Number(config.ATTACKER_ACTIVITY_PING_MS) || 60000; state._recentlyHadActiveDibsUntil = Date.now() + (pingMs * 2); } catch(_) {}
                            if (state._fingerprints) state._fingerprints.dibs = null;
                            try { state._mutate.setDibsData(state.dibsData.slice(), { source: 'optimistic-reactivate' }); } catch(_) { storage.set('dibsData', state.dibsData); }
                        }
                    } catch(_) {}
                }
                // Refresh dibs data to ensure authoritative state from backend
                // [Refactor] Removed delay. We want to fetch ASAP. Optimistic UI handles the gap.
                // If the server is slightly behind, the next poll will catch it.
                handlers.fetchGlobalDataForced('dibs');
                // Update UI via centralized helper for all matching dibs buttons (optimistic local state)
                try {
                    const allBtns = document.querySelectorAll(`.dibs-btn[data-opponent-id='${opponentId}']`);
                    if (allBtns.length) {
                        allBtns.forEach(btn => {
                            if (utils.updateDibsButton) utils.updateDibsButton(btn, opponentId, opponentName);
                        });
                    } else if (buttonElement && utils.updateDibsButton) {
                        utils.updateDibsButton(buttonElement, opponentId, opponentName);
                    }
                    // Ensure the clicked button shows the 'YOU Dibbed' state immediately
                    try {
                        if (buttonElement && buttonElement instanceof Element) {
                            const active = state.dibsData.find(d => d && d.opponentId === String(opponentId) && d.dibsActive && String(d.userId) === String(state.user.tornId));
                            if (active) {
                                try {
                                    buttonElement.textContent = 'YOU Dibbed';
                                    buttonElement.className = 'btn dibs-btn btn-dibs-success-you';
                                    // clickable only if removable by owner or admin
                                    const dis = !(String(active.userId) === String(state.user.tornId) || (state.script?.canAdministerMedDeals && storage.get('adminFunctionality', true) === true));
                                    buttonElement.disabled = dis;
                                    const removeHandler = (typeof handlers.debouncedRemoveDibsForTarget === 'function') ? handlers.debouncedRemoveDibsForTarget : (typeof handlers.removeDibsForTarget === 'function' ? handlers.removeDibsForTarget : null);
                                    if (removeHandler) buttonElement.onclick = (e) => removeHandler(opponentId, e.currentTarget);
                                } catch(_) { /* silent */ }
                            }
                        }
                    } catch(_) { /* noop */ }
                } catch(_) { /* noop */ }
                // Optional: auto-compose dibs message to chat
                // Defer to allow UI to repaint first (chat operations can be heavy)
                setTimeout(() => ui.sendDibsMessage(opponentId, opponentName, null), 50);
                // Background normal refresh (debounced) for rest of data
                try { if (state.debug.cadence) tdmlogger('debug', `[Cadence] scheduling debounced fetchGlobalData`); } catch(_) {}
                handlers.debouncedFetchGlobalData();
            } catch (error) {
                // Revert button state on error
                if (buttonElement) {
                    try {
                        buttonElement.textContent = originalText || 'Dibs';
                        buttonElement.disabled = false;
                        buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                    } catch(_) {}
                }
                const msg = String(error?.message || 'Unknown error');
                const already = msg.toLowerCase().includes('already') || error?.code === 'already-exists' || error?.alreadyDibbed === true;
                if (!already) {
                    ui.showMessageBox(`Failed to dib: ${msg}`, 'error');
                    buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                    buttonElement.textContent = originalText;
                    return;
                }
                // Resolve current owner name
                let dibberName = error?.dibber?.name || error?.dibberName || null;
                if (!dibberName) {
                    try {
                        const existing = state.dibsData.find(d => d.opponentId === String(opponentId) && d.dibsActive);
                        if (existing) dibberName = existing.username || existing.user || existing.userId;
                    } catch(_) {}
                }
                if (!dibberName) dibberName = 'Someone';
                ui.showMessageBox(`${opponentName} is already dibbed by ${dibberName}.`, 'info');
                const canRemove = !!(state.script?.canAdministerMedDeals && (storage.get('adminFunctionality', true) === true));
                if (dibberName !== 'Someone') {
                    // Use helper for consistency (will compute correct class/disable)
                    if (utils.updateDibsButton) utils.updateDibsButton(buttonElement, opponentId, opponentName);
                } else {
                    // Unknown owner -> neutral inactive until refresh clarifies
                    if (utils.updateDibsButton) utils.updateDibsButton(buttonElement, opponentId, opponentName);
                }
                // Force a dibs-only refresh soon; also a debounced global refresh
                handlers.fetchGlobalDataForced('dibs');
                handlers.debouncedFetchGlobalData();
            }
        },
        removeDibsForTarget: async (opponentId, buttonElement) => {
            const originalText = buttonElement.textContent;
            try {
                const dib = state.dibsData.find(d => d.opponentId === String(opponentId) && d.dibsActive);
                if (dib) {
                    // Provide third button: Send to Chat (posts status then removes)
                    const result = await ui.showConfirmationBox(`Remove ${dib.username}'s dibs for ${dib.opponentname}?`, true, {
                        thirdLabel: 'Send to Chat',
                        onThird: () => {
                            // Re-announce the existing dib with current status (not an undib)
                            ui.sendDibsMessage(opponentId, dib.opponentname, dib.username);
                        }
                    });
                    if (result === 'third') {
                        // "Send to Chat" was clicked - don't remove the dib, just return
                        buttonElement.textContent = originalText;
                        return;
                    } else if (result === true) {
                        // "Yes" was clicked - remove the dib
                        let resp = null;
                        try {
                            resp = await api.post('removeDibs', { dibsDocId: dib.id, removedByUsername: state.user.tornUsername, factionId: state.user.factionId });
                        } catch (e) {
                            const em = String(e?.message||'').toLowerCase();
                            if (!(em.includes('not found') || em.includes('inactive') || e?.code === 'not-found')) throw e;
                        }

                        if (resp && resp.dibsData && Array.isArray(resp.dibsData)) {
                            if (state._mutate?.setDibsData) {
                                const fp = utils.computeDibsFingerprint ? utils.computeDibsFingerprint(resp.dibsData) : null;
                                state._mutate.setDibsData(resp.dibsData, { fingerprint: fp, source: 'removeDibs' });
                            } else {
                                storage.updateStateAndStorage('dibsData', resp.dibsData);
                            }
                            try { state._recentlyHadActiveDibsUntil = 0; } catch(_) {}
                        } else {
                            dib.dibsActive = false;
                            try { state._recentlyHadActiveDibsUntil = 0; } catch(_) {}
                            // Invalidate fingerprint so next fetch forces a re-apply (crucial if we re-add the same dib quickly)
                            if (state._fingerprints) state._fingerprints.dibs = null;
                        }

                        ui.showMessageBox('Dibs removed!', 'success');
                        if (utils.updateDibsButton) utils.updateDibsButton(buttonElement, opponentId, dib.opponentname || opponentName);
                        // handlers.fetchGlobalDataForced('dibs'); // No longer needed if we have data
                    } else {
                        // "No" was clicked
                        buttonElement.textContent = originalText;
                    }
                } else {
                    // Possible race: local state not yet updated; treat as success-idempotent
                    ui.showMessageBox('No active dibs to remove (already cleared).', 'info');
                    if (utils.updateDibsButton) utils.updateDibsButton(buttonElement, opponentId, opponentName);
                }
            } catch (error) {
                ui.showMessageBox(`Failed to remove dibs: ${error.message}`, 'error');
                buttonElement.disabled = false;
                buttonElement.textContent = originalText;
            }
        },
        handleMedDealToggle: async (opponentId, opponentName, setMedDeal, medDealForUserId, medDealForUsername, buttonElement) => {
            const originalText = buttonElement ? (buttonElement.dataset.originalText || buttonElement.innerHTML) : '';
            let previousMedDeals = {};
            try {
                if (!setMedDeal) {
                    const confirmed = await ui.showConfirmationBox(`Remove Med Deal with ${opponentName}?`);
                    if (!confirmed) {
                        buttonElement.disabled = false;
                        buttonElement.innerHTML = originalText;
                        return;
                    }
                }

                // [Refactor] Optimistic UI update
                previousMedDeals = (state.medDeals && typeof state.medDeals === 'object') ? { ...state.medDeals } : {};
                try {
                    if (typeof patchMedDeals === 'function') {
                        patchMedDeals(current => {
                            if (setMedDeal) {
                                current[opponentId] = {
                                    id: opponentId,
                                    isMedDeal: true,
                                    medDealForUserId: medDealForUserId,
                                    medDealForUsername: medDealForUsername,
                                    updatedAt: Date.now()
                                };
                            } else {
                                if (current[opponentId]) delete current[opponentId];
                            }
                            return current;
                        }, { source: 'optimistic-toggle' });
                    }
                    if (buttonElement && utils.updateMedDealButton) {
                        utils.updateMedDealButton(buttonElement, opponentId, opponentName);
                    }
                } catch (e) { tdmlogger('warn', `[medDeals] optimistic update failed: ${e}`); }

                let opponentFactionId = null;
                try {
                    const entry = state.session?.userStatusCache?.[opponentId];
                    if (entry?.factionId) opponentFactionId = String(entry.factionId);
                } catch(_) {}
                if (!opponentFactionId) {
                    try {
                        const snap = state.rankedWarTableSnapshot && state.rankedWarTableSnapshot[opponentId];
                        if (snap?.factionId) opponentFactionId = String(snap.factionId);
                    } catch(_) {}
                }
                if (!opponentFactionId) {
                    try { const st = await utils.getUserStatus(opponentId); if (st?.factionId) opponentFactionId = String(st.factionId); } catch(_) {}
                }
                if (!opponentFactionId) {
                    try { if (state.warData?.opponentFactionId) opponentFactionId = String(state.warData.opponentFactionId); } catch(_) {}
                }
                const resp = await api.post('updateMedDeal', {
                    actionInitiatorUserId: state.user.tornId,
                    actionInitiatorUsername: state.user.tornUsername,
                    targetOpponentId: opponentId,
                    opponentName,
                    setMedDeal,
                    medDealForUserId,
                    medDealForUsername,
                    factionId: state.user.factionId,
                    opponentFactionId
                });

                if (resp && resp.medDeals && Array.isArray(resp.medDeals)) {
                    if (state._mutate?.setMedDeals) {
                        // Convert array to map if needed, or assume setMedDeals handles it?
                        // Wait, medDeals is usually a map in state, but backend returns array?
                        // Let's check backend return format.
                        // Backend: medDeals = medDealsSnap.docs.map(d => ({ id: d.id, ...d.data() })); -> Array
                        // Frontend state.medDeals is usually an object/map.
                        // Let's check setMedDeals implementation.
                        // If setMedDeals expects a map, we need to convert.
                        // If setMedDeals expects an array, we are good.
                        // Let's assume we need to convert array to map for consistency with typical state structure.
                        const map = {};
                        resp.medDeals.forEach(d => { map[d.id] = d; });
                        const fp = utils.computeMedDealsFingerprint ? utils.computeMedDealsFingerprint(map) : null;
                        state._mutate.setMedDeals(map, { fingerprint: fp, source: 'updateMedDeal' });
                    } else {
                        // Fallback: storage.updateStateAndStorage might expect map too?
                        // getGlobalData converts? No, getGlobalData receives object from backend usually?
                        // Let's check getGlobalData backend.
                        // Backend getGlobalData returns object for medDeals?
                        // No, Firestore returns collections as arrays usually, but getGlobalData might format it.
                        // Let's check setMedDeals signature first.
                        const map = {};
                        resp.medDeals.forEach(d => { map[d.id] = d; });
                        storage.updateStateAndStorage('medDeals', map);
                    }
                }

                ui.showMessageBox(`Med Deal with ${opponentName} ${setMedDeal ? 'set' : 'removed'}.`, 'success');
                // await handlers.fetchGlobalData();
            } catch (error) {
                // Revert optimistic update
                try {
                    if (typeof setMedDeals === 'function') {
                        setMedDeals(previousMedDeals, { source: 'revert-error' });
                    }
                    if (buttonElement && utils.updateMedDealButton) {
                        utils.updateMedDealButton(buttonElement, opponentId, opponentName);
                    }
                } catch(_) {}

                ui.showMessageBox(`Failed to update Med Deal: ${error.message}`, 'error');
                buttonElement.innerHTML = originalText;
            }
        },
        checkTermedWarScoreCap: async () => {
            // Only run this check if it's a termed war with caps set
            if (state.warData.warType !== 'Termed War') {
                return;
            }
            
            utils.perf.start('checkTermedWarScoreCap');
            const warId = state.lastRankWar?.id;
            if (!warId) return; // Exit if we don't have a valid war ID
            // Only check while the war is active or within grace window
            if (!utils.isWarInActiveOrGrace(warId, 6)) return;

            const storageKey = `scoreCapAcknowledged_${warId}`; // individual
            const storageKeyFaction = `scoreCapFactionAcknowledged_${warId}`;
            const factionBannerKey = `factionCapNotified_${warId}`;

            // Check if the user has already acknowledged the cap for this specific war
            if (storage.get(storageKey, false)) {
                
                state.user.hasReachedScoreCap = true; // Set session state for attack page warnings
                return; // Exit to prevent showing the popup again
            }
            // Stop if the user has already been notified in this session (fallback check)
            if (state.user.hasReachedScoreCap) {
                return;
            }
            try {
                utils.perf.start('computeUserScoreCapCheck');
                const scoreType = state.warData.individualScoreType || state.warData.scoreType || 'Respect';
                
                let userScore = 0;
                let usedLightweight = false;

                // Enhancement #1: Use lightweight userScore if available and sufficient
                if (state.userScore) {
                     if (scoreType === 'Respect') { userScore = Number(state.userScore.r || 0); usedLightweight = true; }
                     else if (scoreType === 'Attacks') { userScore = Number(state.userScore.s || 0); usedLightweight = true; }
                     else if (scoreType === 'Respect (no chain)') { userScore = Number(state.userScore.rnc || 0); usedLightweight = true; }
                     else if (scoreType === 'Respect (no bonus)') { userScore = Number(state.userScore.rnb || 0); usedLightweight = true; }
                }
                // TODO Prune now that user score comes from fetchGlobalData
                if (!usedLightweight) {
                    const rows = await utils.getSummaryRowsCached(warId, state.user.factionId);
                    if (Array.isArray(rows) && rows.length > 0) {
                        const me = rows.find(r => String(r.attackerId) === String(state.user.tornId));
                        if (me) {
                            userScore = utils.computeScoreFromRow(me, scoreType);
                        }
                    }
                }

                const indivCap = Number((state.warData.individualScoreCap ?? state.warData.scoreCap) || 0) || 0;
                if (indivCap > 0 && userScore >= indivCap) {
                        state.user.hasReachedScoreCap = true;
                        const confirmed = await ui.showConfirmationBox('You have reached your target score. Do not make any more attacks. Your dibs and med deals will be deactivated.', false);
                        if (confirmed) {
                            storage.set(storageKey, true);
                            try { await api.post('deactivateDibsAndDealsForUser', { userId: state.user.tornId, factionId: state.user.factionId }); } catch(_) {}
                            await handlers.fetchGlobalData();
                        }
                }
                utils.perf.stop('computeUserScoreCapCheck');
                utils.perf.start('checkFactionScoreCap');
                // FACTION CAP (authoritative lastRankWar.factions only) - banner removed
                const facCap = Number((state.warData.factionScoreCap ?? state.warData.scoreCap) || 0) || 0;
                if (facCap > 0) {
                    let factionTotal = 0;
                    try {
                        const lw = state.lastRankWar;
                        if (lw && Array.isArray(lw.factions)) {
                            const ourFac = lw.factions.find(f => String(f.id) === String(state.user.factionId));
                            if (ourFac && typeof ourFac.score === 'number') factionTotal = Number(ourFac.score) || 0;
                        }
                    } catch(_) { /* noop */ }
                    const reached = factionTotal >= facCap;
                    if (reached && !storage.get(storageKeyFaction, false)) {
                        await ui.showConfirmationBox('Faction score cap reached. Stop attacks unless leadership directs otherwise.', false);
                        storage.set(storageKeyFaction, true);
                    }
                }
                utils.perf.stop('checkFactionScoreCap');
            } catch(error) {
                tdmlogger('error', `Error checking score cap: ${error}`);
                utils.perf.stop('checkTermedWarScoreCap');
                utils.perf.stop('computeUserScoreCapCheck');
                utils.perf.stop('checkFactionScoreCap');

                
            }
            // utils.perf.stop('checkTermedWarScoreCap');
            utils.perf.stop('checkTermedWarScoreCap');
        },
        setFactionWarData: async (warData, buttonElement) => {
            const originalText = buttonElement.textContent;
            buttonElement.disabled = true;
            buttonElement.innerHTML = '<span class="dibs-spinner"></span> Saving...';
            try {
                await api.post('setWarData', { warId: state.lastRankWar.id.toString(), warData, factionId: state.user.factionId });
                ui.showMessageBox('War data saved!', 'success');
                await handlers.fetchGlobalData();
            } catch (error) {
                ui.showMessageBox(`Failed to save war data: ${error.message}`, 'error');
            } finally {
                buttonElement.disabled = false;
                buttonElement.textContent = originalText;
            }
        },
        handleSaveUserNote: async (noteTargetId, noteContent, buttonElement, { silent = false } = {}) => {
            if (!noteTargetId) return;
            const existing = state.userNotes[noteTargetId]?.noteContent || '';
            const trimmedNew = (noteContent || '').trim();
            // Short-circuit if unchanged
            if (existing.trim() === trimmedNew) {
                if (buttonElement) utils.updateNoteButtonState(buttonElement, trimmedNew);
                return;
            }
            const originalText = buttonElement && buttonElement.textContent;
            try {
                let noteTargetFactionId = null; let noteTargetUsername = null;
                try { const entry = state.session?.userStatusCache?.[noteTargetId]; if (entry?.factionId) noteTargetFactionId = String(entry.factionId); } catch(_) {}
                if (!noteTargetFactionId) {
                    try { const snap = state.rankedWarTableSnapshot && state.rankedWarTableSnapshot[noteTargetId]; if (snap?.factionId) noteTargetFactionId = String(snap.factionId); if (!noteTargetUsername && snap) noteTargetUsername = snap.name || snap.username || null; } catch(_) {}
                }
                if (!noteTargetFactionId) {
                    try { const st = await utils.getUserStatus(noteTargetId); if (st?.factionId) noteTargetFactionId = String(st.factionId); } catch(_) {}
                }
                if (!noteTargetFactionId) {
                    try { if (state.warData?.opponentFactionId) noteTargetFactionId = String(state.warData.opponentFactionId); } catch(_) {}
                }
                if (!noteTargetUsername) {
                    try { const st = state.rankedWarTableSnapshot && state.rankedWarTableSnapshot[noteTargetId]; if (st) noteTargetUsername = st.name || st.username || null; } catch(_) {}
                }
                const resp = await api.post('updateUserNote', { noteTargetId, noteContent: trimmedNew, factionId: state.user.factionId, noteTargetFactionId, noteTargetUsername });
                
                if (resp && resp.userNotes && Array.isArray(resp.userNotes)) {
                    storage.updateStateAndStorage('userNotes', resp.userNotes);
                } else {
                    state.userNotes[noteTargetId] = { noteContent: trimmedNew, updated: Date.now(), updatedBy: state.user.tornId };
                }

                if (!silent) ui.showMessageBox('[TDM] Note saved!', 'success');
                if (buttonElement) {
                    utils.updateNoteButtonState(buttonElement, trimmedNew);
                    if (originalText && buttonElement.textContent !== originalText) buttonElement.textContent = originalText;
                }
                // Local-only refresh of ranked war UI; avoid full global fetch unless needed
                try { ui._renderEpoch.schedule(); } catch(_) {}
            } catch (error) {
                if (!silent) ui.showMessageBox(`[TDM] Failed to save note: ${error.message}`, 'error');
                if (buttonElement) {
                    if (originalText) buttonElement.textContent = originalText;
                }
            }
        },
        assignDibs: async (opponentId, opponentName, dibsForUserId, dibsForUsername, buttonElement) => {
            const originalText = buttonElement ? (buttonElement.dataset.originalText || buttonElement.textContent) : '';
            // Immediate feedback
            if (buttonElement) {
                try {
                    buttonElement.textContent = 'Saving...';
                    buttonElement.disabled = true;
                    buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                } catch(_) {}
            }

            try {
                const opts = utils.getDibsStyleOptions();
                const [oppStat, assigneeStat] = await Promise.all([
                    utils.getUserStatus(opponentId),
                    utils.getUserStatus(dibsForUserId)
                ]);
                const assCanon = assigneeStat.canonical;
                if (opts.allowedUserStatuses && opts.allowedUserStatuses[assCanon] === false) {
                    throw new Error(`User status (${assCanon}) is not allowed to place dibs by faction policy.`);
                }
                const canonOpp = oppStat.canonical;
                if (opts.allowStatuses && opts.allowStatuses[canonOpp] === false) {
                    throw new Error(`Target status (${canonOpp}) is not allowed by faction policy.`);
                }
                if (canonOpp === 'Hospital') {
                    const limitMin = Number(opts.maxHospitalReleaseMinutes || 0);
                    if (limitMin > 0) {
                        const remaining = Math.max(0, (oppStat.until || 0) - Math.floor(Date.now() / 1000));
                        if (remaining > limitMin * 60) {
                            throw new Error(`Target is hospitalized too long (${Math.ceil(remaining/60)}m). Policy allows < ${limitMin}m.`);
                        }
                    }
                }
                let opponentFactionId = null;
                try { const entry = state.session?.userStatusCache?.[opponentId]; if (entry?.factionId) opponentFactionId = String(entry.factionId); } catch(_) {}
                if (!opponentFactionId) { try { if (oppStat?.factionId) opponentFactionId = String(oppStat.factionId); } catch(_) {} }
                if (!opponentFactionId) { try { const snap = state.rankedWarTableSnapshot && state.rankedWarTableSnapshot[opponentId]; if (snap?.factionId) opponentFactionId = String(snap.factionId); } catch(_) {} }
                if (!opponentFactionId) { try { if (state.warData?.opponentFactionId) opponentFactionId = String(state.warData.opponentFactionId); } catch(_) {} }
                
                // If there's an existing active dib locally for this opponent by a different user,
                // require admin to confirm removal before proceeding to assign.
                try {
                    const existingOther = Array.isArray(state.dibsData) ? state.dibsData.find(d => d && d.dibsActive && String(d.opponentId) === String(opponentId) && String(d.userId) !== String(dibsForUserId)) : null;
                    const isAdmin = !!(state.script?.canAdministerMedDeals || state.script?.canAdministerDibs || state.script?.isAdmin);
                    if (existingOther && isAdmin) {
                        const existingName = existingOther.username || existingOther.user || 'Someone';
                        const prompt = `Assigning dibs will remove existing dibs held by ${existingName} on ${opponentName}. Do you want to continue?`;
                        const confirm = await ui.showConfirmationBox(prompt, true);
                        if (confirm !== true) {
                            // User cancelled - revert button state and abort
                            if (buttonElement) {
                                try {
                                    buttonElement.textContent = originalText;
                                    buttonElement.disabled = false;
                                    buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                                } catch(_) {}
                            }
                            return;
                        }
                    }
                } catch(_) {}

                const assignResp = await api.post('dibsTarget', {
                    userid: dibsForUserId,
                    username: dibsForUsername,
                    opponentId,
                    opponentname: opponentName,
                    warType: state.warData.warType,
                    factionId: state.user.factionId,
                    userStatusAtDib: assCanon,
                    opponentStatusAtDib: canonOpp,
                    opponentFactionId
                });
                
                const createdId = assignResp?.id;
                const serverMsg = (assignResp && assignResp.message) ? String(assignResp.message) : '';
                
                if (createdId) {
                    ui.showMessageBox(`[TDM] Assigned dibs on ${opponentName} to ${dibsForUsername}!`, 'success');
                    // Optimistic update
                    try {
                        const previouslyActive = [];
                        for (const d of state.dibsData) {
                            if (d.dibsActive && String(d.opponentId) === String(opponentId)) {
                                d.dibsActive = false;
                            }
                            if (d.dibsActive && String(d.userId) === String(dibsForUserId)) {
                                if (String(d.opponentId) !== String(opponentId)) {
                                     previouslyActive.push({ id: String(d.opponentId), name: d.opponentname || d.opponentName || '' });
                                }
                                d.dibsActive = false;
                            }
                        }
                        const optimistic = {
                            id: createdId,
                            factionId: String(state.user.factionId),
                            userId: String(dibsForUserId),
                            username: dibsForUsername,
                            opponentId: String(opponentId),
                            opponentname: opponentName,
                            dibbedAt: { _seconds: Math.floor(Date.now()/1000) },
                            lastActionTimestamp: { _seconds: Math.floor(Date.now()/1000) },
                            dibsActive: true,
                            warType: state.warData.warType || 'unknown',
                            userStatusAtDib: assCanon,
                            opponentStatusAtDib: canonOpp,
                            opponentFactionId: opponentFactionId || null
                        };
                        state.dibsData.push(optimistic);
                        // handlers._trackPendingDibsId(createdId); // Removed in refactor
                        try { state._mutate.setDibsData(state.dibsData.slice(), { source: 'optimistic-assign' }); } catch(_) { storage.set('dibsData', state.dibsData); }
                        
                        if (previouslyActive.length && utils.updateDibsButton) {
                            previouslyActive.forEach(({ id, name }) => {
                                try {
                                    const btns = document.querySelectorAll(`.dibs-btn[data-opponent-id='${id}']`);
                                    btns.forEach(btn => utils.updateDibsButton(btn, id, name || btn?.dataset?.opponentName || opponentName));
                                } catch(_) { /* ignore */ }
                            });
                        }
                    } catch(e) { tdmlogger('warn', `[assignDibs][optimistic] failed: ${e}`); }

                } else if (serverMsg.toLowerCase().includes('already')) {
                    ui.showMessageBox(serverMsg, 'info');
                     try {
                        const existing = state.dibsData.find(d => String(d.opponentId) === String(opponentId) && String(d.userId) === String(dibsForUserId));
                        if (existing) {
                            existing.dibsActive = true;
                            if (state._fingerprints) state._fingerprints.dibs = null;
                            try { state._mutate.setDibsData(state.dibsData.slice(), { source: 'optimistic-reactivate' }); } catch(_) { storage.set('dibsData', state.dibsData); }
                        }
                    } catch(_) {}
                } else {
                    ui.showMessageBox(serverMsg || `[TDM] Assigned dibs on ${opponentName} to ${dibsForUsername}!`, 'success');
                     try {
                        const existing = state.dibsData.find(d => String(d.opponentId) === String(opponentId) && String(d.userId) === String(dibsForUserId));
                        if (existing) {
                            existing.dibsActive = true;
                            if (state._fingerprints) state._fingerprints.dibs = null;
                            try { state._mutate.setDibsData(state.dibsData.slice(), { source: 'optimistic-reactivate' }); } catch(_) { storage.set('dibsData', state.dibsData); }
                        }
                    } catch(_) {}
                }

                setTimeout(() => handlers.fetchGlobalDataForced('dibs'), 250);

                try {
                    const allBtns = document.querySelectorAll(`.dibs-btn[data-opponent-id='${opponentId}']`);
                    if (allBtns.length) {
                        allBtns.forEach(btn => {
                            if (utils.updateDibsButton) utils.updateDibsButton(btn, opponentId, opponentName);
                        });
                    } else if (buttonElement && utils.updateDibsButton) {
                        utils.updateDibsButton(buttonElement, opponentId, opponentName);
                    }
                } catch(_) {}

                setTimeout(() => ui.sendDibsMessage(opponentId, opponentName, dibsForUsername), 50);
                handlers.debouncedFetchGlobalData();

            } catch (error) {
                const msg = String(error?.message || 'Unknown error');
                const already = msg.toLowerCase().includes('already') || error?.code === 'already-exists' || error?.alreadyDibbed === true;
                if (already) {
                    const dibberName = error?.dibber?.name || error?.dibberName || 'Someone';
                    ui.showMessageBox(`${opponentName} is already dibbed by ${dibberName}.`, 'info');
                    handlers.debouncedFetchGlobalData();
                } else {
                    ui.showMessageBox(`[TDM] Failed to assign dibs: ${msg}`, 'error');
                }
                if (buttonElement) {
                    buttonElement.textContent = originalText;
                    buttonElement.disabled = false;
                    buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                }
            }
        },
        // Auto-enforcement sweep: remove dibs when opponent/user travels if policy enabled
        enforceDibsPolicies: async () => {
            try {
                const now = Date.now();
                if (now - (state.session.lastEnforcementMs || 0) < 8000) return; // every ~8s max
                state.session.lastEnforcementMs = now;
                const opts = utils.getDibsStyleOptions();
                const myActive = state.dibsData.find(d => d.dibsActive && d.userId === state.user.tornId);
                if (!myActive) return;
                // Determine allowances: if travel is allowed for user/opponent, skip corresponding removals even if legacy flags set
                const userAllowedTravel = opts.allowedUserStatuses.Travel !== false && opts.allowedUserStatuses.Abroad !== false;
                const oppAllowedTravel = opts.allowStatuses.Travel !== false && opts.allowStatuses.Abroad !== false;
                // Check opponent travel removal
                if (opts.removeOnFly && !oppAllowedTravel) {
                    try {
                        utils.perf.start('enforceDibsPolicies.getOpponentStatus');
                        const oppStat = await utils.getUserStatus(myActive.opponentId);
                        try { if (storage.get('debugDibsEnforce', false)) tdmlogger('info', '[enforceDibsPolicies] opponentStatus', { opponentId: myActive.opponentId, oppStat }); } catch(_) {}
                        utils.perf.stop('enforceDibsPolicies.getOpponentStatus');
                        if (oppStat.canonical === 'Travel' || oppStat.canonical === 'Abroad') {
                            utils.perf.start('enforceDibsPolicies.api.removeDibs.opponentTravel');
                await api.post('removeDibs', { dibsDocId: myActive.id, removedByUsername: state.user.tornUsername, factionId: state.user.factionId, reason: 'policy-opponent-travel' });
                            utils.perf.stop('enforceDibsPolicies.api.removeDibs.opponentTravel');
                            ui.showMessageBox(`Your dib on ${myActive.opponentname || myActive.opponentId} was removed (opponent traveling policy).`, 'warning');
                            handlers.debouncedFetchGlobalData();
                            return;
                        }
                    } catch (_) { /* ignore */ }
                }
                // Check user travel removal
                if (opts.removeWhenUserTravels && !userAllowedTravel) {
                    try {
                        utils.perf.start('enforceDibsPolicies.getMyStatus');
                        const me = await utils.getUserStatus(null);
                        try { if (storage.get('debugDibsEnforce', false)) tdmlogger('info', '[enforceDibsPolicies] myStatus', { me }); } catch(_) {}
                        utils.perf.stop('enforceDibsPolicies.getMyStatus');
                        if (me.canonical === 'Travel' || me.canonical === 'Abroad') {
                            utils.perf.start('enforceDibsPolicies.api.removeDibs.userTravel');
                            await api.post('removeDibs', { dibsDocId: myActive.id, removedByUsername: state.user.tornUsername, factionId: state.user.factionId, reason: 'policy-user-travel' });
                            utils.perf.stop('enforceDibsPolicies.api.removeDibs.userTravel');
                            ui.showMessageBox(`Your dib on ${myActive.opponentname || myActive.opponentId} was removed (your travel policy).`, 'warning');
                            handlers.debouncedFetchGlobalData();
                            return;
                        }
                    } catch (_) { /* ignore */ }
                }
            } catch (e) { /* non-fatal */ }
        },
        // Check OC status once per userscript load
        checkOCReminder: async () => {
            const ocReminderEnabled = storage.get('ocReminderEnabled', true);
            const ocReminderShown = storage.get('ocReminderShown', false);

            if (ocReminderEnabled && !ocReminderShown) {
                const currentUser = state.factionMembers.find(member => member.id == state.user.tornId);
                if (currentUser && !currentUser.is_in_oc) {
                    ui.showConfirmationBox('[TDM] JOIN AN OC!');
                    storage.set('ocReminderShown', true); // Ensure it only shows once per load
                }
            }
        },
        /* ================= Live Tracking Snapshot/Diff (Phase 2: Retal + Canonical Status) =================
            (Legacy baseline model documentation removed – superseded by unified activity tracking.)
            Baseline entry shape (expanded): {
                rt:1,            // retaliation flag (optional)
                c:'Hospital',    // canonical status string (optional)
                u: epochSec,     // hospital until (if Hospital)
                ab:1,            // abroad hospital flag (if Hospital abroad)
                te: epochSec,    // travel end (arrival) time if traveling
                ls: epochSec,    // last seen timestamp for any tracked dimension
                ver:1            // schema version
            }
            Snapshot entry (transient) mirrors the subset: { retal:1, c:'Hospital', u:1234567890, ab:1, te:1234567, lastSeenSec }
            Events now include: retalOpen/retalClose, statusChange, earlyHospOut, abroadHospEnter, abroadHospExit
        */
        // Legacy live-tracking initializer: intentionally a no-op.
        // The legacy live tracker has been retired in favor of the activity tracking engine
        // which uses state._activityTracking and state.unifiedStatus as the canonical sources.
        // Keep a no-op initializer to avoid surprising side-effects from older call sites.
        _initLiveTracking: () => {
            // Ensure the legacy container is not recreated. If an older runtime expects an object,
            // they should guard access; new code should use state._activityTracking instead.
            state._liveTrack = null;
            return;
        },
        _buildLiveSnapshot: () => {
            handlers._initLiveTracking();
            const t0 = performance.now();
            const snapshot = {};
            // Retal opportunities already normalized into state.retaliationOpportunities (map of opponentId-> data)
            try {
                const retals = state.retaliationOpportunities || {};
                const nowSec = Math.floor(Date.now()/1000);
                for (const oid of Object.keys(retals)) {
                    snapshot[oid] = snapshot[oid] || {};
                    snapshot[oid].retal = 1; // boolean flag for now
                    snapshot[oid].lastSeenSec = nowSec;
                }
            } catch(_) { /* noop */ }
            // Canonical status & hospital info from rankedWarTableSnapshot + meta
            try {
                const tableSnap = state.rankedWarTableSnapshot || {};
                const meta = state.rankedWarChangeMeta || {};
                const nowSec = Math.floor(Date.now()/1000);
                for (const oid of Object.keys(tableSnap)) {
                    const row = tableSnap[oid];
                    const m = meta[oid] || {};
                    const canon = row?.statusCanon || row?.canon || null;
                    if (!canon) continue;
                    const s = (snapshot[oid] = snapshot[oid] || {});
                    s.c = canon; // canonical status
                    if (canon === 'Hospital') {
                        // Hospital until: prefer meta.hospitalUntil; else attempt parse from row.status if numeric pattern present
                        let until = null;
                        if (typeof m.hospitalUntil === 'number' && m.hospitalUntil > 0) until = m.hospitalUntil;
                        if (!until) {
                            try {
                                // row.status might contain remaining mm:ss; we can't reverse exact until precisely without server time; skip.
                            } catch(_) {}
                        }
                        if (until) s.u = until;
                        if (m.hospitalAbroad) s.ab = 1;
                    } else if (canon === 'Travel') {
                        // Attempt parse of remaining time mm:ss or m:ss in row.status or meta.travelUntil
                        try {
                            let travelUntil = null;
                            if (typeof m.travelUntil === 'number' && m.travelUntil > 0) travelUntil = m.travelUntil;
                            if (!travelUntil) {
                                const rawStatus = row.status || row.statusText || '';
                                const match = rawStatus.match(/(\d+):(\d{2})/); // mm:ss
                                if (match) {
                                    const mm = Number(match[1]); const ss = Number(match[2]);
                                    if (Number.isFinite(mm) && Number.isFinite(ss)) {
                                        const remain = mm * 60 + ss;
                                        if (remain > 0 && remain < 6*3600) { // sanity cap 6h
                                            travelUntil = Math.floor(Date.now()/1000) + remain;
                                        }
                                    }
                                }
                            }
                            if (travelUntil) { s.te = travelUntil; }
                        } catch(_) { /* ignore parse */ }
                    }
                    s.lastSeenSec = nowSec;
                }
            } catch(_) { /* non-fatal */ }
            const t1 = performance.now();
            if (state._liveTrack && state._liveTrack.metrics) {
                state._liveTrack.metrics.lastBuildMs = +(t1 - t0).toFixed(2);
            }
            return snapshot;
        },
        _diffLiveSnapshots: (prev, curr) => {
            // Simplified diffing: derive canonical transitions based on prev vs curr snapshots.
            // No longer relies on persisted baseline or multiple per-flag toggles; use the single activityTrackingEnabled toggle.
            handlers._initLiveTracking();
            const t0 = performance.now();
            const events = [];
            // If activity tracking globally disabled, short-circuit
            if (!storage.get('tdmActivityTrackingEnabled', false)) {
                if (state._liveTrack && state._liveTrack.metrics) {
                    state._liveTrack.metrics.lastDiffMs = 0;
                    state._liveTrack.metrics.lastEvents = 0;
                }
                return [];
            }
            // Universe of opponentIds from union
            const ids = new Set([
                ...(prev ? Object.keys(prev) : []),
                ...Object.keys(curr || {})
            ]);
            const nowSec = Math.floor(Date.now()/1000);
            for (const id of ids) {
                const p = (prev && prev[id]) || {};
                const c = curr[id] || {};
                // Retal change
                if (!!p.retal !== !!c.retal) events.push({ type: c.retal ? 'retalOpen' : 'retalClose', opponentId: id, ts: nowSec });
                // Status change
                const prevCanon = p.c || p.canonical || null;
                const currCanon = c.c || c.canonical || null;
                if (prevCanon && currCanon && prevCanon !== currCanon) {
                    events.push({ type: 'statusChange', opponentId: id, ts: nowSec, prevStatus: prevCanon, newStatus: currCanon });
                }
                // Early hospital exit
                if (prevCanon === 'Hospital' && currCanon !== 'Hospital') {
                    const prevUntil = p.u || null;
                    if (prevUntil && prevUntil - nowSec > 30) events.push({ type: 'earlyHospOut', opponentId: id, ts: nowSec, remaining: prevUntil - nowSec, expectedUntil: prevUntil });
                }
                // Abroad hospital transitions with ab flag 
                const prevAb = !!p.ab; const currAb = !!c.ab;
                if (currCanon === 'Hospital' && currAb && !(prevCanon === 'Hospital' && prevAb)) events.push({ type: 'abroadHospEnter', opponentId: id, ts: nowSec });
                if (prevCanon === 'Hospital' && prevAb && !(currCanon === 'Hospital' && currAb)) events.push({ type: 'abroadHospExit', opponentId: id, ts: nowSec });
                // Travel events
                const prevIsTravel = prevCanon === 'Travel' || prevCanon === 'Returning' || prevCanon === 'Abroad';
                const currIsTravel = currCanon === 'Travel' || currCanon === 'Returning' || currCanon === 'Abroad';
                if (!prevIsTravel && currIsTravel) events.push({ type: 'travelDepart', opponentId: id, ts: nowSec });
                if (prevIsTravel && currCanon === 'Abroad' && prevCanon !== 'Abroad') events.push({ type: 'travelArriveAbroad', opponentId: id, ts: nowSec });
                if (prevIsTravel && currCanon === 'Okay') events.push({ type: 'travelReturn', opponentId: id, ts: nowSec });
            }
            const t1 = performance.now();
            if (state._liveTrack && state._liveTrack.metrics) {
                state._liveTrack.metrics.lastDiffMs = +(t1 - t0).toFixed(2);
                state._liveTrack.metrics.lastEvents = events.length;
            }
            return events;
        },
        _mapLiveEventsToAlerts: (events) => {
            if (!events || !events.length) return;
            let needsScan = false;
            const meta = state.rankedWarChangeMeta || (state.rankedWarChangeMeta = {});
            const now = Date.now();
            events.forEach(ev => {
                const id = ev.opponentId;
                if (ev.type === 'retalOpen') {
                    meta[id] = { ...(meta[id]||{}), activeType: 'retal', pendingText: 'Retal', ts: now };
                    needsScan = true;
                } else if (ev.type === 'retalClose') {
                    // Show brief retalDone state
                    meta[id] = { ...(meta[id]||{}), activeType: 'retalDone', pendingText: 'Retal', ts: now };
                    needsScan = true;
                } else if (ev.type === 'earlyHospOut') {
                    meta[id] = { activeType: 'earlyHospOut', pendingText: 'Hosp Early', ts: now, prevStatus: 'Hospital', newStatus: 'Okay' };
                    needsScan = true;
                } else if (ev.type === 'abroadHospEnter') {
                    meta[id] = { ...(meta[id]||{}), activeType: 'abroadHosp', pendingText: 'Hosp Abroad', ts: now, hospitalAbroad: true };
                    needsScan = true;
                } else if (ev.type === 'abroadHospExit') {
                    if (meta[id]?.activeType === 'abroadHosp') delete meta[id];
                    needsScan = true;
                } else if (ev.type === 'travelDepart') {
                    // For now we do not display travel alert (button types restricted); could future-add.
                } else if (ev.type === 'travelArriveAbroad') {
                    // Could mark abroad travel state for other UI; leaving no-op.
                } else if (ev.type === 'travelReturn') {
                    // Clear transient travel meta if present
                    if (meta[id]?.activeType === 'travel') delete meta[id];
                } else if (ev.type === 'statusChange') {
                    // We only create an alert if transition leads to abroad hospital (handled above) or hospital exit early (handled separately)
                }
            });
            if (needsScan) {
                try { state._rankedWarScheduleScan && state._rankedWarScheduleScan(); } catch(_) {}
            }
        },
        _persistLiveBaselineMaybe: () => {
            // No-op: legacy baseline persistence removed in favor of transient unifiedStatus updates.
            return;
        },
        _pruneBaselineOccasionally: () => {
            // No-op: baseline pruning retained as historical artifact; unifiedStatus pruning handled elsewhere if needed.
            return;
        },
        // Legacy live tracking removed. New Activity Tracking engine (diff-on-poll) hooks below.
            /*
            * Activity Tracking Engine (Overview)
            * ----------------------------------
            * Purpose: Lightweight periodic snapshot + diff system replacing the legacy live tracker & timeline UI.
            * Cycle:
            *   1. Poll (api.refreshFactionBundles lightweight path) at configurable cadence (default 10s).
            *   2. Normalize current opponent states via utils.buildCurrentUnifiedActivityState().
            *   3. Compute a compact rolling signature (id:phase) to cheaply skip unchanged ticks (increments skippedTicks metric).
            *   4. When changes detected, derive transitions, inject transient Landed, escalate confidence (LOW→MED→HIGH, no downgrades).
            *   5. Apply transitions to UI, expire transients, render travel ETAs, update debug overlay if enabled.
            * Confidence Model (Unified):
            *   - LOW: Newly observed / insufficient timing data (or post-phase change reset).
            *   - MED: Travel with destination & minutes known but awaiting stability (first poll or < promotion threshold).
            *   - HIGH: Travel stable after promotion (2nd observation or elapsed >= TRAVEL_PROMOTE_MS); precise ETA computed.
            * Transients:
            *   - Landed: Short-lived (config.LANDED_TTL_MS) display replacing the immediate post-travel state before settling to Okay.
            * Performance Notes:
            *   - Signature hashing is O(n) without sort to minimize CPU churn.
            *   - Expiration / rendering paths are guarded by feature toggles & visibility checks elsewhere.
            * Metrics (at.metrics): lastPoll, lastDiffMs, transitions, skippedTicks.
            * Extensibility: Additional state facets (e.g. witness escalation) can hook into _escalateConfidence without altering tick loop.
            */
        _initActivityTracking: () => {
            if (state._hardResetInProgress) { tdmlogger('warn', '[ActivityTrack] init skipped during hard reset'); return; }
            if (state._activityTracking?.initialized) return;
            state._activityTracking = {
                initialized: true,
                prevById: {}, // id -> previous normalized state
                cadenceMs: Number(storage.get('tdmActivityCadenceMs', 10000)) || 10000,
                timerId: null,
                metrics: { 
                    lastPoll: 0,
                    lastDiffMs: 0,
                    transitions: 0,
                    skippedTicks: 0,
                    signatureSkips: 0,
                    totalTicks: 0,
                    lastSignature: '',
                    plannedNextTick: 0,
                    lastTickStart: 0,
                    lastTickEnd: 0,
                    lastDriftMs: 0,
                    lastApiMs: 0,
                    lastBuildMs: 0,
                    lastApplyMs: 0,
                    driftSamples: [],
                    tickSamples: [],
                    bundleSkipsInFlight: 0,
                    bundleSkipsRecent: 0,
                    bundleErrors: 0,
                    fetchHits: 0,
                    fetchErrors: 0,
                    lastFetchReason: 'init',
                    lastTickOutcome: 'init'
                }
            };
            handlers._scheduleActivityTick();
        },
        _teardownActivityTracking: () => {
            try { if (state._activityTracking?.timerId) try { utils.unregisterTimeout(state._activityTracking.timerId); } catch(_) {} } catch(_) {}
            // Purge phase history / logs prior to nulling reference for GC friendliness
            try {
                if (state._activityTracking) {
                    delete state._activityTracking._phaseHistory;
                    delete state._activityTracking._recentTransitions;
                    delete state._activityTracking._transitionLog;
                }
            } catch(_) { /* ignore */ }
            state._activityTracking = null;
            // Remove transient UI (travel etas, overlay) but leave core status cells intact
            document.querySelectorAll('.tdm-travel-eta').forEach(el=>el.remove());
            const ov = document.getElementById('tdm-live-track-overlay'); if (ov) ov.remove();
            ui.ensureDebugOverlayContainer?.({ passive: true, skipShow: true });
        },
        _flushActivityCache: async () => {
            if (state._activityTracking) {
                state._activityTracking.prevById = {};
                const metrics = state._activityTracking.metrics;
                if (metrics) {
                    metrics.transitions = 0;
                    metrics.signatureSkips = 0;
                    metrics.skippedTicks = 0;
                    metrics.totalTicks = 0;
                    metrics.fetchHits = 0;
                    metrics.fetchErrors = 0;
                    metrics.bundleSkipsInFlight = 0;
                    metrics.bundleSkipsRecent = 0;
                }
            }
            // Remove any persisted per-id historical keys (future extension)
            await ui?._kv?.removeByPrefix('tdm.act.prev.id_');
        },
        _updateActivityCadence: (ms) => {
            if (state._activityTracking) {
                state._activityTracking.cadenceMs = ms;
                handlers._scheduleActivityTick(true);
            }
        },
        _scheduleActivityTick: (restart=false) => {
            const at = state._activityTracking; if (!at) return;
            if (restart && at.timerId) { try { utils.unregisterTimeout(at.timerId); } catch(_) {} }
            // Attempt drift-corrected scheduling: base on lastTickStart when available
            let delay = at.cadenceMs;
            const m = at.metrics||{};
            if (m.lastTickStart) {
                const elapsedSinceStart = Date.now() - m.lastTickStart;
                delay = Math.max(0, at.cadenceMs - elapsedSinceStart);
            }
            m.plannedNextTick = Date.now() + delay;
            try {tdmlogger('debug', '[ActivityTick][schedule]', { delay, plannedNextTick: m.plannedNextTick, cadenceMs: at.cadenceMs }); } catch(_) {}
            at.timerId = utils.registerTimeout(setTimeout(handlers._activityTick, delay));
        },

        // Lightweight attacker heartbeat: tiny, dedicated ping when the user has active dibs
        _activityHeartbeatState: { timerId: null, cadenceMs: null },
        _scheduleActivityHeartbeat: (restart = false) => {
            const hb = handlers._activityHeartbeatState;
            // cadence falls back to the configured attacker ping ms or 60s
            hb.cadenceMs = hb.cadenceMs || (Number(config.ATTACKER_ACTIVITY_PING_MS) || 60000);
            if (restart && hb.timerId) {
                try { clearTimeout(hb.timerId); } catch(_) {}
                hb.timerId = null;
            }
            try {
                hb.timerId = utils.registerTimeout(setTimeout(handlers._activityHeartbeatTick, hb.cadenceMs));
            } catch (_) { hb.timerId = null; }
        },

        _activityHeartbeatTick: async () => {
            // Compatibility wrapper: delegate to the immediate sender
            try { await handlers._sendAttackerHeartbeatNow?.(); } catch(_) {}
        },

        // Immediate sender: performs minimal heartbeat check and sends a best-effort ping.
        // This function does NOT reschedule itself; callers (runTick/visibility handlers) control cadence.
        _sendAttackerHeartbeatNow: async () => {
            try {
                const myTornId = String(state.user?.tornId || '');
                if (!myTornId) return;
                const dibs = Array.isArray(state.dibsData) ? state.dibsData : (state.dibsData || []);
                let hasActiveDibs = false;
                try {
                    for (const d of dibs) {
                        if (!d) continue;
                        if (String(d.userId) === myTornId && d.dibsActive) { hasActiveDibs = true; break; }
                    }
                } catch (_) { hasActiveDibs = false; }

                if (!hasActiveDibs) {
                    try { tdmlogger('debug', '[ActivityHeartbeat] not sending (no active dibs)'); } catch(_) {}
                    return;
                }

                if (api.isIpRateLimited?.()) {
                    try { tdmlogger('debug', '[ActivityHeartbeat] not sending (rate-limited)'); } catch(_) {}
                    return;
                }

                const HEARTBEAT_ACTIVE_MS = 30000; // target ~30s cadence while window active
                const nowMsPing = Date.now();
                state._lastAttackerPingMs = state._lastAttackerPingMs || 0;
                if ((nowMsPing - state._lastAttackerPingMs) < HEARTBEAT_ACTIVE_MS) {
                    try { tdmlogger('debug', '[ActivityHeartbeat] skipped due to ping throttle', { nowMsPing, last: state._lastAttackerPingMs, HEARTBEAT_ACTIVE_MS }); } catch(_) {}
                    return;
                }

                state._lastAttackerPingMs = nowMsPing; // best-effort immediate update to avoid bursts
                try { tdmlogger('debug', '[ActivityHeartbeat] sending attacker ping', { nowMsPing, HEARTBEAT_ACTIVE_MS }); } catch(_) {}
                try {
                    api.post('updateAttackerLastAction', { lastActionTimestamp: nowMsPing, factionId: state.user.factionId }).catch(e => {
                        try { tdmlogger('warn', '[ActivityHeartbeat] updateAttackerLastAction failed', { e: e?.message }); } catch(_) {}
                    });
                } catch(_) { /* swallow */ }
            } catch (e) {
                try { tdmlogger('warn', '[ActivityHeartbeat] error', { e: e?.message }); } catch(_) {}
            }
        },
        // Step 2: Normalize an activity record to the canonical contract
        _normalizeActivityRecord: (rec, prev) => {
            if (!rec) return rec;
            const out = { ...rec };
            const raw = (out.canonical || out.phase || '').toString().toLowerCase();
            const mapPhase = (p) => {
                switch (p) {
                    case 'travel':
                    case 'traveling': return 'Travel';
                    case 'returning': return 'Returning';
                    case 'abroad': return 'Abroad';
                    case 'hospital':
                    case 'hosp': return 'Hospital';
                    case 'hospitalabroad':
                    case 'abroad_hosp':
                    case 'hospital_abroad': return 'HospitalAbroad';
                    case 'jail': return 'Jail';
                    case 'landed': return 'Landed';
                    case 'okay':
                    case 'ok':
                    case '': return 'Okay';
                    default: return 'Okay';
                }
            };
            const canon = mapPhase(raw);
            out.canonical = canon; out.phase = canon;
            if (!['LOW','MED','HIGH'].includes(out.confidence)) out.confidence = 'LOW';
            const prevCanon = prev ? (prev.canonical||prev.phase) : null;
            const phaseChanged = prevCanon && prevCanon !== canon;
            const now = Date.now();
            out.startedMs = out.startedMs || out.startMs || (phaseChanged ? now : (prev && prev.startedMs) || now);
            const isTravel = canon === 'Travel' || canon === 'Returning';
            if (!isTravel && canon !== 'Landed') {
                // Allow Abroad and HospitalAbroad to retain destination reference so history and UI
                // can show the foreign destination for hospital abroad cases.
                if (out.dest && canon !== 'Abroad' && canon !== 'HospitalAbroad') delete out.dest;
                if (out.arrivalMs) delete out.arrivalMs;
            }
            // Witness only true on the first snapshot after a phase change
            if (!prev) {
                out.witness = false; // initial load has no witnessed transition yet
            } else if (phaseChanged) {
                out.witness = true;
                out.previousPhase = prevCanon || null;
            } else {
                out.witness = false;
                if (prev.previousPhase && !out.previousPhase) out.previousPhase = prev.previousPhase; // carry forward if desired
            }
            // Plane normalization (future use in confidence heuristics)
            try { out.plane = out.plane || utils.getKnownPlaneTypeForId?.(out.id) || null; } catch(_) { /* ignore */ }
            out.updatedMs = out.updatedMs || out.updated || now;
            return out;
        },
        // Step 3 helper: prune absurd / stale ETAs
        _pruneInvalidEtas: (rec) => {
            const MAX_TRAVEL_MS = 6 * 60 * 60 * 1000; // 6h safety window
            if (!rec) return { rec, prunedEta:false };
            if (rec.arrivalMs) {
                const now = Date.now();
                if (rec.arrivalMs < now - 2000 || rec.arrivalMs - now > MAX_TRAVEL_MS) {
                    const cloned = { ...rec }; delete cloned.arrivalMs; return { rec: cloned, prunedEta:true };
                }
            }
            return { rec, prunedEta:false };
        },
        // Step 3: validate & normalize entire snapshot map prior to hashing
        _validateAndCleanSnapshotMap: (currMap) => {
            if (!currMap || typeof currMap !== 'object') return currMap;
            const at = state._activityTracking;
            let normalized=0, etaPruned=0, malformed=0;
            const out = {};
            for (const [id, rec] of Object.entries(currMap)) {
                if (!rec || !id) { malformed++; continue; }
                const prev = at?.prevById ? at.prevById[id] : null;
                let norm = handlers._normalizeActivityRecord(rec, prev);
                const res = handlers._pruneInvalidEtas(norm); norm = res.rec; if (res.prunedEta) etaPruned++;
                out[id] = norm; normalized++;
            }
            if (at && at.metrics) {
                at.metrics.validationLast = { ts: Date.now(), normalized, etaPruned, malformed };
                at.metrics.validationSamples = at.metrics.validationSamples || [];
                at.metrics.validationSamples.push({ t: Date.now(), normalized, etaPruned, malformed });
                if (at.metrics.validationSamples.length > 30) at.metrics.validationSamples.shift();
            }
            return out;
        },
        _activityTick: async () => {
            const at = state._activityTracking; if (!at) return;
            try { tdmlogger('debug', '[ActivityTick][enter]', { now: Date.now(), plannedNextTick: at.metrics?.plannedNextTick || 0, cadenceMs: at.cadenceMs, lastTickStart: at.metrics?.lastTickStart || 0 }); } catch(_) {}
            const perfNow = () => performance.now?.()||Date.now();
            const metrics = at.metrics || (at.metrics = {});
            metrics.totalTicks = (metrics.totalTicks || 0) + 1;
            metrics.lastTickOutcome = 'running';
            metrics.lastFetchReason = 'pending';
            const t0 = perfNow();
            metrics.lastTickStart = Date.now();
            // drift: actual start minus planned
            if (metrics.plannedNextTick) {
                const rawDrift = metrics.lastTickStart - metrics.plannedNextTick;
                metrics.lastDriftMs = Math.max(0, rawDrift);
            } else {
                metrics.lastDriftMs = 0;
            }
            metrics.driftSamples.push(metrics.lastDriftMs);
            if (metrics.driftSamples.length > 60) metrics.driftSamples.shift();
            let tAfterApi= t0, tAfterBuild = t0, tAfterApply = t0;
            try {
                // Pull faction bundle (respect shared throttle + per-source pacing)
                const nowMs = Date.now();
                const throttleState = state._factionBundleThrottle || null;
                const cadenceMs = at.cadenceMs || (config.DEFAULT_FACTION_BUNDLE_REFRESH_MS || 15000);
                const minGapMs = Math.max(config.MIN_GLOBAL_FETCH_INTERVAL_MS || 2000, Math.floor(cadenceMs * 0.6));
                const lastActivityCall = throttleState?.lastSourceCall?.activityTick || 0;
                let fetchedBundles = false;

                if (throttleState?.inFlight) {
                    metrics.bundleSkipsInFlight = (metrics.bundleSkipsInFlight || 0) + 1;
                    metrics.lastFetchReason = 'in-flight';
                    try { tdmlogger('debug', '[ActivityTick] throttle skip in-flight', { factionId: state.user.factionId, lastActivityCall, throttleState }); } catch(_) {}
                } else if (lastActivityCall && (nowMs - lastActivityCall) < minGapMs) {
                    metrics.bundleSkipsRecent = (metrics.bundleSkipsRecent || 0) + 1;
                    metrics.lastFetchReason = 'cooldown';
                    try { tdmlogger('debug', '[ActivityTick] throttle skip cooldown', { factionId: state.user.factionId, lastActivityCall, minGapMs, elapsed: nowMs - lastActivityCall }); } catch(_) {}
                } else {
                    try {
                        try { tdmlogger('debug', '[ActivityTick] fetching bundles', { factionId: state.user.factionId, source: 'activityTick' }); } catch(_) {}
                        await api.refreshFactionBundles?.({ source: 'activityTick' });
                        fetchedBundles = true;
                        metrics.fetchHits = (metrics.fetchHits || 0) + 1;
                        metrics.lastFetchReason = 'fetched';
                        try { tdmlogger('debug', '[ActivityTick] fetched bundles', { factionId: state.user.factionId }); } catch(_) {}
                    } catch (err) {
                        metrics.bundleErrors = (metrics.bundleErrors || 0) + 1;
                        metrics.fetchErrors = (metrics.fetchErrors || 0) + 1;
                        metrics.lastFetchReason = 'error';
                        tAfterApi = perfNow();
                        throw err;
                    } finally {
                        if (fetchedBundles) {
                            const throttle = state._factionBundleThrottle || (state._factionBundleThrottle = { lastCall: 0, lastIds: [], skipped: 0, lastSkipLog: 0 });
                            throttle.lastSourceCall = throttle.lastSourceCall || {};
                            throttle.lastSourceCall.activityTick = Date.now();
                        }
                    }
                }
                tAfterApi = perfNow();
                if (fetchedBundles) at.metrics.lastPoll = Date.now();
                // Throttled: send attacker activity ping when user has active dibs to ensure backend activity docs stay fresh
                try {
                    // attacker heartbeat removed from _activityTick; handled by dedicated heartbeat scheduler
                } catch(_) { /* best-effort ping, ignore errors */ }
                // Build normalized current states (placeholder – integrate existing unified status builder)
                let curr = utils.buildCurrentUnifiedActivityState?.();
                if (!curr) {
                    // Fallback builder: derive minimal phase objects from unifiedStatus or factionMembers.
                    curr = {};
                    const unified = state.unifiedStatus || {};
                    for (const [id, rec] of Object.entries(unified)) {
                        curr[id] = {
                            id,
                            phase: rec.canonical || null,
                            canonical: ec.canonical || null,
                            confidence: rec.confidence || 'LOW',
                            dest: rec.dest || null,
                            arrivalMs: rec.arrivalMs || rec.etams || 0,
                            startedMs: rec.startedMs || rec.ct0 || 0,
                            updatedMs: rec.updated || Date.now()
                        };
                    }
                }
                // Steps 2 & 3: normalize and validate before signature hashing
                curr = handlers._validateAndCleanSnapshotMap(curr);
                tAfterBuild = perfNow();
                // Optional skip if nothing changed (simple hash of keys + key-phase pairs)
                try {
                    // Optimized signature: deterministic iteration without array sort.
                    // Assumption: object key insertion order for stable id set is sufficient; if membership changes order changes anyway.
                    let hash = 0, count = 0;
                    for (const [id, r] of Object.entries(curr)) {
                        const phase = r.canonical||'';
                        // simple rolling hash: hash = ((hash << 5) - hash) + charCode
                        const frag = id+':'+phase+'|';
                        for (let i=0;i<frag.length;i++) hash = ((hash << 5) - hash) + frag.charCodeAt(i), hash |= 0;
                        count++;
                    }
                    const sig = count+':'+hash; // include count to distinguish permutations of equal hash length sets
                    if (at.lastSig && at.lastSig === sig) {
                        at.metrics.signatureSkips = (at.metrics.signatureSkips||0) + 1;
                        at.metrics.skippedTicks = at.metrics.signatureSkips;
                        metrics.lastTickOutcome = 'sig-skip';
                        handlers._expireTransients();
                        handlers._renderTravelEtas?.();
                        if (storage.get('liveTrackDebugOverlayEnabled', false)) handlers._renderLiveTrackDebugOverlay?.();
                        at.metrics.lastDiffMs = (performance.now?.()||Date.now()) - t0;
                        return;
                    }
                    at.lastSig = sig;
                } catch(_) { /* signature calc failure -> fall through */ }
                const prev = at.prevById || {};
                const transitions = [];
                for (const [id, curState] of Object.entries(curr)) {
                    const p = prev[id];
                    if (!p) { prev[id] = curState; continue; }
                    if (handlers._didStateChange?.(p, curState)) {
                        // Insert Landed transient if travel finished
                        const land = handlers._maybeInjectLanded?.(p, curState);
                        if (land) transitions.push({ id, state: land });
                        // Apply confidence escalation
                        const escalated = handlers._escalateConfidence(p, curState);
                        // Add previousPhase + witness (phase change only)
                        const prevPhase = (p.canonical||p.phase||'');
                        const newPhase = (curState.canonical||curState.phase||'');
                        if (prevPhase !== newPhase) {
                            escalated.previousPhase = prevPhase || null;
                            escalated.witness = true;
                        } else {
                            escalated.witness = false;
                        }
                        transitions.push({ id, state: escalated });
                        // Phase history (now includes Landed)
                        try {
                            if (prevPhase && newPhase && prevPhase !== newPhase) {
                                state._activityTracking._phaseHistory = state._activityTracking._phaseHistory || {};
                                const arr = state._activityTracking._phaseHistory[id] = state._activityTracking._phaseHistory[id] || [];
                                arr.push({ from: prevPhase, to: newPhase, ts: Date.now(), confFrom: p.confidence||'', confTo: escalated.confidence||'', dest: escalated.dest||null, arrivalMs: escalated.arrivalMs||null });
                                if (arr.length > 100) arr.splice(0, arr.length - 100);
                                try { handlers._schedulePhaseHistoryWrite(String(id)); } catch(_) {}
                            }
                        } catch(_) { /* ignore history issues */ }
                        prev[id] = curState;
                        at.metrics.transitions++;
                        try {
                            state._activityTracking._recentTransitions = state._activityTracking._recentTransitions || [];
                            state._activityTracking._recentTransitions.push(Date.now());
                            // trim excessive growth safeguard (retain last 500 timestamps)
                            if (state._activityTracking._recentTransitions.length > 500) {
                                state._activityTracking._recentTransitions.splice(0, state._activityTracking._recentTransitions.length - 500);
                            }
                            // Transition event log (phase changes only) for overlay recent list
                            try {
                                const prevPhase = (p.canonical||p.phase||'');
                                const newPhase = (curState.canonical||curState.phase||'');
                                if (prevPhase && newPhase && prevPhase !== newPhase) {
                                    state._activityTracking._transitionLog = state._activityTracking._transitionLog || [];
                                    state._activityTracking._transitionLog.push({ id, from: prevPhase, to: newPhase, ts: Date.now() });
                                    if (state._activityTracking._transitionLog.length > 50) {
                                        state._activityTracking._transitionLog.splice(0, state._activityTracking._transitionLog.length - 50);
                                    }
                                }
                            } catch(_) { /* ignore log issues */ }
                        } catch(_) { /* ignore metric hook issues */ }
                    }
                }
                // Apply transitions to UI
                if (transitions.length) handlers._applyActivityTransitions?.(transitions);
                metrics.lastTickOutcome = transitions.length ? `transitions:${transitions.length}` : 'steady';
                tAfterApply = perfNow();
                handlers._expireTransients();
                // Update ETAs for any active travel
                handlers._renderTravelEtas?.();
                if (storage.get('liveTrackDebugOverlayEnabled', false)) handlers._renderLiveTrackDebugOverlay?.();
            } catch(e) {
                if (state.debug?.rowLogs) tdmlogger('warn', `[ActivityTick] error: ${e}`);
                metrics.lastTickOutcome = 'error';
            }
            finally {
                const tEnd = perfNow();
                metrics.lastApiMs = Math.max(0, tAfterApi - t0);
                metrics.lastBuildMs = Math.max(0, tAfterBuild - tAfterApi);
                metrics.lastApplyMs = Math.max(0, tAfterApply - tAfterBuild);
                metrics.lastDiffMs = Math.max(0, tEnd - t0); // total tick runtime
                metrics.lastTickEnd = Date.now();
                metrics.tickSamples.push(metrics.lastDiffMs);
                if (metrics.tickSamples.length > 60) metrics.tickSamples.shift();
                try { tdmlogger('debug', '[ActivityTick][exit]', { outcome: metrics.lastTickOutcome, lastDiffMs: metrics.lastDiffMs, plannedNextTick: at.metrics?.plannedNextTick || 0 }); } catch(_) {}
                handlers._scheduleActivityTick();
            }
        },

        // Determine if state meaningfully changed (phase change or dest/arrival update)
        _didStateChange: (prev, curr) => {
            if (!prev || !curr) return true;
            if ((prev.canonical||prev.phase) !== (curr.canonical||curr.phase)) return true;
            if ((prev.dest||'') !== (curr.dest||'')) return true;
            // arrival change > 2s counts
            if (Math.abs((prev.arrivalMs||0) - (curr.arrivalMs||0)) > 2000) return true;
            return false;
        },
        // Inject transient Landed state when leaving Travel/Returning/Abroad into Okay (or domestic phase) with recent arrival
        _maybeInjectLanded: (prev, curr) => {
            const p = (prev.canonical||prev.phase||'').toLowerCase();
            const c = (curr.canonical||curr.phase||'').toLowerCase();
            const travelSet = new Set(['travel','traveling','returning','abroad']);
            if (travelSet.has(p) && (c === 'okay' || c === 'hospital' || c === 'jail' || c === 'abroad_hosp')) {
                // Only if arrivalMs was within last 30s
                const now = Date.now();
                const arr = prev.arrivalMs || prev.etams || 0;
                if (arr && now - arr < 30000) {
                    return {
                        id: prev.id,
                        canonical: 'Landed',
                        phase: 'Landed',
                        confidence: 'HIGH', // arrival verified
                        transient: true,
                        expiresAt: now + (config?.LANDED_TTL_MS || 30000),
                        dest: prev.dest || null,
                        startedMs: prev.startedMs || prev.ct0 || now,
                        arrivalMs: arr,
                        updatedMs: now
                    };
                }
            }
            return null;
        },
        // Confidence escalation ladder (LOW -> MED -> HIGH). No downgrades unless explicit reset (not handled here yet).
        _escalateConfidence: (prev, curr) => {
            // Advanced confidence heuristic with stability tracking and promotion thresholds.
            const out = { ...curr };
            const at = state._activityTracking;
            const metrics = at?.metrics;
            const phase = (curr.canonical||curr.phase||'').toLowerCase();
            const prevPhase = (prev.canonical||prev.phase||'').toLowerCase();
            const now = Date.now();

            // Per-id stability record
            at._confState = at._confState || {};
            let st = at._confState[curr.id];
            if (!st || st.phase !== phase) {
                st = at._confState[curr.id] = {
                    phase,
                    phaseStart: now,
                    lastArrivalMs: curr.arrivalMs || 0,
                    stableArrivalCount: 0,
                    dest: curr.dest || null
                };
            } else {
                // Update arrival stability
                if (curr.arrivalMs && st.lastArrivalMs) {
                    const delta = Math.abs(curr.arrivalMs - st.lastArrivalMs);
                    if (delta <= 1500) st.stableArrivalCount++;
                    else st.stableArrivalCount = 0;
                    st.lastArrivalMs = curr.arrivalMs;
                } else if (curr.arrivalMs) {
                    st.lastArrivalMs = curr.arrivalMs;
                }
                if (curr.dest && !st.dest) st.dest = curr.dest;
            }

            const isTravel = phase === 'travel';
            const isReturning = phase === 'returning';
            const phaseChanged = phase !== prevPhase;
            const prevConf = prev.confidence || 'LOW';
            let next = curr.confidence || prevConf || 'LOW';
            const rank = { LOW:1, MED:2, HIGH:3 };

            if (phaseChanged) {
                const fromPhase = (curr.previousPhase || prevPhase || '').toLowerCase();
                if (isTravel) {
                    const sawOutbound = ['okay','idle','active','landed'].includes(fromPhase);
                    const sawInboundDeparture = fromPhase === 'abroad';
                    if (curr.dest && curr.arrivalMs && (sawOutbound || sawInboundDeparture)) {
                        next = 'MED';
                    } else {
                        next = 'LOW';
                    }
                } else if (isReturning) {
                    if (fromPhase === 'travel') {
                        next = prevConf || 'LOW';
                        if (rank[next] < rank.MED && curr.dest) next = 'MED';
                    } else if (fromPhase === 'abroad') {
                        next = curr.dest ? 'MED' : 'LOW';
                    } else {
                        next = 'LOW';
                    }
                } else {
                    next = 'LOW';
                }
                st.phaseStart = now;
                st.stableArrivalCount = 0;
                st.lastArrivalMs = curr.arrivalMs || 0;
                st.dest = curr.dest || null;
            } else {
                const stableMs = now - st.phaseStart;
                if (isTravel) {
                    const MED_MIN_MS = config.TRAVEL_PROMOTE_MS || 8000; // reuse existing if defined
                    const HIGH_MIN_MS = config.TRAVEL_HIGH_PROMOTE_MS || 25000;
                    if (rank[next] <= rank.LOW && curr.dest && curr.arrivalMs && stableMs >= MED_MIN_MS) {
                        next = 'MED';
                        if (metrics) {
                            metrics.confPromos = metrics.confPromos || {};
                            metrics.confPromos.L2M = (metrics.confPromos.L2M || 0) + 1;
                        }
                    }
                    if (rank[next] <= rank.MED && stableMs >= HIGH_MIN_MS) {
                        if (st.stableArrivalCount >= 2 || (curr.arrivalMs && (curr.arrivalMs - now) < 90000)) {
                            if (next !== 'HIGH' && metrics) {
                                metrics.confPromos = metrics.confPromos || {};
                                metrics.confPromos.M2H = (metrics.confPromos.M2H || 0) + 1;
                            }
                            next = 'HIGH';
                        }
                    }
                } else if (isReturning) {
                    const RETURN_MED_MS = 8000;
                    const RETURN_HIGH_MS = 30000;
                    if (rank[next] <= rank.LOW && stableMs >= RETURN_MED_MS) {
                        next = 'MED';
                        if (metrics) {
                            metrics.confPromos = metrics.confPromos || {};
                            metrics.confPromos.L2M = (metrics.confPromos.L2M || 0) + 1;
                        }
                    }
                    if (rank[next] <= rank.MED && stableMs >= RETURN_HIGH_MS) {
                        if (next !== 'HIGH' && metrics) {
                            metrics.confPromos = metrics.confPromos || {};
                            metrics.confPromos.M2H = (metrics.confPromos.M2H || 0) + 1;
                        }
                        next = 'HIGH';
                    }
                } else {
                    const NON_TRAVEL_MED_MS = 15000;
                    const NON_TRAVEL_HIGH_MS = 60000;
                    if (rank[next] <= rank.LOW && stableMs >= NON_TRAVEL_MED_MS) {
                        next = 'MED';
                        if (metrics) {
                            metrics.confPromos = metrics.confPromos || {};
                            metrics.confPromos.L2M = (metrics.confPromos.L2M || 0) + 1;
                        }
                    }
                    if (rank[next] <= rank.MED && stableMs >= NON_TRAVEL_HIGH_MS) {
                        if (next !== 'HIGH' && metrics) {
                            metrics.confPromos = metrics.confPromos || {};
                            metrics.confPromos.M2H = (metrics.confPromos.M2H || 0) + 1;
                        }
                        next = 'HIGH';
                    }
                }
            }

            const prevPhaseForWitness = (curr.previousPhase || prevPhase || '').toLowerCase();
            let witnessBoost = false;
            if (curr.witness) {
                if (isTravel && ['okay','idle','active','landed','abroad'].includes(prevPhaseForWitness)) {
                    witnessBoost = true;
                } else if (isReturning && prevPhaseForWitness === 'abroad') {
                    witnessBoost = true;
                } else if (!isTravel && !isReturning) {
                    witnessBoost = true;
                }
            }
            if (witnessBoost && next !== 'HIGH') next = 'HIGH';

            const nextRank = rank[next] ?? 0;
            const prevRank = rank[prevConf] ?? 0;
            if (nextRank < prevRank) next = prevConf; // never downgrade
            out.confidence = next;
            return out;
        },
        // Expire transients (Landed)
        _expireTransients: () => {
            const unified = state.unifiedStatus || {}; if (!Object.keys(unified).length) return;
            const now = Date.now(); let changed = false;
            for (const [id, rec] of Object.entries(unified)) {
                if (rec && rec.transient && rec.expiresAt && now >= rec.expiresAt) {
                    // Replace Landed with stable phase (Okay unless hospital/jail already in current snapshot)
                    const stable = { ...rec };
                    stable.transient = false;
                    if ((rec.canonical||'').toLowerCase() === 'landed') stable.canonical = 'Okay';
                    unified[id] = stable; changed = true;
                }
            }
            if (changed) {
                // Trigger minimal UI refresh (reuse apply transitions)
                const transitions = Object.entries(unified).filter(([_,r]) => !r.transient).map(([id,state]) => ({ id, state }));
                handlers._applyActivityTransitions(transitions);
            }
        },

        // Travel ETA rendering (API-only): uses unifiedStatus arrivalMs/startMs
        _renderTravelEtas: () => {
        try {
            if (!storage.get('tdmActivityTrackingEnabled', false)) return;
            const unified = state.unifiedStatus || {};
            const now = Date.now();
            const rows = document.querySelectorAll('#faction-war .table-body .table-row');
            let anyActive = false;
            rows.forEach(row => {
                try {
                    const link = row.querySelector('a[href*="profiles.php?XID="]');
                    if (!link) return; const id = (link.href.match(/XID=(\d+)/)||[])[1];
                    if (!id) return;
                    const rec = unified[id];
                    const etaElOld = row.querySelector('.tdm-travel-eta');
                    const isTravel = rec && rec.canonical === 'Travel' && rec.arrivalMs && rec.startedMs && rec.arrivalMs > now;
                    if (!isTravel) {
                        if (etaElOld) etaElOld.remove();
                        return;
                    }
                    anyActive = true;
                    const remainMs = rec.arrivalMs - now;
                    if (remainMs <= 0) { if (etaElOld) etaElOld.remove(); return; }
                    const remainSec = Math.floor(remainMs/1000);
                    const mm = Math.floor(remainSec/60);
                    const ss = String(remainSec%60).padStart(2,'0');
                    let etaEl = etaElOld;
                    if (!etaEl) {
                        etaEl = document.createElement('span');
                        etaEl.className = 'tdm-travel-eta';
                        etaEl.style.cssText = 'margin-left:4px;font-size:10px;color:#66c;';
                        const statusCell = row.querySelector('.status') || row.lastElementChild;
                        if (statusCell) statusCell.appendChild(etaEl); else row.appendChild(etaEl);
                    }
                    const txt = mm > 99 ? `${mm}m` : `${mm}:${ss}`;
                    if (etaEl.textContent !== txt) etaEl.textContent = txt;
                    const dest = rec.dest ? utils.travel?.abbrevDest?.(rec.dest) || rec.dest : '';
                    // Use unified textual confidence (omit if HIGH)
                    let etaConfText = '';
                    try {
                        const c = rec.confidence;
                        if (c && c !== 'HIGH') etaConfText = ` (${c[0]})`;
                    } catch(_) { /* ignore */ }
                    etaEl.title = `${dest?dest+' ':''}ETA ${new Date(rec.arrivalMs).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'})}${etaConfText}`;
                } catch(_) { /* ignore row */ }
            });
            if (anyActive) {
                utils.unregisterTimeout(state._travelEtaTimer);
                state._travelEtaTimer = utils.registerTimeout(setTimeout(handlers._renderTravelEtas, 1000));
            }
        } catch(_) { /* ignore top-level */ }
        },

    //======================================================================
    // 7. INITIALIZATION & MAIN EXECUTION
    //======================================================================
    };
    // Start the lightweight attacker heartbeat independent of activity-tracking.
    // This keeps the tiny ping loop separate from the heavier activity tick and
    // avoids coupling it to the activity-tracking lifecycle.
    // When the page becomes visible again, attempt an immediate heartbeat tick
    try {
        document.addEventListener('visibilitychange', () => {
            try {
                // Only attempt when the page is visible and window is active
                if (document.visibilityState === 'visible' && !document.hidden && (state.script.isWindowActive !== false)) {
                    try { handlers._sendAttackerHeartbeatNow?.().catch(() => {}); } catch(_) {}
                }
            } catch(_) {}
        });
    } catch(_) {}
    // Ensure debounced handler shims exist in case code calls them before
    // `initializeDebouncedHandlers` runs. These will forward to the
    // non-debounced implementations (immediate) and will be overwritten
    // by `initializeDebouncedHandlers` when it runs.
    try {
        const _ensureShim = (debName, baseName) => {
            try {
                if (typeof handlers[debName] !== 'function') {
                    handlers[debName] = function(...args) {
                        const base = handlers[baseName];
                        if (typeof base === 'function') {
                            try { return base.apply(handlers, args); } catch (e) { return Promise.reject(e); }
                        }
                        return Promise.resolve();
                    };
                }
            } catch(_) {}
        };
        _ensureShim('debouncedDibsTarget', 'dibsTarget');
        _ensureShim('debouncedRemoveDibsForTarget', 'removeDibsForTarget');
        _ensureShim('debouncedHandleMedDealToggle', 'handleMedDealToggle');
        _ensureShim('debouncedFetchGlobalData', 'fetchGlobalData');
        _ensureShim('debouncedAssignDibs', 'assignDibs');
        _ensureShim('debouncedHandleSaveUserNote', 'handleSaveUserNote');
    } catch(_) { /* silent */ }
    const main = {
        init: async () => {
            utils.perf.start('main.init');
            try { utils.loadUnifiedStatusSnapshot(); } catch(_) {}
            // Restore API usage counter for this tab/session to avoid drops after SPA hash changes
            // Reset API usage on full page reload (not SPA navigation)
            try {
                const nav = performance.getEntriesByType && performance.getEntriesByType('navigation');
                const navType = Array.isArray(nav) && nav.length ? nav[0].type : (performance.navigation ? (performance.navigation.type === 1 ? 'reload' : 'navigate') : 'navigate');
                if (navType === 'navigate' || navType === 'reload') {
                    // Fresh load: clear prior session counters
                    sessionStorage.removeItem('tdm.api_calls');
                    sessionStorage.removeItem('tdm.api_calls_client');
                    sessionStorage.removeItem('tdm.api_calls_backend');
                }
            } catch(_) {}
            try { const saved = sessionStorage.getItem('tdm.api_calls'); if (saved !== null) state.session.apiCalls = Number(saved) || 0; } catch(_) {}
            try { const savedC = sessionStorage.getItem('tdm.api_calls_client'); if (savedC !== null) state.session.apiCallsClient = Number(savedC) || 0; } catch(_) {}
            try { const savedB = sessionStorage.getItem('tdm.api_calls_backend'); if (savedB !== null) state.session.apiCallsBackend = Number(savedB) || 0; } catch(_) {}
            main.setupGmFunctions();
            main.registerTampermonkeyMenuCommands();
            // Non-blocking update check (cached)
            main.checkForUserscriptUpdate().catch(() => {});
            // Render from cache as early as possible to avoid blocking UI on network
            try { await main.initializeScriptLogic({ skipFetch: true }); } catch (_) { /* non-fatal */ }
            // Resolve API key: prefer PDA placeholder when present, else use localStorage
            try {
                if (state.script.isPDA && config.PDA_API_KEY_PLACEHOLDER && config.PDA_API_KEY_PLACEHOLDER[0] !== '#') {
                    state.user.actualTornApiKey = config.PDA_API_KEY_PLACEHOLDER;
                } else {
                    state.user.actualTornApiKey = getStoredCustomApiKey();
                }
            } catch (_) { state.user.actualTornApiKey = null; }
            const initResult = await main.initializeUserAndApiKey();
            if (initResult && initResult.ok) {
                main.initializeDebouncedHandlers();
                await main.initializeScriptLogic();
                main.startPolling();
                main.setupActivityListeners();
                try { utils.ensureUnifiedStatusUiListener(); } catch(_) {}
                // Auto-start activity tracking if enabled in storage
                try {
                    if (storage.get('tdmActivityTrackingEnabled', false) || storage.get('liveTrackDebugOverlayEnabled', false)) {
                        handlers._initActivityTracking?.();
                        if (storage.get('liveTrackDebugOverlayEnabled', false)) {
                            ui.ensureDebugOverlayContainer?.({ passive: true });
                        }
                    }
                } catch(_) {}
                // Ensure toggles have defaults
                try {
                    if (storage.get('alertButtonsEnabled', null) === null) storage.set('alertButtonsEnabled', true);
                    if (storage.get('userScoreBadgeEnabled', null) === null) storage.set('userScoreBadgeEnabled', true);
                    if (storage.get('factionScoreBadgeEnabled', null) === null) storage.set('factionScoreBadgeEnabled', true);
                    if (storage.get('liveTrackingEnabled', null) === null) storage.set('liveTrackingEnabled', false); // default off until stable
                    if (storage.get('tdmActivityTrackWhileIdle', null) === null) storage.set('tdmActivityTrackWhileIdle', false);
                } catch(_) {}
                // Render badges once at startup
                ui.ensureUserScoreBadge();
                ui.ensureFactionScoreBadge();
            } else if (initResult && initResult.message) {
                const keyIssueReasons = ['missing-key', 'missing-pda-key', 'missing-scopes', 'no-response', 'exception'];
                if (keyIssueReasons.includes(initResult.reason)) {
                    main.handleApiKeyNotReady(initResult);
                }
            }
            utils.perf.stop('main.init');
        },

        // Centralized note-activity helper so various parts of the app can reset inactivity
        noteActivity: () => {
            try {
                state.script.lastActivityTime = Date.now();
                utils.unregisterTimeout(state.script.activityTimeoutId);
                const keepActive = utils.isActivityKeepActiveEnabled();
                const prevMode = state.script.activityMode || 'inactive';
                state.script.activityMode = 'active';
                if (prevMode !== 'active') {
                    main.startPolling();
                }
                if (!keepActive) {
                    state.script.activityTimeoutId = utils.registerTimeout(setTimeout(() => {
                        const prior = state.script.activityMode || 'active';
                        state.script.activityMode = 'inactive';
                        if (prior !== 'inactive') {
                            main.startPolling();
                        }
                    }, config.ACTIVITY_TIMEOUT_MS));
                } else {
                    if (state.script.activityTimeoutId) { try { utils.unregisterTimeout(state.script.activityTimeoutId); } catch(_) {} state.script.activityTimeoutId = null; }
                }
            } catch (_) { /* non-fatal */ }
        },

        initializeDebouncedHandlers: () => {
            handlers.debouncedFetchGlobalData = utils.debounce(async () => {
                try { await handlers.fetchGlobalData(); } catch(e) { tdmlogger('error', 'debouncedFetchGlobalData error', e); }
            }, 500);
            handlers.debouncedDibsTarget = utils.debounce(handlers.dibsTarget, 500);
            handlers.debouncedRemoveDibsForTarget = utils.debounce(handlers.removeDibsForTarget, 500);
            handlers.debouncedHandleMedDealToggle = utils.debounce(handlers.handleMedDealToggle, 500);
            handlers.debouncedSetFactionWarData = utils.debounce(handlers.setFactionWarData, 500);
            handlers.debouncedHandleSaveUserNote = utils.debounce(handlers.handleSaveUserNote, 500);
            handlers.debouncedAssignDibs = utils.debounce(handlers.assignDibs, 500);
            // Debounced badge updater: only repaint if counts actually changed since last invocation
            handlers.debouncedUpdateDibsDealsBadge = utils.debounce(() => {
                try {
                    const el = state.ui.dibsDealsBadgeEl || document.getElementById('tdm-dibs-deals');
                    if (!el) { ui.updateDibsDealsBadge(); return; }
                    const prevText = el.textContent || '';
                    // Use underlying direct logic to compute fresh text without double DOM writes
                    // We'll call the direct updater, then if text unchanged revert timestamp (minor optimization not crucial)
                    ui.updateDibsDealsBadge();
                    const nextText = el.textContent || '';
                    if (nextText === prevText) {
                        // No visual change; could optionally revert any ts touches or skip future work (noop)
                    }
                } catch(_) { /* swallow */ }
            }, 400);
            // Debounced API usage badge updater to collapse clustered increments
            handlers.debouncedUpdateApiUsageBadge = utils.debounce(() => {
                try { ui.updateApiUsageBadge?.(); } catch(_) { /* noop */ }
            }, 400);
        },
        // Register Tampermonkey menu commands (non-PDA only)
        registerTampermonkeyMenuCommands: () => {
            if (typeof state.gm.rD_registerMenuCommand !== 'function') return;
            try {
                state.gm.rD_registerMenuCommand('TreeDibs: Set/Update Torn API Key', async () => {
                    try {
                        await ui.openSettingsToApiKeySection({ highlight: true, focusInput: true });
                        ui.showMessageBox('Settings opened. Paste your custom Torn API key in the API Key & Access section.', 'info', 4000);
                    } catch(_) {
                        ui.showMessageBox('Open the TreeDibs settings button on the faction page to update your Torn API key.', 'warning', 4000);
                    }
                });

                state.gm.rD_registerMenuCommand('TreeDibs: Clear Torn API Key', async () => {
                    await main.clearStoredApiKey({ confirm: true, viaMenu: true });
                });

                state.gm.rD_registerMenuCommand('TreeDibs: Open Settings Panel', () => ui.toggleSettingsPopup());
                state.gm.rD_registerMenuCommand('TreeDibs: Refresh Now', () => handlers.debouncedFetchGlobalData());
                state.gm.rD_registerMenuCommand('TreeDibs: Check for Update', () => main.checkForUserscriptUpdate(true));
                // (Force Active Polling menu command removed; now managed via settings panel button.)
            } catch (e) {
                tdmlogger('error', `Failed to register Tampermonkey menu commands: ${e}`);
            }
        },
        checkForUserscriptUpdate: async (force = false) => {
            try {
                const now = Date.now();
                const lastCheck = storage.get('lastUpdateCheck', 0);
                const throttled = (!force && (now - lastCheck) < 6 * 60 * 60 * 1000);
                try { tdmlogger('debug', `[Update] checkForUserscriptUpdate start force=${!!force} last=${new Date(Number(lastCheck)||0).toISOString()} throttled=${throttled}`); } catch(_) {}
                if (throttled) return; // 6h throttle
                storage.set('lastUpdateCheck', now);

                // Try meta URL first with a cache buster to avoid CDN edge caching quirks
                const metaUrl = `${config.GREASYFORK.updateMetaUrl}?_=${now % 1e7}`;
                const metaRes = await utils.httpGetDetailed(metaUrl);
                let latest = null;
                if (metaRes && metaRes.text) {
                    const m = metaRes.text.match(/@version\s+([^\n\r]+)/);
                    latest = m ? m[1].trim() : null;
                    if (!latest) { try { tdmlogger('warn', `[Update] Could not parse @version from meta (status=${metaRes.status})`); } catch(_) {} }
                } else {
                    try { tdmlogger('warn', `[Update] No metaText returned from updateMetaUrl (status=${metaRes?.status})`); } catch(_) {}
                }

                // Fallback: scrape the GreasyFork page for the Version: field
                if (!latest) {
                    try {
                        const pageUrl = `${config.GREASYFORK.pageUrl}?_=${now % 1e7}`;
                        const pageRes = await utils.httpGetDetailed(pageUrl);
                        if (pageRes && pageRes.text) {
                            // Common patterns on GreasyFork pages
                            // Example: <dd class="script-show-version" data-script-version="2.4.0">2.4.0</dd>
                            let m = pageRes.text.match(/script-show-version[^>]*>([^<]+)/i);
                            if (!m) {
                                // Another fallback: "Version" label
                                m = pageRes.text.match(/>\s*Version\s*<\/dt>\s*<dd[^>]*>\s*([\d\.]+)\s*<\/dd>/i);
                            }
                            if (!m) {
                                // Loose fallback: find @version in code blocks
                                m = pageRes.text.match(/@version\s+([\d\.]+)/i);
                            }
                            if (m) latest = String(m[1]).trim();
                            if (!latest) { try { tdmlogger('warn', `[Update] Could not extract version from page (status=${pageRes.status})`); } catch(_) {} }
                        } else {
                            try { tdmlogger('warn', `[Update] Page fetch returned empty body (status=${pageRes?.status})`); } catch(_) {}
                        }
                    } catch (e) {
                        try { tdmlogger('warn', `[Update] Page fallback failed: ${e?.message || e}`); } catch(_) {}
                    }
                }

                if (!latest) return; // Nothing to update

                storage.set('lastKnownLatestVersion', latest);
                try { tdmlogger('info', `[Update] Current=${config.VERSION} Latest=${latest}`); } catch(_) {}
                // Non-intrusive: only store flag and adjust UI label
                if (utils.compareVersions(config.VERSION, latest) < 0) {
                    state.script.updateAvailableLatestVersion = latest;
                    ui.updateSettingsButtonUpdateState();
                    try { tdmlogger('info', '[Update] Update available'); } catch(_) {}
                } else {
                    state.script.updateAvailableLatestVersion = null;
                    ui.updateSettingsButtonUpdateState();
                    try { tdmlogger('info', '[Update] Up to date'); } catch(_) {}
                }
            } catch (e) {
                try { tdmlogger('error', `[TDM][Update] check failed: ${e?.message || e}`); } catch(_) {}
                // Silent fail otherwise; not critical
            }
        },

        setupGmFunctions: () => {
            // Robust feature detection for multiple userscript runtimes (GM3/GM4) and PDA
            const placeholder = config.PDA_API_KEY_PLACEHOLDER || '###PDA-APIKEY###';
            state.script.isPDA = (placeholder && placeholder[0] !== '#');

            // Helpers to detect GM variants without throwing ReferenceError
            let hasGM4 = false;
            try { hasGM4 = (typeof GM !== 'undefined' && GM && typeof GM.getValue === 'function'); } catch(_) {}
            
            let hasGM_xmlhttpRequest = false;
            try { hasGM_xmlhttpRequest = (typeof GM_xmlhttpRequest === 'function') || (hasGM4 && typeof GM.xmlHttpRequest === 'function'); } catch(_) {}

            // Fix for "Cannot redefine property: GM" error in some environments
            // We do not attempt to write to window.GM or global GM here to avoid conflicts.
            // We only read from it.

            // Legacy GM storage wrappers removed for get/set/delete/getApiKey/addStyle — using page-local fallbacks where needed

            // registerMenuCommand: best-effort - no-op when unavailable
            state.gm.rD_registerMenuCommand = (caption, cb) => {
                try {
                    if (hasGM4) {
                         try { if (typeof GM.registerMenuCommand === 'function') return GM.registerMenuCommand(caption, cb); } catch(_) {}
                    }
                    if (typeof GM_registerMenuCommand === 'function') return GM_registerMenuCommand(caption, cb);
                } catch (_) {}
                // fallback: no-op
                return null;
            };

            // xmlhttpRequest adapter: PDA inappwebview, GM xmlhttprequest, or fetch-based shim
            let __tdm_xmlPath = 'none';
            if (state.script.isPDA && window.flutter_inappwebview && typeof window.flutter_inappwebview.callHandler === 'function') {
                state.gm.rD_xmlhttpRequest = (details) => {
                    const ret = new Promise((resolve, reject) => {
                        try {
                            const { method = 'GET', url, headers, data: body } = details;
                            const pdaPromise = method.toLowerCase() === 'post'
                                ? window.flutter_inappwebview.callHandler('PDA_httpPost', url, headers || {}, body || '')
                                : window.flutter_inappwebview.callHandler('PDA_httpGet', url, headers || {});
                            // Catch the promise from PDA bridge to prevent unhandled rejections
                            pdaPromise.catch(() => {}); 
                            
                            pdaPromise.then(response => {
                                // Defensive: if PDA bridge returned a falsy response, treat as error
                                if (!response) {
                                    const err = new Error('PDA returned null/undefined response');
                                    try { console.warn('[TDM][PDA] callHandler returned empty response'); } catch(_) {}
                                    try { if (typeof details.onerror === 'function') details.onerror(err); } catch(_) {}
                                    return reject(err);
                                }
                                const responseObj = {
                                    status: response?.status || 200,
                                    statusText: response?.statusText || 'OK',
                                    responseText: response?.responseText || '',
                                    finalUrl: url
                                };
                                try { if (typeof details.onload === 'function') details.onload(responseObj); } catch(_) {}
                                resolve(responseObj);
                            }).catch(error => {
                                // Normalize undefined/null rejection reasons into Error objects to avoid
                                // unhelpful unhandledrejection events with `reason === undefined`.
                                const err = (error instanceof Error) ? error : new Error((error && (error.message || String(error))) || 'PDA callHandler rejected without reason');
                                try { console.warn('[TDM][PDA] callHandler error', err); } catch(_) {}
                                try { if (typeof details.onerror === 'function') details.onerror(err); } catch(_) {}
                                // IMPORTANT: We must NOT re-throw or return a rejected promise here if we want to suppress the global unhandled rejection.
                                // Since we've called onerror/reject, the caller is notified.
                                // However, the caller (state.gm.rD_xmlhttpRequest) returns a Promise.
                                // If we reject that promise, the caller must catch it.
                                // My previous fix added .catch() to the callers.
                                // But if pdaPromise itself rejects, we need to make sure we don't leave a dangling rejection.
                                // We resolve with undefined (or a mock error response) so the outer promise resolves, 
                                // but the error is handled via onerror callback.
                                // This prevents the "Unhandled Rejection" in the console.
                                resolve(undefined);
                            });
                        } catch (e) { try { console.error('[TDM][PDA] callHandler threw', e); } catch(_) {}
                            try { if (typeof details.onerror === 'function') details.onerror(e); } catch(_) {} resolve(undefined); }
                    });
                    // Ensure the returned promise from rD_xmlhttpRequest is also caught if the caller doesn't await it
                    if (ret && typeof ret.catch === 'function') ret.catch(() => {});
                    return ret;
                };
                __tdm_xmlPath = 'pda';
            } else if (hasGM_xmlhttpRequest) {
                try {
                    if (hasGM4) {
                        try { if (typeof GM.xmlHttpRequest === 'function') state.gm.rD_xmlhttpRequest = GM.xmlHttpRequest; } catch(_) {}
                    }
                    if (!state.gm.rD_xmlhttpRequest && typeof GM_xmlhttpRequest === 'function') state.gm.rD_xmlhttpRequest = GM_xmlhttpRequest;
                    
                    if (state.gm.rD_xmlhttpRequest) {
                         __tdm_xmlPath = (hasGM4 && typeof GM.xmlHttpRequest === 'function') ? 'GM4.xmlHttpRequest' : 'GM_xmlhttpRequest';
                         // Wrap the native GM function to ensure it returns a promise (some implementations might not)
                         // and to catch errors.
                         const original = state.gm.rD_xmlhttpRequest;
                         state.gm.rD_xmlhttpRequest = (details) => {
                             // GM_xmlhttpRequest usually returns an object with .abort(), not a promise.
                             // But our code expects a promise-like return or at least something awaitable if we wrapped it.
                             // Actually, our usage throughout the file is `state.gm.rD_xmlhttpRequest({...})` often without await,
                             // or `await new Promise(...)` wrapping it.
                             // However, if we want to be safe, we should just call it.
                             // BUT, if we want to catch synchronous errors:
                             try {
                                 return original(details);
                             } catch (e) {
                                 try { if (typeof details.onerror === 'function') details.onerror(e); } catch(_) {}
                                 return undefined;
                             }
                         };
                    }
                } catch (_) {
                    // fall through to fetch shim
                }
            }

            // Fallback fetch-based shim if nothing else assigned
            if (!state.gm.rD_xmlhttpRequest) {
                state.gm.rD_xmlhttpRequest = (details) => {
                    // Return a promise to match the interface of other adapters (even though GM_xmlhttpRequest doesn't strictly return one, our wrapper does)
                    const ret = (async () => {
                        const method = (details.method || 'GET').toUpperCase();
                        const headers = details.headers || {};
                        const body = details.data;
                        try {
                            const resp = await fetch(details.url, { method, headers, body });
                            const text = await resp.text();
                            const responseObj = { status: resp.status, statusText: resp.statusText, responseText: text, finalUrl: resp.url };
                            try { if (typeof details.onload === 'function') details.onload(responseObj); } catch(_) {}
                            return responseObj;
                        } catch (err) {
                            try { if (typeof details.onerror === 'function') details.onerror(err); } catch(_) {}
                            // Swallow error to avoid unhandled rejection if caller doesn't catch, matching GM_xmlhttpRequest behavior
                            return undefined;
                        }
                    })();
                    // Ensure the returned promise is caught if the caller doesn't await it
                    if (ret && typeof ret.catch === 'function') ret.catch(() => {});
                    return ret;
                };
                __tdm_xmlPath = 'fetch';
            }

            state.script.apiTransport = __tdm_xmlPath;
            state.script.useHttpEndpoints = (__tdm_xmlPath === 'fetch');

            // Report chosen GM/fallback paths under debug
            try {
                let __tdm_registerPath = 'noop';
                try {
                     if (hasGM4 && typeof GM.registerMenuCommand === 'function') __tdm_registerPath = 'GM4.registerMenuCommand';
                     else if (typeof GM_registerMenuCommand === 'function') __tdm_registerPath = 'GM_registerMenuCommand';
                } catch(_) {}
                tdmlogger('debug', `[TDM] chosen gm paths -> xmlhttprequest: ${__tdm_xmlPath}, registerMenu: ${__tdm_registerPath}`);
            } catch(_) {}
            },

            verifyApiKey: async ({ key } = {}) => {
                if (!key || typeof key !== 'string') {
                    return { ok: false, reason: 'missing-key', message: 'TreeDibsMapper needs a custom Torn API key.', validation: null };
                }
                try {
                    const keyInfo = await api.getKeyInfo(key);
                    if (!keyInfo) {
                        return {
                            ok: false,
                            reason: 'no-response',
                            message: '[TDM] Unable to verify your custom API key (Torn API may be unavailable). Try again shortly.',
                            validation: null,
                            keyInfo: null
                        };
                    }
                    const validation = validateApiKeyScopes(keyInfo);
                    if (!validation.ok) {
                        const missingList = (validation.missing || []).map(scope => scope.replace('.', ' -> ')).join(', ');
                        return {
                            ok: false,
                            reason: 'missing-scopes',
                            message: `[TDM] Custom API key is missing required selections (${missingList}). Regenerate it via Torn > Preferences > API.`,
                            validation,
                            keyInfo
                        };
                    }
                    return {
                        ok: true,
                        reason: 'verified',
                        message: validation.isLimited ? '[TDM] Limited key detected.' : 'Custom API key verified.',
                        validation,
                        keyInfo
                    };
                } catch (error) {
                    return {
                        ok: false,
                        reason: 'exception',
                        message: `[TDM] API key verification failed: ${error?.message || error || 'Unknown error.'}`,
                        validation: null,
                        keyInfo: null,
                        error
                    };
                }
            },

            storeCustomApiKey: async (key, { reload = true, validation = null, keyInfo = null } = {}) => {
                if (!key || typeof key !== 'string' || !key.trim()) {
                    throw new Error('Custom API key value is required.');
                }
                const value = setStoredCustomApiKey(key);
                state.user.actualTornApiKey = value;
                state.user.apiKeySource = 'local';
                // If verification metadata was supplied, persist it so the UI
                // reflects verified status after the reload. Otherwise, clear
                // validation so clients re-verify on next init.
                if (validation && typeof validation === 'object') {
                    state.user.keyValidation = validation;
                    state.user.keyValidatedAt = Date.now();
                } else {
                    state.user.keyValidation = null;
                    state.user.keyValidatedAt = null;
                }
                state.user.keyInfoCache = keyInfo || null;
                state.user.actualTornApiKeyAccess = 0;
                state.user.apiKeyUiMessage = {
                    tone: validation ? (validation.isLimited ? 'warning' : 'success') : 'info',
                    text: validation ? (validation.isLimited ? '[TDM] Limited key detected.' : 'Custom API key verified and saved.') : (reload ? 'Custom API key saved. Reloading...' : 'Custom API key saved. Remember to revalidate.'),
                    ts: Date.now(),
                    reason: validation ? (validation.isLimited ? 'verified-limited' : 'verified') : 'stored'
                };
                storage.updateStateAndStorage('user', state.user);
                // Only reload automatically when explicitly requested AND the key is
                // a fully verified custom key (not a "limited" key). Limited keys are
                // saved but do not trigger a reload so users can inspect diagnostics.
                if (reload) {
                    const isLimited = validation?.isLimited === true;
                    if (isLimited) {
                        try { ui.showMessageBox('Custom API key saved (limited access). Not reloading. Check required selections and re-save when corrected.', 'warning'); } catch(_) {}
                    } else {
                        try { ui.showMessageBox('Custom API key saved. Reloading...', 'info'); } catch(_) {}
                        setTimeout(() => { try { location.reload(); } catch(_) {}; }, 300);
                    }
                }
                return { ok: true };
            },

            clearStoredApiKey: async ({ confirm = false, viaMenu = false } = {}) => {
                if (confirm) {
                    const proceed = await ui.showConfirmationBox('Clear the stored Torn custom API key? You will be prompted again next time.');
                    if (!proceed) return { ok: false, cancelled: true };
                }
                clearStoredCustomApiKey();
                const pdaFallback = (state.script.isPDA && config.PDA_API_KEY_PLACEHOLDER && config.PDA_API_KEY_PLACEHOLDER[0] !== '#') ? config.PDA_API_KEY_PLACEHOLDER : null;
                state.user.actualTornApiKey = pdaFallback || null;
                state.user.apiKeySource = pdaFallback ? 'pda' : 'none';
                state.user.keyValidation = null;
                state.user.keyValidatedAt = null;
                state.user.keyInfoCache = null;
                state.user.actualTornApiKeyAccess = 0;
                const message = pdaFallback
                    ? 'Custom override cleared. Falling back to PDA-provided key. Reloading...'
                    : 'Custom API key cleared. Reloading...';
                state.user.apiKeyUiMessage = {
                    tone: pdaFallback ? 'info' : 'warning',
                    text: message,
                    ts: Date.now(),
                    reason: 'cleared'
                };
                storage.updateStateAndStorage('user', state.user);
                try { ui.showMessageBox(message, 'info'); } catch(_) {}
                setTimeout(() => { try { location.reload(); } catch(_) {}; }, viaMenu ? 400 : 250);
                return { ok: true, fallback: !!pdaFallback };
            },

            revalidateStoredApiKey: async ({ showToasts = true } = {}) => {
                const localKey = getStoredCustomApiKey();
                const pdaKey = (state.script.isPDA && config.PDA_API_KEY_PLACEHOLDER && config.PDA_API_KEY_PLACEHOLDER[0] !== '#') ? config.PDA_API_KEY_PLACEHOLDER : null;
                const activeKey = state.user.actualTornApiKey || localKey || pdaKey;
                if (!activeKey) {
                    const msg = 'No Torn API key available to verify.';
                    if (showToasts) ui.showMessageBox(msg, 'warning');
                    state.user.apiKeyUiMessage = { tone: 'warning', text: msg, ts: Date.now(), reason: 'missing-key' };
                    storage.updateStateAndStorage('user', state.user);
                    return { ok: false, tone: 'warning', reason: 'missing-key', message: msg };
                }
                const result = await main.verifyApiKey({ key: activeKey });
                state.user.keyValidation = result.validation || null;
                state.user.keyInfoCache = result.keyInfo || null;
                state.user.keyValidatedAt = Date.now();
                state.user.actualTornApiKeyAccess = result.validation?.level || 0;
                if (!result.ok) {
                    if (showToasts) ui.showMessageBox(result.message, 'error');
                    state.user.apiKeyUiMessage = { tone: 'error', text: result.message, ts: Date.now(), reason: result.reason || 'verify-failed' };
                    storage.updateStateAndStorage('user', state.user);
                    return { ok: false, tone: 'error', reason: result.reason, message: result.message };
                }
                if (showToasts) ui.showMessageBox('API key verified.', 'success');
                state.user.apiKeyUiMessage = {
                    tone: 'success',
                    text: result.message,
                    ts: Date.now(),
                    reason: result.reason || 'verified'
                };
                storage.updateStateAndStorage('user', state.user);
                return {
                    ok: true,
                    tone: 'success',
                    reason: result.reason,
                    message: result.message,
                    validation: result.validation
                };
            },

            handleApiKeyNotReady: (result) => {
                const tone = result?.tone || (result?.reason === 'missing-key' ? 'warning' : 'error');
                const message = result?.message || 'TreeDibsMapper needs a valid Torn API key.';
                state.user.apiKeyUiMessage = { tone, text: message, ts: Date.now(), reason: result?.reason || 'unknown' };
                storage.updateStateAndStorage('user', state.user);
                try { ui.updateSettingsContent?.(); } catch(_) {}
                setTimeout(() => {
                    try { ui.openSettingsToApiKeySection({ highlight: true, focusInput: true }); } catch(_) {}
                }, 300);
            },

        initializeUserAndApiKey: async () => {
            try {
                const storedKey = getStoredCustomApiKey();
                const pdaInjectedKey = (state.script.isPDA && config.PDA_API_KEY_PLACEHOLDER && config.PDA_API_KEY_PLACEHOLDER[0] !== '#') ? config.PDA_API_KEY_PLACEHOLDER : null;
                let activeKey = storedKey && String(storedKey).trim();
                let source = 'local';
                if (!activeKey) {
                    if (pdaInjectedKey) {
                        activeKey = String(pdaInjectedKey).trim();
                        source = 'pda';
                    } else {
                        source = 'none';
                    }
                }
                state.user.actualTornApiKey = activeKey || null;
                state.user.apiKeySource = source;

                if (!activeKey) {
                    const msg = state.script.isPDA
                        ? 'PDA API Key placeholder not replaced. Please enter a custom key with the required selections.'
                        : 'No custom API key provided. TreeDibsMapper will remain idle until a valid key is entered.';
                    ui.showMessageBox(msg, 'warning');
                    storage.updateStateAndStorage('user', state.user);
                    return { ok: false, reason: state.script.isPDA ? 'missing-pda-key' : 'missing-key', message: msg, tone: 'warning' };
                }

                const verification = await main.verifyApiKey({ key: activeKey });
                state.user.keyValidation = verification.validation || null;
                state.user.keyInfoCache = verification.keyInfo || null;
                state.user.keyValidatedAt = Date.now();
                state.user.actualTornApiKeyAccess = verification.validation?.level || 0;

                if (!verification.ok) {
                    ui.showMessageBox(verification.message, 'error');
                    storage.updateStateAndStorage('user', state.user);
                    return { ok: false, reason: verification.reason, message: verification.message, tone: 'error' };
                }

                // Persist a user-facing UI message so the settings panel reflects
                // the verified status immediately after reload.
                state.user.apiKeyUiMessage = {
                    tone: 'success',
                    text: verification.message || (verification.validation?.isLimited ? '[TDM] Limited key detected.' : 'Custom API key verified.'),
                    ts: Date.now(),
                    reason: verification.reason || 'verified'
                };
                // Persist the message immediately so the UI is consistent after the page reload
                storage.updateStateAndStorage('user', state.user);

                const factionData = await api.getTornFaction(state.user.actualTornApiKey, 'basic,members,rankedwars');
                if (factionData && factionData.members && Array.isArray(factionData.members)) {
                    // Store all member keys from v2 response
                    state.factionMembers = factionData.members.map(member => ({
                        id: member.id,
                        name: member.name,
                        level: member.level,
                        days_in_faction: member.days_in_faction,
                        last_action: member.last_action,
                        status: member.status,
                        revive_setting: member.revive_setting,
                        position: member.position,
                        is_revivable: member.is_revivable,
                        is_on_wall: member.is_on_wall,
                        is_in_oc: member.is_in_oc,
                        has_early_discharge: member.has_early_discharge
                    })).filter(m => m.id && m.status !== 'Fallen');
                }

                // Prefer cached faction bundle for self basic info if available
                let tornUser = null;
                if (state.user && state.user.tornUserObject && !tornUser && (state.user.tornUserFetchAt && (Date.now() - state.user.tornUserFetchedAt) < 60000)) {
                    // throttle cache to once a minute
                    tornUser = state.user.tornUserObject;
                } else {
                    tornUser = await api.getTornUser(state.user.actualTornApiKey);
                    state.user.tornUserFetchedAt = Date.now();
                }
            
                if (!tornUser)  { 
                    ui.showMessageBox('Failed to Get User [TreeDibsMapper]', 'error'); 
                    return { ok: false, reason: 'user-fetch-failed', message: 'Failed to load player profile.', tone: 'error' }; }
                state.user.tornUserObject = tornUser;
                state.user.tornId = (tornUser.profile?.id || tornUser.player_id || tornUser.id).toString();
                state.user.tornUsername = tornUser.profile?.name || tornUser.name;
                state.user.factionId = (tornUser.faction?.id || tornUser.profile?.faction_id || tornUser.faction_id || null);
                if (state.user.factionId) state.user.factionId = String(state.user.factionId);
                state.user.factionName = tornUser.faction?.name || state.user.factionName || null;
                storage.updateStateAndStorage('user', state.user);
                try {
                    await firebaseAuth.signIn({
                        tornApiKey: state.user.actualTornApiKey,
                        tornId: state.user.tornId,
                        factionId: state.user.factionId,
                        version: config.VERSION
                    });
                } catch (authError) {
                    try { tdmlogger('error', `[TDM][Auth] sign-in failed: ${authError?.message || authError}`); } catch(_) {}
                    ui.showMessageBox('Failed to authenticate with TreeDibs servers. Some features may not work until you retry.', 'error');
                }
                // Determine admin rights via backend settings for this faction
                
                const settings = await api.get('getFactionSettings', { factionId: state.user.factionId });
                if (settings && settings.options) {
                    // Optionally map future per-faction options here
                }
                if (settings && settings.approved === false) {
                    ui.showMessageBox('Your faction is not approved to use TreeDibsMapper yet. Contact your leader.', 'error');
                    // Return true so the script UI still loads, but avoid admin features implicitly handled by flags
                }
                state.script.factionSettings = settings;
                const currentUserPosition = tornUser.faction?.position || tornUser.profile?.position || tornUser.profile?.role || null;
                const adminRolesRaw = Array.isArray(settings?.adminRoles) ? settings.adminRoles : [];
                const normalizeRole = (role) => (typeof role === 'string') ? role.trim().toLowerCase() : '';
                const normalizedRoles = adminRolesRaw.map(normalizeRole).filter(Boolean);
                const normalizedPosition = normalizeRole(currentUserPosition);
                const hasWildcardRole = normalizedRoles.some(r => r === '*' || r === 'all' || r === 'any');
                const defaultAdminRoles = ['leader', 'co-leader', 'sub-leader', 'officer'];
                let canAdmin = state.script.canAdministerMedDeals;
                if (normalizedPosition) {
                    if (normalizedRoles.length > 0) {
                        canAdmin = normalizedRoles.includes(normalizedPosition) || hasWildcardRole;
                    } else if (settings !== undefined && settings !== null) {
                        canAdmin = defaultAdminRoles.includes(normalizedPosition);
                    }
                }
                applyAdminCapability({ position: currentUserPosition, computedFlag: canAdmin, source: 'backend-settings', refreshTimestamp: settings != null });
                try { ui.updateSettingsContent?.(); } catch(_) {}
                
                
                if (factionData && factionData.rankedwars && Array.isArray(factionData.rankedwars)) {
                    // Normalize to array of wars (most recent first if available)
                    const rankedWars = factionData.rankedwars;
                    storage.updateStateAndStorage('rankWars', rankedWars);
                    if (Array.isArray(rankedWars) && rankedWars.length > 0) {
                        const lastRankWar = rankedWars[0];
                        // If a new ranked war has started, reset per-war saved settings to sensible defaults
                        try {
                            const prevId = state.lastRankWar?.id || null;
                            storage.updateStateAndStorage('lastRankWar', lastRankWar);
                            const newId = lastRankWar?.id || null;
                            if (prevId && newId && String(prevId) !== String(newId)) {
                                // A new war started — reset mutable, per-war fields unless the backend provides warData
                                const defaultInitial = (lastRankWar?.war && Number(lastRankWar.war.target)) || Number(lastRankWar?.target) || 0;
                                const newWarData = Object.assign({}, { warType: 'War Type Not Set', opponentFactionId: undefined, opponentFactionName: undefined, initialTargetScore: defaultInitial, warId: newId });
                                // Populate opponent info below if available (will be reset/overwritten again by opponent assignments)
                                storage.updateStateAndStorage('warData', newWarData);
                            }
                        } catch (_) { storage.updateStateAndStorage('lastRankWar', lastRankWar); }
                        // Derive opponent faction info for quick UI use
                        try {
                            if (lastRankWar && lastRankWar.factions) {
                                const opp = Object.values(lastRankWar.factions).find(f => f.id !== parseInt(state.user.factionId));
                                if (opp) {
                                    storage.updateStateAndStorage('lastOpponentFactionId', opp.id);
                                    storage.updateStateAndStorage('lastOpponentFactionName', opp.name);
                                    // Update warData lightweight fields; full warData still comes from backend if changed
                                    // Defensive: avoid merging stale warData for a previous war into the new war's settings.
                                    let baseWarData = state.warData || {};
                                    try {
                                        if (baseWarData?.warId && state.lastRankWar?.id && String(baseWarData.warId) !== String(state.lastRankWar.id)) {
                                            const defaultInitial = (lastRankWar?.war && Number(lastRankWar.war.target)) || Number(lastRankWar?.target) || 0;
                                            baseWarData = Object.assign({}, { warType: 'War Type Not Set', opponentFactionId: undefined, opponentFactionName: undefined, initialTargetScore: defaultInitial, warId: state.lastRankWar?.id });
                                        }
                                    } catch(_) {}
                                    const wd = Object.assign({}, baseWarData, { opponentFactionId: opp.id, opponentFactionName: opp.name });
                                    storage.updateStateAndStorage('warData', wd);
                                    try { ui.ensureAttackModeBadge?.(); } catch(_) {}
                                }
                            }
                        } catch (_) { /* no-op */ }
                    }
                }
                if (factionData && factionData.basic) {
                    storage.updateStateAndStorage('factionPull', factionData.basic);
                }
                // Fetch ranked wars on the client at most once per hour and update local state
                try {
                    const nowMs = Date.now();
                    const lastWarsFetch = parseInt(storage.get('rankedwars_fetched_at', '0') || '0', 10);
                    if (!Number.isFinite(lastWarsFetch) || (nowMs - lastWarsFetch) > 60 * 60 * 1000) {
                        const warsUrl = `https://api.torn.com/v2/faction/rankedwars?key=${state.user.actualTornApiKey}&comment=TDM_FEgRW`; // removed &timestamp=${Math.floor(Date.now()/1000)} to reduce call counts
                        const warsResp = await fetch(warsUrl);
                        const warsData = await warsResp.json();
                        utils.incrementClientApiCalls(1);
                        if (!warsData.error) {
                            const list = Array.isArray(warsData.rankedwars) ? warsData.rankedwars : (Array.isArray(warsData) ? warsData : []);
                            // Normalize to array of wars (most recent first if available)
                            const rankedWars = list;
                            storage.updateStateAndStorage('rankWars', rankedWars);
                            if (Array.isArray(rankedWars) && rankedWars.length > 0) {
                                const lastRankWar = rankedWars[0];
                                    // If a new ranked war has started, reset per-war saved settings to sensible defaults
                                    try {
                                        const prevId = state.lastRankWar?.id || null;
                                        storage.updateStateAndStorage('lastRankWar', lastRankWar);
                                        const newId = lastRankWar?.id || null;
                                        if (prevId && newId && String(prevId) !== String(newId)) {
                                            const defaultInitial = (lastRankWar?.war && Number(lastRankWar.war.target)) || Number(lastRankWar?.target) || 0;
                                            const newWarData = Object.assign({}, { warType: 'War Type Not Set', opponentFactionId: undefined, opponentFactionName: undefined, initialTargetScore: defaultInitial, warId: newId });
                                            storage.updateStateAndStorage('warData', newWarData);
                                            try { ui.ensureAttackModeBadge?.(); } catch(_) {}
                                        }
                                    } catch (_) { storage.updateStateAndStorage('lastRankWar', lastRankWar); }
                                // Derive opponent faction info for quick UI use
                                try {
                                    if (lastRankWar && lastRankWar.factions) {
                                        const opp = Object.values(lastRankWar.factions).find(f => f.id !== parseInt(state.user.factionId));
                                        if (opp) {
                                            storage.updateStateAndStorage('lastOpponentFactionId', opp.id);
                                            storage.updateStateAndStorage('lastOpponentFactionName', opp.name);
                                            // Update warData lightweight fields; full warData still comes from backend if changed
                                            // Defensive: prevent mixing previous-war warData when lastRankWar changed or mismatched
                                            let baseWarData = state.warData || {};
                                            try {
                                                if (baseWarData?.warId && state.lastRankWar?.id && String(baseWarData.warId) !== String(state.lastRankWar.id)) {
                                                    const defaultInitial = (lastRankWar?.war && Number(lastRankWar.war.target)) || Number(lastRankWar?.target) || 0;
                                                    baseWarData = Object.assign({}, { warType: 'War Type Not Set', opponentFactionId: undefined, opponentFactionName: undefined, initialTargetScore: defaultInitial, warId: state.lastRankWar?.id });
                                                }
                                            } catch(_) {}
                                            const wd = Object.assign({}, baseWarData, { opponentFactionId: opp.id, opponentFactionName: opp.name });
                                            storage.updateStateAndStorage('warData', wd);
                                            try { ui.ensureAttackModeBadge?.(); } catch(_) {}
                                        }
                                    }
                                } catch (_) { /* no-op */ }
                            }
                            storage.set('rankedwars_fetched_at', String(nowMs));
                        }
                    }
                } catch (e) {
                    tdmlogger('warn', `[TDM] Ranked wars (client) fetch failed: ${e?.message || e}`);
                }
                return { ok: true };
            } catch (error) {
                ui.showMessageBox(`API Key Or TDM Version Error: ${error.message}.`, "error");
                return { ok: false, reason: 'exception', message: `API Key Or TDM Version Error: ${error.message}.`, tone: 'error', error };
            }
        },

        initializeScriptLogic: async (options = {}) => {
            const { skipFetch = false } = options;
            // Render-first init: paint from cache, then hydrate in background
            utils.perf.start('initializeScriptLogic');
            state.script.hasProcessedRankedWarTables = false;
            state.script.hasProcessedFactionList = false;

            // 1) Build page context and lightweight UI from cached state (no network)
            utils.perf.start('initializeScriptLogic.updatePageContext');
            ui.updatePageContext();
            utils.perf.stop('initializeScriptLogic.updatePageContext');

            utils.perf.start('initializeScriptLogic.applyGeneralStyles');
            ui.applyGeneralStyles();
            utils.perf.stop('initializeScriptLogic.applyGeneralStyles');

            // Start status time refresh loop if on faction page or ranked-war page
            if (state.page.isFactionPage || state.page.isRankedWarPage) {
                if (state.script.statusRefreshInterval) clearInterval(state.script.statusRefreshInterval);
                state.script.statusRefreshInterval = setInterval(() => {
                    ui._refreshStatusTimes();
                }, 1000);
            }

            utils.perf.start('initializeScriptLogic.updateColumnVisibilityStyles');
            ui.updateColumnVisibilityStyles();
            utils.perf.stop('initializeScriptLogic.updateColumnVisibilityStyles');

            if (state.page.isFactionPage || state.page.isAttackPage) {
                utils.perf.start('initializeScriptLogic.createSettingsButton');
                ui.createSettingsButton();
                utils.perf.stop('initializeScriptLogic.createSettingsButton');
            }
            if (state.page.isAttackPage) {
                utils.perf.start('initializeScriptLogic.injectAttackPageUI');
                await ui.injectAttackPageUI();
                utils.perf.stop('initializeScriptLogic.injectAttackPageUI');
            }
            if (state.dom.factionListContainer) {
                utils.perf.start('initializeScriptLogic.processFactionPageMembers');
                await ui.processFactionPageMembers(state.dom.factionListContainer);
                utils.perf.stop('initializeScriptLogic.processFactionPageMembers');
                state.script.hasProcessedFactionList = true;
                utils.perf.start('initializeScriptLogic.updateFactionPageUI');
                ui._renderEpochMembers.schedule();
                utils.perf.stop('initializeScriptLogic.updateFactionPageUI');
            }
            // On SPA hash changes, ensure timers and counters even if fetch is throttled
            utils.perf.start('initializeScriptLogic.updateRetalsButtonCount');
            ui.updateRetalsButtonCount();
            utils.perf.stop('initializeScriptLogic.updateRetalsButtonCount');
            utils.perf.start('initializeScriptLogic.ensureBadgesSuite');
            ui.ensureBadgesSuite();
            utils.perf.stop('initializeScriptLogic.ensureBadgesSuite');
            utils.perf.start('initializeScriptLogic.checkOCReminder');
            try { handlers.checkOCReminder(); } catch(_) {}
            utils.perf.stop('initializeScriptLogic.checkOCReminder');
            utils.perf.start('initializeScriptLogic.setupMutationObserver');
            main.setupMutationObserver();
            utils.perf.stop('initializeScriptLogic.setupMutationObserver');

            // Ensure alert observer is attached even when warType is not 'Ranked War' (will no-op if tables absent)
            try { ui.ensureRankedWarAlertObserver(); } catch(_) {}

            // 2) Hydrate in the background: fetch global data without blocking first paint
            if (!skipFetch) {
                // Use rAF to yield to rendering, then fire the network work.
                try {
                    (window.requestAnimationFrame || setTimeout)(() => {
                        // Debounced fetch is fine here; we just don't await it.
                        handlers.debouncedFetchGlobalData?.();
                        // deprecated
                        // Warm up Torn faction bundles (ours + opponent); respects 10s freshness
                        try { api.refreshFactionBundles?.(); } catch(_) {}
                        // A second UI refresh will happen inside fetchGlobalData after data updates
                    }, 0);
                } catch (_) {
                    // Fallback: fire immediately if rAF unavailable
                    handlers.debouncedFetchGlobalData?.();
                    // deprecated
                    try { api.refreshFactionBundles?.(); } catch(_) {}
                }
            }

            utils.perf.stop('initializeScriptLogic');
            try { utils.exposeDebugToWindow(); } catch(_) {}
        },

        startPolling: () => {
            // Tear down legacy interval if present
            if (state.script.mainRefreshIntervalId) { try { utils.unregisterInterval(state.script.mainRefreshIntervalId); } catch(_) {} state.script.mainRefreshIntervalId = null; }
            if (state.script._mainDynamicTimeoutId) { utils.unregisterTimeout(state.script._mainDynamicTimeoutId); state.script._mainDynamicTimeoutId = null; }
            if (!state.script.activityMode) {
                state.script.activityMode = 'active';
            }
            const trackingEnabled = !!storage.get('tdmActivityTrackingEnabled', false);
            const idleOverride = utils.isActivityKeepActiveEnabled();
            state.script.idleTrackingOverride = idleOverride;
            const log = (...a) => { if (state.debug.cadence) tdmlogger('debug', '[Cadence]', ...a); };
            log(`startPolling: prevInterval=${state.script.currentRefreshInterval} tracking=${trackingEnabled} idleOverride=${idleOverride}`);
            try { ui.updateApiCadenceInfo?.(); } catch(_) {}
            // Watchdog (unchanged logic but decoupled from dynamic loop)
            try {
                if (state.script.fetchWatchdogIntervalId) try { utils.unregisterInterval(state.script.fetchWatchdogIntervalId); } catch(_) {}
                state.script.fetchWatchdogIntervalId = utils.registerInterval(setInterval(() => {
                    try {
                        const now = Date.now(); const last = state.script._lastGlobalFetch || 0;
                        const active = (state.script.isWindowActive !== false) || utils.isActivityKeepActiveEnabled();
                        if (!active || document.hidden) return;
                        const baseline = state.script.currentRefreshInterval || 10000;
                        const threshold = (baseline * 2) + 5000;
                        if (!last || (now - last) > threshold) { log('Watchdog force fetch', { age: now-last, threshold }); handlers.fetchGlobalData({ force: true }); }
                    } catch(_) {}
                }, 5000));
            } catch(_) {}
            // Interval computation using cached status doc
            const computeInterval = () => {
                const warId = state.lastRankWar?.id;
                const status = state._warStatusCache?.data; // May have been populated by global fetch meta.warStatus
                const warActiveFallback = utils.isWarActive?.(warId);
                if (!status) return warActiveFallback ? 5000 : 15000;
                const phase = status.phase || 'pre';
                const hintSec = Number(status.nextPollHintSec || 0) || (warActiveFallback ? 5 : 15);
                let ms = hintSec * 1000;
                const ageSec = status.lastAttackAgeSec != null ? status.lastAttackAgeSec : null;
                if (phase === 'active' && ageSec != null && ageSec <= 30) ms = Math.min(ms, 15000);
                if ((phase === 'dormant' || phase === 'ended') && ageSec != null && ageSec > 7200) ms = Math.min(Math.max(ms * 2, 300000), 900000);
                // jitter +/-12%
                ms = Math.round(ms * (1 + (Math.random()*2 - 1) * 0.12));
                return Math.max(3000, ms);
            };
            // Seed status quickly (non-blocking)
            (async () => { try {
                const wid = state.lastRankWar?.id;
                if (wid && utils.isWarActive?.(wid)) {
                    const manifestCache = state.rankedWarAttacksCache || {};
                    const cacheEntry = manifestCache[wid] || null;
                    const cachedFingerprint = cacheEntry?._lastManifestFingerprint || null;
                    const bootstrapState = state.script._manifestBootstrapState || (state.script._manifestBootstrapState = {});
                    if (!bootstrapState[wid] && cachedFingerprint) {
                        bootstrapState[wid] = { lastTs: Date.now(), fingerprint: cachedFingerprint };
                    }
                    const prev = bootstrapState[wid] || { lastTs: 0, fingerprint: null };
                    const now = Date.now();
                    const minInterval = config.MANIFEST_BOOTSTRAP_MIN_INTERVAL_MS || (10 * 60 * 1000);
                    const elapsed = now - (prev.lastTs || 0);
                    const intervalExpired = !prev.lastTs || elapsed >= minInterval;
                    const missingPointers = !cacheEntry || !cachedFingerprint;
                    const fingerprintChanged = cachedFingerprint && prev.fingerprint && cachedFingerprint !== prev.fingerprint;
                    if (missingPointers || fingerprintChanged || intervalExpired) {
                        if (state.debug.cadence) {
                            log('manifest bootstrap trigger', { warId: wid, missingPointers, fingerprintChanged, intervalExpired, elapsed });
                        }
                        await api.getWarStatusAndManifest(wid, state.user.factionId, { ensureArtifacts: false, source: 'cadence.manifest-bootstrap' });
                        const updatedEntry = state.rankedWarAttacksCache?.[wid] || null;
                        const nextFingerprint = updatedEntry?._lastManifestFingerprint || null;
                        bootstrapState[wid] = { lastTs: Date.now(), fingerprint: nextFingerprint };
                    } else if (state.debug.cadence) {
                        log('manifest bootstrap skipped', { warId: wid, elapsed, lastFingerprint: prev.fingerprint });
                    }
                }
                const seeded = computeInterval();
                state.script.currentRefreshInterval = seeded;
                log('Seeded interval', seeded);
            } catch(_) {} })();
            const scheduleNextTick = (delayMs) => {
                const fallback = state.script.activityMode === 'inactive' ? (config.REFRESH_INTERVAL_INACTIVE_MS || 60000) : (config.REFRESH_INTERVAL_ACTIVE_MS || 10000);
                const ms = Math.max(1000, Number(delayMs) || fallback);
                state.script.currentRefreshInterval = ms;
                state.script._mainDynamicTimeoutId = utils.registerTimeout(setTimeout(runTick, ms));
            };
            // Dynamic loop via recursive timeout
            const runTick = async () => {
                const keepActive = utils.isActivityKeepActiveEnabled();
                const windowActive = (!document.hidden) && (state.script.isWindowActive !== false);
                
                // Idle detection: > 5 minutes without interaction
                const lastActivity = state.script.lastActivityTime || Date.now();
                const isIdle = (Date.now() - lastActivity) > 300000; // 5 mins

                // Throttle if: Not KeepActive AND (Hidden OR Idle)
                if (!keepActive && (!windowActive || isIdle)) {
                    if (state.debug.cadence) log(`tick paused/throttled - hidden=${!windowActive} idle=${isIdle}`);
                    scheduleNextTick(config.REFRESH_INTERVAL_INACTIVE_MS || 60000);
                    return;
                }

                // Heartbeat integration: only attempt when the window is active.
                // Use a lightweight immediate sender that enforces ~30s throttle.
                if (windowActive) {
                    try { handlers._sendAttackerHeartbeatNow?.().catch(() => {}); } catch(_) {}
                }
                try {
                    if (!document.hidden) {
                        handlers.debouncedFetchGlobalData();
                    } else if (state.debug.cadence) {
                        log('skip fetchGlobalData - document hidden');
                    }
                } catch(e) { log('tick error', e?.message||e); }
                const nextMs = computeInterval();
                log('next interval', nextMs, 'phase', state._warStatusCache?.data?.phase);
                scheduleNextTick(nextMs);
            };
            if (!state.script.currentRefreshInterval) state.script.currentRefreshInterval = computeInterval();
            scheduleNextTick(state.script.currentRefreshInterval);
            // Faction bundle interval (unchanged)
            try {
                if (state.script.factionBundleRefreshIntervalId) try { utils.unregisterInterval(state.script.factionBundleRefreshIntervalId); } catch(_) {}
                const userMs = Number(storage.get('factionBundleRefreshMs', null)) || null;
                const baseMs = Number.isFinite(userMs) && userMs > 0 ? userMs : (state.script.factionBundleRefreshMs || config.DEFAULT_FACTION_BUNDLE_REFRESH_MS);
                state.script.factionBundleRefreshMs = baseMs;
                state.script.factionBundleRefreshIntervalId = utils.registerInterval(setInterval(() => {
                    try { 
                        const keepActive = utils.isActivityKeepActiveEnabled();
                        const windowActive = (state.script.isWindowActive !== false) && !document.hidden;
                        const lastActivity = state.script.lastActivityTime || Date.now();
                        const isIdle = (Date.now() - lastActivity) > 300000; // 5 mins

                        if (keepActive || (windowActive && !isIdle)) { 
                            log('factionBundle tick'); 
                            api.refreshFactionBundles?.().catch(e => { try { tdmlogger('error', `[FactionBundles] tick error: ${e}`); } catch(_) {} }); 
                        } else {
                            if (state.debug.cadence) log(`factionBundle skip - hidden=${!windowActive} idle=${isIdle}`);
                        }
                    } catch(_) {}
                }, baseMs));
                log('factionBundle interval', baseMs);
                try { ui.updateApiCadenceInfo?.(); } catch(_) {}
            } catch(_) {}
        },

        setupMutationObserver: () => {
            // Build list of targets: all ranked-war tables + faction list container; fallback to body
            const targets = [];
            try {
                if (Array.isArray(state.dom?.rankwarfactionTables) && state.dom.rankwarfactionTables.length) {
                    state.dom.rankwarfactionTables.forEach(n => { if (n) targets.push(n); });
                } else if (state.dom?.rankwarContainer) {
                    targets.push(state.dom.rankwarContainer);
                }
                if (state.dom?.factionListContainer) targets.push(state.dom.factionListContainer);
            } catch(_) { /* ignore */ }
            // Ensure at least body as a last-resort observer target
            if (!targets.length) targets.push(document.body);

            // Ensure lightweight instrumentation for tuning
            state.metrics = state.metrics || { mutationsSeen: 0, addedNodesSeen: 0, processRankedMsTotal: 0, processRankedRuns: 0, lastBurstAt: 0 };

            // Determine debounce: longer if observing the whole body
            const baseDebounce = targets.includes(document.body) ? 500 : 200;

            // If there's an existing observer, try to re-attach it to all targets
            if (state.script.mutationObserver) {
                try {
                    try { state.script.mutationObserver.disconnect(); } catch(_) {}
                    targets.forEach(t => {
                        try { state.script.mutationObserver.observe(t, { childList: true, subtree: true }); } catch(_) {}
                    });
                    return; // re-attached existing observer
                } catch(_) {
                    // Fall through and recreate if something failed
                }
            }

            // Create a single observer and observe each target
            state.script.mutationObserver = utils.registerObserver(new MutationObserver(utils.debounce(async (mutations, obs) => {
                try {
                    // Update simple metrics
                    if (Array.isArray(mutations)) {
                        state.metrics.mutationsSeen += mutations.length;
                        state.metrics.addedNodesSeen += mutations.reduce((s, m) => s + (m.addedNodes ? m.addedNodes.length : 0), 0);
                    }
                    // Light-weight common updates
                    ui.updatePageContext();

                    // Detect large external-script mutation bursts and avoid heavy processing
                    const addedCount = Array.isArray(mutations) ? mutations.reduce((s, m) => s + (m.addedNodes ? m.addedNodes.length : 0), 0) : 0;
                    if (addedCount > 20) {
                        // If a heavy burst detected, do minimal updates now and schedule a delayed full pass
                        if (!state.script._heavyMutScheduled) {
                            state.script._heavyMutScheduled = true;
                            setTimeout(async () => {
                                try {
                                    const t0 = Date.now();
                                    await ui.processRankedWarTables?.();
                                    const dur = Date.now() - t0;
                                    state.metrics.processRankedMsTotal += Number(dur) || 0;
                                    state.metrics.processRankedRuns = (state.metrics.processRankedRuns || 0) + 1;
                                    if (state.dom.factionListContainer && !state.script.hasProcessedFactionList) {
                                        await ui.processFactionPageMembers(state.dom.factionListContainer);
                                        state.script.hasProcessedFactionList = true;
                                        ui._renderEpochMembers.schedule();
                                    }
                                } catch(_) {}
                                state.script._heavyMutScheduled = false;
                            }, 600);
                        }
                        // keep lightweight badge updates responsive
                        try { ui.updateUserScoreBadge?.(); ui.updateFactionScoreBadge?.(); } catch(_) {}
                        return;
                    }

                    // Normal path: when not in a burst, perform standard heavy processing
                    if (state.dom.rankwarContainer && !state.script.hasProcessedRankedWarTables) {
                        const t0 = Date.now();
                        await ui.processRankedWarTables();
                        const dur = Date.now() - t0;
                        state.metrics.processRankedMsTotal += Number(dur) || 0;
                        state.metrics.processRankedRuns = (state.metrics.processRankedRuns || 0) + 1;
                    }
                    if (state.dom.factionListContainer && !state.script.hasProcessedFactionList) {
                        await ui.processFactionPageMembers(state.dom.factionListContainer);
                        state.script.hasProcessedFactionList = true;
                        ui._renderEpochMembers.schedule();
                    }
                    if (state.page.isAttackPage || (state.script.hasProcessedRankedWarTables && state.script.hasProcessedFactionList)) {
                        obs.disconnect();
                    }
                } catch(_) { /* noop */ }
            }, baseDebounce)));
            try {
                targets.forEach(t => {
                    try { state.script.mutationObserver.observe(t, { childList: true, subtree: true }); } catch(_) {}
                });
            } catch(_) {}
        },

        setupActivityListeners: () => {
            if (state.script.activityListenersInitialized) return;
            state.script.activityListenersInitialized = true;
            const resetActivityTimer = () => main.noteActivity();
            // Broaden event coverage so clicks and mouse down reset inactivity too
            const evts = ['mousemove', 'mousedown', 'click', 'keydown', 'keyup', 'scroll', 'wheel', 'touchstart', 'touchend'];
            evts.forEach(event => document.addEventListener(event, resetActivityTimer, { passive: true }));
            document.addEventListener('visibilitychange', () => {
            const keepActive = utils.isActivityKeepActiveEnabled();
            state.script.idleTrackingOverride = keepActive;
            state.script.isWindowActive = !document.hidden;
                if (state.debug.cadence) {
                    tdmlogger('debug', `[Cadence] visibilitychange hidden=${document.hidden} idleOverride=${keepActive} isWindowActive -> ${state.script.isWindowActive}`);
                }
                try { ui.updateApiCadenceInfo?.(); } catch(_) {}
                
                // MutationObserver Management: Save resources when hidden
                if (document.hidden) {
                    try { state.script._lastVisibilityHiddenAt = Date.now(); } catch(_) {}
                    if (state.script.mutationObserver) {
                        try { state.script.mutationObserver.disconnect(); } catch(_) {}
                        // We don't null it out, just disconnect. It will be re-attached on show.
                    }
                } else {
                    // Re-enable observer if it was disconnected or missing
                    main.setupMutationObserver();
                }

                if (!document.hidden) {
                    // For a short period after regaining focus, force badges to show using cached labels
                    ui._badgesForceShowUntil = Date.now() + 3000; // 3s
                    // Compute how long tab was hidden
                    let hiddenDur = 0;
                    try {
                        const now = Date.now();
                        const hiddenAt = state.script._lastVisibilityHiddenAt || 0;
                        hiddenDur = hiddenAt ? (now - hiddenAt) : 0;
                    } catch(_) {}

                    // If hidden for more than 25s force an immediate refresh (skip debounce/throttle)
                    if (hiddenDur > 25000) {
                        if (state.debug.cadence) {
                            tdmlogger('debug', `[Cadence] refocus after long hide: ${hiddenDur} ms -> force immediate fetchGlobalData(force=true)`);
                        }
                        // Hospital reconciliation first (local only)
                        try { ui.forceHospitalCountdownRefocus?.(); } catch(_) {}
                        try {
                            (async () => {
                                try {
                                    await handlers.fetchGlobalData({ force: true, focus: true });
                                } catch(_) {}
                                // Refresh badges explicitly (ensure creates if missing, updates values)
                                try {
                                    ui.ensureUserScoreBadge?.();
                                    ui.ensureFactionScoreBadge?.();
                                    ui.ensureDibsDealsBadge?.();
                                    ui.updateUserScoreBadge?.();
                                    ui.updateFactionScoreBadge?.();
                                    ui.updateDibsDealsBadge?.();
                                    // Retry shortly to cover DOM readiness and data coalescing
                                    setTimeout(() => { try { ui.ensureUserScoreBadge?.(); ui.ensureFactionScoreBadge?.(); ui.updateUserScoreBadge?.(); ui.updateFactionScoreBadge?.(); } catch(_) {} }, 200);
                                    setTimeout(() => { try { ui.ensureUserScoreBadge?.(); ui.ensureFactionScoreBadge?.(); ui.updateUserScoreBadge?.(); ui.updateFactionScoreBadge?.(); } catch(_) {} }, 600);
                                } catch(_) {}
                                // deprecated
                                // faction bundles are handled by decoupled interval; we can opportunistically trigger one now
                                try { api.refreshFactionBundles?.().catch(() => {}); } catch(_) {}
                            })();
                        } catch(_) { /* non-fatal */ }
                    } else {
                        // Short hide: to reduce reopen cost, skip any global fetch on quick refocus
                        if (state.debug.cadence) {
                            tdmlogger('debug', `[Cadence] refocus after short hide: ${hiddenDur} ms -> SKIP fetch to avoid reopen cost`);
                        }
                        // Still update badges from cached values to avoid transient zeros
                        try {
                            ui.ensureUserScoreBadge?.();
                            ui.ensureFactionScoreBadge?.();
                            ui.ensureDibsDealsBadge?.();
                            ui.updateUserScoreBadge?.();
                            ui.updateFactionScoreBadge?.();
                            ui.updateDibsDealsBadge?.();
                            setTimeout(() => { try { ui.ensureUserScoreBadge?.(); ui.ensureFactionScoreBadge?.(); ui.updateUserScoreBadge?.(); ui.updateFactionScoreBadge?.(); } catch(_) {} }, 200);
                        } catch(_) { /* noop */ }
                    }
                    // Ensure decoupled faction bundle refresh interval is running after regaining focus
                    try {
                        if (!state.script.factionBundleRefreshIntervalId) {
                            const userMs = Number(storage.get('factionBundleRefreshMs', null)) || null;
                            const baseMs = Number.isFinite(userMs) && userMs > 0 ? userMs : (state.script.factionBundleRefreshMs || config.DEFAULT_FACTION_BUNDLE_REFRESH_MS);
                            state.script.factionBundleRefreshMs = baseMs;
                            state.script.factionBundleRefreshIntervalId = utils.registerInterval(setInterval(() => {
                                try {
                                    if ((state.script.isWindowActive !== false) || utils.isActivityKeepActiveEnabled()) {
                                        
                                        api.refreshFactionBundles?.().catch(e => { try { tdmlogger('error', `[FactionBundles] resume tick error: ${e}`); } catch(_) {} });
                                    }
                                } catch(_) {}
                            }, baseMs));
                            if (state.debug.cadence) {
                                tdmlogger('debug', `[Cadence] factionBundle interval resumed on focus: ${baseMs} ms`);
                            }
                        }
                    } catch(_) { /* noop */ }
                    try { ui.updateApiCadenceInfo?.(); } catch(_) {}
                    resetActivityTimer();
                    // Ensure watchdog is running on focus
                    try {
                        if (!state.script.fetchWatchdogIntervalId) {
                            state.script.fetchWatchdogIntervalId = utils.registerInterval(setInterval(() => {
                                try {
                                    const now = Date.now();
                                    const last = state.script._lastGlobalFetch || 0;
                                    const active2 = (state.script.isWindowActive !== false) || utils.isActivityKeepActiveEnabled();
                                    if (!active2) return;
                                    const baseline = state.script.currentRefreshInterval || (config.REFRESH_INTERVAL_ACTIVE_MS || 10000);
                                    const threshold = (baseline * 2) + 5000;
                                    if (!last || (now - last) > threshold) {
                                        if (state.debug.cadence) tdmlogger('debug', `[Cadence] Watchdog (resume): forcing fetch`);
                                        handlers.fetchGlobalData({ force: true });
                                    }
                                } catch(_) {}
                            }, 5000));
                            if (state.debug.cadence) tdmlogger('debug', `[Cadence] fetch watchdog resumed`);
                        }
                    } catch(_) { /* noop */ }
                } else {
                    state.script._lastVisibilityHiddenAt = Date.now();
                    if (!keepActive) {
                        if (state.debug.cadence) {
                            tdmlogger('debug', `[Cadence] visibility hidden (no activity tracking) -> stopping intervals`);
                        }
                        try { utils.unregisterInterval(state.script.mainRefreshIntervalId); } catch(_) {}
                        try { utils.unregisterTimeout(state.script.activityTimeoutId); } catch(_) {}
                        if (state.script.factionBundleRefreshIntervalId) { try { utils.unregisterInterval(state.script.factionBundleRefreshIntervalId); } catch(_) {} state.script.factionBundleRefreshIntervalId = null; }
                        if (state.script.fetchWatchdogIntervalId) { try { utils.unregisterInterval(state.script.fetchWatchdogIntervalId); } catch(_) {} state.script.fetchWatchdogIntervalId = null; }
                        if (state.script.lightPingIntervalId) { try { utils.unregisterInterval(state.script.lightPingIntervalId); } catch(_) {} state.script.lightPingIntervalId = null; }
                        try { ui.updateApiCadenceInfo?.(); } catch(_) {}
                    }
                }
            });
            // Avoid duplicate initializations on SPA navigation: debounce and teardown before re-init
            if (state.script._hashHandler) {
                try { utils.unregisterWindowListener('hashchange', state.script._hashHandler); } catch(_) {}
            }
            state.script._hashHandler = utils.debounce(() => {
                try {
                    // Teardown intervals/observers before re-initializing
                    if (state.script.mainRefreshIntervalId) { try { utils.unregisterInterval(state.script.mainRefreshIntervalId); } catch(_) {} state.script.mainRefreshIntervalId = null; }
                    if (state.script.activityTimeoutId) { try { utils.unregisterTimeout(state.script.activityTimeoutId); } catch(_) {} state.script.activityTimeoutId = null; }
                    if (state.script.mutationObserver) { try { utils.unregisterObserver(state.script.mutationObserver); } catch(_) {} state.script.mutationObserver = null; }
                    // Ranked war observer teardown
                    if (state._rankedWarObserver) { try { utils.unregisterObserver(state._rankedWarObserver); } catch(_) {} state._rankedWarObserver = null; }
                    if (state._rankedWarScoreObserver) { try { utils.unregisterObserver(state._rankedWarScoreObserver); } catch(_) {} state._rankedWarScoreObserver = null; }
                    // Re-init lightweight logic then restart polling
                    main.initializeScriptLogic();
                    main.startPolling();
                } catch(_) { /* non-fatal */ }
            }, 200);
            utils.registerWindowListener('hashchange', state.script._hashHandler);
            // Ensure we teardown long-lived runtime handles on unload/navigation to avoid leaks across SPA nav
            try { utils.registerWindowListener('beforeunload', utils.cleanupAllResources); } catch(_) {}
            try { utils.registerWindowListener('pagehide', utils.cleanupAllResources); } catch(_) {}
        // Initialize timer state immediately
        resetActivityTimer();
        }
    };

    // Logging helper to set levels with [TDM][level][HH:MM:SS] prefix
    // usage tdmlogger('info', `message`)
    function tdmlogger(level, ...args) {
        try {
            const levels = logLevels;
            // Read persisted log level from storage each call so UI changes apply immediately
            const currentLevel = storage.get('logLevel', 'warn');
            const currentLevelIdx = Math.max(0, levels.indexOf(currentLevel));
            const messageLevelIdx = levels.indexOf(level);
            if (messageLevelIdx === -1 || messageLevelIdx < currentLevelIdx) return; // skip low-level logs

            if (!levels.includes(level)) level = 'log';
            const now = new Date();
            const timestamp = now.toTimeString().split(' ')[0]; // HH:MM:SS
            const prefix = `[TDM][${level.toUpperCase()}][${timestamp}]`;
            switch(level) {
                case 'debug': console.debug(prefix, ...args); break;
                case 'info': console.info(prefix, ...args); break;
                case 'warn': console.warn(prefix, ...args); break;
                case 'error': console.error(prefix, ...args); break;
                default: console.log(prefix, ...args);
            }
        } catch (e) {
            try { console.log('[TDM][LOGGER][ERROR]', e); } catch(_) {}
        }
    }

    // Global handlers to capture otherwise-unhandled errors/promises and log them
    try {
        window.addEventListener('unhandledrejection', (evt) => {
            try {
                const reason = evt && evt.reason;
                if (reason === undefined) {
                    // Some runtimes deliver undefined reasons; log the whole event for diagnosis
                    tdmlogger('warn', '[UnhandledRejection] reason=undefined', evt);
                    try { console.warn('[UnhandledRejection] reason=undefined, event:', evt); } catch(_) {}
                    // Probe the underlying promise to capture rejection value/stack when possible.
                    try {
                        const p = evt && evt.promise;
                        if (p && typeof p.catch === 'function') {
                            // p.catch(r => {
                            //     try {
                            //         if (r === undefined) {
                            //             tdmlogger('warn', '[UnhandledRejection][probe] reason still undefined', r);
                            //             try { console.warn('[UnhandledRejection][probe] reason still undefined', r); } catch(_) {}
                            //         } else if (r && typeof r === 'object') {
                            //             const msg = r.message || JSON.stringify(r);
                            //             tdmlogger('warn', '[UnhandledRejection][probe]', msg, r.stack ? { stack: r.stack } : null);
                            //             try { console.warn('[UnhandledRejection][probe]', r); } catch(_) {}
                            //         } else {
                            //             tdmlogger('warn', '[UnhandledRejection][probe]', String(r));
                            //             try { console.warn('[UnhandledRejection][probe]', r); } catch(_) {}
                            //         }
                            //     } catch (e) { try { console.warn('[UnhandledRejection][probe] logger failure', e); } catch(_) {} }
                            //     // return a rejection so this .catch doesn't swallow the original rejection semantics
                            //     return Promise.reject(r);
                            // }).catch(()=>{});
                        }
                    } catch (_) {}
                } else if (reason && typeof reason === 'object') {
                    // Prefer message + stack when available
                    const msg = reason.message || JSON.stringify(reason);
                    tdmlogger('warn', '[UnhandledRejection]', msg, reason.stack ? { stack: reason.stack } : null);
                } else {
                    tdmlogger('warn', '[UnhandledRejection]', String(reason));
                }
            } catch (e) {
                try { console.warn('[UnhandledRejection] (logger failure)', e, evt); } catch(_) {}
            }
        });
        // Also log when a previously unhandled rejection is later handled
        window.addEventListener('rejectionhandled', (evt) => {
            try {
                const reason = evt && evt.reason;
                tdmlogger('info', '[RejectionHandled]', reason === undefined ? 'undefined' : (reason && reason.message) || String(reason));
            } catch (e) { try { console.info('[RejectionHandled]', evt); } catch(_) {} }
        });
        window.addEventListener('error', (errEvent) => {
            try {
                const ev = errEvent || {};
                const msg = ev.message || (ev.error && ev.error.message) || String(ev);
                const stack = (ev.error && ev.error.stack) || ev.stack || null;
                const loc = (ev.filename ? `${ev.filename}:${ev.lineno || 0}:${ev.colno || 0}` : null);
                tdmlogger('error', '[WindowError]', msg, loc, stack ? { stack } : null);
            } catch (_) {
                try { console.error(errEvent); } catch(_) {}
            }
        });
    } catch (_) { /* best-effort global handlers */ }

    setTimeout(() => {
        try { console.info('[TDM] startup - url:', window.location.href, 'version:', config.VERSION); } catch(_) {}
        ui.updatePageContext();
        try { console.info('[TDM] page flags:', { isFactionPage: state.page.isFactionPage, isAttackPage: state.page.isAttackPage, url: state.page.url?.href }); } catch(_) {}
        if (!state.page.isFactionPage && !state.page.isAttackPage) {
            return;
        }
        tdmlogger('info', `Script execution started - Version: ${config.VERSION}`);
        // Leader election: only one visible tab should perform structural ensures aggressively
        try {
            const LEADER_KEY = 'tdm_leader_tab';
            const token = state.script._tabToken || (state.script._tabToken = Math.random().toString(36).slice(2));
            const now = Date.now();
            const raw = localStorage.getItem(LEADER_KEY);
            let leader = null;
            try { leader = raw ? JSON.parse(raw) : null; } catch(_) {}
            const staleMs = 15000; // 15s
            const visible = !document.hidden;
            const shouldClaim = !leader || (now - (leader.ts||0) > staleMs) || leader.token === token || (!visible ? false : (leader.hidden || false));
            if (shouldClaim) {
                const record = { token, ts: now, hidden: document.hidden };
                localStorage.setItem(LEADER_KEY, JSON.stringify(record));
                state.script.isLeaderTab = true;
            } else {
                state.script.isLeaderTab = leader.token === token;
            }
            // Heartbeat to renew leadership if we are leader
            if (!state.script._leaderHeartbeat) {
                state.script._leaderHeartbeat = utils.registerInterval(setInterval(() => {
                    try {
                        const raw2 = localStorage.getItem(LEADER_KEY);
                        let leader2 = null; try { leader2 = raw2 ? JSON.parse(raw2) : null; } catch(_) {}
                        const amLeader = leader2 && leader2.token === token;
                        if (amLeader) {
                            localStorage.setItem(LEADER_KEY, JSON.stringify({ token, ts: Date.now(), hidden: document.hidden }));
                            state.script.isLeaderTab = true;
                        } else if (!leader2 || (Date.now() - (leader2.ts||0) > staleMs)) {
                            // Attempt claim
                            localStorage.setItem(LEADER_KEY, JSON.stringify({ token, ts: Date.now(), hidden: document.hidden }));
                            state.script.isLeaderTab = true;
                        } else {
                            state.script.isLeaderTab = false;
                        }
                    } catch(_) {}
                }, 4000));
            }
            window.addEventListener('visibilitychange', () => {
                try {
                    const raw3 = localStorage.getItem(LEADER_KEY);
                    let leader3 = null; try { leader3 = raw3 ? JSON.parse(raw3) : null; } catch(_) {}
                    if (!leader3 || leader3.token === token) {
                        localStorage.setItem(LEADER_KEY, JSON.stringify({ token, ts: Date.now(), hidden: document.hidden }));
                        state.script.isLeaderTab = true;
                    } else {
                        state.script.isLeaderTab = false;
                    }
                } catch(_) {}
            });
        } catch(_) { /* non-fatal leader election failure */ }
        main.init();
        // Memory safety valve
        try {
            utils.registerInterval(setInterval(() => {
                try { utils.enforceMemoryLimits(); } catch(_) {}
            }, 30000)); // Check every 30s
        } catch(_) {}
    try { setTimeout(()=>{ ui.updateDebugOverlayFingerprints?.(); }, 300); } catch(_) {}
    // One-time TOS popup after init (only on faction / attack pages)
    try { ui.ensureTosComplianceLink(); if (!storage.get(TDM_TOS_ACK_KEY, null)) { setTimeout(() => ui.showTosComplianceModal(), 800); } } catch(_) {}
    }, 0);

    // =====================================================================
    // Adaptive Ranked War Polling Integration (wires to backend bundle meta)
    // =====================================================================
    (function integrateAdaptiveWarPolling(){
        if (!window.TDMAdaptiveWar || typeof window.TDMAdaptiveWar.start !== 'function') return; // module injected separately or not loaded
        // Auto-start when we have faction + active war context in state once known
        const attemptStart = () => {
            try {
                const factionId = state.user?.factionId || state.user?.tornUserObject?.faction?.faction_id || null;
                const rankedWarId = state.lastRankWar?.id || null;
                if (!factionId || !rankedWarId) return;
                if (window.TDMAdaptiveWar.state.active) return;
                window.TDMAdaptiveWar.start({ factionId, rankedWarId });
                if (state.debug.cadence) tdmlogger('info', '[AdaptiveWar] auto-started', { factionId, rankedWarId });
            } catch(_) { /* ignore */ }
        };
        // Poll for readiness (user + last war loaded) then start
        let guard = 0;
        const readyInterval = utils.registerInterval(setInterval(() => {
            guard++;
            attemptStart();
            if (window.TDMAdaptiveWar.state.active || guard > 40) try { utils.unregisterInterval(readyInterval); } catch(_) {}
        }, 1500));
        // Hook events to existing summary refresh logic if present
        document.addEventListener('tdm:warSummaryVersionChanged', (e) => {
            try {
                if (state.debug.cadence) tdmlogger('debug', '[AdaptiveWar] summaryVersionChanged event', e.detail);
                const factionId = state.user?.factionId || state.user?.tornUserObject?.faction?.faction_id;
                const warId = e?.detail?.rankedWarId || state.lastRankWar?.id;
                if (!warId || !factionId) return;
                // Use existing smart summary fetch (etag / 304 aware) to update cache.
                (async () => {
                    try {
                        await api.getRankedWarSummarySmart(warId, factionId);
                        // If summary modal currently open, re-render it (best-effort detection)
                        const modalOpen = document.querySelector('.tdm-ranked-war-summary-modal');
                        if (modalOpen && typeof ui.showRankedWarSummaryModal === 'function') {
                            const cacheEntry = state.rankedWarSummaryCache?.[`${warId}:${factionId}`];
                            if (cacheEntry && Array.isArray(cacheEntry.summary)) {
                                ui.showRankedWarSummaryModal(cacheEntry.summary, warId);
                            }
                        }
                    } catch(err) {
                        if (state.debug.cadence) tdmlogger('warn', '[AdaptiveWar] smart summary refresh failed', { err: err?.message });
                    }
                })();
            } catch(_) { /* noop */ }
        });
        document.addEventListener('tdm:warManifestFingerprintChanged', (e) => {
            try {
                if (state.debug.cadence) tdmlogger('debug', '[AdaptiveWar] manifestFingerprintChanged event', e.detail);
                // Invalidate local manifest/attacks cache entry so next UI access triggers fetch
                const warId = String(e.detail?.rankedWarId || state.lastRankWar?.id || '');
                if (warId && state.rankedWarAttacksCache[warId]) {
                    delete state.rankedWarAttacksCache[warId].lastManifestFetchMs;
                    state.rankedWarAttacksCache[warId].__invalidateDueToManifestChange = Date.now();
                    try { persistRankedWarAttacksCache(state.rankedWarAttacksCache); } catch(_) {}
                    try { idb.delAttacks(warId).catch(()=>{}); } catch(_) {}
                }
            } catch(_) { /* noop */ }
        });
    })();
})();