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.12.9
// @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.12.9',
        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: '#22c55e',
                dibsSuccessHover: '#16a34a',
                dibsOther: '#ef4444',
                dibsOtherHover: '#dc2626',
                dibsInactive: '#2196F3',
                dibsInactiveHover: '#1976D2',
                noteInactive: 'rgba(33, 150, 243, 0.7)',
                noteInactiveHover: 'rgba(33, 150, 243, 0.85)',
                noteActive: '#2196F3',
                noteActiveHover: '#1976D2',
                medDealInactive: '#f97316',
                medDealInactiveHover: '#ea580c',
                medDealSet: '#22c55e',
                medDealSetHover: '#16a34a',
                medDealMine: '#a855f7',
                medDealMineHover: '#9333ea',
                assistButton: '#40004bff',
                assistButtonHover: '#35003aff',
                modalBg: '#1a1a1a',
                modalBorder: '#333',
                buttonBg: '#2c2c2c',
                mainColor: '#2196F3'
            }
        }
    };

    // 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();
            }
        };
    })();
    // Lightweight IndexedDB reader for FF Scouter v2.71+ cache; old versions stay on localStorage.
    const ffscouterIdb = (() => {
        const DB_NAME = 'ffscouter-cache';
        const STORE = 'cache';
        let warmed = false;
        let pending = null;

        const openDb = () => new Promise((resolve, reject) => {
            const req = indexedDB.open(DB_NAME, 1);
            req.onupgradeneeded = (ev) => {
                const db = ev.target.result;
                if (!db.objectStoreNames.contains(STORE)) {
                    const store = db.createObjectStore(STORE, { keyPath: 'player_id' });
                    store.createIndex('expiry', ['expiry'], { unique: false });
                }
            };
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });

        const getAll = async () => {
            const db = await openDb();
            return new Promise((resolve, reject) => {
                const tx = db.transaction(STORE, 'readonly');
                const req = tx.objectStore(STORE).getAll();
                req.onsuccess = () => resolve(req.result || []);
                req.onerror = () => reject(req.error);
            });
        };

        const warmCache = () => {
            if (warmed || pending) return pending || Promise.resolve();
            if (typeof indexedDB === 'undefined') return Promise.resolve();
            warmed = true;
            pending = getAll()
                .then((rows) => {
                    if (!rows || !rows.length) return;
                    const now = Date.now();
                    state.ffscouterCache = state.ffscouterCache || {};
                    let merged = 0;
                    for (const row of rows) {
                        const sid = String(row?.player_id || '').trim();
                        if (!sid) continue;
                        if (row.expiry && row.expiry < now) continue;
                        state.ffscouterCache[sid] = row;
                        merged++;
                    }
                    if (merged) {
                        try { storage.set('ffscouterCache', state.ffscouterCache); } catch (_) {}
                        try { adapterMemoController.clear(); } catch (_) {}
                    }
                })
                .catch((err) => {
                    try { tdmlogger('debug', `[FFScouter] IndexedDB warm failed: ${err?.message || err}`); } catch (_) {}
                });
            return pending;
        };

        return { warmCache };
    })();
    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', 'JPN'),
                    '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;
                    // visual sizing handled by CSS (applyGeneralStyles)
                    // Compact option: tightly constrain dimensions so button doesn't grow row height
                    const compact = !!options.compact;
                    // If withLabel, show small text; else show icon-only (SVG) to match existing CSS expectations
                    if (options.withLabel) {
                        btn.textContent = 'Note';
                    } else {
                        if (compact) {
                            // compact variant: add class and minimal markup; sizing is in CSS
                            btn.className += ' tdm-note-compact';
                            btn.innerHTML = '<svg width="16" height="16" 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>';
                        } 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;

                    // Compact-mode preview: show a single-line truncated text preview
                    if (btn.classList && btn.classList.contains('tdm-note-compact')) {
                        try {
                            if (has) {
                                const maxLen = 120; // allow longer preview inside doubled width
                                const preview = txt.length > maxLen ? (txt.slice(0, maxLen - 1) + '\u2026') : txt;
                                if (btn.textContent !== preview || btn.querySelector('svg')) btn.textContent = preview;
                                // Visual truncation handled by CSS via .tdm-note-preview
                                btn.classList.add('tdm-note-preview');
                                const sv = btn.querySelector('svg'); if (sv) sv.remove();
                            } else {
                                try { const sv = btn.querySelector('svg'); if (sv) sv.remove(); } catch(_) {}
                                if (btn.textContent !== 'Notes') btn.textContent = 'Notes';
                                btn.classList.remove('tdm-note-preview');
                                btn.classList.add('tdm-note-empty');
                            }
                        } catch(_) {}
                        if (btn.title !== txt) btn.title = txt;
                        if (btn.getAttribute('aria-label') !== txt) btn.setAttribute('aria-label', txt);
                        return;
                    }

                    // 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;
                            // Let CSS handle multiline/truncation and layout
                            btn.classList.add('tdm-note-expanded');
                        } else {
                            if (btn.textContent !== 'Note') btn.textContent = 'Note';
                            btn.classList.remove('tdm-note-expanded');
                        }
                    
                    }
                    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 {
                            // If the description explicitly indicates an abroad hospital using
                            // the strict "In a/an <adjective> hospital for <duration>" form,
                            // treat that as HospitalAbroad. Do NOT fall back to prevRec.dest
                            // here to avoid stale travel data causing mislabels.
                            try {
                                const hospDest = utils.parseHospitalAbroadDestination(rawDesc);
                                if (hospDest) {
                                    canonical = 'HospitalAbroad';
                                    dest = hospDest;
                                }
                            } catch (_) { /* ignore parse failures */ }
                        }
                    } else { canonical = rawState; }
                    // Returning / landed recent logic
                    // Landed subtype convenience flags (separate from canonical; consumer can inspect)
                    let landedTornRecent = false;
                    let landedAbroadRecent = false;
                    // If an inbound flight reached its ETA but Torn still reports the player as Abroad,
                    // treat this as a landed-in-Torn state to avoid showing stale "Abroad <dest>" after arrival.
                    if (prevRec && prevRec.isreturn && prevRec.arrivalMs && (now - prevRec.arrivalMs) >= 0 && (now - prevRec.arrivalMs) <= graceMs && canonical === 'Abroad') {
                        canonical = 'Okay';
                        landedTornRecent = true;
                        landedGrace = true;
                        dest = 'Torn';
                    }
                    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 || ''); }
        },
        // Extract a cleaned player name from an anchor element while ignoring
        // injected BSP stat nodes (e.g., TDup_ColoredStatsInjectionDiv / iconStats).
        extractPlayerNameFromAnchor: (anchor) => {
            try {
                if (!anchor) return '';
                // Clone so we can safely remove injected nodes without mutating page DOM
                const clone = anchor.cloneNode(true);
                // Remove known BSP-injected containers that prefix the name
                clone.querySelectorAll('.TDup_ColoredStatsInjectionDiv, .iconStats, [class*="TDup_ColoredStatsInjectionDiv"], [class*="iconStats"]').forEach(n => n.remove());
                // Also remove any small meta nodes often found before names (safeguard)
                clone.querySelectorAll('[data-bsp], .bsp-stats, .tdm-injected-stat').forEach(n => n.remove());
                return (clone.textContent || '').trim();
            } catch (_) {
                return (anchor.textContent || '').trim();
            }
        },
        // Extract a cleaned player name from a row element (li / .table-row) by
        // preferring profile anchors or image alt text and falling back to row text.
        extractPlayerNameFromRow: (row) => {
            try {
                if (!row) return '';
                if (row.dataset && row.dataset.tdmMemberName) return String(row.dataset.tdmMemberName).trim();
                const link = row.querySelector('a[href*="profiles.php?XID="]');
                if (link) return utils.extractPlayerNameFromAnchor(link);
                const img = row.querySelector('img[alt]');
                if (img && img.alt) return String(img.alt).trim();
                return (row.textContent || '').trim();
            } catch (_) { return (row && (row.textContent || '') || '').trim(); }
        },
        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;
            const fs = (state.script && state.script.factionSettings) || {};
            const currentAttackMode = (fs.options && fs.options.attackMode) || fs.attackMode || null;
            const warType = state.warData?.warType || null;
            const attackModeLocksDibs = warType === 'Ranked War' && ['FFA','Turtle'].includes(String(currentAttackMode || '').trim());

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

            if (attackModeLocksDibs) {
                const modeLabel = currentAttackMode || 'Unknown';
                const msg = `Dibs disabled in Ranked War (${modeLabel}).`;
                const cls = 'btn dibs-btn btn-dibs-disabled';
                if (btn.textContent !== 'Dibs Disabled') btn.textContent = 'Dibs Disabled';
                if (btn.className !== cls) btn.className = cls;
                btn.disabled = false; // keep clickable for the explanatory toast
                btn.title = msg;
                btn.onclick = () => ui.showMessageBox(msg, 'info', 4000);
                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 key = String(dest || '');
            let meta = eq.byName[key];
            if (!meta) {
                // If caller passed an alias or an abbreviation (e.g. 'UAE' or 'mex'),
                // resolve it to the canonical name and return that canonical abbr.
                try {
                    const mapped = eq.aliasToNameLower && eq.aliasToNameLower[key.toLowerCase()];
                    if (mapped) meta = eq.byName[mapped];
                } catch(_) { /* ignore */ }
            }
            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.
        }
    };

// Warm FF Scouter IndexedDB cache (v2.71+) into in-memory cache for readFFScouter; older versions still use localStorage fallback.
try { ffscouterIdb.warmCache(); } catch (_) {}

        // Validate/migrate persisted userScore: accept new shape { warId, v }
        try {
            const rawUserScore = storage.get('userScore', null);
            if (rawUserScore && typeof rawUserScore === 'object') {
                // New format: { warId, v }
                if (Object.prototype.hasOwnProperty.call(rawUserScore, 'warId') && Object.prototype.hasOwnProperty.call(rawUserScore, 'v')) {
                    const storedWarId = rawUserScore.warId;
                    const currentWarId = state.lastRankWar?.id || null;
                    if (storedWarId == null || currentWarId == null || String(storedWarId) !== String(currentWarId)) {
                        // Different war (or unknown current war) — remove stale cache
                        try { storage.remove('userScore'); } catch(_) {}
                        state.userScore = null;
                    } else {
                        // Hydrate lightweight value
                        state.userScore = rawUserScore.v || null;
                    }
                } else {
                    try { storage.remove('userScore'); } catch(_) {}
                    state.userScore = null;
                }
            }
        } catch(_) { /* best-effort migration; ignore errors */ }

    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);
                                // Some transports (notably certain callable/http wrappers) may nest the error
                                // under `result.data.error`. Treat that as a hard error so callers don't
                                // mistakenly treat conflicts (e.g. already-exists) as success.
                                const fbError = payload?.error || payload?.result?.error || payload?.data?.error || payload?.result?.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) {
                        // Backend may return either a raw array or a wrapper object: { items: [...], scoreBleed?: {...} }
                        const items = Array.isArray(json) ? json : (json && Array.isArray(json.items) ? json.items : null);
                        const scoreBleed = (json && !Array.isArray(json) && typeof json === 'object') ? (json.scoreBleed || null) : null;
                        if (Array.isArray(items)) {
                            const next = { ...clientEntry, summaryUrl, etag: etag || null, lastModified: lastModified || null, updatedAt: Date.now(), summary: items, scoreBleed: scoreBleed || (clientEntry.scoreBleed || null), source: 'storage200' };
                            state.rankedWarSummaryCache[cacheKey] = next;
                            storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                            try {
                                state.rankedWarLastSummarySource = 'storage200';
                                state.rankedWarLastSummaryMeta = { source: 'storage', etag: etag || null, lastModified: lastModified || null, count: items.length, url: summaryUrl, scoreBleed: next.scoreBleed || null };
                                storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                                storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                            } catch(_) { /* noop */ }
                            tdmlogger('debug', `getRankedWarSummarySmart: 200 OK for war ${warId}, fetched fresh summary (${items.length} 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, scoreBleed: clientEntry.scoreBleed || null };
                            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) {
                            const forcedItems = Array.isArray(forced.json) ? forced.json : (forced.json && Array.isArray(forced.json.items) ? forced.json.items : null);
                            const forcedScoreBleed = (forced.json && !Array.isArray(forced.json) && typeof forced.json === 'object') ? (forced.json.scoreBleed || null) : null;
                            if (Array.isArray(forcedItems)) {
                                const next = { ...clientEntry, summaryUrl, etag: forced.etag || null, lastModified: forced.lastModified || null, updatedAt: Date.now(), summary: forcedItems, scoreBleed: forcedScoreBleed || (clientEntry.scoreBleed || null), source: 'storage200' };
                                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, scoreBleed: next.scoreBleed || null };
                                    storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                                    storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                                } catch(_) {}
                                tdmlogger('debug', `getRankedWarSummarySmart: Forced 200 OK for war ${warId}, fetched fresh summary (${forcedItems.length} 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) {
                                    const againItems = Array.isArray(again.json) ? again.json : (again.json && Array.isArray(again.json.items) ? again.json.items : null);
                                    const againScoreBleed = (again.json && !Array.isArray(again.json) && typeof again.json === 'object') ? (again.json.scoreBleed || null) : null;
                                    if (Array.isArray(againItems)) {
                                        const next = { ...clientEntry, summaryUrl, etag: again.etag || null, lastModified: again.lastModified || null, updatedAt: Date.now(), summary: againItems, scoreBleed: againScoreBleed || (clientEntry.scoreBleed || null), source: 'storage200' };
                                        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, scoreBleed: next.scoreBleed || null };
                                            storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                                            storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                                        } catch(_) {}
                                        tdmlogger('debug', `getRankedWarSummarySmart: Retry 200 OK for war ${warId}, fetched fresh summary (${againItems.length} 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;
                    const scoreBleed = summaryCache.scoreBleed || null;
                    // 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 and keep score-bleed meta when present
                    try {
                        state.rankedWarLastSummarySource = 'summary-json-304';
                        state.rankedWarLastSummaryMeta = { source: 'summary-json-304', count: summaryRows.length, url: urls.summaryUrl, scoreBleed };
                        storage.set('rankedWarLastSummarySource', state.rankedWarLastSummarySource);
                        storage.set('rankedWarLastSummaryMeta', state.rankedWarLastSummaryMeta);
                    } catch(_) {}
                    // Preserve score-bleed hint for downstream UI
                    if (scoreBleed) {
                        try { state.rankedWarLastSummaryMeta.scoreBleed = scoreBleed; } catch(_) {}
                    }
                    return summaryRows;
                }

                if (status === 200 && (Array.isArray(json) || (json && Array.isArray(json.items)))) {
                        const scoreBleed = json && !Array.isArray(json) ? (json.scoreBleed || null) : null;
                        if (Array.isArray(json)) {
                            summaryRows = json;
                        } else if (json && Array.isArray(json.items)) {
                            summaryRows = json.items;
                        } 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,
                            scoreBleed: scoreBleed || null,
                            source: 'storage200'
                        };
                        state.rankedWarSummaryCache = state.rankedWarSummaryCache || {};
                        state.rankedWarSummaryCache[warId] = nextCache;
                        storage.set('rankedWarSummaryCache', state.rankedWarSummaryCache);
                    }

                    let provenanceStamped = false;
                    try {
                        state.rankedWarLastSummarySource = 'summary-json';
                        // Carry score-bleed metadata forward so UI can display it
                        const scoreBleed = (json && !Array.isArray(json)) ? (json.scoreBleed || null) : null;
                        state.rankedWarLastSummaryMeta = { source: 'summary-json', count: summaryRows.length, url: urls.summaryUrl, scoreBleed };
                        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) Attack mode indicator (ranked wars only)
            exec(ui.ensureAttackModeBadge, ui.ensureAttackModeBadge);
            // 3) Inactivity timer
            exec(ui.ensureInactivityTimer, null);
            // 4) Opponent status
            exec(ui.ensureOpponentStatus, null);
            // 5) Faction score
            exec(ui.ensureFactionScoreBadge, ui.updateFactionScoreBadge);
            // 6) User score
            exec(ui.ensureUserScoreBadge, ui.updateUserScoreBadge);
            // 7) Dibs/Deals
            exec(ui.ensureDibsDealsBadge, ui.updateDibsDealsBadge);
            // 8) 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,
                    className: 'tdm-report-modal',
                    style: {
                        position: 'fixed',
                        top: '50%',
                        left: '50%',
                        transform: 'translate(-50%, -50%)',
                        backgroundColor: 'var(--tdm-modal-bg)',
                        border: '1px solid var(--tdm-modal-border)',
                        borderRadius: 'var(--tdm-radius-lg)',
                        padding: 'var(--tdm-space-lg)',
                        zIndex: 'var(--tdm-z-modal)',
                        color: 'var(--tdm-text-primary)',
                        width,
                        maxWidth,
                        minWidth: '320px',
                        maxHeight: '80vh',
                        overflowY: 'auto',
                        overflowX: 'auto',
                        boxShadow: 'var(--tdm-shadow-modal)',
                        animation: 'tdmFadeIn var(--tdm-transition-normal) forwards'
                    }
                });
                document.body.appendChild(modal);
            }
            modal.innerHTML = '';
            modal.style.display = 'block';

            const closeBtn = utils.createElement('button', {
                className: `${id}-close tdm-btn tdm-btn--danger tdm-btn--sm`,
                style: {
                    position: 'absolute',
                    top: 'var(--tdm-space-md)',
                    right: 'var(--tdm-space-lg)',
                    minWidth: '32px',
                    fontWeight: 'bold'
                },
                textContent: 'X'
            });
            const header = utils.createElement('h2', {
                style: {
                    marginTop: '0',
                    marginRight: '40px',
                    marginBottom: 'var(--tdm-space-md)',
                    fontSize: 'var(--tdm-font-size-lg)',
                    color: 'var(--tdm-text-accent)'
                },
                textContent: title || 'Report'
            });
            const controls = utils.createElement('div', { id: `${id}-controls`, style: { marginBottom: 'var(--tdm-space-md)' } });
            const tableWrap = utils.createElement('div', { id: `${id}-table`, style: { width: '100%' } });
            const footer = utils.createElement('div', { id: `${id}-footer`, style: { marginTop: 'var(--tdm-space-lg)', fontSize: 'var(--tdm-font-size-sm)', color: 'var(--tdm-text-secondary)' } });

            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: 'var(--tdm-color-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,
                className: 'tdm-report-table',
                style: {
                    width: '100%',
                    minWidth: '900px',
                    borderCollapse: 'collapse',
                    background: 'var(--tdm-bg-secondary)',
                    color: 'var(--tdm-text-primary)',
                    borderRadius: 'var(--tdm-radius-sm)',
                    overflow: 'hidden'
                }
            });
            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: 'var(--tdm-bg-card)' } });
            columns.forEach(col => {
                const th = utils.createElement('th', {
                    style: { padding: 'var(--tdm-space-md)', cursor: 'pointer', textAlign: col.align || 'left', width: col.width || undefined, fontWeight: '600', color: 'var(--tdm-text-accent)' }
                }, [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: 'var(--tdm-bg-card)', color: 'var(--tdm-text-primary)', borderBottom: '1px solid var(--tdm-bg-secondary)' } });
                    for (const c of columns) {
                        const td = utils.createElement('td', { style: { padding: 'var(--tdm-space-md)', color: 'var(--tdm-text-primary)', 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: () => {
            const enabled = storage.get('attackModeBadgeEnabled', true);
            const existing = document.getElementById('tdm-attack-mode');
            const warId = state.lastRankWar?.id;
            const isRanked = (state.warData?.warType === 'Ranked War');
            const warActive = !!(warId && utils.isWarInActiveOrGrace?.(warId, 6));
            // Only show attack mode during active ranked wars
            if (!enabled || !isRanked || !warActive) {
                if (existing) existing.remove();
                state.ui.attackModeEl = null;
                return;
            }

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

            const fs = (state.script && state.script.factionSettings) || {};
            const mode = (fs.options && fs.options.attackMode) || fs.attackMode || 'FFA';

            let el = existing;
            if (!el) {
                el = utils.createElement('div', {
                    id: 'tdm-attack-mode',
                    title: 'Faction attack mode',
                    style: ui._composeDockBadgeStyle({
                        background: 'rgba(52, 33, 3, 0.9)',
                        border: '1px solid rgba(255, 179, 71, 0.55)',
                        color: '#ffe3b3'
                    })
                });
                items.appendChild(el);
            } else if (el.parentNode !== items) {
                items.appendChild(el);
            }
            state.ui.attackModeEl = el;

            const prev = state.ui._attackModeRendered || {};
            const valueSpan = el.querySelector('.tdm-attack-mode-value');
            if (valueSpan && prev.mode === mode) {
                if (valueSpan.textContent !== String(mode)) valueSpan.textContent = String(mode);
                return;
            }

            el.innerHTML = '';
            const label = utils.createElement('span', { textContent: 'Atk Mode:', style: { opacity: 0.8 } });
            const valueNode = utils.createElement('span', { className: 'tdm-attack-mode-value', textContent: mode, style: { color: '#fff', fontWeight: '700' } });
            el.appendChild(label);
            el.appendChild(valueNode);
            state.ui._attackModeRendered = { mode };
        },
        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+)/i) || [])[1] : null;
                                const name = link ? utils.sanitizePlayerName(utils.extractPlayerNameFromAnchor(link), id) : 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 tdm-btn dibs-btn btn-dibs-inactive tdm-soften', textContent: 'Dibs' }));
                }
                if (!subrow.querySelector('.btn-med-deal-default')) {
                    subrow.appendChild(utils.createElement('button', { className: 'btn tdm-btn btn-med-deal-default tdm-soften', textContent: 'Med Deal', style: { display: 'none' } }));
                }
                utils.ensureNoteButton(subrow, { compact: true, withLabel: true });
                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 tdm-btn retal-btn tdm-soften', textContent: 'Retal', style: { display: 'none' } }));
                }
            } else {
                utils.ensureNoteButton(subrow, { disabled: true, compact: true, withLabel: 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 tdm-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 tdm-btn dibs-button tdm-soften', textContent: 'Dibs' }));
                    dibsCell.appendChild(utils.createElement('button', { className: 'btn tdm-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(utils.extractPlayerNameFromAnchor(userLink), 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(utils.extractPlayerNameFromAnchor(userLink), 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 ? utils.sanitizePlayerName(utils.extractPlayerNameFromAnchor(link), (link.href.match(/[?&]XID=(\d+)/i)||[])[1]) : (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(utils.extractPlayerNameFromAnchor(memberIdLink), 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);

            const isTermedWar = state.warData?.warType === 'Termed War';
            if (!isTermedWar) {
                // Ranked wars should not surface score-cap UI; clear any stale state from prior termed wars
                state.user.hasReachedScoreCap = false;
                state.user._scoreCapWarId = null;
                try {
                    const stale = attackContainer.querySelector('.score-cap-warning');
                    if (stale) stale.remove();
                } catch(_) { /* noop */ }
            }

            // --- 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 in termed wars
                if (!isTermedWar || !state.user.hasReachedScoreCap) { if (existingWarning) existingWarning.remove(); return; }
                (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 && isTermedWar && state.user.hasReachedScoreCap) {
                            if (!existingWarning) {
                                const scoreCapWarning = utils.createElement('div', {
                                    className: 'score-cap-warning',
                                    style: { padding: '10px', marginBottom: '10px', backgroundColor: 'var(--tdm-color-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 tdm-btn dibs-btn', style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', 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 tdm-btn med-deal-btn', style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', 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 tdm-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', 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 tdm-btn retal-btn btn-retal-inactive', style: { minWidth: '70px', maxWidth: '70px', minHeight: '24px', boxSizing: 'border-box', fontSize: '0.75em', padding: '4px 6px', 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 tdm-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', 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.
                // NOTE: This is client-driven (calls removeDibs) and must be resilient across Torn UI class changes.
                // Also, factionSettings can load slightly later for some users, so we support a short grace window.
                try {
                    // 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) {
                        const opponentNameLower = String(opponentName || '').toLowerCase();

                        const isSettingsLoaded = () => {
                            const fs = state.script && state.script.factionSettings;
                            return !!(fs && fs.options && fs.options.dibsStyle);
                        };

                        const shouldAutoRemove = () => {
                            try {
                                const optsNow = utils.getDibsStyleOptions();
                                return !!optsNow.mustRedibAfterSuccess;
                            } catch (_) {
                                return false;
                            }
                        };

                        // Match any visible "You defeated ..." message without relying on obfuscated classnames.
                        const nodeHasDefeatText = (root) => {
                            try {
                                const txt = (root && typeof root.textContent === 'string') ? root.textContent : '';
                                if (!txt) return false;
                                const t = txt.trim();
                                if (!t) return false;
                                // Keep this strict to avoid false positives.
                                // Torn typically renders: "You defeated <name>".
                                const lower = t.toLowerCase();
                                if (!lower.startsWith('you defeated')) return false;
                                if (!opponentNameLower) return true;
                                return lower.includes(opponentNameLower);
                            } catch (_) {
                                return false;
                            }
                        };

                        let removed = false;
                        const stopObserver = () => {
                            if (state.script.mutationObserver) {
                                try { state.script.mutationObserver.disconnect(); } catch (_) {}
                            }
                        };

                        const tryRemove = async (root) => {
                            if (removed) return;
                            // If settings are loaded and policy is OFF, do nothing.
                            if (isSettingsLoaded() && !shouldAutoRemove()) return;
                            if (!nodeHasDefeatText(root)) return;
                            // At this point we saw a defeat message; only remove if policy is ON.
                            if (!shouldAutoRemove()) return;
                            removed = true;
                            stopObserver();
                            try {
                                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();
                            } catch (_) {
                                // Non-fatal: if the request fails, allow future manual removal.
                            }
                        };

                        // Initial check in case the result message is already present
                        tryRemove(document);

                        // Observe for a short window (settings load + result message timing)
                        stopObserver();
                        state.script.mutationObserver = utils.registerObserver(new MutationObserver((mutations) => {
                            for (const m of mutations) {
                                for (const n of m.addedNodes) {
                                    // Fast-path: check just the added subtree
                                    tryRemove(n);
                                }
                            }
                        }));
                        state.script.mutationObserver.observe(document.body, { childList: true, subtree: true });

                        // Safety: stop observing after 45s to avoid leaks
                        setTimeout(() => {
                            try { stopObserver(); } catch (_) {}
                        }, 45000);
                    }
                } 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 var(--tdm-bg-secondary);background:var(--tdm-bg-card);color:var(--tdm-text-primary);">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 var(--tdm-bg-secondary);background:var(--tdm-bg-card);border-radius:6px;font:11px/1.4 monospace;color:var(--tdm-text-primary)"></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:var(--tdm-modal-bg);
                            color:var(--tdm-text-primary);
                            border:1px solid var(--tdm-modal-border);
                            border-radius:var(--tdm-radius-xl);
                            z-index:var(--tdm-z-modal);
                            box-shadow:var(--tdm-shadow-modal);
                            width:520px;
                            max-width:80vw;
                            padding:var(--tdm-space-xl) var(--tdm-space-xl);
                            font:var(--tdm-font-size-base)/1.45 'Inter','Segoe UI',sans-serif;
                            backdrop-filter:blur(12px);
                            max-height:85vh;
                            overflow-y:auto;
                            display:none;
                        }
                        .tdm-note-modal[style*="display: block"]{
                            display:block !important;
                            opacity:1;
                        }
                        .tdm-note-modal-header{
                            display:flex;
                            align-items:flex-start;
                            justify-content:space-between;
                            margin-bottom:var(--tdm-space-lg);
                            gap:var(--tdm-space-lg);
                            border-bottom:1px solid var(--tdm-modal-border);
                            padding-bottom:var(--tdm-space-lg);
                        }
                        .tdm-note-title-wrap{display:flex;flex-direction:column;gap:var(--tdm-space-sm);min-width:0;}
                        .tdm-note-title{font-weight:600;font-size:var(--tdm-font-size-xl);letter-spacing:.02em;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--tdm-text-primary);}
                        .tdm-note-actions{display:flex;gap:var(--tdm-space-md);}
                        .tdm-note-btn{
                            background:var(--tdm-bg-card);
                            border:1px solid var(--tdm-bg-secondary);
                            color:var(--tdm-text-secondary);
                            cursor:pointer;
                            border-radius:var(--tdm-radius-md);
                            width:32px;
                            height:32px;
                            display:flex;
                            align-items:center;
                            justify-content:center;
                            font-size:14px;
                            padding:0;
                            transition:all var(--tdm-transition-fast);
                        }
                        .tdm-note-btn:hover{background:var(--tdm-color-info);border-color:var(--tdm-color-info);color:var(--tdm-text-primary);}
                        .tdm-note-btn:active{transform:scale(0.96);}
                        .tdm-note-text{
                            width:85%;
                            background:var(--tdm-bg-secondary);
                            border:1px solid var(--tdm-bg-secondary);
                            color:var(--tdm-text-primary);
                            padding:var(--tdm-space-md) var(--tdm-space-lg);
                            border-radius:var(--tdm-radius-md);
                            resize:vertical;
                            font:var(--tdm-font-size-base)/1.55 'JetBrains Mono',monospace;
                            min-height:40px;
                            transition:border-color var(--tdm-transition-fast);
                        }
                        .tdm-note-text:focus{outline:none;border-color:var(--tdm-color-info);box-shadow:0 0 0 1px var(--tdm-color-info);}
                        .tdm-note-footer{display:grid;grid-template-columns:auto auto 1fr;align-items:center;gap:var(--tdm-space-lg);margin-top:var(--tdm-space-md);font-size:var(--tdm-font-size-sm);color:var(--tdm-text-secondary);}
                        .tdm-note-status.saving{color:var(--tdm-color-warning);} .tdm-note-status.saved{color:var(--tdm-color-success);} .tdm-note-status.error{color:var(--tdm-color-error);}
                        .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:var(--tdm-space-md);margin:var(--tdm-space-md) 0;align-items:center;}
                        .tdm-note-tag{
                            background:var(--tdm-color-info);
                            color:var(--tdm-text-primary);
                            border:none;
                            padding:var(--tdm-space-xs) var(--tdm-space-md);
                            font-size:var(--tdm-font-size-sm);
                            border-radius:var(--tdm-radius-full);
                            display:inline-flex;
                            align-items:center;
                            gap:var(--tdm-space-md);
                            cursor:pointer;
                            box-shadow:var(--tdm-shadow-sm);
                            transition:background var(--tdm-transition-fast),transform var(--tdm-transition-fast);
                        }
                        .tdm-note-tag:hover{background:#1976D2;transform:translateY(-1px);}
                        .tdm-note-tag-remove{font-size:12px;line-height:1;cursor:pointer;color:var(--tdm-text-secondary);}
                        .tdm-note-tag-remove:hover{color:var(--tdm-color-error);}
                        .tdm-note-tag-input{
                            flex:1;
                            min-width:140px;
                            background:var(--tdm-bg-secondary);
                            border:1px solid var(--tdm-bg-secondary);
                            color:var(--tdm-text-primary);
                            padding:var(--tdm-space-sm) var(--tdm-space-md);
                            border-radius:var(--tdm-radius-md);
                            font:var(--tdm-font-size-sm)/1.35 'JetBrains Mono',monospace;
                            transition:border-color var(--tdm-transition-fast);
                        }
                        .tdm-note-tag-input:focus{outline:none;border-color:var(--tdm-color-info);}
                        .tdm-note-tags-empty{font-size:var(--tdm-font-size-sm);color:var(--tdm-text-muted);margin-bottom:var(--tdm-space-md);display:flex;flex-wrap:wrap;gap:var(--tdm-space-md);align-items:center;}
                        .tdm-note-tags-empty span{background:var(--tdm-bg-card);color:var(--tdm-text-accent);padding:1px 6px;border-radius:var(--tdm-radius-full);font-family:'JetBrains Mono',monospace;}
                        .tdm-note-help{margin-top:var(--tdm-space-md);font-size:var(--tdm-font-size-xs);color:var(--tdm-text-muted);display:flex;flex-wrap:wrap;gap:var(--tdm-space-md);}
                        .tdm-conf-badge{display:inline-block;font:var(--tdm-font-size-xs)/1.1 monospace;padding:2px 4px;border-radius:var(--tdm-radius-sm);margin:0 2px 0 4px;vertical-align:baseline;letter-spacing:.5px}
                        .tdm-conf-LOW{background:var(--tdm-bg-card);color:var(--tdm-text-primary);border:1px solid var(--tdm-bg-secondary);text-decoration:underline dotted;}
                        .tdm-conf-MED{background:var(--tdm-color-warning);color:#000;border:1px solid var(--tdm-color-warning);}
                        .tdm-conf-HIGH{background:var(--tdm-color-success);color:var(--tdm-text-primary);border:1px solid var(--tdm-color-success);}
                        .tdm-note-snapshot-line{white-space:normal;word-break:break-word;margin:0 0 2px;}
                        .tdm-note-snapshot-line span.label{color:var(--tdm-text-muted);font-weight:600;margin-right:var(--tdm-space-sm);}
                        .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:var(--tdm-space-lg);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: 'var(--tdm-radius-md)', padding: 'var(--tdm-space-md) var(--tdm-space-lg)', fontSize: 'var(--tdm-font-size-sm)', lineHeight: '1.3',
                    color: 'var(--tdm-text-primary)', boxShadow: 'var(--tdm-shadow-md)', opacity: '0', transform: 'translateY(-4px)',
                    transition: 'opacity var(--tdm-transition-fast), transform var(--tdm-transition-fast)', 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', {
                    className: 'tdm-confirm-modal',
                    style: {
                        position: 'fixed',
                        top: '50%',
                        left: '50%',
                        transform: 'translate(-50%, -50%)',
                        backgroundColor: '#1a1a2e',
                        border: '1px solid #333',
                        borderRadius: '8px',
                        padding: '20px',
                        zIndex: '10001',
                        boxShadow: '0 10px 40px rgba(0,0,0,0.5)',
                        maxWidth: '380px',
                        width: '90%',
                        color: '#e2e8f0',
                        textAlign: 'center',
                        display: 'block'
                    }
                });
                const messagePara = utils.createElement('p', {
                    style: {
                        marginBottom: '16px',
                        fontSize: '14px',
                        lineHeight: '1.4'
                    },
                    textContent: message
                });
                const buttonsContainer = utils.createElement('div', {
                    style: {
                        display: 'flex',
                        justifyContent: 'center',
                        gap: '8px',
                        flexWrap: 'wrap'
                    }
                });
                const yesBtn = utils.createElement('button', {
                    id: 'confirm-ok',
                    className: 'settings-btn settings-btn-green',
                    style: {
                        minWidth: '80px',
                        padding: '8px 16px',
                        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',
                        className: 'settings-btn settings-btn-blue',
                        style: {
                            minWidth: '80px',
                            padding: '8px 16px',
                            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',
                        className: 'settings-btn settings-btn-red',
                        style: {
                            minWidth: '80px',
                            padding: '8px 16px',
                            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, #42a5f5, #1976d2); border: 2px solid #90caf9; border-radius: 4px; box-sizing: border-box; color: #ffffff; text-shadow: 1px 1px 1px #0d47a1; 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, #ab47bc, #7b1fa2); border: 2px solid #ce93d8; border-radius: 4px; box-sizing: border-box; color: #ffffff; text-shadow: 1px 1px 1px #4a148c; 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%', maxWidth: '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', overflowX: 'hidden', boxSizing: 'border-box' } });
            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: 'var(--tdm-modal-header)', 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', maxWidth: '100%', overflowX: 'hidden', boxSizing: 'border-box' } });
            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:#fff;margin-bottom:2px;font-weight:500}`+
                            `.tdm-mini-checkbox{font-size:12px;color:#fff;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:#fff}`+
                            /* 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 with tabs
            const activeTab = storage.get('tdm_settings_active_tab', 'wardibs');
            content.innerHTML = `
                <!-- Settings Tab Navigation -->
                <div class="tdm-settings-tabs">
                    <button class="tdm-settings-tab ${activeTab === 'display' ? 'tdm-settings-tab--active' : ''}" data-tab="display">Display</button>
                    <button class="tdm-settings-tab ${activeTab === 'wardibs' ? 'tdm-settings-tab--active' : ''}" data-tab="wardibs">War/Dibs</button>
                    <button class="tdm-settings-tab ${activeTab === 'advanced' ? 'tdm-settings-tab--active' : ''}" data-tab="advanced">Advanced</button>
                </div>

                <!-- DISPLAY TAB PANEL -->
                <div class="tdm-settings-panel ${activeTab === 'display' ? 'tdm-settings-panel--active' : ''}" data-panel="display">
                    <!-- Column Settings -->
                    <div class="settings-section" data-section="column-settings">
                        <div class="settings-header">Column Settings</div>
                        <div style="display:flex; flex-direction:column; gap:8px;">
                            <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:8px;">
                                <div class="cv-group">
                                    <div class="mini-label" style="font-size:12px; color:#fff; margin-bottom:4px; font-weight:bold; text-align:center;">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:12px; color:#fff; margin-bottom:4px; font-weight:bold; text-align:center;">Members List Table</div>
                                    <div id="column-visibility-ml" class="settings-button-group" style="gap:6px;"></div>
                                </div>
                            </div>
                        </div>
                    </div>

                    <!-- Badges Dock -->
                    <div class="settings-section" data-section="badges-dock">
                        <div class="settings-header">Badges Dock</div>
                        <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:12px;">
                            <div style="display:flex; flex-direction:column; gap:8px;">
                                <div style="font-size:12px; color:#fff; font-weight:bold; text-align:center;">Core Timers</div>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('chainTimerEnabled', true) ? 'active' : ''}" id="chain-timer-toggle" data-key="chainTimerEnabled"></div>
                                    Chain Timer
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('inactivityTimerEnabled', false) ? 'active' : ''}" id="inactivity-timer-toggle" data-key="inactivityTimerEnabled"></div>
                                    Inactivity Timer
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('opponentStatusTimerEnabled', true) ? 'active' : ''}" id="opponent-status-toggle" data-key="opponentStatusTimerEnabled"></div>
                                    Opponent Status
                                </label>
                            </div>
                            <div style="display:flex; flex-direction:column; gap:8px;">
                                <div style="font-size:12px; color:#fff; font-weight:bold; text-align:center;">Badges</div>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('apiUsageCounterEnabled', false) ? 'active' : ''}" id="api-usage-toggle" data-key="apiUsageCounterEnabled"></div>
                                    API Counter
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('attackModeBadgeEnabled', true) ? 'active' : ''}" id="attack-mode-badge-toggle" data-key="attackModeBadgeEnabled"></div>
                                    Attack Mode Badge
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('chainWatcherBadgeEnabled', true) ? 'active' : ''}" id="chainwatcher-badge-toggle" data-key="chainWatcherBadgeEnabled"></div>
                                    Chain Watchers Badge
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('userScoreBadgeEnabled', true) ? 'active' : ''}" id="user-score-badge-toggle" data-key="userScoreBadgeEnabled"></div>
                                    User Score Badge
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('factionScoreBadgeEnabled', true) ? 'active' : ''}" id="faction-score-badge-toggle" data-key="factionScoreBadgeEnabled"></div>
                                    Faction Score Badge
                                </label>
                                <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                    <div class="tdm-toggle-switch ${storage.get('dibsDealsBadgeEnabled', true) ? 'active' : ''}" id="dibs-deals-badge-toggle" data-key="dibsDealsBadgeEnabled"></div>
                                    Dibs/Deals Badge
                                </label>
                            </div>
                        </div>
                    </div>

                    <!-- Note Tag Presets -->
                    <div class="settings-section" data-section="note-tags">
                        <div class="settings-header">Note Tag Presets</div>
                        <div style="display:flex; flex-direction:column; gap:8px;">
                            <div style="font-size:12px; color:#fff;">Configure up to 12 quick-add tags (comma or space separated).</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" />
                            <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">Save Presets</button>
                                <button id="note-tags-reset-btn" class="settings-btn">Reset Default</button>
                                <div id="note-tags-status" style="font-size:12px; align-self:center; color:#fff;">Idle</div>
                            </div>
                        </div>
                    </div>

                    <!-- Alerts & Messaging -->
                    <div class="settings-section" data-section="alerts-messaging">
                        <div class="settings-header">Alerts &amp; Messaging</div>
                        <div style="display:flex; flex-direction:column; gap:8px;">
                            <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                <div class="tdm-toggle-switch ${storage.get('alertButtonsEnabled', true) ? 'active' : ''}" id="alert-buttons-toggle" data-key="alertButtonsEnabled"></div>
                                Alert Buttons
                            </label>
                            <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                <div class="tdm-toggle-switch ${storage.get('pasteMessagesToChatEnabled', true) ? 'active' : ''}" id="paste-messages-toggle" data-key="pasteMessagesToChatEnabled"></div>
                                Paste Messages to Chat
                            </label>
                            <label style="display:flex; align-items:center; gap:8px; color:#fff; font-size:12px;">
                                <div class="tdm-toggle-switch ${storage.get('ocReminderEnabled', true) ? 'active' : ''}" id="oc-reminder-toggle" data-key="ocReminderEnabled"></div>
                                OC Reminder
                            </label>
                        </div>
                    </div>
                </div><!-- END DISPLAY TAB PANEL -->

                <!-- WAR/DIBS TAB PANEL -->
                <div class="tdm-settings-panel ${activeTab === 'wardibs' ? 'tdm-settings-panel--active' : ''}" data-panel="wardibs">
                <div class="settings-section" data-section="latest-war">
                    <div class="settings-header">RW Details: <span id="rw-warstring" style="color:#ffd600;">${warstring}</span></div>
                    <div>
                        <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:#fff; 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:#fff; font-size:12px;" ></div>
                                <div id="rw-term-info" style="margin-top:8px;font-size:12px;color:#fff;display:block;width:100%;box-sizing:border-box;grid-column:1 / -1;line-height:1.6"></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:#fff;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" data-section="ranked-war-tools">
                    <div class="settings-header">Ranked War Reports</div>
                    <div 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" data-section="chain-watcher">
                    <div class="settings-header">ChainWatcher: <span id="tdm-chainwatcher-header-names" style="color:#ffd600;">—</span></div>
                    <div>
                <div style="font-size:12px;color:#fff;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:#fff;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.</div>
                    </div>
                </div>



                </div><!-- END WAR/DIBS TAB PANEL -->

                <!-- ADVANCED TAB PANEL -->
                <div class="tdm-settings-panel ${activeTab === 'advanced' ? 'tdm-settings-panel--active' : ''}" data-panel="advanced">
                <div class="settings-section" data-section="api-keys">
                    <div class="settings-header">API Keys</div>
                    <div>
                        <!-- Torn API Key Card -->
                        <div id="tdm-api-key-card" class="settings-card" data-tone="${apiKeyTone}" style="margin-bottom:12px;border:1px solid var(--tdm-bg-secondary);background:var(--tdm-bg-secondary);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:#fff;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 var(--tdm-bg-secondary);background:var(--tdm-bg-secondary);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:#fff;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 var(--tdm-bg-secondary);background:var(--tdm-bg-secondary);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>

                <!-- API Cadence & Polling -->
                <div class="settings-section" data-section="api-cadence">
                    <div class="settings-header">API Cadence &amp; Polling</div>
                    <div style="display:flex; flex-direction:column; gap:8px;">
                        <div style="font-size:12px; color:#fff; line-height:1.4;">
                            Controls how often TreeDibsMapper pulls faction bundles to keep scores, dibs, and status data fresh.
                        </div>
                        <div style="display:flex; gap:8px; align-items:center; flex-wrap:wrap; background:var(--tdm-bg-secondary); padding:8px; border-radius:6px;">
                            <label style="display:flex; align-items:center; gap:6px; color:#fff;">
                                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 id="tdm-last-faction-refresh" style="font-size:12px; color:#fff; flex-basis:100%;">Last faction refresh: —</div>
                            <div id="tdm-polling-status" style="font-size:12px; color:#fff; flex-basis:100%;">Polling status: —</div>
                            <div id="tdm-opponent-poll-line" style="font-size:12px; color:#fff; flex-basis:100%;">Opponent polling: —</div>
                        </div>
                        <div style="background:var(--tdm-bg-secondary); 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:#fff; 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" />
                            </label>
                            <button id="tdm-save-additional-factions-btn" class="settings-btn">Save</button>
                            <div id="tdm-additional-factions-status" style="font-size:12px; color:#fff; align-self:center;">No extra factions configured.</div>
                            <div id="tdm-additional-factions-summary" style="flex-basis:100%; font-size:12px; color:#777;">—</div>
                        </div>
                    </div>
                    </div>

                    <!-- Activity Tracking -->
                    <div class="settings-section" data-section="activity-tracking">
                        <div class="settings-header">Activity Tracking ${storage.get('tdmActivityTrackingEnabled', false) ? '(Enabled)' : '(Disabled)'}</div>
                        <div class="activity-tracking-content" style="display:flex; flex-wrap:wrap; gap:12px; width:100%; padding:8px; border-radius:6px;">
                            <label style="flex:1; min-width:200px; color:#fff; font-size:12px;">
                                <input type="checkbox" id="tdm-activity-tracking-toggle" ${storage.get('tdmActivityTrackingEnabled', false)?'checked':''} /> Ranked War Faction Members Activity Tracking
                            </label>
                            <label style="flex:1; min-width:200px; color:#fff; font-size:12px;">
                                <input type="checkbox" id="tdm-activity-track-idle" ${storage.get('tdmActivityTrackWhileIdle', false)?'checked':''} /> Track while idle (higher resource usage)
                            </label>
                            <label style="display:flex; align-items:center; gap:6px; color:#fff; 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;" />s
                            </label>
                            <button id="tdm-apply-activity-cadence-btn" class="settings-btn settings-btn-green">Apply</button>
                            <div style="flex-basis:100%; font-size:12px; color:#888;">Runs on top of the Torn API cadence, diffing each pull to expose live status & travel transitions.</div>
                            <div style="flex-basis:100%; margin-top:4px; display:flex; flex-wrap:wrap; gap:12px; align-items:center; background:var(--tdm-bg-secondary); padding:8px; border-radius:6px;">
                                <label style="display:flex; align-items:center; gap:6px; color:#fff; font-size:12px;">IDB Max Size
                                    <select id="tdm-idb-maxsize-select" class="settings-input" style="width:140px;">
                                        ${(()=>{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:12px; color:#fff;">IDB Usage: —</div>
                                <button id="tdm-flush-activity-cache-btn" class="settings-btn">Flush Activity Cache</button>
                                <button id="tdm-clear-idb-btn" class="settings-btn settings-btn-red">Clear IDB Storage</button>
                            </div>
                            <label style="flex:1; min-width:160px; color:#fff; font-size:12px;">
                                <input type="checkbox" id="tdm-debug-overlay-toggle" ${storage.get('liveTrackDebugOverlayEnabled', false)?'checked':''} /> Debug Overlay
                            </label>
                        </div>
                    </div>

                <!-- Admin Settings -->
                ${state.script.canAdministerMedDeals ? `
                <div class="settings-section" data-section="Admin-Settings">
                    <div class="settings-header">Admin Settings</div>
                    <div style="display:flex; flex-direction:column; gap:8px;">
                        <button id="admin-functionality-btn" class="settings-btn ${storage.get('adminFunctionality', true) ? 'settings-btn-green' : 'settings-btn-red'}">Manage Others Dibs/Deals: ${storage.get('adminFunctionality', true) ? 'On' : 'Off'}</button>
                        ${state.script.canAdministerMedDeals && storage.get('adminFunctionality', true) === true ?  `
                        <button id="view-unauthorized-attacks-btn" class="settings-btn">View Unauthorized Attacks</button>
                        <button id="tdm-adoption-btn" class="settings-btn ${storage.get('debugAdoptionInfo', false) ? 'settings-btn-green' : ''}">TDM Adoption Info</button>
                        <div style="padding:8px; border:1px solid #444; border-radius:6px;">
                            <div style="margin-bottom:6px; color:#fca5a5; font-weight:bold;">Bulk Cleanup (current enemy faction)</div>
                            <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
                                <label style="color:#fff; font-size:12px;"><input type="checkbox" id="cleanup-notes" checked /> Notes</label>
                                <label style="color:#fff; font-size:12px;"><input type="checkbox" id="cleanup-dibs" checked /> Dibs</label>
                                <label style="color:#fff; font-size:12px;"><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">Run Cleanup</button>
                            </div>
                            <div style="font-size:12px; color:#aaa; margin-top:4px;">Cleans data for opponents visible in the current Ranked War table.</div>
                            <div id="cleanup-results-line" style="font-size:12px; color:#fff; margin-top:4px; display:none;"></div>
                        </div>
                        ` : ''}
                    </div>
                </div>
                ` : ''}

                <!-- Dev Toggles -->
                <div class="settings-section" data-section="dev-settings">
                    <div class="settings-header">Dev Toggles</div>
                    <div style="display:flex; flex-direction:column; gap:8px;">
                        <div style="display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
                            <label style="display:flex; align-items:center; gap:6px; color:#fff; font-size:12px;">
                                <div class="tdm-toggle-switch ${storage.get('debugRowLogs', false) ? 'active' : ''}" id="verbose-row-logs-toggle" data-key="debugRowLogs"></div>
                                Verbose Row Logs
                            </label>
                            <label style="display:flex; align-items:center; gap:6px; color:#fff; font-size:12px;">
                                <div class="tdm-toggle-switch ${storage.get('debugStatusWatch', false) ? 'active' : ''}" id="status-watch-toggle" data-key="debugStatusWatch"></div>
                                Status Watch Logs
                            </label>
                            <label style="display:flex; align-items:center; gap:6px; color:#fff; font-size:12px;">
                                <div class="tdm-toggle-switch ${storage.get('debugPointsParseLogs', false) ? 'active' : ''}" id="points-parse-logs-toggle" data-key="debugPointsParseLogs"></div>
                                Points Parse Logs
                            </label>
                        </div>
                        <div style="display:flex; gap:8px; align-items:center;">
                            <label style="color:#fff; font-size:12px;">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>

                <!-- Maintenance -->
                <div class="settings-section" data-section="maintenance">
                    <div class="settings-header">Maintenance</div>
                    <div style="display:flex; gap:6px; flex-wrap:wrap;">
                        <button id="reset-settings-btn" class="settings-btn settings-btn-red">Reset All Settings</button>
                    </div>
                </div>
                </div><!-- END ADVANCED TAB PANEL -->
            `;
            renderPhaseComplete = true;
            perf?.stop?.('ui.updateSettingsContent.render');
            tdmlogger('debug', 'UI: Settings content rendered');
            perf?.start?.('ui.updateSettingsContent.bind');
            bindPhaseStarted = true;

            // Tab switching handler
            try {
                const tabs = content.querySelectorAll('.tdm-settings-tab');
                const panels = content.querySelectorAll('.tdm-settings-panel');
                tabs.forEach(tab => {
                    tab.addEventListener('click', () => {
                        const targetTab = tab.dataset.tab;
                        // Update active tab
                        tabs.forEach(t => t.classList.remove('tdm-settings-tab--active'));
                        tab.classList.add('tdm-settings-tab--active');
                        // Update active panel
                        panels.forEach(p => {
                            if (p.dataset.panel === targetTab) {
                                p.classList.add('tdm-settings-panel--active');
                            } else {
                                p.classList.remove('tdm-settings-panel--active');
                            }
                        });
                        // Persist active tab
                        storage.set('tdm_settings_active_tab', targetTab);
                    });
                });
            } catch(_) {}

            // 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 tdm-toggle-container', style: { display: 'flex', gap: '8px', alignItems: 'center' } });
                            // Create toggle switch
                            const toggleSwitch = utils.createElement('div', {
                                className: `tdm-toggle-switch ${active}`,
                                dataset: { column: col.key, table: 'rankedWar' },
                                onclick: () => {
                                    const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY) || {};
                                    if (!vis.rankedWar) vis.rankedWar = {};
                                    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();
                                }
                            });
                            const label = utils.createElement('span', {
                                className: 'tdm-toggle-label',
                                textContent: col.label,
                                style: { minWidth: '70px', fontSize: '12px', color: '#fff' }
                            });
                            // Keep button reference for backward compatibility
                            const button = toggleSwitch;
                            // 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(toggleSwitch);
                            wrapper.appendChild(label);
                            if (widthInput) {
                                wrapper.appendChild(widthInput);
                                wrapper.appendChild(utils.createElement('div', { textContent: '%', style: { color: '#fff', 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 tdm-toggle-container', style: { display: 'flex', gap: '8px', alignItems: 'center' } });
                            // Create toggle switch
                            const toggleSwitch = utils.createElement('div', {
                                className: `tdm-toggle-switch ${active}`,
                                dataset: { column: col.key, table: 'membersList' },
                                onclick: () => {
                                    const vis = storage.get('columnVisibility', config.DEFAULT_COLUMN_VISIBILITY) || {};
                                    if (!vis.membersList) vis.membersList = {};
                                    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();
                                }
                            });
                            const label = utils.createElement('span', {
                                className: 'tdm-toggle-label',
                                textContent: col.label,
                                style: { minWidth: '70px', fontSize: '12px', color: '#fff' }
                            });
                            // Keep button reference for backward compatibility
                            const button = toggleSwitch;
                            // 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(toggleSwitch);
                            wrapper.appendChild(label);
                            if (widthInput) {
                                wrapper.appendChild(widthInput);
                                wrapper.appendChild(utils.createElement('div', { textContent: '%', style: { color: '#fff', 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 for Badges Dock toggle switches
            document.getElementById('chain-timer-toggle')?.addEventListener('click', function() {
                const cur = storage.get('chainTimerEnabled', true);
                storage.set('chainTimerEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureChainTimer(); else ui.removeChainTimer();
            });
            document.getElementById('inactivity-timer-toggle')?.addEventListener('click', function() {
                const cur = storage.get('inactivityTimerEnabled', false);
                storage.set('inactivityTimerEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureInactivityTimer(); else ui.removeInactivityTimer();
            });
            document.getElementById('opponent-status-toggle')?.addEventListener('click', function() {
                const cur = storage.get('opponentStatusTimerEnabled', true);
                storage.set('opponentStatusTimerEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureOpponentStatus(); else ui.removeOpponentStatus();
            });
            document.getElementById('api-usage-toggle')?.addEventListener('click', function() {
                const cur = storage.get('apiUsageCounterEnabled', false);
                storage.set('apiUsageCounterEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureApiUsageBadge();
                if (handlers?.debouncedUpdateApiUsageBadge) { handlers.debouncedUpdateApiUsageBadge(); } else { ui.updateApiUsageBadge(); }
            });
            document.getElementById('attack-mode-badge-toggle')?.addEventListener('click', function() {
                const cur = storage.get('attackModeBadgeEnabled', true);
                storage.set('attackModeBadgeEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureAttackModeBadge(); else ui.removeAttackModeBadge?.();
            });
            document.getElementById('chainwatcher-badge-toggle')?.addEventListener('click', function() {
                const cur = storage.get('chainWatcherBadgeEnabled', true);
                storage.set('chainWatcherBadgeEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureChainWatcherBadge(); else ui.removeChainWatcherBadge?.();
            });
            document.getElementById('alert-buttons-toggle')?.addEventListener('click', function() {
                const cur = storage.get('alertButtonsEnabled', true);
                storage.set('alertButtonsEnabled', !cur);
                this.classList.toggle('active', !cur);
                // Observer will respect this; force a UI refresh
                ui.updateAllPages?.();
            });
            document.getElementById('user-score-badge-toggle')?.addEventListener('click', function() {
                const cur = storage.get('userScoreBadgeEnabled', true);
                storage.set('userScoreBadgeEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureUserScoreBadge(); else ui.removeUserScoreBadge?.();
                ui.updateUserScoreBadge?.();
            });
            // Removed: refreshing shimmer toggle and related CSS injection
            document.getElementById('faction-score-badge-toggle')?.addEventListener('click', function() {
                const cur = storage.get('factionScoreBadgeEnabled', true);
                storage.set('factionScoreBadgeEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureFactionScoreBadge(); else ui.removeFactionScoreBadge?.();
                ui.updateFactionScoreBadge?.();
            });
            document.getElementById('dibs-deals-badge-toggle')?.addEventListener('click', function() {
                const cur = storage.get('dibsDealsBadgeEnabled', true);
                storage.set('dibsDealsBadgeEnabled', !cur);
                this.classList.toggle('active', !cur);
                if (!cur) ui.ensureDibsDealsBadge(); else ui.removeDibsDealsBadge?.();
                ui.updateDibsDealsBadge?.();
            });
            document.getElementById('paste-messages-toggle')?.addEventListener('click', function() {
                const cur = storage.get('pasteMessagesToChatEnabled', true);
                storage.set('pasteMessagesToChatEnabled', !cur);
                this.classList.toggle('active', !cur);
            });
            document.getElementById('oc-reminder-toggle')?.addEventListener('click', function() {
                const currentState = storage.get('ocReminderEnabled', true);
                storage.set('ocReminderEnabled', !currentState);
                this.classList.toggle('active', !currentState);
            });
            document.getElementById('verbose-row-logs-toggle')?.addEventListener('click', function() {
                const current = storage.get('debugRowLogs', false);
                storage.set('debugRowLogs', !current);
                this.classList.toggle('active', !current);
                if (!state.debug) state.debug = {};
                state.debug.rowLogs = !current;
                ui.showMessageBox(`Verbose Row Logs ${!current ? 'Enabled' : 'Disabled'}`, !current ? 'success' : 'info', 2000);
            });
            document.getElementById('status-watch-toggle')?.addEventListener('click', function() {
                const current = storage.get('debugStatusWatch', false);
                storage.set('debugStatusWatch', !current);
                this.classList.toggle('active', !current);
                if (!state.debug) state.debug = {};
                state.debug.statusWatch = !current;
                ui.showMessageBox(`Status Watch Logs ${!current ? 'Enabled' : 'Disabled'}`,'success',1500);
            });
            document.getElementById('points-parse-logs-toggle')?.addEventListener('click', function() {
                const current = storage.get('debugPointsParseLogs', false);
                storage.set('debugPointsParseLogs', !current);
                this.classList.toggle('active', !current);
                if (!state.debug) state.debug = {};
                state.debug.pointsParseLogs = !current;
                ui.showMessageBox(`Points Parse Logs ${!current ? 'Enabled' : 'Disabled'}`,'success',1500);
            });
            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: () => {
            // Ensure scope attribute is present and idempotent
            try { document.body.setAttribute('data-tdm-root','true'); } catch(_) {}
            const ensureDibsClickListener = () => {
                try {
                    if (window.__tdm_dibs_click_listener_added) return;
                    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 */ }
                    });
                    window.__tdm_dibs_click_listener_added = true;
                } catch(_) { /* noop */ }
            };
            const existingStyle = document.getElementById('dibs-general-styles');
            if (existingStyle) { ensureDibsClickListener(); return; }
            const styleTag = utils.createElement('style', {
                type: 'text/css',
                id: 'dibs-general-styles',
                textContent: `
                    /* ============================================
                       CSS VARIABLES - Design System Foundation
                       ============================================ */
                    #tdm-root, [data-tdm-root] {
                        /* Background Colors */
                        --tdm-bg-main: #666;
                        --tdm-bg-secondary: #333;
                        --tdm-bg-tertiary: #1a1a1a;
                        --tdm-bg-card: #2c2c2c;

                        /* Semantic Colors (mirrors config.CSS.colors) */
                        --tdm-color-success: ${config.CSS.colors.success};
                        --tdm-color-error: ${config.CSS.colors.error};
                        --tdm-color-warning: ${config.CSS.colors.warning};
                        --tdm-color-info: ${config.CSS.colors.info};

                        /* Dibs System Colors (from config.CSS.colors) */
                        --tdm-dibs-success: ${config.CSS.colors.dibsSuccess};
                        --tdm-dibs-success-hover: ${config.CSS.colors.dibsSuccessHover};
                        --tdm-dibs-other: ${config.CSS.colors.dibsOther};
                        --tdm-dibs-other-hover: ${config.CSS.colors.dibsOtherHover};
                        --tdm-dibs-inactive: ${config.CSS.colors.dibsInactive};
                        --tdm-dibs-inactive-hover: ${config.CSS.colors.dibsInactiveHover};

                        /* Note Button Colors */
                        --tdm-note-inactive: ${config.CSS.colors.noteInactive};
                        --tdm-note-inactive-hover: ${config.CSS.colors.noteInactiveHover};
                        --tdm-note-active: ${config.CSS.colors.noteActive};
                        --tdm-note-active-hover: ${config.CSS.colors.noteActiveHover};
                        /* Text colors for note buttons (use for strong contrast when active) */
                        --tdm-note-inactive-text: ${config.CSS.colors.noteInactiveText || '#ffffff'};
                        --tdm-note-active-text: ${config.CSS.colors.noteActiveText || '#ffffff'};

                        /* Med Deal Colors */
                        --tdm-med-inactive: ${config.CSS.colors.medDealInactive};
                        --tdm-med-inactive-hover: ${config.CSS.colors.medDealInactiveHover};
                        --tdm-med-set: ${config.CSS.colors.medDealSet};
                        --tdm-med-set-hover: ${config.CSS.colors.medDealSetHover};
                        --tdm-med-mine: ${config.CSS.colors.medDealMine};
                        --tdm-med-mine-hover: ${config.CSS.colors.medDealMineHover};

                        /* Assist Button Colors */
                        --tdm-assist: ${config.CSS.colors.assistButton};
                        --tdm-assist-hover: ${config.CSS.colors.assistButtonHover};

                        /* Modal Colors */
                        --tdm-modal-bg: ${config.CSS.colors.modalBg};
                        --tdm-modal-border: ${config.CSS.colors.modalBorder};
                        --tdm-modal-header: ${config.CSS.colors.mainColor};

                        /* Text Colors */
                        --tdm-text-primary: #ffffff;
                        --tdm-text-secondary: #9ca3af;
                        --tdm-text-muted: #6b7280;
                        --tdm-text-accent: #93c5fd;

                        /* Spacing System (4px base grid) */
                        --tdm-space-xs: 2px;
                        --tdm-space-sm: 4px;
                        --tdm-space-md: 8px;
                        --tdm-space-lg: 12px;
                        --tdm-space-xl: 16px;
                        --tdm-space-2xl: 24px;

                        /* Typography */
                        --tdm-font-size-xs: 10px;
                        --tdm-font-size-sm: 11px;
                        --tdm-font-size-base: 13px;
                        --tdm-font-size-lg: 16px;
                        --tdm-font-size-xl: 18px;

                        /* Border Radius */
                        --tdm-radius-sm: 3px;
                        --tdm-radius-md: 4px;
                        --tdm-radius-lg: 8px;
                        --tdm-radius-xl: 14px;
                        --tdm-radius-full: 9999px;

                        /* Z-Index Scale */
                        --tdm-z-dropdown: 1000;
                        --tdm-z-modal: 10000;
                        --tdm-z-modal-backdrop: 9999;
                        --tdm-z-tooltip: 10010;
                        --tdm-z-toast: 10020;

                        /* Transitions */
                        --tdm-transition-fast: 0.15s ease;
                        --tdm-transition-normal: 0.25s ease;
                        --tdm-transition-slow: 0.35s ease;

                        /* Shadows */
                        --tdm-shadow-sm: 0 1px 2px rgba(0,0,0,0.3);
                        --tdm-shadow-md: 0 4px 8px rgba(0,0,0,0.4);
                        --tdm-shadow-lg: 0 8px 16px rgba(0,0,0,0.5);
                        --tdm-shadow-modal: 0 22px 46px -12px rgba(15,23,42,0.75);

                        /* Button Heights */
                        --tdm-btn-height-sm: 20px;
                        --tdm-btn-height-md: 24px;
                        --tdm-btn-height-lg: 32px;
                    }

                    /* ============================================
                       BEM BUTTON SYSTEM - Base & Modifiers
                       ============================================ */

                    /* Base Button */
                    .tdm-btn {
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                        padding: var(--tdm-space-sm) var(--tdm-space-md);
                        font-size: var(--tdm-font-size-sm);
                        font-weight: 500;
                        line-height: 1.2;
                        color: var(--tdm-text-primary);
                        background-color: var(--tdm-bg-card);
                        border: 1px solid var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-sm);
                        cursor: pointer;
                        white-space: nowrap;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        transition: background-color var(--tdm-transition-fast),
                                    border-color var(--tdm-transition-fast),
                                    transform var(--tdm-transition-fast);
                        box-sizing: border-box;
                        user-select: none;
                    }
                    .tdm-btn:hover {
                        background-color: var(--tdm-bg-secondary);
                        border-color: var(--tdm-bg-main);
                    }
                    .tdm-btn:active {
                        transform: scale(0.98);
                    }
                    .tdm-btn:disabled, .tdm-btn.disabled {
                        opacity: 0.6;
                        cursor: not-allowed;
                        pointer-events: none;
                    }

                    /* Size Modifiers */
                    .tdm-btn--sm {
                        height: var(--tdm-btn-height-sm);
                        padding: var(--tdm-space-xs) var(--tdm-space-sm);
                        font-size: var(--tdm-font-size-xs);
                    }
                    .tdm-btn--md {
                        height: var(--tdm-btn-height-md);
                        padding: var(--tdm-space-sm) var(--tdm-space-md);
                        font-size: var(--tdm-font-size-sm);
                    }
                    .tdm-btn--lg {
                        height: var(--tdm-btn-height-lg);
                        padding: var(--tdm-space-md) var(--tdm-space-lg);
                        font-size: var(--tdm-font-size-base);
                    }

                    /* Color Modifiers */
                    .tdm-btn--primary {
                        background-color: var(--tdm-color-info);
                        border-color: var(--tdm-color-info);
                    }
                    .tdm-btn--primary:hover {
                        background-color: #1976D2;
                        border-color: #1976D2;
                    }

                    .tdm-btn--success {
                        background-color: var(--tdm-color-success);
                        border-color: var(--tdm-color-success);
                    }
                    .tdm-btn--success:hover {
                        background-color: #45a049;
                        border-color: #45a049;
                    }

                    .tdm-btn--danger {
                        background-color: var(--tdm-color-error);
                        border-color: var(--tdm-color-error);
                    }
                    .tdm-btn--danger:hover {
                        background-color: #e53935;
                        border-color: #e53935;
                    }

                    .tdm-btn--warning {
                        background-color: var(--tdm-color-warning);
                        border-color: var(--tdm-color-warning);
                        color: #000;
                    }
                    .tdm-btn--warning:hover {
                        background-color: #f57c00;
                        border-color: #f57c00;
                    }

                    /* State Modifiers for Dibs System */
                    .tdm-btn--dibs-inactive {
                        background-color: var(--tdm-dibs-inactive);
                        border-color: var(--tdm-dibs-inactive);
                    }
                    .tdm-btn--dibs-inactive:hover {
                        background-color: var(--tdm-dibs-inactive-hover);
                        border-color: var(--tdm-dibs-inactive-hover);
                    }

                    .tdm-btn--dibs-yours {
                        background-color: var(--tdm-dibs-success);
                        border-color: var(--tdm-dibs-success);
                    }
                    .tdm-btn--dibs-yours:hover {
                        background-color: var(--tdm-dibs-success-hover);
                        border-color: var(--tdm-dibs-success-hover);
                    }

                    .tdm-btn--dibs-other {
                        background-color: var(--tdm-dibs-other);
                        border-color: var(--tdm-dibs-other);
                    }
                    .tdm-btn--dibs-other:hover {
                        background-color: var(--tdm-dibs-other-hover);
                        border-color: var(--tdm-dibs-other-hover);
                    }

                    /* State Modifiers for Notes */
                    .tdm-btn--note-inactive {
                        background-color: var(--tdm-note-inactive);
                        border-color: var(--tdm-note-inactive);
                    }
                    .tdm-btn--note-inactive:hover {
                        background-color: var(--tdm-note-inactive-hover);
                        border-color: var(--tdm-note-inactive-hover);
                    }

                    .tdm-btn--note-active {
                        background-color: var(--tdm-note-active);
                        border-color: var(--tdm-note-active);
                    }
                    .tdm-btn--note-active:hover {
                        background-color: var(--tdm-note-active-hover);
                        border-color: var(--tdm-note-active-hover);
                    }

                    /* State Modifiers for Med Deals */
                    .tdm-btn--med-inactive {
                        background-color: var(--tdm-med-inactive);
                        border-color: var(--tdm-med-inactive);
                    }
                    .tdm-btn--med-inactive:hover {
                        background-color: var(--tdm-med-inactive-hover);
                        border-color: var(--tdm-med-inactive-hover);
                    }

                    .tdm-btn--med-set {
                        background-color: var(--tdm-med-set);
                        border-color: var(--tdm-med-set);
                    }
                    .tdm-btn--med-set:hover {
                        background-color: var(--tdm-med-set-hover);
                        border-color: var(--tdm-med-set-hover);
                    }

                    .tdm-btn--med-mine {
                        background-color: var(--tdm-med-mine);
                        border-color: var(--tdm-med-mine);
                    }
                    .tdm-btn--med-mine:hover {
                        background-color: var(--tdm-med-mine-hover);
                        border-color: var(--tdm-med-mine-hover);
                    }

                    /* Assist Button */
                    .tdm-btn--assist {
                        background-color: var(--tdm-assist);
                        border-color: var(--tdm-assist);
                    }
                    .tdm-btn--assist:hover {
                        background-color: var(--tdm-assist-hover);
                        border-color: var(--tdm-assist-hover);
                    }

                    /* Ghost/Outline Variant */
                    .tdm-btn--ghost {
                        background-color: transparent;
                        border-color: var(--tdm-bg-secondary);
                        color: var(--tdm-text-secondary);
                    }
                    .tdm-btn--ghost:hover {
                        background-color: var(--tdm-bg-secondary);
                        color: var(--tdm-text-primary);
                    }

                    /* Full Width Modifier */
                    .tdm-btn--full {
                        width: 100%;
                    }

                    /* Icon Button (square) */
                    .tdm-btn--icon {
                        padding: var(--tdm-space-sm);
                        min-width: var(--tdm-btn-height-md);
                    }

                    /* ============================================
                       LEGACY .btn CLASS COMPATIBILITY
                       Maps old class names to new CSS variable system
                       ============================================ */

                    /* Base .btn inherits modern button styles */
                    .dibs-notes-subrow .btn,
                    .tdm-dibs-deals-container .btn,
                    .tdm-notes-container .btn,
                    .dibs-cell .btn,
                    .notes-cell .btn,
                    button.dibs-btn,
                    button.med-deal-btn,
                    button.retal-btn,
                    button.req-assist-button {
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                        font-weight: 500;
                        line-height: 1.2;
                        color: var(--tdm-text-primary);
                        border-radius: var(--tdm-radius-sm);
                        cursor: pointer;
                        white-space: nowrap;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        transition: background-color var(--tdm-transition-fast),
                                    border-color var(--tdm-transition-fast),
                                    transform var(--tdm-transition-fast);
                        box-sizing: border-box;
                        user-select: none;
                    }
                    .dibs-notes-subrow .btn:active,
                    button.dibs-btn:active,
                    button.med-deal-btn:active,
                    button.retal-btn:active {
                        transform: scale(0.98);
                    }

                    /* Dibs Button States - Map old classes to new variables */
                    .btn-dibs-inactive,
                    .btn.btn-dibs-inactive {
                        background-color: var(--tdm-dibs-inactive) !important;
                        border-color: var(--tdm-dibs-inactive) !important;
                        color: var(--tdm-text-primary) !important;
                    }
                    .btn-dibs-inactive:hover,
                    .btn.btn-dibs-inactive:hover {
                        background-color: var(--tdm-dibs-inactive-hover) !important;
                        border-color: var(--tdm-dibs-inactive-hover) !important;
                    }

                    .btn-dibs-success-you,
                    .btn.btn-dibs-success-you {
                        background-color: var(--tdm-dibs-success) !important;
                        border-color: var(--tdm-dibs-success) !important;
                        color: var(--tdm-text-primary) !important;
                    }
                    .btn-dibs-success-you:hover,
                    .btn.btn-dibs-success-you:hover {
                        background-color: var(--tdm-dibs-success-hover) !important;
                        border-color: var(--tdm-dibs-success-hover) !important;
                    }

                    .btn-dibs-success-other,
                    .btn.btn-dibs-success-other {
                        background-color: var(--tdm-dibs-other) !important;
                        border-color: var(--tdm-dibs-other) !important;
                        color: var(--tdm-text-primary) !important;
                    }
                    .btn-dibs-success-other:hover,
                    .btn.btn-dibs-success-other:hover {
                        background-color: var(--tdm-dibs-other-hover) !important;
                        border-color: var(--tdm-dibs-other-hover) !important;
                    }

                    .btn-dibs-disabled,
                    .btn.btn-dibs-disabled {
                        opacity: 0.6;
                        cursor: not-allowed;
                        pointer-events: none;
                    }

                    /* Med Deal Button States */
                    .btn-med-deal-default,
                    .btn.btn-med-deal-default,
                    .med-deal-btn.btn-med-deal-default {
                        background-color: var(--tdm-med-inactive) !important;
                        border-color: var(--tdm-med-inactive) !important;
                        color: var(--tdm-text-primary) !important;
                    }
                    .btn-med-deal-default:hover,
                    .btn.btn-med-deal-default:hover {
                        background-color: var(--tdm-med-inactive-hover) !important;
                        border-color: var(--tdm-med-inactive-hover) !important;
                    }

                    .btn-med-deal-set,
                    .btn.btn-med-deal-set,
                    .med-deal-btn.btn-med-deal-set {
                        background-color: var(--tdm-med-set) !important;
                        border-color: var(--tdm-med-set) !important;
                        color: var(--tdm-text-primary) !important;
                    }
                    .btn-med-deal-set:hover,
                    .btn.btn-med-deal-set:hover {
                        background-color: var(--tdm-med-set-hover) !important;
                        border-color: var(--tdm-med-set-hover) !important;
                    }

                    .btn-med-deal-mine,
                    .btn.btn-med-deal-mine,
                    .med-deal-btn.btn-med-deal-mine {
                        background-color: var(--tdm-med-mine) !important;
                        border-color: var(--tdm-med-mine) !important;
                        color: #000 !important;
                    }
                    .btn-med-deal-mine:hover,
                    .btn.btn-med-deal-mine:hover {
                        background-color: var(--tdm-med-mine-hover) !important;
                        border-color: var(--tdm-med-mine-hover) !important;
                    }

                    /* Retal Button States */
                    .btn-retal-inactive,
                    .btn.btn-retal-inactive,
                    .retal-btn.btn-retal-inactive {
                        background-color: var(--tdm-bg-card) !important;
                        border-color: var(--tdm-bg-secondary) !important;
                        color: var(--tdm-text-secondary) !important;
                    }
                    .btn-retal-active,
                    .btn.btn-retal-active,
                    .retal-btn.btn-retal-active {
                        background-color: var(--tdm-color-warning) !important;
                        border-color: var(--tdm-color-warning) !important;
                        color: #000 !important;
                    }
                    .btn-retal-active:hover,
                    .btn.btn-retal-active:hover {
                        background-color: #f57c00 !important;
                        border-color: #f57c00 !important;
                    }

                    /* Assist Request Button */
                    .req-assist-button,
                    .btn.req-assist-button {
                        background-color: var(--tdm-assist) !important;
                        border-color: var(--tdm-assist) !important;
                        color: var(--tdm-text-primary) !important;
                    }
                    .req-assist-button:hover,
                    .btn.req-assist-button:hover {
                        background-color: var(--tdm-assist-hover) !important;
                        border-color: var(--tdm-assist-hover) !important;
                    }

                    /* Note Button States */
                    .inactive-note-button,
                    .btn.inactive-note-button {
                        background-color: transparent !important;
                        border-color: var(--tdm-note-inactive) !important;
                    }
                    .inactive-note-button:hover,
                    .btn.inactive-note-button:hover {
                        border-color: var(--tdm-note-inactive-hover) !important;
                    }
                    .active-note-button:hover,
                    .btn.active-note-button:hover {
                        border-color: var(--tdm-note-active-hover) !important;
                    }

                    /* Settings Panel Buttons */
                    .tdm-note-btn {
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                        padding: var(--tdm-space-sm) var(--tdm-space-md);
                        font-size: var(--tdm-font-size-sm);
                        font-weight: 500;
                        color: var(--tdm-text-primary);
                        background-color: var(--tdm-bg-card);
                        border: 1px solid var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-sm);
                        cursor: pointer;
                        transition: background-color var(--tdm-transition-fast);
                    }
                    .tdm-note-btn:hover {
                        background-color: var(--tdm-bg-secondary);
                    }
                    .tdm-note-save {
                        background-color: var(--tdm-color-success) !important;
                        border-color: var(--tdm-color-success) !important;
                    }
                    .tdm-note-save:hover {
                        background-color: #45a049 !important;
                    }
                    .tdm-note-cancel {
                        background-color: var(--tdm-bg-secondary) !important;
                        border-color: var(--tdm-bg-secondary) !important;
                    }
                    .tdm-note-cancel:hover {
                        background-color: var(--tdm-bg-main) !important;
                    }

                    /* ============================================
                       TABLE LAYOUT - Column alignment fixes
                       ============================================ */

                    /* Ensure header and body columns align */
                    .f-war-list .table-header,
                    .f-war-list .table-body > li {
                        display: flex;
                        align-items: stretch;
                        width: 100%;
                        box-sizing: border-box;
                    }

                    /* Table cell base styling */
                    .f-war-list .table-cell,
                    .f-war-list .table-header > li {
                        display: flex;
                        align-items: center;
                        justify-content: center;
                        padding: var(--tdm-space-xs) var(--tdm-space-sm);
                        box-sizing: border-box;
                        min-height: 28px;
                    }

                    /* Consistent borders for visual column separation */
                    .f-war-list .table-cell:not(:last-child),
                    .f-war-list .table-header > li:not(:last-child) {
                        border-right: 1px solid rgba(255, 255, 255, 0.05);
                    }

                    /* Header styling */
                    .f-war-list .table-header {
                        background: var(--tdm-bg-secondary);
                        border-bottom: 1px solid rgba(255, 255, 255, 0.1);
                    }

                    /* Body row alternate styling */
                    .f-war-list .table-body > li:nth-child(even) {
                        background: rgba(255, 255, 255, 0.02);
                    }

                    /* Flex grow for variable width columns */
                    .f-war-list .table-cell.flex-grow,
                    .f-war-list .table-header > li.flex-grow {
                        flex: 1 1 auto;
                    }

                    /* Fixed width columns */
                    .f-war-list .table-cell.flex-fixed,
                    .f-war-list .table-header > li.flex-fixed {
                        flex: 0 0 auto;
                    }

                    /* TDM column containers alignment */
                    .tdm-dibs-deals-container,
                    .tdm-notes-container {
                        display: flex;
                        flex-direction: row;
                        align-items: stretch;
                        gap: var(--tdm-space-xs);
                    }

                    /* ============================================
                       MODAL SYSTEM - Unified modals with sizes
                       ============================================ */

                    /* Modal Backdrop */
                    .tdm-modal-backdrop {
                        position: fixed;
                        top: 0;
                        left: 0;
                        right: 0;
                        bottom: 0;
                        background: rgba(0, 0, 0, 0.6);
                        backdrop-filter: blur(4px);
                        z-index: var(--tdm-z-modal-backdrop);
                        opacity: 0;
                        pointer-events: none;
                        transition: opacity var(--tdm-transition-normal);
                    }
                    .tdm-modal-backdrop.visible {
                        opacity: 1;
                        pointer-events: auto;
                    }

                    /* Base Modal */
                    .tdm-modal {
                        position: fixed;
                        top: 50%;
                        left: 50%;
                        transform: translate(-50%, -50%) scale(0.95);
                        background: var(--tdm-modal-bg);
                        border: 1px solid var(--tdm-modal-border);
                        border-radius: var(--tdm-radius-lg);
                        z-index: var(--tdm-z-modal);
                        color: var(--tdm-text-primary);
                        box-shadow: var(--tdm-shadow-modal);
                        max-height: 85vh;
                        overflow: hidden;
                        display: none;
                        flex-direction: column;
                        opacity: 0;
                        pointer-events: none;
                        transition: opacity var(--tdm-transition-normal), transform var(--tdm-transition-normal);
                    }
                    .tdm-modal.visible {
                        display: flex;
                        opacity: 1;
                        pointer-events: auto;
                        transform: translate(-50%, -50%) scale(1);
                    }

                    /* Size Variants */
                    .tdm-modal--sm {
                        width: 400px;
                        max-width: 90vw;
                    }
                    .tdm-modal--md {
                        width: 600px;
                        max-width: 90vw;
                    }
                    .tdm-modal--lg {
                        width: 800px;
                        max-width: 95vw;
                    }
                    .tdm-modal--xl {
                        width: 1000px;
                        max-width: 95vw;
                    }
                    .tdm-modal--full {
                        width: 95vw;
                        height: 90vh;
                    }

                    /* Modal Header */
                    .tdm-modal-header {
                        display: flex;
                        align-items: center;
                        justify-content: space-between;
                        padding: var(--tdm-space-md) var(--tdm-space-lg);
                        background: var(--tdm-modal-header);
                        border-bottom: 1px solid var(--tdm-modal-border);
                        flex-shrink: 0;
                    }
                    .tdm-modal-title {
                        font-size: var(--tdm-font-size-lg);
                        font-weight: 600;
                        margin: 0;
                        color: var(--tdm-text-primary);
                    }
                    .tdm-modal-close {
                        background: transparent;
                        border: none;
                        color: var(--tdm-text-secondary);
                        font-size: var(--tdm-font-size-xl);
                        cursor: pointer;
                        padding: var(--tdm-space-xs);
                        line-height: 1;
                        transition: color var(--tdm-transition-fast);
                    }
                    .tdm-modal-close:hover {
                        color: var(--tdm-color-error);
                    }

                    /* Modal Body */
                    .tdm-modal-body {
                        padding: var(--tdm-space-lg);
                        overflow-y: auto;
                        flex: 1;
                    }

                    /* Modal Footer */
                    .tdm-modal-footer {
                        display: flex;
                        align-items: center;
                        justify-content: flex-end;
                        gap: var(--tdm-space-md);
                        padding: var(--tdm-space-md) var(--tdm-space-lg);
                        background: var(--tdm-bg-tertiary);
                        border-top: 1px solid var(--tdm-modal-border);
                        flex-shrink: 0;
                    }

                    /* ============================================
                       TIMER COMPONENT - Countdown displays
                       ============================================ */

                    .tdm-timer {
                        display: inline-flex;
                        align-items: center;
                        gap: var(--tdm-space-xs);
                        font-family: 'Monaco', 'Consolas', monospace;
                        font-size: var(--tdm-font-size-sm);
                        color: var(--tdm-text-primary);
                        padding: var(--tdm-space-xs) var(--tdm-space-sm);
                        background: var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-sm);
                        white-space: nowrap;
                    }
                    .tdm-timer--urgent {
                        color: var(--tdm-color-error);
                        animation: tdm-pulse 1s ease-in-out infinite;
                    }
                    .tdm-timer--warning {
                        color: var(--tdm-color-warning);
                    }
                    .tdm-timer--success {
                        color: var(--tdm-color-success);
                    }
                    .tdm-timer-icon {
                        font-size: var(--tdm-font-size-xs);
                        opacity: 0.8;
                    }

                    /* ============================================
                       BADGE COMPONENT - Status indicators
                       ============================================ */

                    .tdm-badge {
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                        padding: var(--tdm-space-xs) var(--tdm-space-sm);
                        font-size: var(--tdm-font-size-xs);
                        font-weight: 600;
                        line-height: 1;
                        color: var(--tdm-text-primary);
                        background: var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-full);
                        white-space: nowrap;
                        text-transform: uppercase;
                        letter-spacing: 0.5px;
                    }

                    /* Badge Color Variants */
                    .tdm-badge--success {
                        background: var(--tdm-color-success);
                        color: #fff;
                    }
                    .tdm-badge--error, .tdm-badge--danger {
                        background: var(--tdm-color-error);
                        color: #fff;
                    }
                    .tdm-badge--warning {
                        background: var(--tdm-color-warning);
                        color: #000;
                    }
                    .tdm-badge--info {
                        background: var(--tdm-color-info);
                        color: #fff;
                    }
                    .tdm-badge--neutral {
                        background: var(--tdm-text-muted);
                        color: #fff;
                    }

                    /* Badge Size Variants */
                    .tdm-badge--sm {
                        padding: 2px var(--tdm-space-xs);
                        font-size: 9px;
                    }
                    .tdm-badge--lg {
                        padding: var(--tdm-space-sm) var(--tdm-space-md);
                        font-size: var(--tdm-font-size-sm);
                    }

                    /* Badge with dot indicator */
                    .tdm-badge--dot {
                        width: 8px;
                        height: 8px;
                        padding: 0;
                        border-radius: 50%;
                    }
                    .tdm-badge--dot.tdm-badge--lg {
                        width: 12px;
                        height: 12px;
                    }

                    /* Online/Status Badge Dock */
                    .tdm-badge-dock {
                        display: flex;
                        align-items: center;
                        gap: var(--tdm-space-sm);
                        flex-wrap: wrap;
                    }

                    /* ============================================
                       ANIMATIONS & TRANSITIONS
                       ============================================ */

                    /* Keyframe Animations */
                    @keyframes tdm-pulse {
                        0%, 100% { opacity: 1; }
                        50% { opacity: 0.6; }
                    }
                    @keyframes tdm-fade-in {
                        from { opacity: 0; }
                        to { opacity: 1; }
                    }
                    @keyframes tdm-fade-out {
                        from { opacity: 1; }
                        to { opacity: 0; }
                    }
                    @keyframes tdm-slide-up {
                        from { transform: translateY(10px); opacity: 0; }
                        to { transform: translateY(0); opacity: 1; }
                    }
                    @keyframes tdm-slide-down {
                        from { transform: translateY(-10px); opacity: 0; }
                        to { transform: translateY(0); opacity: 1; }
                    }
                    @keyframes tdm-scale-in {
                        from { transform: scale(0.95); opacity: 0; }
                        to { transform: scale(1); opacity: 1; }
                    }
                    @keyframes tdm-shimmer {
                        0% { background-position: -200% 0; }
                        100% { background-position: 200% 0; }
                    }

                    /* Animation Utility Classes */
                    .tdm-fade-in {
                        animation: tdm-fade-in var(--tdm-transition-normal) forwards;
                    }
                    .tdm-fade-out {
                        animation: tdm-fade-out var(--tdm-transition-normal) forwards;
                    }
                    .tdm-slide-up {
                        animation: tdm-slide-up var(--tdm-transition-normal) forwards;
                    }
                    .tdm-slide-down {
                        animation: tdm-slide-down var(--tdm-transition-normal) forwards;
                    }
                    .tdm-scale-in {
                        animation: tdm-scale-in var(--tdm-transition-normal) forwards;
                    }

                    /* Loading Skeleton */
                    .tdm-skeleton {
                        background: linear-gradient(
                            90deg,
                            var(--tdm-bg-secondary) 25%,
                            var(--tdm-bg-main) 50%,
                            var(--tdm-bg-secondary) 75%
                        );
                        background-size: 200% 100%;
                        animation: tdm-shimmer 1.5s ease-in-out infinite;
                        border-radius: var(--tdm-radius-sm);
                    }
                    .tdm-skeleton--text {
                        height: 14px;
                        width: 100%;
                        margin-bottom: var(--tdm-space-xs);
                    }
                    .tdm-skeleton--circle {
                        border-radius: 50%;
                    }
                    .tdm-skeleton--button {
                        height: var(--tdm-btn-height-md);
                        width: 80px;
                    }

                    /* Loading Spinner */
                    .tdm-spinner {
                        display: inline-block;
                        width: 16px;
                        height: 16px;
                        border: 2px solid var(--tdm-bg-main);
                        border-top-color: var(--tdm-color-info);
                        border-radius: 50%;
                        animation: spin 0.8s linear infinite;
                    }
                    .tdm-spinner--sm {
                        width: 12px;
                        height: 12px;
                        border-width: 1.5px;
                    }
                    .tdm-spinner--lg {
                        width: 24px;
                        height: 24px;
                        border-width: 3px;
                    }

                    /* Transition Utilities */
                    .tdm-transition-all {
                        transition: all var(--tdm-transition-normal);
                    }
                    .tdm-transition-colors {
                        transition: color var(--tdm-transition-fast),
                                    background-color var(--tdm-transition-fast),
                                    border-color var(--tdm-transition-fast);
                    }
                    .tdm-transition-transform {
                        transition: transform var(--tdm-transition-fast);
                    }
                    .tdm-transition-opacity {
                        transition: opacity var(--tdm-transition-fast);
                    }

                    /* ============================================
                       RESPONSIVE DESIGN - Mobile/PDA support
                       ============================================ */

                    /* Responsive Visibility Utilities */
                    .tdm-hide-mobile { display: block; }
                    .tdm-show-mobile { display: none; }
                    .tdm-hide-tablet { display: block; }
                    .tdm-show-tablet { display: none; }

                    /* Tablet Breakpoint (768px) */
                    @media (max-width: 768px) {
                        .tdm-hide-tablet { display: none !important; }
                        .tdm-show-tablet { display: block !important; }

                        /* Larger touch targets */
                        .tdm-btn {
                            min-height: 36px;
                            padding: var(--tdm-space-sm) var(--tdm-space-md);
                        }

                        /* Settings tabs stack */
                        .tdm-settings-tabs {
                            flex-wrap: wrap;
                        }
                        .tdm-settings-tab {
                            flex: 1 1 auto;
                            min-width: 80px;
                        }

                        /* Modal adjustments */
                        .tdm-modal--lg,
                        .tdm-modal--xl {
                            width: 95vw;
                        }

                        /* Toggle switch larger for touch */
                        .tdm-toggle-switch {
                            width: 50px;
                            height: 26px;
                        }
                        .tdm-toggle-switch::after {
                            width: 20px;
                            height: 20px;
                        }
                        .tdm-toggle-switch.active::after {
                            transform: translateX(24px);
                        }
                    }

                    /* Mobile Breakpoint (480px) */
                    @media (max-width: 480px) {
                        .tdm-hide-mobile { display: none !important; }
                        .tdm-show-mobile { display: block !important; }

                        /* Minimum touch target 44px (Apple HIG) */
                        .tdm-btn {
                            min-height: 44px;
                            font-size: var(--tdm-font-size-base);
                        }

                        /* Full width buttons on mobile */
                        .tdm-btn--mobile-full {
                            width: 100%;
                        }

                        /* Stack settings content */
                        .tdm-settings-panel {
                            padding: 4px;
                        }
                        .settings-section {
                            padding: 6px !important;
                        }
                        .settings-header {
                            font-size: 12px !important;
                            padding: 4px 6px !important;
                        }

                        /* Modal full screen on mobile */
                        .tdm-modal {
                            width: 100vw !important;
                            max-width: 100vw !important;
                            height: 100vh !important;
                            max-height: 100vh !important;
                            border-radius: 0;
                        }
                        .tdm-modal-body {
                            padding: var(--tdm-space-md);
                        }

                        /* Column grid single column */
                        .tdm-column-grid {
                            grid-template-columns: 1fr;
                            gap: var(--tdm-space-md);
                        }

                        /* Column visibility - 2 columns on mobile, compact */
                        #column-visibility-rw,
                        #column-visibility-ml {
                            grid-template-columns: repeat(2, 1fr) !important;
                            gap: 6px 8px !important;
                        }

                        /* Compact column control on mobile - aligned inputs */
                        .column-control.tdm-toggle-container {
                            display: flex !important;
                            flex-wrap: wrap !important;
                            align-items: center !important;
                            gap: 4px !important;
                            width: 100% !important;
                            height: auto !important;
                        }
                        .column-control .tdm-toggle-label {
                            flex: 1 !important;
                            min-width: unset !important;
                            font-size: 11px !important;
                            line-height: 24px !important;
                        }
                        .column-control .column-width-input {
                            width: 36px !important;
                            min-width: 36px !important;
                            height: 20px !important;
                            padding: 0 2px !important;
                            font-size: 11px !important;
                            line-height: 20px !important;
                            text-align: center !important;
                        }
                        .column-control > div:last-child {
                            font-size: 11px !important;
                            line-height: 24px !important;
                        }

                        /* Badge adjustments */
                        .tdm-badge {
                            padding: var(--tdm-space-sm) var(--tdm-space-md);
                            font-size: var(--tdm-font-size-sm);
                        }
                    }

                    /* TornPDA specific adjustments */
                    @media (max-width: 400px) {
                        .tdm-settings-tabs {
                            font-size: var(--tdm-font-size-xs);
                        }
                        .tdm-settings-tab {
                            padding: var(--tdm-space-sm) var(--tdm-space-md);
                        }
                        /* Single column for very narrow screens */
                        #column-visibility-rw,
                        #column-visibility-ml {
                            grid-template-columns: 1fr !important;
                        }
                    }

                    /* ============================================
                       CONSOLIDATED STYLES - From scattered blocks
                       ============================================ */

                    /* Mini Settings Grid (from tdm-mini-style) */
                    .tdm-grid-war {
                        display: grid;
                        grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
                        gap: var(--tdm-space-md);
                        align-items: end;
                    }
                    .tdm-mini-lbl {
                        display: block;
                        font-size: var(--tdm-font-size-sm);
                        color: var(--tdm-text-secondary);
                        margin-bottom: var(--tdm-space-xs);
                        font-weight: 500;
                    }
                    .tdm-mini-checkbox {
                        font-size: var(--tdm-font-size-base);
                        color: var(--tdm-text-secondary);
                        display: flex;
                        align-items: center;
                        gap: var(--tdm-space-sm);
                    }
                    .tdm-check-grid {
                        display: grid;
                        grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
                        gap: var(--tdm-space-sm);
                        align-items: center;
                    }
                    .tdm-term-required {
                        border-color: var(--tdm-color-info) !important;
                        outline: 1px solid var(--tdm-color-info);
                    }
                    .tdm-term-calculated {
                        border-color: var(--tdm-color-success) !important;
                    }
                    .tdm-term-error {
                        border-color: var(--tdm-color-error) !important;
                        outline: 1px solid var(--tdm-color-error);
                    }
                    .tdm-initial-readonly {
                        background: var(--tdm-bg-secondary) !important;
                        color: var(--tdm-text-muted) !important;
                    }

                    /* API Key Card States (from tdm-api-key-style) */
                    #tdm-api-key-card {
                        transition: border-color var(--tdm-transition-fast), box-shadow var(--tdm-transition-fast);
                    }
                    #tdm-api-key-card[data-tone="success"] {
                        border-color: var(--tdm-color-success);
                    }
                    #tdm-api-key-card[data-tone="warning"] {
                        border-color: var(--tdm-color-warning);
                    }
                    #tdm-api-key-card[data-tone="info"] {
                        border-color: var(--tdm-color-info);
                    }
                    #tdm-api-key-card[data-tone="error"] {
                        border-color: var(--tdm-color-error);
                    }
                    .tdm-api-key-highlight {
                        box-shadow: 0 0 12px 2px rgba(59, 130, 246, 0.5);
                    }

                    /* War Attack Faction Colors (from tdm-war-attack-color-css) */
                    .tdm-war-our {
                        color: var(--tdm-color-success) !important;
                        font-weight: 600;
                    }
                    .tdm-war-opp {
                        color: var(--tdm-color-error) !important;
                        font-weight: 600;
                    }
                    .tdm-war-our.t-blue,
                    .tdm-war-opp.t-blue {
                        color: inherit;
                    }

                    /* 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:#bc7100fa;transform:scale(1.05);}
                    .tdm-fav-heart--active{color:#f00404fa;}
                    .tdm-favorite-row{background:rgba(250, 204, 21, 0.05)!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;border-bottom:1px solid var(--tdm-modal-bg);}
                    /* Note button visuals for ranked-war subrow: match dibs/med-deal sizing and colors via CSS vars.
                       Note button width intentionally doubled relative to other subrow buttons. */
                    .dibs-notes-subrow .note-button.btn{
                        background: transparent;
                        border: 1px solid var(--tdm-note-inactive) !important;
                        padding: 1px 1px !important;
                        min-width: 40px !important;
                        max-width: 40px !important;
                        max-height: 30px !important;
                        font-size: 0.75em !important;
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                        border-radius: 3px;
                        box-sizing: border-box;
                        color: var(--tdm-text-primary) !important;
                    }
                    .dibs-notes-subrow .note-button.btn.inactive-note-button:hover{ border-color: var(--tdm-note-inactive-hover) !important; background-color: var(--tdm-note-inactive-hover); color: var(--tdm-note-inactive-text) !important; }
                    .dibs-notes-subrow .note-button.btn.active-note-button{ border-color: var(--tdm-note-active) !important; background-color: var(--tdm-note-active); color: var(--tdm-note-active-text) !important; font-weight:600; }
                    .dibs-notes-subrow .note-button.btn.active-note-button:hover{ border-color: var(--tdm-note-active-hover) !important; background-color: var(--tdm-note-active-hover); color: var(--tdm-note-active-text) !important; }
                    /* Icon sizing uses currentColor so it follows border/text coloring */
                    .dibs-notes-subrow .note-button.btn svg{ width: 18px; height: 18px; stroke: currentColor; background: none; padding: 0; border-radius: 0; }
                    /* Keep compact class for logic but let CSS control sizing */
                    .dibs-notes-subrow .note-button.btn.tdm-note-compact{ min-width: 40px !important; max-width: 40px !important; height: auto !important; }
                    /* Preview state applied by JS when compact and note exists; CSS handles truncation */
                    .dibs-notes-subrow .note-button.btn.tdm-note-preview{ white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; max-width: 140px !important; display: inline-block !important; text-align: left !important; padding: 2px 6px !important; }
                    /* Expanded text-label state (non-compact) - keep single-line visual in subrow
                       JS may add this class for a populated note; keep the button a single-line
                       pill and truncate with ellipsis so it doesn't grow taller. */
                    .dibs-notes-subrow .note-button.btn.tdm-note-expanded{
                        white-space: nowrap !important;
                        overflow: hidden !important;
                        text-overflow: ellipsis !important;
                        text-align: left !important;
                        display: inline-block !important;
                        padding: 2px 6px !important;
                        max-height: 30px !important;
                    }
                    /* Empty-compact placeholder */
                    .dibs-notes-subrow .note-button.btn.tdm-note-empty{ opacity: 0.95; }

                    /* --- 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: var(--tdm-dibs-inactive) !important; color: #fff !important; }
                    .btn-dibs-inactive:hover { background-color: var(--tdm-dibs-inactive-hover) !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: var(--tdm-dibs-success) !important; color: #fff !important; }
                    .btn-dibs-success-you:hover { background-color: var(--tdm-dibs-success-hover) !important; }
                    .btn-dibs-success-other { background-color: var(--tdm-dibs-other) !important; color: #fff !important; }
                    .btn-dibs-success-other:hover { background-color: var(--tdm-dibs-other-hover) !important; }
                    .inactive-note-button, .note-button {
                        background-color: var(--tdm-note-inactive) !important;
                        border: 1px solid var(--tdm-note-inactive) !important;
                        color: var(--tdm-note-inactive-text) !important;
                    }
                    .inactive-note-button:hover, .note-button:hover {
                        background-color: var(--tdm-note-inactive-hover) !important;
                        border-color: var(--tdm-note-inactive-hover) !important;
                        color: var(--tdm-note-inactive-text) !important;
                    }
                    .active-note-button {
                        background-color: var(--tdm-note-active) !important;
                        border: 1px solid var(--tdm-note-active) !important;
                        color: var(--tdm-note-active-text) !important;
                        font-weight: 600 !important;
                    }
                    .active-note-button:hover {
                        background-color: var(--tdm-note-active-hover) !important;
                        border-color: var(--tdm-note-active-hover) !important;
                        color: var(--tdm-note-active-text) !important;
                    }
                    .btn-med-deal-inactive, .btn-med-deal-default { background-color: var(--tdm-med-inactive) !important; color: #fff !important; }
                    .btn-med-deal-inactive:hover, .btn-med-deal-default:hover { background-color: var(--tdm-med-inactive-hover) !important; }
                    .btn-med-deal-set { background-color: var(--tdm-med-set) !important; color: #fff !important; }
                    .btn-med-deal-set:hover { background-color: var(--tdm-med-set-hover) !important; }
                    .btn-med-deal-mine { background-color: var(--tdm-med-mine) !important; color: #fff !important; }
                    .btn-med-deal-mine:hover { background-color: var(--tdm-med-mine-hover) !important; }
                    .req-assist-button { background-color: var(--tdm-assist) !important; color: #fff !important; }
                    .req-assist-button:hover { background-color: var(--tdm-assist-hover) !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-bottom:1px solid var(--tdm-modal-bg); 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) */
                    #tdm-settings-popup,
                    #tdm-settings-popup *,
                    #tdm-settings-content,
                    #tdm-settings-content * {
                        max-width: 100%;
                        box-sizing: border-box;
                    }
                    #tdm-settings-popup {
                        overflow-x: hidden !important;
                    }
                    .settings-section {
                        margin-bottom: var(--tdm-space-md);
                        width: 100%;
                        max-width: 100%;
                        background-color: var(--tdm-bg-card);
                        border: 2px solid var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-md);
                        padding: var(--tdm-space-md);
                        box-sizing: border-box;
                        overflow-x: hidden;
                    }
                    .settings-section-divided {
                        padding-top: var(--tdm-space-md);
                        border-top: 1px solid var(--tdm-bg-secondary);
                        display: flex;
                        gap: var(--tdm-space-md);
                        justify-content: center;
                        align-items: center;
                        flex-wrap: wrap;
                        width: 100%;
                    }
                    .settings-header {
                        font-size: var(--tdm-font-size-base);
                        font-weight: 700;
                        margin: 0 0 var(--tdm-space-md) 0;
                        text-align: center;
                        color: var(--tdm-text-accent);
                        background-color: var(--tdm-bg-secondary);
                        padding: var(--tdm-space-sm) var(--tdm-space-md);
                        border-radius: var(--tdm-radius-sm);
                        width: 100%;
                        user-select: none;
                        box-sizing: border-box;
                    }
                    /* Make subsection headers same style but slightly different color */
                    .settings-subsection > .settings-header {
                        font-size: var(--tdm-font-size-base);
                        font-weight: 700;
                        color: #7ab3ec !important;
                        background-color: var(--tdm-bg-tertiary);
                    }
                    .settings-subheader {
                        font-size: var(--tdm-font-size-base);
                        font-weight: 700;
                        color: var(--tdm-text-secondary);
                    }
                    /* Mini labels as subheadings */
                    .mini-label, .tdm-mini-lbl {
                        font-size: var(--tdm-font-size-sm);
                        font-weight: 700;
                    }
                    .settings-button-group { display: flex; flex-wrap: wrap; gap: var(--tdm-space-sm); justify-content: center; width: 100%; }
                    .settings-input, .settings-input-display {
                        width: 100%;
                        padding: var(--tdm-space-sm);
                        background-color: var(--tdm-bg-secondary);
                        color: var(--tdm-text-primary);
                        border: 1px solid #888;
                        border-radius: var(--tdm-radius-sm);
                        box-sizing: border-box;
                        font-size: var(--tdm-font-size-sm);
                        transition: border-color var(--tdm-transition-fast);
                    }
                    .settings-input:focus, .settings-input-display:focus {
                        outline: none;
                        border-color: var(--tdm-color-info);
                    }
                    /* Apply light grey border to all text inputs globally */
                    input[type="text"], input[type="number"], input[type="password"], textarea, select {
                        border: 1px solid #888 !important;
                    }
                    input[type="text"]:focus, input[type="number"]:focus, input[type="password"]:focus, textarea:focus, select:focus {
                        border-color: var(--tdm-color-info) !important;
                    }
                    .settings-input-display { padding: var(--tdm-space-md) var(--tdm-space-sm); }
                    .settings-btn {
                        display: inline-flex;
                        align-items: center;
                        justify-content: center;
                        background-color: var(--tdm-color-info);
                        color: var(--tdm-text-primary);
                        border: 1px solid var(--tdm-color-info);
                        padding: var(--tdm-space-sm) var(--tdm-space-md);
                        font-size: var(--tdm-font-size-sm);
                        font-weight: 500;
                        border-radius: var(--tdm-radius-sm);
                        cursor: pointer;
                        transition: background-color var(--tdm-transition-fast),
                                    border-color var(--tdm-transition-fast),
                                    transform var(--tdm-transition-fast);
                        white-space: nowrap;
                    }
                    .settings-btn:hover { background-color: #1976D2; border-color: #1976D2; }
                    .settings-btn:active { transform: scale(0.98); }
                    .settings-btn-green { background-color: var(--tdm-color-success); border-color: var(--tdm-color-success); }
                    .settings-btn-green:hover { background-color: #45a049; border-color: #45a049; }
                    .settings-btn-red { background-color: var(--tdm-color-error); border-color: var(--tdm-color-error); }
                    .settings-btn-red:hover { background-color: #e53935; border-color: #e53935; }
                    .settings-btn-blue { background-color: var(--tdm-color-info); border-color: var(--tdm-color-info); }
                    .settings-btn-blue:hover { background-color: #1976D2; border-color: #1976D2; }
                    .settings-btn-yellow { background-color: var(--tdm-color-warning); border-color: var(--tdm-color-warning); color: #000; }
                    .settings-btn-yellow:hover { background-color: #f57c00; border-color: #f57c00; }
                    .settings-btn-grey, .settings-btn-gray { background-color: var(--tdm-bg-secondary); border-color: var(--tdm-bg-main); }
                    .settings-btn-grey:hover, .settings-btn-gray:hover { background-color: var(--tdm-bg-main); border-color: #888; }
                    .war-type-controls { display: flex; gap: var(--tdm-space-md); justify-content: center; margin-bottom: var(--tdm-space-lg); }
                    .war-type-controls .settings-btn { flex: 1; }
                    .column-control {
                        display: grid;
                        grid-template-columns: 28px 80px 50px 20px;
                        gap: var(--tdm-space-sm);
                        align-items: center;
                        padding: var(--tdm-space-xs) 0;
                    }
                    /* Column settings grid container */
                    #column-visibility-rw,
                    #column-visibility-ml {
                        display: grid !important;
                        grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)) !important;
                        gap: var(--tdm-space-sm) var(--tdm-space-md) !important;
                        justify-items: start;
                    }
                    /* Force ALL settings panel toggles to stay small */
                    .tdm-settings-panel .tdm-toggle-switch,
                    .settings-section .tdm-toggle-switch {
                        width: 24px !important;
                        height: 12px !important;
                    }
                    .tdm-settings-panel .tdm-toggle-switch::after,
                    .settings-section .tdm-toggle-switch::after {
                        width: 8px !important;
                        height: 8px !important;
                        top: 1px !important;
                        left: 1px !important;
                    }
                    .tdm-settings-panel .tdm-toggle-switch.active::after,
                    .settings-section .tdm-toggle-switch.active::after {
                        transform: translateX(12px) !important;
                    }
                    .column-width-input {
                        padding: var(--tdm-space-sm);
                        border-radius: var(--tdm-radius-sm);
                        border: 1px solid #888;
                        background: var(--tdm-bg-secondary);
                        color: var(--tdm-text-primary);
                        font-size: var(--tdm-font-size-sm);
                        text-align: center;
                        box-sizing: border-box;
                        transition: border-color var(--tdm-transition-fast);
                    }
                    .column-width-input:focus {
                        outline: none;
                        border-color: var(--tdm-color-info);
                    }
                    .column-toggle-btn {
                        padding: var(--tdm-space-sm) var(--tdm-space-lg);
                        border: 2px solid var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-md);
                        background: var(--tdm-bg-card);
                        color: var(--tdm-text-secondary);
                        cursor: pointer;
                        transition: all var(--tdm-transition-normal);
                        font-size: var(--tdm-font-size-sm);
                        font-weight: 500;
                        min-width: 92px;
                        text-align: center;
                        line-height: 1.2;
                    }
                    .column-toggle-btn.active {
                        background: var(--tdm-color-success);
                        border-color: var(--tdm-color-success);
                        color: var(--tdm-text-primary);
                    }
                    .column-toggle-btn.active:hover {
                        background: #45a049;
                        border-color: #45a049;
                    }
                    .column-toggle-btn.inactive {
                        background: var(--tdm-color-error);
                        border-color: var(--tdm-color-error);
                        color: var(--tdm-text-primary);
                    }

                    /* ============================================
                       SETTINGS TABS - Display / Dibs / Advanced
                       ============================================ */
                    .tdm-settings-tabs {
                        display: flex;
                        border-bottom: 2px solid var(--tdm-bg-secondary);
                        margin-bottom: var(--tdm-space-md);
                        gap: var(--tdm-space-xs);
                        width: 100%;
                    }
                    .tdm-settings-tab {
                        flex: 1;
                        padding: var(--tdm-space-md) var(--tdm-space-lg);
                        background: var(--tdm-bg-tertiary);
                        border: none;
                        border-bottom: 3px solid transparent;
                        color: var(--tdm-text-secondary);
                        font-size: var(--tdm-font-size-sm);
                        font-weight: 600;
                        cursor: pointer;
                        text-align: center;
                        transition: all var(--tdm-transition-fast);
                        border-radius: var(--tdm-radius-sm) var(--tdm-radius-sm) 0 0;
                    }
                    .tdm-settings-tab:hover {
                        background: var(--tdm-bg-secondary);
                        color: var(--tdm-text-primary);
                    }
                    .tdm-settings-tab--active {
                        background: var(--tdm-bg-secondary);
                        color: var(--tdm-text-accent);
                        border-bottom-color: var(--tdm-color-info);
                    }
                    .tdm-settings-panel {
                        display: none;
                        padding: var(--tdm-space-md);
                        width: 100%;
                        max-width: 100%;
                        box-sizing: border-box;
                        overflow-x: hidden;
                    }
                    .tdm-settings-panel--active {
                        display: block;
                        max-height: 70vh;
                        overflow-y: auto;
                    }

                    /* ============================================
                       TOGGLE SWITCH - Modern on/off switches
                       ============================================ */
                    .tdm-toggle-switch {
                        position: relative;
                        width: 28px;
                        height: 14px;
                        background: var(--tdm-bg-secondary);
                        border: 1px solid #555;
                        border-radius: var(--tdm-radius-full);
                        cursor: pointer;
                        transition: background var(--tdm-transition-fast), border-color var(--tdm-transition-fast);
                        flex-shrink: 0;
                    }
                    .tdm-toggle-switch::after {
                        content: '';
                        position: absolute;
                        width: 10px;
                        height: 10px;
                        background: var(--tdm-text-secondary);
                        border-radius: 50%;
                        top: 1px;
                        left: 1px;
                        transition: transform var(--tdm-transition-fast), background var(--tdm-transition-fast);
                    }
                    .tdm-toggle-switch:hover::after {
                        background: var(--tdm-text-primary);
                    }
                    .tdm-toggle-switch.active {
                        background: var(--tdm-color-success);
                        border-color: var(--tdm-color-success);
                    }
                    .tdm-toggle-switch.active::after {
                        transform: translateX(14px);
                        background: var(--tdm-text-primary);
                    }
                    .tdm-toggle-switch.inactive {
                        background: var(--tdm-color-error);
                        border-color: var(--tdm-color-error);
                    }
                    .tdm-toggle-switch.inactive::after {
                        background: var(--tdm-text-primary);
                    }

                    /* Toggle with label container */
                    .tdm-toggle-container {
                        display: flex;
                        align-items: center;
                        gap: var(--tdm-space-md);
                        padding: var(--tdm-space-sm) 0;
                    }
                    .tdm-toggle-label {
                        font-size: var(--tdm-font-size-sm);
                        color: var(--tdm-text-primary);
                        flex: 1;
                        min-width: 80px;
                    }

                    /* Column control grid layout for alignment */
                    .tdm-column-grid {
                        display: grid;
                        grid-template-columns: 1fr auto auto;
                        gap: var(--tdm-space-sm) var(--tdm-space-md);
                        align-items: center;
                        width: 100%;
                    }
                    .tdm-column-grid .tdm-toggle-label {
                        text-align: left;
                    }
                    .tdm-column-grid .column-width-input {
                        width: 60px;
                        text-align: center;
                    }

                    /* Number input fix - remove up/down arrows from ALL number inputs */
                    input[type="number"] {
                        text-align: center;
                        -moz-appearance: textfield;
                    }
                    input[type="number"]::-webkit-outer-spin-button,
                    input[type="number"]::-webkit-inner-spin-button {
                        -webkit-appearance: none;
                        margin: 0;
                    }

                    /* Collapsible sections - now always expanded with 2px borders */
                    .collapsible {
                        background-color: var(--tdm-bg-card);
                        border: 2px solid var(--tdm-bg-secondary);
                        border-radius: var(--tdm-radius-md);
                        padding: var(--tdm-space-md);
                        margin-bottom: var(--tdm-space-md);
                        box-sizing: border-box;
                    }
                    .collapsible .collapsible-header { cursor: default; position: relative; }
                    .collapsible .chevron { display: none; } /* Hide chevrons since we're not collapsing */
                    .collapsible .collapsible-content { display: block !important; } /* Always show content */
                    .collapsible.collapsed .collapsible-content { display: block !important; } /* Override collapsed state */
                    .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);
            // Ensure delegated click visual feedback for dibs buttons (short glow)
            ensureDibsClickListener();
        },

        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: '#fff' }, 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 '';
                };

                // Prefer backend-enriched fields (mode/category) when present.
                // Backend emits: attack_mode + attack_category.
                const deriveAttackModeForDisplay = (a) => {
                    try {
                        const v = a?.attack_mode || a?.attackMode || '';
                        return v ? String(v) : '';
                    } catch(_) { return ''; }
                };
                const deriveAttackCategoryForDisplay = (a) => {
                    try {
                        const v = a?.attack_category || a?.attackCategory || '';
                        return v ? String(v) : '';
                    } catch(_) { 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 || '',
                            attack_mode: deriveAttackModeForDisplay(a),
                            category: deriveAttackCategoryForDisplay(a),
                            // 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: 'attack_mode', label: 'Mode', align: 'center' },
                    { key: 'category', label: 'Category' },
                    { 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','AttackMode','Category','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 category = deriveAttackCategoryForDisplay(a);
                                const attackMode = deriveAttackModeForDisplay(a);
                                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.defender?.id || '',
                                    a.defender?.name || '',
                                    a.defender?.faction?.name || a.defenderFactionName || a.defender_faction_name || '',
                                    a.defender_status || '',
                                    a.result || '',
                                    a.direction || '',
                                    attackMode,
                                    category,
                                    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),
                                    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_activity || '',
                                    a.defender_last_action_ts ? new Date(a.defender_last_action_ts * 1000).toISOString() : '',
                                    a.defender_la_diff != null ? a.defender_la_diff : '',
                                    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: '#fff', 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: 'var(--tdm-color-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: 'var(--tdm-color-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: 'var(--tdm-color-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='var(--tdm-color-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: 'scoreBleedCount', label: 'ScoreBleed Hits', align: 'center', render: (r)=> (r && r.scoreBleedCount != null ? (Number(r.scoreBleedCount) || 0) : ''), sortValue: (r)=> Number(r?.scoreBleedCount || 0) },
                { key: 'scoreBleedRespect', label: 'ScoreBleed Respect', align: 'center', render: (r)=> (r && r.scoreBleedRespect != null ? Number(r.scoreBleedRespect || 0).toFixed(2) : ''), sortValue: (r)=> Number(r?.scoreBleedRespect || 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?'var(--tdm-color-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 = 'var(--tdm-color-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:'var(--tdm-color-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','scoreBleedCount','scoreBleedRespect'
                    ];
                    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,
                            (Number(r.scoreBleedCount || 0)),
                            (Number(r.scoreBleedRespect || 0)).toFixed(2)
                        ].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: 'var(--tdm-color-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 = 'var(--tdm-color-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)
                    const warIdUsed = globalData?.meta?.warIdUsed || null;
                    const currentWarId = state.lastRankWar?.id || null;
                    if (globalData?.meta?.userScore) {
                        // Only accept the score if it matches the current war; otherwise drop any cached value
                        if (!warIdUsed || !currentWarId || String(warIdUsed) === String(currentWarId)) {
                            state.userScore = globalData.meta.userScore;
                            try {
                                tdmlogger('debug', `[GlobalData] Received userScore: ${JSON.stringify(state.userScore)}`);
                                // Persist lightweight userScore together with the warId to avoid cross-war bleed
                                try { storage.set('userScore', { warId: currentWarId, v: state.userScore }); } catch(_) { storage.set('userScore', state.userScore); }
                            } catch(_) {}
                            if (typeof ui.updateUserScoreBadge === 'function') {
                                ui.updateUserScoreBadge();
                            }
                        } else {
                            state.userScore = null;
                            try { if (storage && typeof storage.remove === 'function') storage.remove('userScore'); } catch(_) {}
                            try { tdmlogger('debug', `[GlobalData] Ignored userScore (war mismatch meta=${warIdUsed} current=${currentWarId})`); } catch(_) {}
                        }
                    } else {
                        // If backend responded for this war but omitted userScore, clear stale cache so badge can recompute from summary rows
                        if (warIdUsed && currentWarId && String(warIdUsed) === String(currentWarId) && state.userScore) {
                            state.userScore = null;
                            try { if (storage && typeof storage.remove === 'function') storage.remove('userScore'); } catch(_) {}
                            try { ui.updateUserScoreBadge?.(); } catch(_) {}
                        }
                        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.
                let createdId = resp?.id;
                const serverMsg = (resp && resp.message) ? String(resp.message) : '';
                let needsDibsRefresh = false;

                // Robustness: some transports may omit `id` but include fresh dibsData.
                // Treat it as created/owned only when dibsData shows us as the active dibber.
                if (!createdId && resp?.dibsData && Array.isArray(resp.dibsData)) {
                    try {
                        const activeMine = resp.dibsData.find(d => d && d.dibsActive && String(d.opponentId) === String(opponentId) && String(d.userId) === String(state.user.tornId));
                        if (activeMine && activeMine.id) createdId = String(activeMine.id);
                    } catch(_) {}
                }
                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: do NOT treat as success.
                    // This commonly occurs when backend errors are embedded in an OK response
                    // (or when a conflict is returned without throwing). Force a refresh.
                    ui.showMessageBox(serverMsg || `Unexpected response while dibbing ${opponentName}. Refreshing…`, serverMsg ? 'info' : 'warning');
                    handlers.fetchGlobalDataForced('dibs');
                    // Re-enable button; authoritative state will repaint on refresh.
                    if (buttonElement) {
                        try {
                            buttonElement.textContent = originalText || 'Dibs';
                            buttonElement.disabled = false;
                            buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                        } catch(_) {}
                    }
                    return;
                }
                // 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 ONLY when we actually created dibs.
                // Avoid false-positive messaging during conflicts/races.
                if (createdId) {
                    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)) {
                // War is over (or outside grace) — clear any lingering cap state
                state.user.hasReachedScoreCap = false;
                state.user._scoreCapWarId = null;
                return;
            }

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

            // If a prior war left the session flag set, clear it so we can re-evaluate for this war
            if (state.user.hasReachedScoreCap && state.user._scoreCapWarId && state.user._scoreCapWarId !== warId) {
                state.user.hasReachedScoreCap = false;
            }

            // 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
                state.user._scoreCapWarId = warId;
                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 && state.user._scoreCapWarId === warId) {
                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;
                        state.user._scoreCapWarId = warId;
                        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();
                        }
                } else {
                    state.user.hasReachedScoreCap = false;
                    state.user._scoreCapWarId = warId;
                }

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

                // Defensive handling: backend may return userNotes in different shapes
                // (array or object). Normalize to a map and merge into local state to
                // avoid accidentally replacing the entire store with an empty array.
                try {
                    const current = (state.userNotes && typeof state.userNotes === 'object') ? { ...state.userNotes } : {};
                    if (resp && resp.userNotes) {
                        // If backend returned an array, convert to map by id
                        if (Array.isArray(resp.userNotes)) {
                            const map = {};
                            resp.userNotes.forEach(n => {
                                if (!n) return;
                                const id = n.id || n.userId || n.noteTargetId || n.targetId || null;
                                if (!id) return;
                                map[String(id)] = {
                                    noteContent: (n.noteContent || n.content || '').toString(),
                                    updated: n.updated || Date.now(),
                                    updatedBy: n.updatedBy || null
                                };
                            });
                            const merged = { ...current, ...map };
                            storage.updateStateAndStorage('userNotes', merged);
                        } else if (typeof resp.userNotes === 'object') {
                            // Assume it's already a map; merge with existing
                            const merged = { ...current, ...resp.userNotes };
                            storage.updateStateAndStorage('userNotes', merged);
                        } else {
                            // Unexpected shape - fallback to per-key update
                            if (trimmedNew === '') {
                                if (current[noteTargetId]) delete current[noteTargetId];
                                storage.updateStateAndStorage('userNotes', current);
                            } else {
                                current[noteTargetId] = { noteContent: trimmedNew, updated: Date.now(), updatedBy: state.user.tornId };
                                storage.updateStateAndStorage('userNotes', current);
                            }
                        }
                    } else {
                        // No bulk data returned; perform local per-note update (or delete)
                        if (trimmedNew === '') {
                            if (current[noteTargetId]) delete current[noteTargetId];
                        } else {
                            current[noteTargetId] = { noteContent: trimmedNew, updated: Date.now(), updatedBy: state.user.tornId };
                        }
                        storage.updateStateAndStorage('userNotes', current);
                    }
                } catch (e) {
                    // If normalization fails for any reason, fall back to safe per-key update
                    try {
                        if (trimmedNew === '') {
                            if (state.userNotes && state.userNotes[noteTargetId]) delete state.userNotes[noteTargetId];
                        } else {
                            state.userNotes[noteTargetId] = { noteContent: trimmedNew, updated: Date.now(), updatedBy: state.user.tornId };
                        }
                        storage.updateStateAndStorage('userNotes', state.userNotes);
                    } catch(_) { /* swallow */ }
                }

                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
                });
                
                let createdId = assignResp?.id;
                const serverMsg = (assignResp && assignResp.message) ? String(assignResp.message) : '';

                // Robustness: some transports may omit `id` but include fresh dibsData.
                if (!createdId && assignResp?.dibsData && Array.isArray(assignResp.dibsData)) {
                    try {
                        const activeAssignee = assignResp.dibsData.find(d => d && d.dibsActive && String(d.opponentId) === String(opponentId) && String(d.userId) === String(dibsForUserId));
                        if (activeAssignee && activeAssignee.id) createdId = String(activeAssignee.id);
                    } catch(_) {}
                }
                
                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] Unexpected response while assigning dibs for ${opponentName}. Refreshing…`, serverMsg ? 'info' : 'warning');
                    handlers.fetchGlobalDataForced('dibs');
                    if (buttonElement) {
                        try {
                            buttonElement.textContent = originalText;
                            buttonElement.disabled = false;
                            buttonElement.className = 'btn dibs-btn btn-dibs-inactive';
                        } catch(_) {}
                    }
                    return;
                }

                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(_) {}

                // Auto-compose dibs message ONLY when a dib was created.
                if (createdId) {
                    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 hydrating = !state._travelEtaHydrated;
            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);
                    }
                    let txt = mm > 99 ? `${mm}m` : `${mm}:${ss}`;
                    if (hydrating) txt += ' ...';
                    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) {
                state._travelEtaHydrated = true;
                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(_) {}
            try { handlers._renderTravelEtas?.(); } 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 */ }
        });
    })();
})();