Chain cordinator

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==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));
})();