Chain cordinator

Torn / Torn PDA chain watch coordinator with mobile-safe chain countdown, IndexedDB storage, synced target cache, draggable settings, and target finder

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Chain cordinator
// @namespace    https://torn.com/
// @version      26.5.6.0
// @description  Torn / Torn PDA chain watch coordinator with mobile-safe chain countdown, IndexedDB storage, synced target cache, draggable settings, and target finder
// @author       Vreebn [4149405]
// @match        https://www.torn.com/*
// @match        https://torn.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @connect      torn-chain-coordinator.ebnoreza.workers.dev
// @connect      ffscouter.com
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    if (window.top !== window.self) return;
    if (window.__CHAIN_COORDINATOR_V265140_ACTIVE__) return;
    window.__CHAIN_COORDINATOR_V265140_ACTIVE__ = true;

    const WORKER_BASE = 'https://torn-chain-coordinator.ebnoreza.workers.dev';

    const PRESENCE_SYNC_EVERY_MS = 30 * 1000;
    const IDENTITY_SCAN_MS = 2500;
    const DISCOVERY_SCAN_MS = 20000;
    const UI_REFRESH_MS = 250;
    const CHAIN_FAST_SCAN_MS = 250;
    const ROUTE_CHECK_MS = 1000;
    const TARGET_CACHE_POLL_MS = 2000;

    const BOOT_DOM_DELAY_MS = 1800;
    const BOOT_SYNC_DELAY_MS = 2600;

    const SLOT_MINUTES = 30;
    const TIMELINE_CELL_MINUTES = 30;
    const GRAPH_BAR_COUNT = 48;
    const CHAIN_COOLDOWN_MS = 5 * 60 * 1000;
    const MOBILE_WIDTH_PX = 768;

    const FF_BATCH_SIZE = 200;
    const TARGET_MAX_BATCHES = 10;
    const DEFAULT_TARGET_COUNT = 5;
    const DEFAULT_FF_MIN = '2.9';
    const DEFAULT_FF_MAX = '3.1';

    const IDB_NAME = 'chain_coordinator_v265120';
    const IDB_STORE = 'kv';
    const STORE_KEY = 'state';
    const TARGET_STORE_KEY = 'targets';

    const BC_NAME = 'chain_coordinator_target_sync_v265120';
    const LOG_PREFIX = '[CC]';

    let panel = null;
    let miniBtn = null;
    let settingsBtn = null;
    let settingsModal = null;
    let targetChannel = null;

    const mem = {
        player_id: '',
        player_name: '',
        player_url: '',

        faction_id: '',
        faction_name: '',
        faction_url: '',
        last_identity_source: '',

        share_energy: '1',

        ff_api_key: '',
        ff_min: DEFAULT_FF_MIN,
        ff_max: DEFAULT_FF_MAX,
        target_count: String(DEFAULT_TARGET_COUNT),

        watch_start_mode: 'now',
        watch_start_hhmm: '',
        watch_duration_hours: '1',

        chain_value: '',
        chain_max: '',
        chain_time_left: '',

        energy_current: '',
        energy_max: '',

        last_status: '',
        next_sync_label: '',
        ui_mode: 'summary',

        board: {
            online_count: 0,
            energy_total_current: 0,
            energy_total_max: 0,
            current_watch: null,
            next_watch: null,
            watch_slots: []
        },

        me: {
            active_watch: null,
            next_watch: null
        },

        target_rows: [],
        target_revision: 0,
        targets_loading: '',
        targets_error: ''
    };

    const state = {
        paused: false,
        hidden: false,
        collapsed: false,

        panelX: null,
        panelY: null,

        miniAnchorX: 'right',
        miniAnchorY: 'bottom',
        miniRight: 18,
        miniBottom: 52,
        miniLeft: 18,
        miniTop: 80,

        settingsAnchorX: 'right',
        settingsAnchorY: 'bottom',
        settingsRight: 14,
        settingsBottom: 104,
        settingsLeft: 14,
        settingsTop: 120,

        chainDeadlineMs: null,
        chainLastDomText: '',
        chainLastDomReadAt: 0,
        chainTimerChainKey: '',
        chainTimerFromTooltip: false,

        lastRoute: location.href,

        identityTimerId: null,
        chainFastTimerId: null,
        discoveryTimerId: null,
        uiTimerId: null,
        routeTimerId: null,
        syncIntervalId: null,
        syncTimeoutId: null,
        targetPollTimerId: null,

        saveTimer: null,
        storageBroken: false,

        syncingPresence: false,
        syncingBoard: false,
        syncingWatch: false,
        syncingTargets: false,
        sendingDiscovery: false,

        lastSavedJson: '',
        lastDiscoverySignature: ''
    };

    function log(...args) { console.log(LOG_PREFIX, ...args); }
    function warn(...args) { console.warn(LOG_PREFIX, ...args); }
    function errlog(...args) { console.error(LOG_PREFIX, ...args); }

    function qs(sel, root = document) {
        try { return root.querySelector(sel); } catch { return null; }
    }

    function qsa(sel, root = document) {
        try { return Array.from(root.querySelectorAll(sel)); } catch { return []; }
    }

    function safeText(el) {
        return (el?.textContent || '').replace(/\s+/g, ' ').trim();
    }

    function cleanString(v) {
        if (v == null) return '';
        return String(v).trim();
    }

    function esc(s) {
        return String(s ?? '')
            .replaceAll('&', '&')
            .replaceAll('<', '&lt;')
            .replaceAll('>', '&gt;')
            .replaceAll('"', '&quot;')
            .replaceAll("'", '&#39;');
    }

    function safeJsonParse(v, fallback = null) {
        try { return JSON.parse(v); } catch { return fallback; }
    }

    function clamp(v, lo, hi) {
        return Math.max(lo, Math.min(hi, v));
    }

    function parseIntOrNull(v) {
        if (v == null || v === '') return null;
        const n = Number(v);
        return Number.isFinite(n) ? Math.trunc(n) : null;
    }

    function parseFloatOrNull(v) {
        if (v == null || v === '') return null;
        const n = Number(v);
        return Number.isFinite(n) ? n : null;
    }

    function nowIso() {
        return new Date().toISOString();
    }

    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    function isMobileWidth() {
        return window.innerWidth <= MOBILE_WIDTH_PX;
    }

    function makeChainKey(value, max) {
        const v = cleanString(value);
        const m = cleanString(max);
        if (!v && !m) return '';
        return `${v}/${m || ''}`;
    }

    function invalidateMobileChainTimer() {
        state.chainDeadlineMs = null;
        state.chainLastDomText = '';
        state.chainLastDomReadAt = 0;
        state.chainTimerChainKey = '';
        state.chainTimerFromTooltip = false;
        mem.chain_time_left = '';
    }

    function isValidPlayerId(v) {
        const s = cleanString(v);
        return /^\d{1,12}$/.test(s) && Number(s) > 0;
    }

    function isValidFactionId(v) {
        const s = cleanString(v);
        return /^\d{1,12}$/.test(s) && Number(s) > 0;
    }

    function playerProfileUrl(playerId) {
        return `https://www.torn.com/profiles.php?XID=${encodeURIComponent(playerId)}`;
    }

    function factionProfileUrl(factionId) {
        return `https://www.torn.com/factions.php?step=profile&ID=${encodeURIComponent(factionId)}`;
    }

    function extractParamFromUrl(rawUrl, paramName) {
        const raw = cleanString(rawUrl);
        if (!raw) return '';
        const m = raw.match(new RegExp(`[?&]${paramName}=([0-9]+)`, 'i'));
        return m ? m[1] : '';
    }

    function normalizeTime(str) {
        const m = String(str || '').trim().match(/^(\d{1,2}):(\d{2})$/);
        if (!m) return null;

        const hh = Number(m[1]);
        const mm = Number(m[2]);

        if (!Number.isInteger(hh) || !Number.isInteger(mm)) return null;
        if (hh < 0 || hh > 23 || mm < 0 || mm > 59) return null;

        return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`;
    }

    function hhmmToMinutes(hhmm) {
        const t = normalizeTime(hhmm);
        if (!t) return null;

        const [hh, mm] = t.split(':').map(Number);
        return hh * 60 + mm;
    }

    function minutesToHHMM(totalMinutes) {
        const mins = ((Number(totalMinutes) % 1440) + 1440) % 1440;
        const hh = Math.floor(mins / 60);
        const mm = mins % 60;
        return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}`;
    }

    function floorNowToSlotHHMM() {
        const now = new Date();
        const total = now.getUTCHours() * 60 + now.getUTCMinutes();
        const floored = Math.floor(total / SLOT_MINUTES) * SLOT_MINUTES;
        return minutesToHHMM(floored);
    }

    function floorToUtcHalfHour(ms) {
        const d = new Date(ms);
        d.setUTCSeconds(0, 0);
        const m = d.getUTCMinutes();
        d.setUTCMinutes(m < 30 ? 0 : 30);
        return d.getTime();
    }

    function getWeekdayLabel(weekday) {
        return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][Number(weekday)] || '-';
    }

    function isoToParts(iso) {
        if (!iso) return null;

        const d = new Date(iso);
        if (!Number.isFinite(d.getTime())) return null;

        return {
            weekday: d.getUTCDay(),
            hhmm: `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`
        };
    }

    function formatIsoAsTct(iso) {
        const p = isoToParts(iso);
        return p ? `${getWeekdayLabel(p.weekday)} ${p.hhmm}` : '-';
    }

    function formatIsoRangeAsTct(startIso, endIso) {
        const s = isoToParts(startIso);
        const e = isoToParts(endIso);
        if (!s || !e) return '-';

        if (s.weekday === e.weekday) return `${getWeekdayLabel(s.weekday)} ${s.hhmm} → ${e.hhmm}`;
        return `${getWeekdayLabel(s.weekday)} ${s.hhmm} → ${getWeekdayLabel(e.weekday)} ${e.hhmm}`;
    }

    function formatWatchUntil(iso) {
        const p = isoToParts(iso);
        return p ? `until ${p.hhmm}` : '';
    }

    function parseChainTimeLeftToMs(value) {
        const raw = String(value || '').trim();
        if (!raw) return null;

        let m = raw.match(/^(\d{1,2}):(\d{2})$/);
        if (m) return (Number(m[1]) * 60 + Number(m[2])) * 1000;

        m = raw.match(/^(\d{1,2}):(\d{2}):(\d{2})$/);
        if (m) return (Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3])) * 1000;

        return null;
    }

    function formatDuration(ms) {
        if (!(ms > 0)) return '00:00:00';

        const total = Math.floor(ms / 1000);
        const h = Math.floor(total / 3600);
        const m = Math.floor((total % 3600) / 60);
        const s = total % 60;

        return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
    }

    function formatMiniCountdown(ms) {
        if (!(ms > 0)) return '⏱';
        if (ms > CHAIN_COOLDOWN_MS) return '⏱';

        const total = Math.floor(ms / 1000);
        const m = Math.floor(total / 60);
        const s = total % 60;

        return `${m}:${String(s).padStart(2, '0')}`;
    }

    function countdownToneClass(ms) {
        if (!(ms > 0)) return 'cc-good';
        if (ms > CHAIN_COOLDOWN_MS) return 'cc-good';
        if (ms > 4 * 60 * 1000) return 'cc-good';
        if (ms > 3 * 60 * 1000) return 'cc-info';
        if (ms > 2 * 60 * 1000) return 'cc-warn';
        if (ms > 1 * 60 * 1000) return 'cc-orange';
        return 'cc-bad';
    }

    function miniToneClass(ms) {
        if (!(ms > 0)) return 'cc-mini-blue';
        if (ms > CHAIN_COOLDOWN_MS) return 'cc-mini-blue';
        if (ms > 4 * 60 * 1000) return 'cc-mini-green';
        if (ms > 3 * 60 * 1000) return 'cc-mini-blue';
        if (ms > 2 * 60 * 1000) return 'cc-mini-yellow';
        if (ms > 1 * 60 * 1000) return 'cc-mini-orange';
        return 'cc-mini-red';
    }

    function openDb() {
        return new Promise((resolve, reject) => {
            try {
                const req = indexedDB.open(IDB_NAME, 1);

                req.onupgradeneeded = () => {
                    const db = req.result;
                    if (!db.objectStoreNames.contains(IDB_STORE)) db.createObjectStore(IDB_STORE);
                };

                req.onsuccess = () => resolve(req.result);
                req.onerror = () => reject(req.error || new Error('IDB open failed'));
            } catch (e) {
                reject(e);
            }
        });
    }

    async function idbGet(key) {
        if (state.storageBroken) return null;

        let db;
        try {
            db = await openDb();

            return await new Promise((resolve, reject) => {
                const tx = db.transaction(IDB_STORE, 'readonly');
                const store = tx.objectStore(IDB_STORE);
                const req = store.get(key);

                req.onsuccess = () => resolve(req.result ?? null);
                req.onerror = () => reject(req.error || new Error('IDB get failed'));
            });
        } catch (e) {
            state.storageBroken = true;
            warn('IDB get failed; storage disabled', e?.message || e);
            return null;
        } finally {
            try { if (db) db.close(); } catch {}
        }
    }

    async function idbSet(key, value) {
        if (state.storageBroken) return false;

        let db;
        try {
            db = await openDb();

            await new Promise((resolve, reject) => {
                const tx = db.transaction(IDB_STORE, 'readwrite');
                const store = tx.objectStore(IDB_STORE);
                const req = store.put(value, key);

                req.onsuccess = () => resolve();
                req.onerror = () => reject(req.error || new Error('IDB set failed'));
            });

            return true;
        } catch (e) {
            state.storageBroken = true;
            warn('IDB set failed; storage disabled', e?.message || e);
            return false;
        } finally {
            try { if (db) db.close(); } catch {}
        }
    }

    function getCompactSaveObject() {
        return {
            version: '26.5.14.0',
            saved_at: nowIso(),

            player_id: mem.player_id,
            player_name: mem.player_name,
            player_url: mem.player_url,

            faction_id: mem.faction_id,
            faction_name: mem.faction_name,
            faction_url: mem.faction_url,
            last_identity_source: mem.last_identity_source,

            share_energy: mem.share_energy,

            ff_api_key: mem.ff_api_key,
            ff_min: mem.ff_min,
            ff_max: mem.ff_max,
            target_count: mem.target_count,

            watch_start_mode: mem.watch_start_mode,
            watch_start_hhmm: mem.watch_start_hhmm,
            watch_duration_hours: mem.watch_duration_hours,

            ui_mode: mem.ui_mode,

            paused: state.paused,
            hidden: state.hidden,
            collapsed: state.collapsed,
            panelX: state.panelX,
            panelY: state.panelY,

            miniAnchorX: state.miniAnchorX,
            miniAnchorY: state.miniAnchorY,
            miniRight: state.miniRight,
            miniBottom: state.miniBottom,
            miniLeft: state.miniLeft,
            miniTop: state.miniTop,

            settingsAnchorX: state.settingsAnchorX,
            settingsAnchorY: state.settingsAnchorY,
            settingsRight: state.settingsRight,
            settingsBottom: state.settingsBottom,
            settingsLeft: state.settingsLeft,
            settingsTop: state.settingsTop
        };
    }

    async function persistNow() {
        const raw = JSON.stringify(getCompactSaveObject());
        if (raw === state.lastSavedJson) return;

        const ok = await idbSet(STORE_KEY, raw);
        if (ok) state.lastSavedJson = raw;
    }

    function queuePersist() {
        if (state.saveTimer) clearTimeout(state.saveTimer);
        state.saveTimer = setTimeout(() => persistNow().catch(() => {}), 900);
    }

    async function loadPersistedState() {
        const raw = await idbGet(STORE_KEY);
        const saved = safeJsonParse(raw, null);

        if (!saved || typeof saved !== 'object') return;

        mem.player_id = cleanString(saved.player_id);
        mem.player_name = cleanString(saved.player_name);
        mem.player_url = cleanString(saved.player_url);

        mem.faction_id = cleanString(saved.faction_id);
        mem.faction_name = cleanString(saved.faction_name);
        mem.faction_url = cleanString(saved.faction_url);
        mem.last_identity_source = cleanString(saved.last_identity_source) || 'cache';

        mem.share_energy = cleanString(saved.share_energy) || '1';

        mem.ff_api_key = cleanString(saved.ff_api_key);
        mem.ff_min = cleanString(saved.ff_min) || DEFAULT_FF_MIN;
        mem.ff_max = cleanString(saved.ff_max) || DEFAULT_FF_MAX;
        mem.target_count = cleanString(saved.target_count) || String(DEFAULT_TARGET_COUNT);

        mem.watch_start_mode = cleanString(saved.watch_start_mode) || 'now';
        mem.watch_start_hhmm = cleanString(saved.watch_start_hhmm);
        mem.watch_duration_hours = cleanString(saved.watch_duration_hours) || '1';

        mem.ui_mode = cleanString(saved.ui_mode) || 'summary';

        state.paused = !!saved.paused;
        state.hidden = !!saved.hidden;
        state.collapsed = !!saved.collapsed;

        state.panelX = typeof saved.panelX === 'number' ? saved.panelX : null;
        state.panelY = typeof saved.panelY === 'number' ? saved.panelY : null;

        state.miniAnchorX = saved.miniAnchorX === 'left' ? 'left' : 'right';
        state.miniAnchorY = saved.miniAnchorY === 'top' ? 'top' : 'bottom';
        state.miniRight = typeof saved.miniRight === 'number' ? saved.miniRight : 18;
        state.miniBottom = typeof saved.miniBottom === 'number' ? saved.miniBottom : 52;
        state.miniLeft = typeof saved.miniLeft === 'number' ? saved.miniLeft : 18;
        state.miniTop = typeof saved.miniTop === 'number' ? saved.miniTop : 80;

        state.settingsAnchorX = saved.settingsAnchorX === 'left' ? 'left' : 'right';
        state.settingsAnchorY = saved.settingsAnchorY === 'top' ? 'top' : 'bottom';
        state.settingsRight = typeof saved.settingsRight === 'number' ? saved.settingsRight : 14;
        state.settingsBottom = typeof saved.settingsBottom === 'number' ? saved.settingsBottom : 104;
        state.settingsLeft = typeof saved.settingsLeft === 'number' ? saved.settingsLeft : 14;
        state.settingsTop = typeof saved.settingsTop === 'number' ? saved.settingsTop : 120;

        state.lastSavedJson = raw || '';
    }

    function getTargetCachePayload() {
        return {
            revision: Number(mem.target_revision || 0),
            updated_at: nowIso(),
            rows: Array.isArray(mem.target_rows) ? mem.target_rows.slice(0, 25).map(r => ({
                player_id: r.player_id,
                player_name: r.player_name,
                player_level: r.player_level,
                player_faction_id: r.player_faction_id,
                player_faction_name: r.player_faction_name,
                ff_score: r.ff_score,
                ff_label: r.ff_label,
                profile_url: r.profile_url,
                attack_url: r.attack_url
            })) : []
        };
    }

    async function saveTargetCache(broadcast = true) {
        mem.target_revision = Date.now();

        const payload = getTargetCachePayload();
        await idbSet(TARGET_STORE_KEY, JSON.stringify(payload));

        if (broadcast && targetChannel) {
            try {
                targetChannel.postMessage({
                    type: 'targets_updated',
                    revision: payload.revision,
                    rows: payload.rows
                });
            } catch {}
        }
    }

    async function loadTargetCache(applyEvenIfEmpty = true) {
        const raw = await idbGet(TARGET_STORE_KEY);
        const saved = safeJsonParse(raw, null);

        if (!saved || typeof saved !== 'object') return false;

        const revision = Number(saved.revision || 0);
        if (revision <= Number(mem.target_revision || 0)) return false;

        const rows = Array.isArray(saved.rows) ? saved.rows : [];
        if (!applyEvenIfEmpty && !rows.length) return false;

        mem.target_revision = revision;
        mem.target_rows = rows.slice(0, 25);

        if (mem.ui_mode === 'targets') renderActiveView();

        return true;
    }

    function setupTargetBroadcast() {
        try {
            targetChannel = new BroadcastChannel(BC_NAME);
            targetChannel.onmessage = (ev) => {
                const msg = ev?.data || {};
                if (msg.type !== 'targets_updated') return;

                const revision = Number(msg.revision || 0);
                if (revision <= Number(mem.target_revision || 0)) return;

                mem.target_revision = revision;
                mem.target_rows = Array.isArray(msg.rows) ? msg.rows.slice(0, 25) : [];

                if (mem.ui_mode === 'targets') renderActiveView();
            };
        } catch {
            targetChannel = null;
        }
    }

    function xhrJson({ method = 'GET', url, data = null, headers = {}, timeout = 30000 }) {
        return new Promise((resolve, reject) => {
            if (typeof GM_xmlhttpRequest !== 'function') {
                reject(new Error('GM_xmlhttpRequest unavailable'));
                return;
            }

            GM_xmlhttpRequest({
                method,
                url,
                data: data ? JSON.stringify(data) : undefined,
                headers: data ? { 'Content-Type': 'application/json', ...headers } : headers,
                timeout,
                onload: (res) => {
                    let json = {};
                    try { json = JSON.parse(res.responseText || '{}'); } catch {}

                    if (res.status >= 200 && res.status < 300) resolve(json);
                    else reject(new Error(json?.error || `HTTP ${res.status}`));
                },
                onerror: () => reject(new Error('Network error')),
                ontimeout: () => reject(new Error('Request timeout'))
            });
        });
    }

    async function workerJson({ method = 'GET', path, data = null, timeout = 30000 }) {
        const url = path.startsWith('http') ? path : `${WORKER_BASE}${path}`;
        return await xhrJson({ method, url, data, timeout });
    }

    async function setStatus(text) {
        mem.last_status = String(text || '');
        const el = qs('#cc-status');
        if (el) el.textContent = mem.last_status;
    }

    function getPlayerFromHiddenInput() {
        const input = qs('#torn-user');
        if (!input) return null;

        try {
            const data = JSON.parse(input.value || '{}');
            const id = cleanString(data?.id);
            const name = cleanString(data?.playername);

            if (!isValidPlayerId(id) || !name) return null;

            return {
                player_id: id,
                player_name: name,
                player_url: playerProfileUrl(id)
            };
        } catch {
            return null;
        }
    }

    function isHomePageForSelf() {
        const path = location.pathname || '';
        const search = location.search || '';
        const hash = location.hash || '';

        if (path === '/' || path === '/index.php') return true;
        if (/sid=home/i.test(search)) return true;
        if (/\/home/i.test(path)) return true;
        if (/home/i.test(hash) && !/profiles/i.test(hash)) return true;

        return false;
    }

    function isFactionYourPage() {
        try {
            const url = new URL(location.href);
            return /factions\.php$/i.test(url.pathname || '') &&
                String(url.searchParams.get('step') || '').toLowerCase() === 'your';
        } catch {
            return /factions\.php\?step=your/i.test(location.href);
        }
    }

    function buildFactionObject(id, name, href, source) {
        const factionId = cleanString(id);
        const factionName = cleanString(name);

        if (!isValidFactionId(factionId) || !factionName) return null;

        return {
            faction_id: factionId,
            faction_name: factionName,
            faction_url: href ? new URL(href, location.origin).href : factionProfileUrl(factionId),
            source: source || 'Home'
        };
    }

    function readSelfFactionFromHomeOnly() {
        if (!isHomePageForSelf()) return null;

        const links = qsa('a[href*="factions.php?step=profile"][href*="ID="]');

        for (const a of links) {
            const href = a.getAttribute('href') || '';
            const id = extractParamFromUrl(href, 'ID');
            const name = safeText(a);

            if (!isValidFactionId(id) || !name) continue;

            const block = a.closest('li, div, section, article, table, tbody, tr') || a.parentElement || a;
            const blockText = safeText(block);

            if (
                /faction/i.test(blockText) ||
                /general information/i.test(blockText) ||
                /your faction/i.test(blockText)
            ) {
                const obj = buildFactionObject(id, name, href, 'Home faction info');
                if (obj) return obj;
            }
        }

        return null;
    }

    function detectEnergyFromDom() {
        const selectors = [
            'div[class*="bar"][class*="energy"]',
            '[class*="energy"]',
            '[aria-label*="Energy"]',
            '[class*="energy__"]',
            '[class*="bar-mobile__"][class*="energy__"]'
        ];

        for (const sel of selectors) {
            for (const node of qsa(sel)) {
                const txt = safeText(node);
                const m = txt.match(/(\d+)\s*\/\s*(\d+)/);
                if (m) return { current: Number(m[1]), max: Number(m[2]) };
            }
        }

        return null;
    }

    function parseChainRatioFromText(txt) {
        const raw = String(txt || '').replace(/\s+/g, ' ').trim();
        if (!raw) return null;

        const ratio =
            raw.match(/([0-9][0-9,]*)\s*\/\s*([0-9][0-9,]*)(k)?/i) ||
            raw.match(/([0-9][0-9,]*)\s+\/\s+([0-9][0-9,]*)(k)?/i);

        if (!ratio) return null;

        let maxNum = Number(String(ratio[2]).replace(/,/g, ''));
        if (ratio[3]) maxNum *= 1000;

        return {
            value: String(Number(String(ratio[1]).replace(/,/g, ''))),
            max: String(Math.trunc(maxNum || 0))
        };
    }

    function readMobileChainBar() {
        const tooltipCandidates = [
            ...qsa('[role="tooltip"]'),
            ...qsa('[data-floating-ui-portal] [role="tooltip"]'),
            ...qsa('[id^="__r_"][role="tooltip"]'),
            ...qsa('div[class*="tooltip"]')
        ].filter(Boolean);

        for (const tip of tooltipCandidates) {
            const txt = safeText(tip);
            if (!txt || !/\bchain\b/i.test(txt)) continue;

            const ratio = parseChainRatioFromText(txt);

            const timeNode =
                qs('[class*="bar-timeleft"]', tip) ||
                qsa('span,p,div', tip).find(n => /\d{1,2}:\d{2}(?::\d{2})?/.test(safeText(n)));

            const timeTxt = safeText(timeNode) || txt;
            const time = timeTxt.match(/(\d{1,2}:\d{2}(?::\d{2})?)/);

            if (ratio || time) {
                return {
                    chain_value: ratio ? ratio.value : '',
                    chain_max: ratio ? ratio.max : '',
                    chain_time_left: time ? time[1] : '',
                    timer_source: time ? 'tooltip' : 'tooltip_no_timer'
                };
            }
        }

        const bars = [
            ...qsa('a[class*="chain-bar"]'),
            ...qsa('a[class*="bar-mobile"][class*="chain"]'),
            ...qsa('a[href="#"][class*="chain"]'),
            ...qsa('[class*="bar-mobile"][class*="chain"]')
        ].filter(Boolean);

        for (const bar of bars) {
            const allText = safeText(bar);
            if (!/\bchain\b/i.test(allText)) continue;

            const nameNode = qsa('p,span,div', bar).find(n => /^chain$/i.test(safeText(n)));
            if (!nameNode && !/\bchain\b/i.test(allText)) continue;

            const valueNode =
                qs('p[class*="bar-value"], [class*="bar-value"]', bar) ||
                qsa('p,span,div', bar).find(n => parseChainRatioFromText(safeText(n)));

            const timeNode =
                qs('p[class*="bar-timeleft"], [class*="bar-timeleft"]', bar) ||
                qsa('p,span,div', bar).find(n => /\d{1,2}:\d{2}(?::\d{2})?/.test(safeText(n)));

            const valueTxt = safeText(valueNode) || allText;
            const timeTxt = safeText(timeNode) || allText;

            const ratio = parseChainRatioFromText(valueTxt) || parseChainRatioFromText(allText);
            const time = timeTxt.match(/(\d{1,2}:\d{2}(?::\d{2})?)/);

            if (ratio || time) {
                return {
                    chain_value: ratio ? ratio.value : '',
                    chain_max: ratio ? ratio.max : '',
                    chain_time_left: time ? time[1] : '',
                    timer_source: time ? 'bar' : 'bar_no_timer'
                };
            }
        }

        return null;
    }

    function detectChainFromDom() {
        const mobile = readMobileChainBar();
        if (mobile) return mobile;

        const chainBar = qs('a[class*="chain-bar"], a[href*="chainreport"], a[href*="war.php?step=chain"]');

        if (chainBar) {
            const valueNode = qs('p[class*="bar-value"], [class*="bar-value"]', chainBar);
            const timeNode = qs('p[class*="bar-timeleft"], [class*="bar-timeleft"]', chainBar);

            const valueTxt = safeText(valueNode);
            const timeTxt = safeText(timeNode);

            const ratioObj = parseChainRatioFromText(valueTxt || safeText(chainBar));
            const time = (timeTxt || safeText(chainBar)).match(/(\d{1,2}:\d{2}(?::\d{2})?)/);

            if (ratioObj || time) {
                return {
                    chain_value: ratioObj ? ratioObj.value : '',
                    chain_max: ratioObj ? ratioObj.max : '',
                    chain_time_left: time ? time[1] : '',
                    timer_source: time ? 'desktop_bar' : 'desktop_bar_no_timer'
                };
            }
        }

        const candidates = qsa('[class*="chain"], a[href*="chainreport"], a[href*="war.php?step=chain"]');

        for (const node of candidates) {
            const txt = safeText(node);
            if (!txt || !/chain/i.test(txt)) continue;
            if (txt.length > 180) continue;

            const ratioObj = parseChainRatioFromText(txt);
            const time = txt.match(/(\d{1,2}:\d{2}(?::\d{2})?)/);

            if (ratioObj || time) {
                return {
                    chain_value: ratioObj ? ratioObj.value : '',
                    chain_max: ratioObj ? ratioObj.max : '',
                    chain_time_left: time ? time[1] : '',
                    timer_source: time ? 'fallback' : 'fallback_no_timer'
                };
            }
        }

        return null;
    }

    function applyChainDom(chain) {
        if (!chain) return false;

        let changed = false;

        const mobile = isMobileWidth();
        const oldChainKey = makeChainKey(mem.chain_value, mem.chain_max);

        const incomingValue = chain.chain_value !== '' ? cleanString(chain.chain_value) : mem.chain_value;
        const incomingMax = chain.chain_max !== '' ? cleanString(chain.chain_max) : mem.chain_max;
        const incomingChainKey = makeChainKey(incomingValue, incomingMax);

        const chainNumberChanged =
            mobile &&
            oldChainKey &&
            incomingChainKey &&
            incomingChainKey !== oldChainKey;

        if (chainNumberChanged && !chain.chain_time_left) {
            invalidateMobileChainTimer();
            changed = true;
        }

        if (chain.chain_value !== '' && mem.chain_value !== chain.chain_value) {
            mem.chain_value = chain.chain_value;
            changed = true;
        }

        if (chain.chain_max !== '' && mem.chain_max !== chain.chain_max) {
            mem.chain_max = chain.chain_max;
            changed = true;
        }

        if (chain.chain_time_left !== '') {
            const ms = parseChainTimeLeftToMs(chain.chain_time_left);
            if (ms != null) {
                state.chainDeadlineMs = Date.now() + ms;
                state.chainLastDomText = chain.chain_time_left;
                state.chainLastDomReadAt = Date.now();
                state.chainTimerChainKey = makeChainKey(
                    chain.chain_value || mem.chain_value,
                    chain.chain_max || mem.chain_max
                );
                state.chainTimerFromTooltip = String(chain.timer_source || '').includes('tooltip');
            }

            if (mem.chain_time_left !== chain.chain_time_left) {
                mem.chain_time_left = chain.chain_time_left;
                changed = true;
            }
        } else if (mobile) {
            const currentKey = makeChainKey(mem.chain_value, mem.chain_max);
            if (state.chainTimerChainKey && currentKey && state.chainTimerChainKey !== currentKey) {
                invalidateMobileChainTimer();
                changed = true;
            }
        }

        return changed;
    }

    function fastChainScan() {
        if (state.paused) return;

        const chain = detectChainFromDom();
        const changed = applyChainDom(chain);

        if (changed) updateChainSummaryOnly();
    }

    async function refreshIdentityFromActivePage() {
        if (state.paused) return;

        try {
            const player = getPlayerFromHiddenInput();

            if (player) {
                mem.player_id = player.player_id;
                mem.player_name = player.player_name;
                mem.player_url = player.player_url;
            }

            const faction = readSelfFactionFromHomeOnly();

            if (faction) {
                mem.faction_id = faction.faction_id;
                mem.faction_name = faction.faction_name;
                mem.faction_url = faction.faction_url;
                mem.last_identity_source = faction.source || 'Home';
            }

            const energy = detectEnergyFromDom();
            if (energy) {
                mem.energy_current = String(energy.current);
                mem.energy_max = String(energy.max);
            }

            fastChainScan();

            queuePersist();
        } catch (e) {
            warn('refreshIdentityFromActivePage failed', e?.message || e);
        }
    }

    function getIdentityForSync() {
        return {
            player_id: cleanString(mem.player_id),
            player_name: cleanString(mem.player_name),
            player_url: cleanString(mem.player_url),
            faction_id: cleanString(mem.faction_id),
            faction_name: cleanString(mem.faction_name),
            faction_url: cleanString(mem.faction_url)
        };
    }

    function getPresencePayload() {
        const identity = getIdentityForSync();

        return {
            ...identity,
            share_energy: String(mem.share_energy || '1') === '1',
            energy_current: parseIntOrNull(mem.energy_current),
            energy_max: parseIntOrNull(mem.energy_max),
            chain_value: parseIntOrNull(mem.chain_value),
            chain_max: parseIntOrNull(mem.chain_max),
            chain_time_left: cleanString(mem.chain_time_left) || null
        };
    }

    function extractDiscoveredPlayersFromDom() {
        const out = new Map();
        const selfId = cleanString(mem.player_id);

        for (const a of qsa('a[href*="profiles.php?XID="]')) {
            const href = a.getAttribute('href') || '';
            const id = extractParamFromUrl(href, 'XID');

            if (!isValidPlayerId(id) || id === selfId) continue;

            const name = safeText(a).replace(/\[[0-9]+\]/g, '').trim();
            if (!out.has(id)) out.set(id, { player_id: id, player_name: name || '', source: 'profile_link' });
        }

        for (const a of qsa('a[href*="user2ID="]')) {
            const href = a.getAttribute('href') || '';
            const id = extractParamFromUrl(href, 'user2ID');

            if (!isValidPlayerId(id) || id === selfId) continue;

            if (!out.has(id)) out.set(id, { player_id: id, player_name: safeText(a) || '', source: 'attack_link' });
        }

        return [...out.values()].slice(0, 80);
    }

    async function sendDiscoveredPlayers() {
        if (state.paused || state.sendingDiscovery) return;

        const identity = getIdentityForSync();
        if (!identity.player_id) return;

        const players = extractDiscoveredPlayersFromDom();
        if (!players.length) return;

        const signature = players.map(x => x.player_id).sort().join(',');
        if (signature && signature === state.lastDiscoverySignature) return;

        state.sendingDiscovery = true;

        try {
            await workerJson({
                method: 'POST',
                path: '/api/players/discovered',
                data: {
                    source_player_id: identity.player_id,
                    source_faction_id: identity.faction_id || '',
                    players
                },
                timeout: 20000
            });

            state.lastDiscoverySignature = signature;
        } catch (e) {
            warn('sendDiscoveredPlayers failed', e?.message || e);
        } finally {
            state.sendingDiscovery = false;
        }
    }

    function applyBoardSummary(rawBoard) {
        const board = rawBoard && typeof rawBoard === 'object' ? rawBoard : {};
        const onlineUsers = Array.isArray(board.online_users) ? board.online_users : [];
        const watchSlots = Array.isArray(board.watch_slots)
            ? board.watch_slots
            : Array.isArray(board.timeline) ? board.timeline : [];

        const summary = board.summary && typeof board.summary === 'object' ? board.summary : {};

        const sortedSlots = watchSlots
            .filter(x => x && x.start_time_utc && x.end_time_utc)
            .sort((a, b) => Date.parse(a.start_time_utc) - Date.parse(b.start_time_utc));

        const now = Date.now();

        const currentWatch = sortedSlots.find(slot => {
            const s = Date.parse(slot.start_time_utc || '');
            const e = Date.parse(slot.end_time_utc || '');
            return Number.isFinite(s) && Number.isFinite(e) && now >= s && now < e;
        }) || null;

        const nextWatch = sortedSlots.find(slot => {
            const s = Date.parse(slot.start_time_utc || '');
            return Number.isFinite(s) && s > now;
        }) || null;

        mem.board = {
            online_count: Number(summary.online_count || onlineUsers.length || 0),
            energy_total_current: Number(summary.energy_total_current || 0),
            energy_total_max: Number(summary.energy_total_max || 0),
            current_watch: currentWatch,
            next_watch: nextWatch,
            watch_slots: sortedSlots.slice(0, 120)
        };

        const serverChainValue = parseIntOrNull(summary.chain_value);
        const serverChainMax = parseIntOrNull(summary.chain_max);
        const serverChainTime = cleanString(summary.chain_time_left);

        if (!mem.chain_value && serverChainValue != null) mem.chain_value = String(serverChainValue);

        if (!mem.chain_max && serverChainMax != null && serverChainMax > 0 && serverChainMax !== 10000) {
            mem.chain_max = String(serverChainMax);
        }

        if (!mem.chain_time_left && serverChainTime) {
            mem.chain_time_left = serverChainTime;
            const ms = parseChainTimeLeftToMs(serverChainTime);
            if (ms != null) {
                state.chainDeadlineMs = Date.now() + ms;
                state.chainTimerChainKey = makeChainKey(mem.chain_value, mem.chain_max);
                state.chainTimerFromTooltip = false;
            }
        }
    }

    async function applyServerState(boardRes, meRes) {
        if (boardRes && typeof boardRes === 'object') applyBoardSummary(boardRes.board || boardRes);

        if (meRes && typeof meRes === 'object') {
            const me = meRes.me || {};
            mem.me = {
                active_watch: me.active_watch && typeof me.active_watch === 'object' ? me.active_watch : null,
                next_watch: me.next_watch && typeof me.next_watch === 'object' ? me.next_watch : null
            };

            if (me.share_energy != null) mem.share_energy = String(me.share_energy ? 1 : 0);
        }
    }

    async function syncPresence() {
        if (state.paused || state.syncingPresence) return;

        state.syncingPresence = true;

        try {
            const identity = getIdentityForSync();
            if (!identity.player_id || !identity.player_name || !identity.faction_id || !identity.faction_name) return;

            await workerJson({
                method: 'POST',
                path: '/api/presence',
                data: getPresencePayload(),
                timeout: 20000
            });
        } catch (e) {
            warn('syncPresence failed', e?.message || e);
        } finally {
            state.syncingPresence = false;
        }
    }

    async function fetchFullState(reason = 'sync') {
        if (state.paused || state.syncingBoard) return;

        state.syncingBoard = true;

        try {
            const identity = getIdentityForSync();

            if (!identity.player_id || !identity.player_name) {
                await setStatus('Could not detect player.');
                return;
            }

            if (!identity.faction_id || !identity.faction_name) {
                await setStatus('Open Torn Home once so I can read your faction.');
                return;
            }

            await setStatus('Syncing...');

            const boardPath = `/api/board?faction_id=${encodeURIComponent(identity.faction_id)}`;
            const mePath = `/api/me?player_id=${encodeURIComponent(identity.player_id)}&faction_id=${encodeURIComponent(identity.faction_id)}`;

            const [boardRes, meRes] = await Promise.all([
                workerJson({ method: 'GET', path: boardPath, timeout: 25000 }),
                workerJson({ method: 'GET', path: mePath, timeout: 25000 })
            ]);

            await applyServerState(boardRes, meRes);
            fastChainScan();

            await setStatus('Synced.');

            renderActiveView();
            updateUI();
        } catch (e) {
            warn('fetchFullState failed', e?.message || e);
            await setStatus('Sync failed.');
        } finally {
            state.syncingBoard = false;
        }
    }

    function computeNextAlignedSyncInfo() {
        const now = Date.now();
        const next = Math.ceil(now / PRESENCE_SYNC_EVERY_MS) * PRESENCE_SYNC_EVERY_MS;
        const d = new Date(next);

        return {
            nextMs: next,
            label: `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}:${String(d.getUTCSeconds()).padStart(2, '0')} UTC`
        };
    }

    function scheduleAlignedSync() {
        if (state.syncTimeoutId) clearTimeout(state.syncTimeoutId);
        if (state.syncIntervalId) clearInterval(state.syncIntervalId);

        const info = computeNextAlignedSyncInfo();
        mem.next_sync_label = info.label;

        const delay = Math.max(50, info.nextMs - Date.now());

        state.syncTimeoutId = setTimeout(async () => {
            if (!state.paused) {
                await refreshIdentityFromActivePage();
                await syncPresence();
                await fetchFullState('board');
            }

            state.syncIntervalId = setInterval(async () => {
                const i = computeNextAlignedSyncInfo();
                mem.next_sync_label = i.label;

                if (!state.paused) {
                    await refreshIdentityFromActivePage();
                    await syncPresence();
                    await fetchFullState('board');
                }
            }, PRESENCE_SYNC_EVERY_MS);
        }, delay);
    }

    function getCurrentWatch() {
        return mem.board?.current_watch || null;
    }

    function getNextWatch() {
        return mem.board?.next_watch || null;
    }

    function getFactionEnergyTotal() {
        return Number(mem.board?.energy_total_current || 0);
    }

    function getMyWatchPlan() {
        const startMode = String(mem.watch_start_mode || 'now');

        const startHHMM = startMode === 'custom'
            ? (normalizeTime(mem.watch_start_hhmm || '') || floorNowToSlotHHMM())
            : floorNowToSlotHHMM();

        const durationHours = Math.max(0.5, Math.min(12, Number(mem.watch_duration_hours || 1) || 1));
        const startMinutes = hhmmToMinutes(startHHMM);
        if (startMinutes == null) return null;

        const now = new Date();

        const start = new Date(Date.UTC(
            now.getUTCFullYear(),
            now.getUTCMonth(),
            now.getUTCDate(),
            0, 0, 0, 0
        ) + startMinutes * 60000);

        if (startMode === 'custom' && start.getTime() < Date.now() - 60 * 1000) start.setUTCDate(start.getUTCDate() + 1);

        const end = new Date(start.getTime() + durationHours * 60 * 60 * 1000);

        return {
            start_iso: start.toISOString(),
            end_iso: end.toISOString(),
            start_hhmm: `${String(start.getUTCHours()).padStart(2, '0')}:${String(start.getUTCMinutes()).padStart(2, '0')}`,
            end_hhmm: `${String(end.getUTCHours()).padStart(2, '0')}:${String(end.getUTCMinutes()).padStart(2, '0')}`
        };
    }

    async function createWatch() {
        if (state.paused || state.syncingWatch) return;

        state.syncingWatch = true;

        try {
            const identity = getIdentityForSync();

            if (!identity.player_id || !identity.faction_id) {
                await setStatus('Could not detect player/faction.');
                return;
            }

            const plan = getMyWatchPlan();
            if (!plan) {
                await setStatus('Invalid watch time.');
                return;
            }

            await setStatus('Saving watch...');

            await workerJson({
                method: 'POST',
                path: '/api/watch',
                data: {
                    ...getPresencePayload(),
                    start_time_utc: plan.start_iso,
                    end_time_utc: plan.end_iso
                },
                timeout: 25000
            });

            await fetchFullState('watch');
        } catch (e) {
            await setStatus('Save failed.');
        } finally {
            state.syncingWatch = false;
        }
    }

    async function clearMyWatch() {
        if (state.paused || state.syncingWatch) return;

        state.syncingWatch = true;

        try {
            const identity = getIdentityForSync();

            if (!identity.player_id || !identity.faction_id) {
                await setStatus('Could not detect player/faction.');
                return;
            }

            await setStatus('Clearing watch...');

            await workerJson({
                method: 'POST',
                path: '/api/watch/clear',
                data: {
                    player_id: identity.player_id,
                    player_name: identity.player_name,
                    player_url: identity.player_url,
                    faction_id: identity.faction_id,
                    faction_name: identity.faction_name,
                    faction_url: identity.faction_url,
                    clear_all_mine: true
                },
                timeout: 25000
            });

            await fetchFullState('clear-watch');
        } catch (e) {
            await setStatus('Clear failed.');
        } finally {
            state.syncingWatch = false;
        }
    }

    function chunk(arr, size) {
        const out = [];
        for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
        return out;
    }

    async function removeTargetFromList(targetId) {
        const id = String(targetId || '');
        if (!id) return;

        mem.target_rows = (Array.isArray(mem.target_rows) ? mem.target_rows : [])
            .filter(row => String(row.player_id || '') !== id);

        await saveTargetCache(true);

        if (mem.ui_mode === 'targets') renderActiveView();
    }

    async function markTargetOpened(targetId) {
        try {
            const identity = getIdentityForSync();
            if (!identity.player_id || !targetId) return;

            await workerJson({
                method: 'POST',
                path: '/api/targets/opened',
                data: {
                    player_id: identity.player_id,
                    target_ids: [String(targetId)]
                },
                timeout: 15000
            });
        } catch {}
    }

    async function reportFfFailed(targetIds, reason = 'not_returned_by_ffscouter') {
        try {
            const ids = Array.from(new Set((targetIds || []).map(String).filter(isValidPlayerId))).slice(0, 100);
            if (!ids.length) return;

            await workerJson({
                method: 'POST',
                path: '/api/targets/ff-failed',
                data: {
                    failed_target_ids: ids,
                    reason
                },
                timeout: 15000
            });
        } catch {}
    }

    function normalizeFfRows(raw) {
        if (Array.isArray(raw)) return raw;
        if (Array.isArray(raw?.data)) return raw.data;
        if (Array.isArray(raw?.results)) return raw.results;
        if (Array.isArray(raw?.targets)) return raw.targets;
        if (raw && typeof raw === 'object') return Object.values(raw).filter(x => x && typeof x === 'object');
        return [];
    }

    function readFfRowPlayerId(row) {
        return Number(row?.player_id || row?.id || row?.user_id || row?.target_id || row?.XID || 0);
    }

    function readFfScore(row) {
        return parseFloatOrNull(row?.fair_fight ?? row?.fairFight ?? row?.ff ?? row?.score);
    }

    function ffLabelFromScore(ffScore) {
        if (ffScore <= 1) return 'Extremely easy';
        if (ffScore <= 2) return 'Easy';
        if (ffScore <= 3.5) return 'Moderately difficult';
        if (ffScore <= 4.5) return 'Difficult';
        return 'May be impossible';
    }

    async function loadTargets() {
        if (state.paused || state.syncingTargets) return;

        state.syncingTargets = true;

        try {
            const identity = getIdentityForSync();
            const ffKey = cleanString(mem.ff_api_key);

            if (!identity.player_id) {
                mem.targets_error = 'Could not detect player.';
                mem.targets_loading = '';
                renderActiveView();
                return;
            }

            if (!identity.faction_id) {
                mem.targets_error = 'Could not detect faction. Open Torn Home once.';
                mem.targets_loading = '';
                renderActiveView();
                return;
            }

            if (!ffKey) {
                mem.targets_error = 'Enter your FFScouter API key from Settings on faction page.';
                mem.targets_loading = '';
                renderActiveView();
                return;
            }

            const minFF = parseFloatOrNull(mem.ff_min);
            const maxFF = parseFloatOrNull(mem.ff_max);
            const targetCount = clamp(parseIntOrNull(mem.target_count) || DEFAULT_TARGET_COUNT, 1, 25);

            mem.targets_loading = '1';
            mem.targets_error = '';
            mem.target_rows = [];
            renderActiveView();

            const rows = [];
            const matchedIds = new Set();
            let offset = 0;

            for (let batchIndex = 0; batchIndex < TARGET_MAX_BATCHES && rows.length < targetCount; batchIndex++) {
                const workerPath =
                    `/api/targets/candidates?player_id=${encodeURIComponent(identity.player_id)}` +
                    `&faction_id=${encodeURIComponent(identity.faction_id)}` +
                    `&limit=${FF_BATCH_SIZE}&offset=${offset}`;

                const candidateRes = await workerJson({
                    method: 'GET',
                    path: workerPath,
                    timeout: 30000
                });

                const candidates = Array.isArray(candidateRes?.candidates) ? candidateRes.candidates : [];
                if (!candidates.length) break;

                offset = Number(candidateRes?.next_offset || (offset + candidates.length));

                const ids = candidates.map(x => Number(x.player_id)).filter(Boolean);
                if (!ids.length) continue;

                for (const idsBatch of chunk(ids, FF_BATCH_SIZE)) {
                    if (rows.length >= targetCount) break;

                    const ffUrl = `https://ffscouter.com/api/v1/get-stats?key=${encodeURIComponent(ffKey)}&targets=${idsBatch.join(',')}`;
                    const ffRes = await xhrJson({
                        method: 'GET',
                        url: ffUrl,
                        headers: {
                            'Accept': 'application/json,text/plain,*/*',
                            'Cache-Control': 'no-cache'
                        },
                        timeout: 45000
                    });

                    const ffRows = normalizeFfRows(ffRes);
                    const returnedIds = new Set();

                    for (const row of ffRows) {
                        const playerId = readFfRowPlayerId(row);
                        const ffScore = readFfScore(row);

                        if (!playerId || ffScore == null) continue;

                        returnedIds.add(String(playerId));

                        if (matchedIds.has(String(playerId))) continue;
                        if (minFF != null && ffScore < minFF) continue;
                        if (maxFF != null && ffScore > maxFF) continue;

                        const source = candidates.find(c => Number(c.player_id) === playerId) || {};

                        rows.push({
                            player_id: playerId,
                            player_name: cleanString(source.player_name || row.name || row.player_name || `ID ${playerId}`),
                            player_level: parseIntOrNull(source.player_level || row.level),
                            player_faction_id: cleanString(source.player_faction_id || ''),
                            player_faction_name: cleanString(source.player_faction_name || ''),
                            ff_score: ffScore,
                            ff_label: ffLabelFromScore(ffScore),
                            profile_url: playerProfileUrl(playerId),
                            attack_url: `https://www.torn.com/loader.php?sid=attack&user2ID=${encodeURIComponent(playerId)}`
                        });

                        matchedIds.add(String(playerId));

                        if (rows.length >= targetCount) break;
                    }

                    const missing = idsBatch.map(String).filter(id => !returnedIds.has(id));
                    if (missing.length) reportFfFailed(missing, 'not_returned_by_ffscouter').catch(() => {});
                }
            }

            rows.sort((a, b) => (a.ff_score ?? 999) - (b.ff_score ?? 999));

            mem.target_rows = rows.slice(0, targetCount);
            mem.targets_loading = '';
            mem.targets_error = mem.target_rows.length ? '' : 'No targets matched your FF range.';

            await saveTargetCache(true);
            renderActiveView();
        } catch (e) {
            mem.targets_loading = '';
            mem.targets_error = 'Target load failed.';
            renderActiveView();
        } finally {
            state.syncingTargets = false;
        }
    }

    async function clearTargets() {
        mem.target_rows = [];
        mem.targets_error = '';
        await saveTargetCache(true);
        renderActiveView();
    }

    function getCountdownTarget() {
        if (state.chainDeadlineMs && Number.isFinite(state.chainDeadlineMs)) {
            const currentKey = makeChainKey(mem.chain_value, mem.chain_max);

            if (isMobileWidth() && state.chainTimerChainKey && currentKey && state.chainTimerChainKey !== currentKey) {
                invalidateMobileChainTimer();
                return null;
            }

            if (state.chainDeadlineMs <= Date.now()) {
                if (isMobileWidth()) invalidateMobileChainTimer();
                return null;
            }

            return { target_ms: state.chainDeadlineMs, type: 'chain' };
        }

        const chainLeft = parseChainTimeLeftToMs(mem.chain_time_left);
        if (chainLeft != null) {
            state.chainDeadlineMs = Date.now() + chainLeft;
            state.chainTimerChainKey = makeChainKey(mem.chain_value, mem.chain_max);
            return { target_ms: state.chainDeadlineMs, type: 'chain' };
        }

        return null;
    }

    function refreshCountdown() {
        const target = getCountdownTarget();
        const countEl = qs('#cc-countdown');
        const headEl = qs('#cc-header-countdown');

        if (!target?.target_ms) {
            if (countEl) {
                countEl.textContent = '--:--:--';
                countEl.className = 'cc-main-value';
            }

            if (headEl) headEl.textContent = '--:--:--';

            if (miniBtn) {
                miniBtn.textContent = '⏱';
                setMiniBtnStyleByRemaining(null);
                positionMiniButton();
            }

            return;
        }

        const ms = Math.max(0, target.target_ms - Date.now());
        const txt = formatDuration(ms);

        if (countEl) {
            countEl.textContent = txt;
            countEl.className = `cc-main-value ${countdownToneClass(ms)}`;
        }

        if (headEl) headEl.textContent = txt;

        if (miniBtn) {
            miniBtn.textContent = formatMiniCountdown(ms);
            setMiniBtnStyleByRemaining(ms);
            positionMiniButton();
        }
    }

    function updateChainSummaryOnly() {
        const summaryEl = qs('#cc-chain-summary-line');
        if (summaryEl) {
            summaryEl.textContent = `Chain: ${mem.chain_value || '-'} / ${mem.chain_max || '-'} • Timer: ${mem.chain_time_left || '-'}`;
        }
        refreshCountdown();
    }

    function setUiMode(mode) {
        mem.ui_mode = mode;
        queuePersist();
        renderActiveView();
    }

    function makeDurationOptionsHtml(selectedValue = '1') {
        const vals = [0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10, 12];
        const selected = String(selectedValue || '1');

        return vals.map(v => {
            const label = Number.isInteger(v) ? `${v} hour${v === 1 ? '' : 's'}` : `${v} hours`;
            return `<option value="${v}"${selected === String(v) ? ' selected' : ''}>${label}</option>`;
        }).join('');
    }

    function makeTctOptionsHtml(selectedValue = '') {
        const selected = normalizeTime(selectedValue || '') || '';
        const out = [];

        for (let m = 0; m < 24 * 60; m += SLOT_MINUTES) {
            const hhmm = minutesToHHMM(m);
            out.push(`<option value="${hhmm}"${selected === hhmm ? ' selected' : ''}>${hhmm} TCT</option>`);
        }

        return out.join('');
    }

    function renderSummaryView() {
        const host = qs('#cc-view-host');
        if (!host) return;

        const onlineCount = Number(mem.board?.online_count || 0);
        const current = getCurrentWatch();
        const next = getNextWatch();
        const myActive = mem.me?.active_watch || null;
        const factionEnergy = getFactionEnergyTotal();

        const currentWatchHtml = current
            ? `
                <div class="cc-main-value cc-wrap-value">
                    ${esc(current.player_name || '-')}
                    <span class="cc-small-muted">${esc(formatWatchUntil(current.end_time_utc))}</span>
                </div>
            `
            : `<div class="cc-main-value cc-wrap-value">-</div>`;

        host.innerHTML = `
            <div class="cc-view cc-view-summary">
                <div class="cc-section">
                    <div class="cc-main-row">
                        <div class="cc-main-block">
                            <div class="cc-main-label">Online now</div>
                            <div class="cc-main-value">${onlineCount}</div>
                        </div>
                        <div class="cc-main-block">
                            <div class="cc-main-label">Current watch</div>
                            ${currentWatchHtml}
                        </div>
                    </div>
                    <div class="cc-summary-subline" style="margin-top:8px;">Faction energy: ${factionEnergy}</div>
                </div>

                <div class="cc-section">
                    <div class="cc-main-row">
                        <div class="cc-main-block">
                            <div class="cc-main-label">Next watch</div>
                            <div class="cc-main-value cc-wrap-value">${next ? esc(formatIsoAsTct(next.start_time_utc)) : '-'}</div>
                        </div>
                        <div class="cc-main-block">
                            <div class="cc-main-label">Chain timer</div>
                            <div class="cc-main-value" id="cc-countdown">--:--:--</div>
                        </div>
                    </div>
                </div>

                <div class="cc-section">
                    <div class="cc-main-label" style="margin-bottom:6px;">Chain summary</div>
                    <div class="cc-main-value cc-wrap-value" id="cc-chain-summary-line">Chain: ${esc(mem.chain_value || '-')} / ${esc(mem.chain_max || '-')} • Timer: ${esc(mem.chain_time_left || '-')}</div>
                    <div class="cc-summary-subline">My watch: ${myActive ? esc(formatIsoRangeAsTct(myActive.start_time_utc, myActive.end_time_utc)) : '-'}</div>
                    <div class="cc-toolbar-row" style="margin-top:8px;">
                        <button type="button" id="cc-open-watch-btn" class="cc-small-btn cc-grow-btn">Watch</button>
                        <button type="button" id="cc-open-targets-btn" class="cc-small-btn cc-grow-btn">Find target</button>
                    </div>
                </div>

                <div id="cc-status">-</div>
            </div>
        `;

        qs('#cc-open-watch-btn', host)?.addEventListener('click', () => setUiMode('watch'));
        qs('#cc-open-targets-btn', host)?.addEventListener('click', () => setUiMode('targets'));

        refreshCountdown();
    }

    function renderGraph() {
        const startMs = floorToUtcHalfHour(Date.now());
        const slots = Array.isArray(mem.board?.watch_slots) ? mem.board.watch_slots : [];
        const cells = [];

        for (let i = 0; i < GRAPH_BAR_COUNT; i++) {
            const cellStart = startMs + i * TIMELINE_CELL_MINUTES * 60000;
            const cellEnd = cellStart + TIMELINE_CELL_MINUTES * 60000;
            const cellMid = cellStart + (cellEnd - cellStart) / 2;

            const names = [];

            for (const slot of slots) {
                const s = Date.parse(slot.start_time_utc || '');
                const e = Date.parse(slot.end_time_utc || '');

                if (!Number.isFinite(s) || !Number.isFinite(e)) continue;
                if (s <= cellMid && e > cellMid) names.push(slot.player_name || '-');
            }

            const count = names.length;
            const d = new Date(cellStart);
            const hhmm = `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`;
            const title = `${hhmm} UTC • ${count ? names.join(', ') : 'gap'}`;

            let cls = 'gap';
            if (count >= 3) cls = 'high';
            else if (count === 2) cls = 'mid';
            else if (count === 1) cls = 'low';

            cells.push(`<div class="cc-graph-cell ${cls}" title="${esc(title)}"></div>`);
        }

        return `
            <div class="cc-graph-head"><span>Now</span><span>+24h</span></div>
            <div class="cc-graph-wrap">${cells.join('')}</div>
            <div class="cc-graph-legend">
                <span><i class="cc-dot gap"></i>Gap</span>
                <span><i class="cc-dot low"></i>1 watcher</span>
                <span><i class="cc-dot mid"></i>+2</span>
                <span><i class="cc-dot high"></i>+3</span>
            </div>
        `;
    }

    function renderTimelineRows() {
        const slots = Array.isArray(mem.board?.watch_slots) ? mem.board.watch_slots : [];
        const current = getCurrentWatch();

        if (!slots.length) return `<div class="cc-empty-day">No watch slots yet.</div>`;

        return slots.slice(0, 20).map(slot => {
            const active = current && current.player_id === slot.player_id && current.start_time_utc === slot.start_time_utc;

            return `
                <div class="cc-timeline-row${active ? ' active' : ''}">
                    <div class="cc-timeline-main">
                        <div class="cc-timeline-title">${esc(slot.player_name || '-')}</div>
                        <div class="cc-timeline-sub">${esc(formatIsoRangeAsTct(slot.start_time_utc, slot.end_time_utc))}</div>
                    </div>
                </div>
            `;
        }).join('');
    }

    function renderWatchView() {
        const host = qs('#cc-view-host');
        if (!host) return;

        const plan = getMyWatchPlan();
        const myActive = mem.me?.active_watch || null;
        const onlineCount = Number(mem.board?.online_count || 0);
        const factionEnergy = getFactionEnergyTotal();

        host.innerHTML = `
            <div class="cc-view cc-view-schedule">
                <div class="cc-section cc-schedule-section">
                    <div class="cc-editor-meta">
                        <div class="cc-editor-meta-item">
                            <div class="cc-main-label">Online now</div>
                            <div class="cc-main-value">${onlineCount}</div>
                        </div>
                        <div class="cc-editor-meta-item">
                            <div class="cc-main-label">Faction energy</div>
                            <div class="cc-main-value">${factionEnergy}</div>
                        </div>
                    </div>

                    <div class="cc-ranges-title">My watch</div>

                    <div class="cc-watch-mode-row">
                        <button type="button" class="cc-week-tab ${String(mem.watch_start_mode || 'now') === 'now' ? 'active' : ''}" id="cc-start-now-btn">Now</button>
                        <button type="button" class="cc-week-tab ${String(mem.watch_start_mode || 'now') === 'custom' ? 'active' : ''}" id="cc-start-custom-btn">Custom</button>
                    </div>

                    <div class="cc-range-row watch-time-row">
                        <select class="cc-range-select" id="cc-watch-start-select" ${String(mem.watch_start_mode || 'now') === 'custom' ? '' : 'disabled'}>
                            ${makeTctOptionsHtml(String(mem.watch_start_hhmm || floorNowToSlotHHMM()))}
                        </select>
                        <span class="cc-range-sep">→</span>
                        <select class="cc-range-select" id="cc-watch-duration-select">
                            ${makeDurationOptionsHtml(mem.watch_duration_hours || '1')}
                        </select>
                    </div>

                    <div class="cc-empty-day">Planned: ${plan ? `${plan.start_hhmm} → ${plan.end_hhmm}` : '-'}</div>

                    <div class="cc-toolbar-row" style="margin-top:8px;">
                        <button type="button" id="cc-create-watch-btn" class="cc-small-btn cc-grow-btn cc-save-watch-main">Save watch</button>
                    </div>

                    ${myActive ? `
                        <div class="cc-toolbar-row" style="margin-top:8px;">
                            <button type="button" id="cc-clear-watch-btn" class="cc-small-btn cc-grow-btn cc-clear-watch-main">Clear my watch</button>
                        </div>
                    ` : ''}

                    <div class="cc-ranges-title" style="margin-top:14px;">Coverage graph</div>
                    ${renderGraph()}

                    <div class="cc-ranges-title" style="margin-top:12px;">Timeline</div>
                    <div class="cc-range-list">${renderTimelineRows()}</div>

                    <div class="cc-toolbar-row" style="margin-top:10px;">
                        <button type="button" id="cc-open-home-btn" class="cc-small-btn cc-grow-btn">Home</button>
                        <button type="button" id="cc-open-targets-btn" class="cc-small-btn cc-grow-btn">Find target</button>
                    </div>
                </div>

                <div id="cc-status">-</div>
            </div>
        `;

        qs('#cc-start-now-btn', host)?.addEventListener('click', () => {
            mem.watch_start_mode = 'now';
            queuePersist();
            renderWatchView();
        });

        qs('#cc-start-custom-btn', host)?.addEventListener('click', () => {
            mem.watch_start_mode = 'custom';
            if (!normalizeTime(mem.watch_start_hhmm || '')) mem.watch_start_hhmm = floorNowToSlotHHMM();
            queuePersist();
            renderWatchView();
        });

        qs('#cc-watch-start-select', host)?.addEventListener('change', (e) => {
            mem.watch_start_hhmm = normalizeTime(e.target.value || '') || floorNowToSlotHHMM();
            queuePersist();
            renderWatchView();
        });

        qs('#cc-watch-duration-select', host)?.addEventListener('change', (e) => {
            mem.watch_duration_hours = String(e.target.value || '1');
            queuePersist();
            renderWatchView();
        });

        qs('#cc-create-watch-btn', host)?.addEventListener('click', () => createWatch().catch(() => {}));
        qs('#cc-clear-watch-btn', host)?.addEventListener('click', () => clearMyWatch().catch(() => {}));
        qs('#cc-open-home-btn', host)?.addEventListener('click', () => setUiMode('summary'));
        qs('#cc-open-targets-btn', host)?.addEventListener('click', () => setUiMode('targets'));
    }

    function renderTargetRows() {
        if (mem.targets_loading === '1') return `<div class="cc-empty-day">Scanning targets...</div>`;
        if (mem.targets_error) return `<div class="cc-empty-day">${esc(mem.targets_error)}</div>`;

        const rows = Array.isArray(mem.target_rows) ? mem.target_rows : [];
        if (!rows.length) return `<div class="cc-empty-day">No targets loaded yet.</div>`;

        return rows.map(row => `
            <div class="cc-target-row">
                <div class="cc-target-main">
                    <div class="cc-target-title">${esc(row.player_name || `ID ${row.player_id}`)}</div>
                    <div class="cc-target-sub">
                        ${row.player_level != null ? `Lvl ${esc(row.player_level)} • ` : ''}
                        ${row.player_faction_name ? `${esc(row.player_faction_name)} • ` : ''}
                        ${esc(row.ff_label || '-')}
                        ${row.ff_score != null ? ` • FF ${Number(row.ff_score).toFixed(2)}` : ''}
                    </div>
                </div>
                <div class="cc-target-actions">
                    <a class="cc-target-btn attack" href="${esc(row.attack_url)}" target="_blank" rel="noopener noreferrer" data-use-target-id="${esc(row.player_id)}">Attack</a>
                    <a class="cc-target-btn profile" href="${esc(row.profile_url)}" target="_blank" rel="noopener noreferrer" data-use-target-id="${esc(row.player_id)}">Profile</a>
                </div>
            </div>
        `).join('');
    }

    function renderTargetsView() {
        const host = qs('#cc-view-host');
        if (!host) return;

        const hasList = Array.isArray(mem.target_rows) && mem.target_rows.length > 0;
        const ffMissingHtml = mem.ff_api_key
            ? ``
            : `<div class="cc-section cc-alert-section"><div class="cc-empty-day">Add your FFScouter API key from Settings on your faction page.</div></div>`;

        host.innerHTML = `
            <div class="cc-view cc-view-schedule">
                <div class="cc-section cc-schedule-section">
                    ${ffMissingHtml}

                    <div class="cc-ranges-title">Target finder</div>
                    <div class="cc-main-label cc-ff-label">Fair Fight:</div>

                    <div class="cc-range-row targets">
                        <input class="cc-range-input" id="cc-ff-min" type="text" inputmode="decimal" placeholder="Min FF" value="${esc(mem.ff_min || DEFAULT_FF_MIN)}">
                        <span class="cc-range-sep">→</span>
                        <input class="cc-range-input" id="cc-ff-max" type="text" inputmode="decimal" placeholder="Max FF" value="${esc(mem.ff_max || DEFAULT_FF_MAX)}">
                    </div>

                    <div class="cc-toolbar-row" style="margin-top:10px;">
                        <button type="button" id="cc-load-targets-btn" class="cc-small-btn cc-grow-btn">${hasList ? 'Refresh' : 'Scan'}</button>
                        ${hasList ? `<button type="button" id="cc-clear-targets-btn" class="cc-small-btn cc-grow-btn danger">Clear list</button>` : ''}
                    </div>

                    <div class="cc-range-list" style="margin-top:12px;">
                        ${renderTargetRows()}
                    </div>

                    <div class="cc-toolbar-row" style="margin-top:10px;">
                        <button type="button" id="cc-targets-home-btn" class="cc-small-btn cc-grow-btn">Home</button>
                        <button type="button" id="cc-targets-watch-btn" class="cc-small-btn cc-grow-btn">Watch</button>
                    </div>
                </div>

                <div id="cc-status">-</div>
            </div>
        `;

        qs('#cc-ff-min', host)?.addEventListener('input', (e) => {
            mem.ff_min = String(e.target.value || '').replace(/[^0-9.]/g, '') || DEFAULT_FF_MIN;
            queuePersist();
        });

        qs('#cc-ff-max', host)?.addEventListener('input', (e) => {
            mem.ff_max = String(e.target.value || '').replace(/[^0-9.]/g, '') || DEFAULT_FF_MAX;
            queuePersist();
        });

        qs('#cc-load-targets-btn', host)?.addEventListener('click', () => loadTargets().catch(() => {}));
        qs('#cc-clear-targets-btn', host)?.addEventListener('click', () => clearTargets().catch(() => {}));

        qsa('[data-use-target-id]', host).forEach(a => {
            a.addEventListener('click', () => {
                const id = a.getAttribute('data-use-target-id');
                removeTargetFromList(id).catch(() => {});
                markTargetOpened(id).catch(() => {});
            });
        });

        qs('#cc-targets-home-btn', host)?.addEventListener('click', () => setUiMode('summary'));
        qs('#cc-targets-watch-btn', host)?.addEventListener('click', () => setUiMode('watch'));
    }

    function renderActiveView() {
        try {
            if (!qs('#cc-view-host')) return;

            if (mem.ui_mode === 'watch') renderWatchView();
            else if (mem.ui_mode === 'targets') renderTargetsView();
            else renderSummaryView();

            updateUI();
        } catch (e) {
            errlog('renderActiveView failed', e);
        }
    }

    function updateUI() {
        const subtitle = qs('#cc-subtitle');

        if (subtitle) {
            if (mem.faction_name) {
                subtitle.textContent = `${mem.player_name || 'Unknown'} • ${mem.faction_name}`;
            } else {
                subtitle.textContent = `${mem.player_name || 'Unknown'} • Open Torn Home once`;
            }
        }

        const statusEl = qs('#cc-status');

        if (statusEl) {
            statusEl.textContent = state.paused ? 'Stopped.' : (mem.last_status || 'Waiting...');
        }

        if (panel) {
            panel.classList.toggle('cc-compact', window.innerWidth <= 768);
            panel.classList.toggle('cc-paused', state.paused);
        }

        refreshCountdown();
        applyVisibilityState();
        updateSettingsButtonVisibility();
    }

    function openSettingsModal() {
        if (!isFactionYourPage()) return;

        closeSettingsModal();

        settingsModal = document.createElement('div');
        settingsModal.id = 'cc-settings-modal';

        const startStopClass = state.paused ? 'start' : 'stop';
        const startStopText = state.paused ? 'Start' : 'Stop';

        settingsModal.innerHTML = `
            <div id="cc-settings-backdrop"></div>
            <div id="cc-settings-card">
                <div id="cc-settings-head">
                    <div>
                        <div id="cc-settings-title">Chain cordinator settings</div>
                        <div id="cc-settings-sub">Only available on faction page</div>
                    </div>
                    <button type="button" id="cc-settings-close">✕</button>
                </div>

                <div class="cc-settings-body">
                    <label class="cc-settings-field">
                        <span>FFScouter API key</span>
                        <input id="cc-set-ff-key" type="text" value="${esc(mem.ff_api_key || '')}" placeholder="Enter FFScouter API key">
                    </label>

                    <div class="cc-settings-grid">
                        <label class="cc-settings-field">
                            <span>Min FF</span>
                            <input id="cc-set-ff-min" type="text" inputmode="decimal" value="${esc(mem.ff_min || DEFAULT_FF_MIN)}">
                        </label>
                        <label class="cc-settings-field">
                            <span>Max FF</span>
                            <input id="cc-set-ff-max" type="text" inputmode="decimal" value="${esc(mem.ff_max || DEFAULT_FF_MAX)}">
                        </label>
                    </div>

                    <div class="cc-settings-grid">
                        <label class="cc-settings-field">
                            <span>Target count</span>
                            <input id="cc-set-target-count" type="text" inputmode="numeric" value="${esc(mem.target_count || DEFAULT_TARGET_COUNT)}">
                        </label>
                        <label class="cc-settings-field">
                            <span>Default watch hours</span>
                            <input id="cc-set-watch-hours" type="text" inputmode="decimal" value="${esc(mem.watch_duration_hours || '1')}">
                        </label>
                    </div>

                    <label class="cc-settings-field">
                        <span>Share energy</span>
                        <select id="cc-set-share-energy">
                            <option value="1"${String(mem.share_energy || '1') === '1' ? ' selected' : ''}>Yes</option>
                            <option value="0"${String(mem.share_energy || '1') === '0' ? ' selected' : ''}>No</option>
                        </select>
                    </label>

                    <div class="cc-settings-actions">
                        <button type="button" id="cc-save-settings">Save</button>
                        <button type="button" id="cc-reset-position">Reset position</button>
                        <button type="button" id="cc-toggle-pause-now" class="cc-start-stop-btn ${startStopClass}">${startStopText}</button>
                    </div>
                </div>
            </div>
        `;

        (document.body || document.documentElement).appendChild(settingsModal);

        qs('#cc-settings-close', settingsModal)?.addEventListener('click', closeSettingsModal);
        qs('#cc-settings-backdrop', settingsModal)?.addEventListener('click', closeSettingsModal);

        qs('#cc-save-settings', settingsModal)?.addEventListener('click', async () => {
            mem.ff_api_key = cleanString(qs('#cc-set-ff-key', settingsModal)?.value || '');
            mem.ff_min = cleanString(qs('#cc-set-ff-min', settingsModal)?.value || DEFAULT_FF_MIN).replace(/[^0-9.]/g, '') || DEFAULT_FF_MIN;
            mem.ff_max = cleanString(qs('#cc-set-ff-max', settingsModal)?.value || DEFAULT_FF_MAX).replace(/[^0-9.]/g, '') || DEFAULT_FF_MAX;
            mem.target_count = cleanString(qs('#cc-set-target-count', settingsModal)?.value || DEFAULT_TARGET_COUNT).replace(/[^0-9]/g, '') || String(DEFAULT_TARGET_COUNT);
            mem.watch_duration_hours = cleanString(qs('#cc-set-watch-hours', settingsModal)?.value || '1').replace(/[^0-9.]/g, '') || '1';
            mem.share_energy = String(qs('#cc-set-share-energy', settingsModal)?.value || '1') === '0' ? '0' : '1';

            await persistNow();
            closeSettingsModal();
            renderActiveView();
            updateUI();

            if (!state.paused) {
                syncPresence().catch(() => {});
                fetchFullState('settings').catch(() => {});
            }
        });

        qs('#cc-reset-position', settingsModal)?.addEventListener('click', () => {
            state.panelX = null;
            state.panelY = null;

            state.miniAnchorX = 'right';
            state.miniAnchorY = 'bottom';
            state.miniRight = 18;
            state.miniBottom = 52;
            state.miniLeft = 18;
            state.miniTop = 80;

            state.settingsAnchorX = 'right';
            state.settingsAnchorY = 'bottom';
            state.settingsRight = 14;
            state.settingsBottom = 104;
            state.settingsLeft = 14;
            state.settingsTop = 120;

            positionPanelDefault();
            positionMiniButton();
            positionSettingsButton();
            queuePersist();
        });

        qs('#cc-toggle-pause-now', settingsModal)?.addEventListener('click', async () => {
            setPaused(!state.paused);
            await persistNow();
            closeSettingsModal();
            updateUI();

            if (!state.paused) {
                syncPresence().catch(() => {});
                fetchFullState('start').catch(() => {});
            }
        });
    }

    function closeSettingsModal() {
        if (settingsModal) {
            try { settingsModal.remove(); } catch {}
            settingsModal = null;
        }
    }

    function setPaused(next) {
        state.paused = !!next;

        if (state.paused) {
            state.hidden = true;
            mem.last_status = 'Stopped.';
        } else {
            state.hidden = false;
            mem.last_status = 'Started.';
        }

        queuePersist();
        applyVisibilityState();
        updateUI();
    }

    function positionPanelDefault() {
        if (!panel) return;

        const defaultX = window.innerWidth - 290;
        const defaultY = 16;

        const x = clamp(state.panelX ?? defaultX, 0, Math.max(0, window.innerWidth - 260));
        const y = clamp(state.panelY ?? defaultY, 0, Math.max(0, window.innerHeight - 80));

        panel.style.left = `${x}px`;
        panel.style.top = `${y}px`;
    }

    function buildPanel() {
        if (document.getElementById('cc-panel')) {
            panel = document.getElementById('cc-panel');
            return;
        }

        panel = document.createElement('div');
        panel.id = 'cc-panel';

        if (state.collapsed) panel.classList.add('collapsed');

        panel.innerHTML = `
            <div id="cc-header">
                <div id="cc-header-icon">⏱</div>
                <div id="cc-header-title-wrap">
                    <div id="cc-header-title">Chain cordinator</div>
                    <div id="cc-header-countdown">--:--:--</div>
                </div>
                <div id="cc-header-actions">
                    <button class="cc-hdr-btn" id="cc-collapse-btn" title="Collapse / Expand">${state.collapsed ? '▲' : '▼'}</button>
                    <button class="cc-hdr-btn" id="cc-hide-btn" title="Minimize">✕</button>
                </div>
            </div>

            <div id="cc-body">
                <div id="cc-subtitle">Waiting for active page data...</div>
                <div id="cc-view-host"></div>
            </div>
        `;

        (document.body || document.documentElement).appendChild(panel);

        positionPanelDefault();

        const header = qs('#cc-header', panel);
        const collapseBtn = qs('#cc-collapse-btn', panel);
        const hideBtn = qs('#cc-hide-btn', panel);

        makeDraggable(header, panel, 'panel');

        collapseBtn?.addEventListener('click', (e) => {
            e.stopPropagation();

            state.collapsed = !state.collapsed;
            panel.classList.toggle('collapsed', state.collapsed);
            collapseBtn.textContent = state.collapsed ? '▲' : '▼';

            queuePersist();
            updateUI();
        });

        hideBtn?.addEventListener('click', (e) => {
            e.stopPropagation();
            state.hidden = true;
            queuePersist();
            applyVisibilityState();
            updateUI();
        });

        renderActiveView();
        applyVisibilityState();
    }

    function ensureMiniButton() {
        if (miniBtn) return;

        miniBtn = document.createElement('button');
        miniBtn.id = 'cc-mini-btn';
        miniBtn.textContent = '⏱';
        miniBtn.title = 'Open Chain cordinator';
        (document.body || document.documentElement).appendChild(miniBtn);

        makeDraggable(miniBtn, miniBtn, 'mini');

        miniBtn.addEventListener('click', (e) => {
            if (miniBtn.__dragMoved) {
                e.preventDefault();
                e.stopPropagation();
                return;
            }

            if (state.paused) return;

            state.hidden = false;
            state.collapsed = false;

            if (panel) panel.classList.remove('collapsed');

            queuePersist();
            applyVisibilityState();
            updateUI();
        });

        positionMiniButton();
    }

    function ensureSettingsButton() {
        if (settingsBtn) return;

        settingsBtn = document.createElement('button');
        settingsBtn.id = 'cc-settings-launcher';
        settingsBtn.textContent = '⚙ Chain';
        settingsBtn.title = 'Chain cordinator settings';

        (document.body || document.documentElement).appendChild(settingsBtn);

        makeDraggable(settingsBtn, settingsBtn, 'settings');

        settingsBtn.addEventListener('click', (e) => {
            if (settingsBtn.__dragMoved) {
                e.preventDefault();
                e.stopPropagation();
                return;
            }
            openSettingsModal();
        });

        updateSettingsButtonVisibility();
        positionSettingsButton();
    }

    function updateSettingsButtonVisibility() {
        if (!settingsBtn) return;
        settingsBtn.style.display = isFactionYourPage() ? 'flex' : 'none';
        if (isFactionYourPage()) positionSettingsButton();
    }

    function setMiniBtnStyleByRemaining(ms) {
        if (!miniBtn) return;

        miniBtn.classList.remove(
            'cc-mini-blue',
            'cc-mini-green',
            'cc-mini-yellow',
            'cc-mini-orange',
            'cc-mini-red',
            'cc-mini-icon-mode',
            'cc-mini-text-mode'
        );

        const txt = String(miniBtn.textContent || '');
        const iconMode = txt === '⏱';

        miniBtn.classList.add(iconMode ? 'cc-mini-icon-mode' : 'cc-mini-text-mode');
        miniBtn.classList.add(miniToneClass(ms));
    }

    function positionMiniButton() {
        if (!miniBtn) return;

        const width = miniBtn.offsetWidth || 44;
        const height = miniBtn.offsetHeight || 44;

        let x;
        let y;

        if (state.miniAnchorX === 'left') x = clamp(state.miniLeft, 0, Math.max(0, window.innerWidth - width));
        else x = clamp(window.innerWidth - width - state.miniRight, 0, Math.max(0, window.innerWidth - width));

        if (state.miniAnchorY === 'top') y = clamp(state.miniTop, 0, Math.max(0, window.innerHeight - height));
        else y = clamp(window.innerHeight - height - state.miniBottom, 0, Math.max(0, window.innerHeight - height));

        miniBtn.style.left = `${x}px`;
        miniBtn.style.top = `${y}px`;
    }

    function positionSettingsButton() {
        if (!settingsBtn) return;

        const width = settingsBtn.offsetWidth || 74;
        const height = settingsBtn.offsetHeight || 34;

        let x;
        let y;

        if (state.settingsAnchorX === 'left') x = clamp(state.settingsLeft, 0, Math.max(0, window.innerWidth - width));
        else x = clamp(window.innerWidth - width - state.settingsRight, 0, Math.max(0, window.innerWidth - width));

        if (state.settingsAnchorY === 'top') y = clamp(state.settingsTop, 0, Math.max(0, window.innerHeight - height));
        else y = clamp(window.innerHeight - height - state.settingsBottom, 0, Math.max(0, window.innerHeight - height));

        settingsBtn.style.left = `${x}px`;
        settingsBtn.style.top = `${y}px`;
        settingsBtn.style.right = 'auto';
        settingsBtn.style.bottom = 'auto';
    }

    function updateAnchorFromPosition(kind, x, y, width, height) {
        const distLeft = x;
        const distRight = window.innerWidth - (x + width);
        const distTop = y;
        const distBottom = window.innerHeight - (y + height);

        if (kind === 'mini') {
            if (distLeft <= distRight) {
                state.miniAnchorX = 'left';
                state.miniLeft = Math.max(0, distLeft);
            } else {
                state.miniAnchorX = 'right';
                state.miniRight = Math.max(0, distRight);
            }

            if (distTop <= distBottom) {
                state.miniAnchorY = 'top';
                state.miniTop = Math.max(0, distTop);
            } else {
                state.miniAnchorY = 'bottom';
                state.miniBottom = Math.max(0, distBottom);
            }
            return;
        }

        if (kind === 'settings') {
            if (distLeft <= distRight) {
                state.settingsAnchorX = 'left';
                state.settingsLeft = Math.max(0, distLeft);
            } else {
                state.settingsAnchorX = 'right';
                state.settingsRight = Math.max(0, distRight);
            }

            if (distTop <= distBottom) {
                state.settingsAnchorY = 'top';
                state.settingsTop = Math.max(0, distTop);
            } else {
                state.settingsAnchorY = 'bottom';
                state.settingsBottom = Math.max(0, distBottom);
            }
        }
    }

    function applyVisibilityState() {
        if (!panel) return;

        if (state.paused) {
            panel.style.display = 'none';
            if (miniBtn) miniBtn.style.display = 'none';
            return;
        }

        if (state.hidden) {
            panel.style.display = 'none';
            ensureMiniButton();
            miniBtn.style.display = 'flex';
            positionMiniButton();
            return;
        }

        panel.style.display = '';
        if (miniBtn) miniBtn.style.display = 'none';
    }

    function makeDraggable(handle, target, kind = 'panel') {
        let dragging = false;
        let pointerId = null;
        let ox = 0;
        let oy = 0;
        let sx = 0;
        let sy = 0;

        handle.style.touchAction = 'none';

        handle.addEventListener('pointerdown', (e) => {
            if (e.button !== undefined && e.button !== 0) return;
            if (kind === 'panel' && e.target && e.target.closest('.cc-hdr-btn')) return;

            dragging = true;
            pointerId = e.pointerId;

            sx = e.clientX;
            sy = e.clientY;

            if (kind === 'mini' || kind === 'settings') target.__dragMoved = false;

            const rect = target.getBoundingClientRect();
            ox = e.clientX - rect.left;
            oy = e.clientY - rect.top;

            try { handle.setPointerCapture(pointerId); } catch {}
            e.preventDefault();
        });

        handle.addEventListener('pointermove', (e) => {
            if (!dragging || e.pointerId !== pointerId) return;

            if (kind === 'mini' || kind === 'settings') {
                if (Math.abs(e.clientX - sx) > 5 || Math.abs(e.clientY - sy) > 5) target.__dragMoved = true;
            }

            const x = clamp(e.clientX - ox, 0, Math.max(0, window.innerWidth - target.offsetWidth));
            const y = clamp(e.clientY - oy, 0, Math.max(0, window.innerHeight - target.offsetHeight));

            target.style.left = `${x}px`;
            target.style.top = `${y}px`;
            target.style.right = 'auto';
            target.style.bottom = 'auto';

            if (kind === 'panel') {
                state.panelX = x;
                state.panelY = y;
            } else if (kind === 'mini' || kind === 'settings') {
                updateAnchorFromPosition(kind, x, y, target.offsetWidth || 44, target.offsetHeight || 44);
            }
        });

        const stop = (e) => {
            if (!dragging || e.pointerId !== pointerId) return;

            dragging = false;
            try { handle.releasePointerCapture(pointerId); } catch {}

            pointerId = null;
            queuePersist();

            if (kind === 'mini' || kind === 'settings') {
                setTimeout(() => { target.__dragMoved = false; }, 60);
            }
        };

        handle.addEventListener('pointerup', stop);
        handle.addEventListener('pointercancel', stop);
    }

    function startTimers() {
        if (state.chainFastTimerId) clearInterval(state.chainFastTimerId);
        state.chainFastTimerId = setInterval(() => {
            try {
                fastChainScan();
                refreshCountdown();
            } catch {}
        }, CHAIN_FAST_SCAN_MS);

        if (state.identityTimerId) clearInterval(state.identityTimerId);
        state.identityTimerId = setInterval(() => {
            refreshIdentityFromActivePage().catch(() => {});
        }, IDENTITY_SCAN_MS);

        if (state.discoveryTimerId) clearInterval(state.discoveryTimerId);
        state.discoveryTimerId = setInterval(() => {
            sendDiscoveredPlayers().catch(() => {});
        }, DISCOVERY_SCAN_MS);

        if (state.uiTimerId) clearInterval(state.uiTimerId);
        state.uiTimerId = setInterval(() => {
            updateUI();
        }, UI_REFRESH_MS);

        if (state.targetPollTimerId) clearInterval(state.targetPollTimerId);
        state.targetPollTimerId = setInterval(() => {
            loadTargetCache(true).catch(() => {});
        }, TARGET_CACHE_POLL_MS);

        if (state.routeTimerId) clearInterval(state.routeTimerId);
        state.routeTimerId = setInterval(() => {
            if (location.href !== state.lastRoute) {
                state.lastRoute = location.href;

                setTimeout(async () => {
                    try {
                        await sleep(900);
                        await loadTargetCache(true);
                        await refreshIdentityFromActivePage();
                        sendDiscoveredPlayers().catch(() => {});
                        renderActiveView();
                        updateUI();

                        const identity = getIdentityForSync();

                        if (!state.paused && identity.player_id && identity.faction_id) {
                            await syncPresence();
                            await fetchFullState('route');
                        }
                    } catch (e) {
                        warn('route refresh failed', e?.message || e);
                    }
                }, 450);
            }
        }, ROUTE_CHECK_MS);

        scheduleAlignedSync();

        window.addEventListener('resize', () => {
            try {
                if (panel && panel.style.display !== 'none') {
                    const x = clamp(parseInt(panel.style.left || '0', 10), 0, Math.max(0, window.innerWidth - panel.offsetWidth));
                    const y = clamp(parseInt(panel.style.top || '0', 10), 0, Math.max(0, window.innerHeight - panel.offsetHeight));

                    panel.style.left = `${x}px`;
                    panel.style.top = `${y}px`;

                    state.panelX = x;
                    state.panelY = y;
                }

                positionMiniButton();
                positionSettingsButton();
                queuePersist();
                updateUI();
            } catch {}
        });

        window.addEventListener('focus', () => {
            loadTargetCache(true).catch(() => {});
            fastChainScan();
            refreshCountdown();
        });

        window.addEventListener('beforeunload', () => {
            persistNow().catch(() => {});
        });

        document.addEventListener('visibilitychange', () => {
            if (document.hidden) {
                persistNow().catch(() => {});
            } else {
                loadTargetCache(true).catch(() => {});
                fastChainScan();
                refreshCountdown();
            }
        });
    }

    function injectStyles() {
        const css = cssText();

        if (typeof GM_addStyle === 'function') {
            GM_addStyle(css);
            return;
        }

        const style = document.createElement('style');
        style.textContent = css;
        (document.head || document.documentElement).appendChild(style);
    }

    async function waitForMountPoint() {
        if (document.body || document.documentElement) return;

        await new Promise(resolve => {
            const t = setInterval(() => {
                if (document.body || document.documentElement) {
                    clearInterval(t);
                    resolve();
                }
            }, 50);

            setTimeout(() => {
                clearInterval(t);
                resolve();
            }, 5000);
        });
    }

    async function boot() {
        await waitForMountPoint();
        await loadPersistedState();
        await loadTargetCache(true);
        setupTargetBroadcast();

        injectStyles();
        buildPanel();
        ensureSettingsButton();

        renderActiveView();
        updateUI();

        await sleep(BOOT_DOM_DELAY_MS);

        if (!state.paused) {
            fastChainScan();
            await refreshIdentityFromActivePage();
            await sleep(500);
            fastChainScan();
            await refreshIdentityFromActivePage();
        }

        renderActiveView();
        updateUI();

        startTimers();

        if (!state.paused) sendDiscoveredPlayers().catch(() => {});

        const identity = getIdentityForSync();

        if (!state.paused && identity.player_id && identity.faction_id) {
            setTimeout(async () => {
                fastChainScan();
                await refreshIdentityFromActivePage();
                await syncPresence();
                await fetchFullState('boot');
            }, BOOT_SYNC_DELAY_MS);
        } else if (!state.paused) {
            setTimeout(() => {
                fetchFullState('boot').catch(() => {});
            }, BOOT_SYNC_DELAY_MS);
        }
    }

    function cssText() {
        return `
#cc-panel, #cc-panel * {
  box-sizing: border-box;
  font-family: 'Segoe UI', system-ui, sans-serif;
  line-height: 1.35;
}

#cc-panel {
  position: fixed;
  z-index: 2147483647;
  width: 255px;
  border-radius: 14px;
  overflow: hidden;
  box-shadow:
    0 0 0 1px rgba(120,170,255,0.16),
    0 8px 40px rgba(0,0,0,0.55),
    0 2px 8px rgba(0,0,0,0.40);
  backdrop-filter: blur(12px) saturate(1.35);
  -webkit-backdrop-filter: blur(12px) saturate(1.35);
  background: linear-gradient(160deg, rgba(20,25,38,0.95) 0%, rgba(14,18,27,0.96) 100%);
  border: 1px solid rgba(120,170,255,0.20);
  user-select: none;
}

#cc-header {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 9px 10px;
  cursor: grab;
  background: linear-gradient(90deg, rgba(90,130,220,0.14) 0%, rgba(60,100,190,0.08) 100%);
  border-bottom: 1px solid rgba(120,170,255,0.15);
}

#cc-header:active { cursor: grabbing; }

#cc-header-icon {
  width: 18px;
  height: 18px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13px;
}

#cc-header-title-wrap { flex: 1; min-width: 0; }

#cc-header-title {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: #d8e7ff;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#cc-header-countdown {
  margin-top: 2px;
  font-size: 10px;
  color: rgba(255,255,255,0.72);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

#cc-header-actions {
  display: flex;
  gap: 4px;
  flex-shrink: 0;
}

.cc-hdr-btn {
  width: 20px;
  height: 20px;
  border: none;
  background: rgba(255,255,255,0.07);
  border-radius: 5px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(255,255,255,0.62);
  font-size: 10px;
  padding: 0;
}

.cc-hdr-btn:hover {
  background: rgba(120,170,255,0.18);
  color: #d8e7ff;
}

#cc-body {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  height: 390px;
  max-height: 70vh;
}

#cc-panel.collapsed #cc-body { display: none; }
#cc-panel.collapsed #cc-header { padding: 7px 10px; }
#cc-panel.collapsed #cc-header-title { font-size: 9px; }
#cc-panel.collapsed #cc-header-countdown { font-size: 11px; color: #fff; }

#cc-subtitle {
  padding: 8px 12px 6px;
  font-size: 11px;
  color: rgba(255,255,255,0.78);
  border-bottom: 1px solid rgba(255,255,255,0.06);
  min-height: 42px;
}

#cc-view-host {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.cc-view {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
}

.cc-section {
  padding: 8px 12px;
  border-bottom: 1px solid rgba(255,255,255,0.06);
}

.cc-alert-section {
  margin: 0 0 10px 0;
  border: 1px solid rgba(255,217,120,0.22);
  border-radius: 10px;
  background: rgba(255,217,120,0.08);
}

.cc-view-summary,
.cc-view-schedule {
  height: 100%;
}

.cc-view-schedule .cc-schedule-section {
  flex: 1 1 auto;
  overflow-y: auto;
}

.cc-main-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.cc-main-block { min-width: 0; }

.cc-main-label {
  font-size: 10px;
  color: rgba(255,255,255,0.42);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.cc-ff-label {
  margin-top: 8px;
  margin-bottom: -4px;
}

.cc-main-value {
  margin-top: 3px;
  font-size: 13px;
  font-weight: 700;
  color: #fff;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.cc-small-muted {
  display: inline-block;
  margin-left: 4px;
  font-size: 10px;
  font-weight: 600;
  color: rgba(255,255,255,0.52);
}

.cc-wrap-value {
  white-space: normal;
  overflow: visible;
  text-overflow: clip;
  line-height: 1.4;
}

.cc-summary-subline {
  margin-top: 7px;
  font-size: 11px;
  color: rgba(255,255,255,0.72);
  font-weight: 700;
}

#cc-status {
  margin-top: auto;
  padding: 8px 12px 10px;
  font-size: 10px;
  color: rgba(255,255,255,0.46);
  line-height: 1.25;
  border-top: 1px solid rgba(255,255,255,0.06);
}

.cc-toolbar-row {
  display: flex;
  gap: 6px;
  align-items: center;
}

.cc-grow-btn { flex: 1; }

.cc-small-btn {
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-size: 12px;
  font-weight: 700;
  padding: 8px 10px;
  color: #fff;
  background: rgba(255,255,255,0.08);
}

.cc-small-btn:hover { background: rgba(255,255,255,0.12); }
.cc-small-btn.danger:hover { background: rgba(255,80,80,0.20); }

.cc-save-watch-main {
  background: rgba(80,170,120,0.22);
  border: 1px solid rgba(120,255,170,0.20);
}

.cc-save-watch-main:hover {
  background: rgba(80,170,120,0.34);
}

.cc-clear-watch-main {
  background: rgba(255,80,80,0.28);
  border: 1px solid rgba(255,120,120,0.32);
}

.cc-clear-watch-main:hover {
  background: rgba(255,80,80,0.46);
}

#cc-mini-btn {
  position: fixed;
  height: 44px;
  min-height: 44px;
  padding: 0;
  border-radius: 50%;
  cursor: pointer;
  z-index: 2147483647;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 800;
  color: #fff;
  box-shadow: 0 4px 15px rgba(0,0,0,0.5);
  touch-action: none;
  border: 1px solid rgba(255,255,255,0.18);
  white-space: nowrap;
  overflow: hidden;
}

#cc-mini-btn.cc-mini-icon-mode {
  width: 44px !important;
  min-width: 44px !important;
  max-width: 44px !important;
  height: 44px !important;
  padding: 0 !important;
  border-radius: 50% !important;
  font-size: 20px;
}

#cc-mini-btn.cc-mini-text-mode {
  width: 58px;
  min-width: 58px;
  max-width: 58px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: 13px;
}

#cc-mini-btn.cc-mini-blue {
  background: linear-gradient(135deg, #15203a 0%, #1b2c52 100%);
  border-color: rgba(120,170,255,0.42);
}

#cc-mini-btn.cc-mini-green {
  background: linear-gradient(135deg, #113921 0%, #1f6b39 100%);
  border-color: rgba(120,255,170,0.45);
}

#cc-mini-btn.cc-mini-yellow {
  background: linear-gradient(135deg, #5f4a14 0%, #8e6f16 100%);
  border-color: rgba(255,217,120,0.55);
}

#cc-mini-btn.cc-mini-orange {
  background: linear-gradient(135deg, #6a3810 0%, #b35f12 100%);
  border-color: rgba(255,170,90,0.60);
}

#cc-mini-btn.cc-mini-red {
  background: linear-gradient(135deg, #5a1717 0%, #aa2323 100%);
  border-color: rgba(255,130,130,0.70);
}

.cc-good { color: #93efaf !important; }
.cc-info { color: #8fc1ff !important; }
.cc-warn { color: #ffd978 !important; }
.cc-orange { color: #ffbd7a !important; }
.cc-bad { color: #ff9b9b !important; font-weight: 900 !important; }

.cc-week-tab {
  width: 100%;
  border: 1px solid rgba(255,255,255,0.10);
  background: rgba(255,255,255,0.05);
  border-radius: 10px;
  padding: 8px 4px;
  color: #fff;
  cursor: pointer;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 12px;
  font-weight: 700;
}

.cc-week-tab.active {
  background: rgba(120,170,255,0.18);
  border-color: rgba(120,170,255,0.40);
}

.cc-editor-meta {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin: 0 0 10px;
}

.cc-ranges-title {
  font-size: 13px;
  font-weight: 700;
  color: #fff;
}

.cc-range-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.cc-range-row {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto;
  gap: 6px;
  align-items: center;
  margin-top: 10px;
}

.cc-range-row.watch-time-row {
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}

.cc-range-row.targets {
  grid-template-columns: 1fr auto 1fr;
}

.cc-range-select,
.cc-range-input {
  width: 100%;
  min-width: 0;
  background: rgba(255,255,255,0.07);
  border: 1px solid rgba(255,255,255,0.10);
  border-radius: 7px;
  padding: 7px 8px;
  font-size: 12px;
  color: #fff;
  outline: none;
}

.cc-range-select:disabled { opacity: 0.5; }

.cc-range-select option {
  color: #fff;
  background: #162038;
}

.cc-range-sep {
  font-size: 12px;
  color: rgba(255,255,255,0.72);
}

.cc-empty-day {
  font-size: 12px;
  color: rgba(255,255,255,0.72);
  line-height: 1.45;
}

.cc-watch-mode-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin-top: 10px;
}

.cc-timeline-row,
.cc-target-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding: 8px 10px;
  border-radius: 10px;
  border: 1px solid rgba(255,255,255,0.08);
  background: rgba(255,255,255,0.04);
}

.cc-timeline-row.active {
  border-color: rgba(120,170,255,0.38);
  background: rgba(120,170,255,0.12);
}

.cc-timeline-main,
.cc-target-main {
  min-width: 0;
  flex: 1;
}

.cc-timeline-title,
.cc-target-title {
  font-size: 12px;
  font-weight: 800;
  color: #fff;
}

.cc-timeline-sub,
.cc-target-sub {
  font-size: 11px;
  color: rgba(255,255,255,0.72);
  margin-top: 3px;
}

.cc-target-actions {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.cc-target-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 72px;
  padding: 6px 8px;
  border-radius: 8px;
  text-decoration: none;
  color: #fff;
  border: 1px solid rgba(255,255,255,0.18);
  font-size: 11px;
  font-weight: 800;
}

.cc-target-btn.attack {
  background: rgba(255,80,80,0.34);
  border-color: rgba(255,120,120,0.46);
}

.cc-target-btn.attack:hover {
  background: rgba(255,80,80,0.50);
}

.cc-target-btn.profile {
  background: rgba(80,210,130,0.30);
  border-color: rgba(120,255,170,0.42);
}

.cc-target-btn.profile:hover {
  background: rgba(80,210,130,0.46);
}

.cc-graph-head {
  display: flex;
  justify-content: space-between;
  color: rgba(255,255,255,0.62);
  font-size: 10px;
  margin: 10px 0 6px;
}

.cc-graph-wrap {
  display: grid;
  grid-template-columns: repeat(48, minmax(0, 1fr));
  gap: 2px;
}

.cc-graph-cell {
  height: 18px;
  border-radius: 4px;
  background: rgba(255,255,255,0.05);
}

.cc-graph-cell.gap { background: rgba(255,80,80,0.12); }
.cc-graph-cell.low { background: rgba(120,170,255,0.22); }
.cc-graph-cell.mid { background: rgba(255,217,120,0.28); }
.cc-graph-cell.high { background: rgba(120,255,170,0.28); }

.cc-graph-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 8px;
  font-size: 10px;
  color: rgba(255,255,255,0.72);
}

.cc-dot {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  margin-right: 4px;
  vertical-align: -1px;
}

.cc-dot.gap { background: rgba(255,80,80,0.55); }
.cc-dot.low { background: rgba(120,170,255,0.7); }
.cc-dot.mid { background: rgba(255,217,120,0.8); }
.cc-dot.high { background: rgba(120,255,170,0.75); }

#cc-settings-launcher {
  position: fixed;
  z-index: 2147483646;
  display: none;
  align-items: center;
  justify-content: center;
  border: 1px solid rgba(120,170,255,0.35);
  background: rgba(20,25,38,0.94);
  color: #fff;
  border-radius: 999px;
  padding: 8px 10px;
  font-size: 12px;
  font-weight: 800;
  box-shadow: 0 4px 15px rgba(0,0,0,0.45);
  cursor: grab;
  touch-action: none;
  user-select: none;
}

#cc-settings-launcher:active {
  cursor: grabbing;
}

#cc-settings-modal,
#cc-settings-modal * {
  box-sizing: border-box;
  font-family: 'Segoe UI', system-ui, sans-serif;
}

#cc-settings-modal {
  position: fixed;
  inset: 0;
  z-index: 2147483647;
}

#cc-settings-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.55);
}

#cc-settings-card {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(420px, calc(100vw - 24px));
  max-height: calc(100vh - 40px);
  transform: translate(-50%, -50%);
  background: #141923;
  color: #fff;
  border: 1px solid rgba(120,170,255,0.28);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: 0 16px 60px rgba(0,0,0,0.65);
}

#cc-settings-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 14px 16px;
  border-bottom: 1px solid rgba(255,255,255,0.08);
  background: rgba(120,170,255,0.08);
}

#cc-settings-title {
  font-size: 15px;
  font-weight: 900;
}

#cc-settings-sub {
  margin-top: 3px;
  font-size: 11px;
  color: rgba(255,255,255,0.55);
}

#cc-settings-close {
  border: none;
  background: rgba(255,255,255,0.08);
  color: #fff;
  border-radius: 8px;
  width: 30px;
  height: 30px;
  cursor: pointer;
}

.cc-settings-body {
  padding: 14px 16px 16px;
  overflow-y: auto;
  max-height: calc(100vh - 130px);
}

.cc-settings-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
}

.cc-settings-field {
  display: block;
  margin-bottom: 10px;
}

.cc-settings-field span {
  display: block;
  margin-bottom: 5px;
  font-size: 11px;
  color: rgba(255,255,255,0.62);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.cc-settings-field input,
.cc-settings-field select {
  width: 100%;
  background: rgba(255,255,255,0.08);
  border: 1px solid rgba(255,255,255,0.12);
  color: #fff;
  border-radius: 9px;
  padding: 9px 10px;
  outline: none;
  font-size: 13px;
}

.cc-settings-field select option {
  background: #141923;
  color: #fff;
}

.cc-settings-actions {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
}

.cc-settings-actions button {
  flex: 1;
  min-width: 120px;
  border: none;
  background: rgba(120,170,255,0.18);
  color: #fff;
  padding: 10px 12px;
  border-radius: 10px;
  cursor: pointer;
  font-weight: 800;
}

.cc-settings-actions button:hover {
  background: rgba(120,170,255,0.26);
}

.cc-start-stop-btn.start {
  background: rgba(80,210,130,0.30) !important;
  border: 1px solid rgba(120,255,170,0.36) !important;
}

.cc-start-stop-btn.start:hover {
  background: rgba(80,210,130,0.46) !important;
}

.cc-start-stop-btn.stop {
  background: rgba(255,80,80,0.34) !important;
  border: 1px solid rgba(255,120,120,0.42) !important;
}

.cc-start-stop-btn.stop:hover {
  background: rgba(255,80,80,0.50) !important;
}

#cc-panel.cc-compact { width: 238px; }

@media (max-width: 768px) {
  #cc-panel {
    width: 238px;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    background: #141923;
  }
}
        `;
    }

    boot().catch((e) => errlog('boot fatal failed', e));
})();