Project Blon Openfront Cheats

CHEAT GUI! Macros + Lobby + Adblock + Embargo + Combat + Diplomacy control panel for openfront.io

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Project Blon Openfront Cheats
// @namespace    http://tampermonkey.net/
// @version      22.8
// @description  CHEAT GUI! Macros + Lobby + Adblock + Embargo + Combat + Diplomacy control panel for openfront.io
// @author       blon
// @match        *://openfront.io/*
// @match        *://*.openfront.io/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const BLON_GAME_CACHE_KEY = '__blonGameView';

    function looksLikeGameView(value) {
        return !!value &&
            typeof value === 'object' &&
            typeof value.myPlayer === 'function' &&
            (
                typeof value.units === 'function' ||
                typeof value.unitStates === 'function' ||
                typeof value.players === 'function' ||
                typeof value.playerViews === 'function'
            );
    }

    function rememberGameView(value) {
        if (!looksLikeGameView(value)) return false;
        try {
            window[BLON_GAME_CACHE_KEY] = value;
        } catch (e) {}
        return true;
    }

    function readCachedGameView() {
        try {
            const game = window[BLON_GAME_CACHE_KEY];
            if (looksLikeGameView(game)) return game;
            for (const frame of document.querySelectorAll('iframe')) {
                try {
                    const framedGame = frame.contentWindow && frame.contentWindow[BLON_GAME_CACHE_KEY];
                    if (looksLikeGameView(framedGame)) return framedGame;
                } catch (e) {}
            }
            return null;
        } catch (e) {
            return null;
        }
    }

    function getAccessibleDocuments() {
        const docs = [document];
        try {
            for (const frame of document.querySelectorAll('iframe')) {
                try {
                    if (frame.contentDocument) docs.push(frame.contentDocument);
                } catch (e) {}
            }
        } catch (e) {}
        return docs;
    }

    function patchGameProperty(proto, prop) {
        const patchedKey = `__blon_${prop}_patched`;
        if (!proto || Object.prototype.hasOwnProperty.call(proto, patchedKey)) return;
        let desc = null;
        let cursor = proto;
        while (cursor && cursor !== Object.prototype) {
            desc = Object.getOwnPropertyDescriptor(cursor, prop);
            if (desc) break;
            cursor = Object.getPrototypeOf(cursor);
        }

        const storageKey = Symbol(`blon_${prop}`);
        try {
            Object.defineProperty(proto, prop, {
                configurable: true,
                get() {
                    if (desc && typeof desc.get === 'function') return desc.get.call(this);
                    return this[storageKey];
                },
                set(value) {
                    rememberGameView(value);
                    if (desc && typeof desc.set === 'function') {
                        desc.set.call(this, value);
                    } else {
                        this[storageKey] = value;
                    }
                },
            });
            Object.defineProperty(proto, patchedKey, {
                configurable: true,
                value: true,
            });
        } catch (e) {}
    }
    // this took longer than im proud of also they store the variable as 10x its normal value and that fooled me lmao
    function patchLeaderboardCtor(ctor) {
        if (!ctor || !ctor.prototype || ctor.prototype.__blonLeaderboardPatched) return;
        ctor.prototype.__blonLeaderboardPatched = true;
        const origUpdated = ctor.prototype.updated;
        ctor.prototype.updated = function(changedProperties) {
            if (origUpdated) {
                try {
                    origUpdated.call(this, changedProperties);
                } catch(e) {
                    console.error('[Blon] Error in original updated:', e);
                }
            }
            try {
                patchLeaderboardDOM(this);
            } catch(e) {
                console.error('[Blon] Error patching leaderboard in updated:', e);
            }
        };
    }

    let leaderboardIntervalId = null;
    function startLeaderboardLoop() {
        if (leaderboardIntervalId) {
            clearInterval(leaderboardIntervalId);
        }
        leaderboardIntervalId = setInterval(() => {
            try {
                patchLeaderboardDOM(document.querySelector('leader-board'));
            } catch(e) {}
        }, cfg.troopsCheckIntervalMs || 500);
    }

    function patchLeaderboardDOM(lb) {
        if (!lb) return;
        const grid = lb.querySelector('.grid');
        if (!grid) return;

        if (!cfg.showTroopsOverlay) {
            removeLeaderboardTroops(grid);
            return;
        }

        const targetStyle = "minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px) minmax(55px, 105px)";
        if (grid.style.gridTemplateColumns !== targetStyle) {
            grid.style.gridTemplateColumns = targetStyle;
        }

        const headerRow = grid.querySelector('.contents.font-bold');
        if (headerRow) {
            let troopsHeader = headerRow.querySelector('.blon-troops-header');
            if (!troopsHeader) {
                troopsHeader = document.createElement('div');
                troopsHeader.className = 'py-1 md:py-2 text-center border-b border-slate-500 truncate blon-troops-header';
                troopsHeader.textContent = 'Troops';
                headerRow.appendChild(troopsHeader);
            }
        }

        const rows = Array.from(grid.children).slice(1);
        const players = lb.players || [];
        for (let i = 0; i < rows.length; i++) {
            const row = rows[i];
            const playerEntry = players[i];
            if (!playerEntry) continue;
            if (!row.classList.contains('contents')) continue;

            let troopsCell = row.querySelector('.blon-troops-cell');
            if (!troopsCell) {
                troopsCell = document.createElement('div');
                troopsCell.className = 'py-1 md:py-2 text-center blon-troops-cell';
                row.appendChild(troopsCell);
            }

            const siblingCell = row.firstElementChild;
            if (siblingCell) {
                if (siblingCell.classList.contains('border-b')) {
                    troopsCell.classList.add('border-b', 'border-slate-500');
                } else {
                    troopsCell.classList.remove('border-b', 'border-slate-500');
                }
            }

            const player = playerEntry.player;
            let val = '0';
            if (player && typeof player.troops === 'function') {
                const tr = player.troops();
                const numericTroops = typeof tr === 'bigint' ? Number(tr) : tr;
                val = fmtNum(numericTroops / 10);
            }
            if (troopsCell.textContent !== val) {
                troopsCell.textContent = val;
            }
        }
    }

    function removeLeaderboardTroops(grid) {
        if (!grid) return;
        const origStyle = "minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px)";
        if (grid.style.gridTemplateColumns === "minmax(24px, 30px) minmax(60px, 100px) minmax(45px, 70px) minmax(40px, 55px) minmax(55px, 105px) minmax(55px, 105px)") {
            grid.style.gridTemplateColumns = origStyle;
        }

        const header = grid.querySelector('.blon-troops-header');
        if (header) header.remove();

        const cells = grid.querySelectorAll('.blon-troops-cell');
        cells.forEach(c => c.remove());
    }

    function installGameViewCapture() {
        try {
            patchGameProperty(window.HTMLElement && window.HTMLElement.prototype, 'game');
            patchGameProperty(window.HTMLElement && window.HTMLElement.prototype, 'g');
            if (window.customElements) {
                const lbCtor = window.customElements.get('leader-board');
                if (lbCtor && lbCtor.prototype && !lbCtor.prototype.__blonLeaderboardPatched) {
                    patchLeaderboardCtor(lbCtor);
                }
            }
            if (!window.customElements || window.customElements.__blonGameCapturePatched) return;
            const nativeDefine = window.customElements.define.bind(window.customElements);
            window.customElements.define = function(name, ctor, options) {
                try {
                    if (ctor && ctor.prototype) {
                        patchGameProperty(ctor.prototype, 'game');
                        patchGameProperty(ctor.prototype, 'g');
                        if (name === 'leader-board') {
                            patchLeaderboardCtor(ctor);
                        }
                    }
                } catch (e) {}
                return nativeDefine(name, ctor, options);
            };
            Object.defineProperty(window.customElements, '__blonGameCapturePatched', {
                configurable: true,
                value: true,
            });
        } catch (e) {}
    }

    installGameViewCapture();









    // ill probably move this method too
    // the bottom later its just i recently worked on it so i kept it at the top
    (function blonAdBlock() {
        const AD_DOMAINS = [
            'googlesyndication.com',
            'doubleclick.net',
            'doubleverify.com',
            'googleadservices.com',
            'googletag',
            'adservice.google',
            'pagead2.googlesyndication',
            'tpc.googlesyndication',
            'vpaid.doubleverify',
            'vtrk.dv.tech',
            'innovid.com',
            'ads.pubmatic.com',
            'secure.adnxs.com',
            'ib.adnxs.com',
            'rubiconproject.com',
            'openx.net',
            'advertising.com',
            'ad.doubleclick',
            'amazon-adsystem.com',
            'adsafeprotected.com',
            'moatads.com',
            'chartboost.com',
            'criteo.com',
            'bidswitch.net',
            'pubads.g.doubleclick',
            'securepubads.g.doubleclick',
        ];

        function isAdUrl(url) {
            if (!url) return false;
            try {
                const str = String(url).toLowerCase();
                return AD_DOMAINS.some(d => str.includes(d));
            } catch(e) { return false; }
        }

        const _fetch = window.fetch;
        window.fetch = function(input, init) {
            const url = (input && typeof input === 'object') ? input.url : input;
            if (isAdUrl(url)) {
                console.log('[Blon] blocked fetch:', url);
                return Promise.reject(new TypeError('hah'));
            }
            return _fetch.apply(this, arguments);
        };

        const _XHROpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            if (isAdUrl(url)) {
                console.log('[Blon] Blocked XHR:', url);
                this._blonBlocked = true;
                return;
            }
            return _XHROpen.apply(this, arguments);
        };
        const _XHRSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function() {
            if (this._blonBlocked) return;
            return _XHRSend.apply(this, arguments);
        };

        const AD_SELECTORS = [
            'iframe[src*="googlesyndication"]',
            'iframe[src*="doubleclick"]',
            'iframe[src*="doubleverify"]',
            'iframe[src*="googleadservices"]',
            'iframe[src*="innovid"]',
            'iframe[src*="adnxs"]',
            'iframe[src*="rubiconproject"]',
            'iframe[src*="openx"]',
            'iframe[src*="amazon-adsystem"]',
            'iframe[src*="criteo"]',
            'iframe[src*="bidswitch"]',
            'iframe[src*="moatads"]',
            'iframe[src*="adsafeprotected"]',
            'iframe[src*="pubads"]',
            'iframe[src*="vtrk.dv"]',
            'iframe[title="Advertisement"]',
            'iframe[title="advertisement"]',
            'script[src*="googlesyndication"]',
            'script[src*="doubleclick"]',
            'script[src*="doubleverify"]',
            'script[src*="googleadservices"]',
            'script[src*="adservice.google"]',
            'ins.adsbygoogle',
            'div[id*="google_ads"]',
            'div[class*="google_ads"]',
            'div[id*="ad-container"]',
            'div[data-ad]',
        ];

        function nukeAdElements(root) {
            AD_SELECTORS.forEach(sel => {
                try {
                    root.querySelectorAll(sel).forEach(el => {
                        console.log('[Blon] Removed element:', el.tagName, el.src || el.id || '');
                        el.remove();
                    });
                } catch(e) {}
            });
        }

        if (document.body) nukeAdElements(document);

        const adObserver = new MutationObserver(mutations => {
            for (const m of mutations) {
                for (const node of m.addedNodes) {
                    if (node.nodeType !== 1) continue; // elements only
                    const src = (node.src || node.href || '').toLowerCase();
                    if (isAdUrl(src)) {
                        console.log('[Blon] Removed injected node:', node.tagName, src);
                        node.remove();
                        continue;
                    }
                    //  title=Advertisement
                    if (node.tagName === 'IFRAME' && /advert/i.test(node.title || '')) {
                        console.log('[Blon] Removed ad iframe by title:', node.title);
                        node.remove();
                        continue;
                    }
                    if (node.querySelectorAll) nukeAdElements(node);
                }
            }
        });

        function startAdObserver() {
            adObserver.observe(document.documentElement || document.body, {
                childList: true,
                subtree: true
            });
        }

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                nukeAdElements(document);
                startAdObserver();
            });
        } else {
            nukeAdElements(document);
            startAdObserver();
        }
    })();
    // USED JLL Beautifier THX
    // btw i think i fixed header idk tho
    //cfg
    const STORAGE_KEY = 'blon_v10_cfg';

    let cfg = {
        useChargeTime: true,
        blockPassThrough: true,
        holdDelay: 3000,
        spamInterval: 25,
        selectedUnit: 'City',
        toggleMode: false,
        spamMethod: 'websocket',
        hotkeyMap: {
            'q': '1', 'w': '2', 'e': '3', 'r': '4', 't': '5',
            'y': '6', 'u': '7', 'i': '8', 'o': '9', 'p': '0',
            'z': 'atk_1', 'x': 'atk_2', 'c': 'atk_3', 'v': 'atk_4'
        },
        combatPercentages: {
            'atk_1': 10,
            'atk_2': 25,
            'atk_3': 50,
            'atk_4': 100
        },
        tradePercentages: {
            'donate_troops_1': 10,
            'donate_troops_2': 25,
            'donate_troops_3': 50,
            'donate_troops_4': 100,
            'donate_gold_1': 10,
            'donate_gold_2': 25,
            'donate_gold_3': 50,
            'donate_gold_4': 100
        },
        combatHotkeysPriority: false,
        combatSiloIndicator: false,
        combatSiloPanel: false,
        combatSiloOnlyAllies: false,
        combatSiloKeepAllPlaced: false,
        showGoldOverlay: false,
        showTroopsOverlay: false,
        troopsCheckIntervalMs: 500,
        goldRateWindowSeconds: 240,
        goldTopCount: 10,
        actionHotkeyMap: {
            'embargo_fire':   { mod: 'alt', key: 'x' },
            'embargo_lift':   { mod: 'alt', key: 'z' },
            'donate_troops_1': { mod: 'alt', key: 't' },
            'donate_troops_2': { mod: '', key: '' },
            'donate_troops_3': { mod: '', key: '' },
            'donate_troops_4': { mod: '', key: '' },
            'donate_gold_1':   { mod: 'alt', key: 'g' },
            'donate_gold_2':   { mod: '', key: '' },
            'donate_gold_3':   { mod: '', key: '' },
            'donate_gold_4':   { mod: '', key: '' }
        },
        guiOpacity: 1.0,
        overlayOpacity: 1.0,
        guiColor: '#00ff66',
        guiColorHue: 150,
        overlayColor: '#ffcc00',

        // Feature toggles (do not delete any hotkey/config; just disable behavior)
        features: {
            spamHotkeys: true,
            combatHotkeys: true,
            actionHotkeys: true,
            quickChat: true,
            embargo: true,
            overlays: true,
            missilePredictor: true
        }
    };

    function clamp(value, min, max) { return Math.max(min, Math.min(max, value)); }
    function hexToRgb(hex) {
        const cleaned = String(hex || '').trim().replace(/^#/, '');
        if (!/^[0-9a-f]{3,6}$/i.test(cleaned)) return null;
        const normalized = cleaned.length === 3
            ? cleaned.split('').map(ch => ch + ch).join('')
            : cleaned;
        const int = parseInt(normalized, 16);
        return {
            r: (int >> 16) & 255,
            g: (int >> 8) & 255,
            b: int & 255
        };
    }
    function rgbToHsl(r, g, b) {
        r /= 255; g /= 255; b /= 255;
        const max = Math.max(r, g, b), min = Math.min(r, g, b);
        let h = 0, s = 0, l = (max + min) / 2;
        if (max !== min) {
            const diff = max - min;
            s = l > 0.5 ? diff / (2 - max - min) : diff / (max + min);
            switch (max) {
                case r: h = ((g - b) / diff + (g < b ? 6 : 0)); break;
                case g: h = ((b - r) / diff + 2); break;
                case b: h = ((r - g) / diff + 4); break;
            }
            h *= 60;
        }
        return { h, s, l };
    }
    function hslToHex(h, s, l) {
        h = ((h % 360) + 360) % 360;
        s = clamp(s, 0, 1);
        l = clamp(l, 0, 1);
        const c = (1 - Math.abs(2 * l - 1)) * s;
        const x = c * (1 - Math.abs((h / 60) % 2 - 1));
        const m = l - c / 2;
        let r = 0, g = 0, b = 0;
        if (h < 60) [r, g, b] = [c, x, 0];
        else if (h < 120) [r, g, b] = [x, c, 0];
        else if (h < 180) [r, g, b] = [0, c, x];
        else if (h < 240) [r, g, b] = [0, x, c];
        else if (h < 300) [r, g, b] = [x, 0, c];
        else [r, g, b] = [c, 0, x];
        const toHex = v => Math.round((v + m) * 255).toString(16).padStart(2, '0');
        return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
    }
    function normalizeHexColor(value) {
        const rgb = hexToRgb(value);
        if (!rgb) return null;
        return `#${((1 << 24) + (rgb.r << 16) + (rgb.g << 8) + rgb.b).toString(16).slice(1)}`;
    }
    function syncGuiColorFromHex(value) {
        const normalized = normalizeHexColor(value);
        if (!normalized) return null;
        const rgb = hexToRgb(normalized);
        if (!rgb) return null;
        const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
        cfg.guiColor = normalized;
        cfg.guiColorHue = Math.round(hsl.h);
        return normalized;
    }
    function syncGuiColorFromHue(hue) {
        cfg.guiColorHue = clamp(Math.round(hue), 0, 360);
        cfg.guiColor = hslToHex(cfg.guiColorHue, 1, 0.55);
        return cfg.guiColor;
    }

    try {
        const stored = localStorage.getItem(STORAGE_KEY);
        if (stored) {
            const p = JSON.parse(stored);
            if (p && typeof p === 'object') {
                if (typeof p.useChargeTime === 'boolean') cfg.useChargeTime = p.useChargeTime;
                if (typeof p.blockPassThrough === 'boolean') cfg.blockPassThrough = p.blockPassThrough;
                if (!isNaN(parseInt(p.holdDelay))) cfg.holdDelay = parseInt(p.holdDelay);
                if (!isNaN(parseInt(p.spamInterval))) cfg.spamInterval = parseInt(p.spamInterval);
                if (typeof p.selectedUnit === 'string') cfg.selectedUnit = p.selectedUnit;
                if (typeof p.toggleMode === 'boolean') cfg.toggleMode = p.toggleMode;
                if (typeof p.spamMethod === 'string' && ['websocket','click'].includes(p.spamMethod)) cfg.spamMethod = p.spamMethod;
                if (p.hotkeyMap && typeof p.hotkeyMap === 'object') {
                    cfg.hotkeyMap = {};
                    for (const [k, v] of Object.entries(p.hotkeyMap)) {
                        if (typeof k === 'string' && typeof v === 'string') {
                            cfg.hotkeyMap[k.toLowerCase()] = v;
                        }
                    }
                }
                if (p.actionHotkeyMap && typeof p.actionHotkeyMap === 'object') {
                    Object.assign(cfg.actionHotkeyMap, p.actionHotkeyMap);
                }
                if (p.combatPercentages && typeof p.combatPercentages === 'object') {
                    Object.assign(cfg.combatPercentages, p.combatPercentages);
                }
                if (p.tradePercentages && typeof p.tradePercentages === 'object') {
                    Object.assign(cfg.tradePercentages, p.tradePercentages);
                }
                if (typeof p.combatHotkeysPriority === 'boolean') {
                    cfg.combatHotkeysPriority = p.combatHotkeysPriority;
                }
                if (typeof p.combatSiloIndicator === 'boolean') {
                    cfg.combatSiloIndicator = p.combatSiloIndicator;
                }
                if (typeof p.combatSiloPanel === 'boolean') {
                    cfg.combatSiloPanel = p.combatSiloPanel;
                }
                if (typeof p.combatSiloOnlyAllies === 'boolean') {
                    cfg.combatSiloOnlyAllies = p.combatSiloOnlyAllies;
                }
                if (typeof p.combatSiloKeepAllPlaced === 'boolean') {
                    cfg.combatSiloKeepAllPlaced = p.combatSiloKeepAllPlaced;
                }
                if (typeof p.showGoldOverlay === 'boolean') {
                    cfg.showGoldOverlay = p.showGoldOverlay;
                }
                if (typeof p.showTroopsOverlay === 'boolean') {
                    cfg.showTroopsOverlay = p.showTroopsOverlay;
                }
                if (!isNaN(parseInt(p.troopsCheckIntervalMs))) {
                    cfg.troopsCheckIntervalMs = Math.max(1, Math.min(1000, parseInt(p.troopsCheckIntervalMs)));
                }
                if (p.features && typeof p.features === 'object') {
                    const defaultFeatureState = {
                        spamHotkeys: true,
                        combatHotkeys: true,
                        actionHotkeys: true,
                        quickChat: true,
                        embargo: true,
                        overlays: true,
                        missilePredictor: true,
                    };
                    cfg.features = Object.assign({}, defaultFeatureState, p.features);
                    for (const [key, defaultValue] of Object.entries(defaultFeatureState)) {
                        if (typeof cfg.features[key] !== 'boolean') {
                            cfg.features[key] = defaultValue;
                        }
                    }
                }
                if (!isNaN(parseInt(p.goldRateWindowSeconds))) {
                    cfg.goldRateWindowSeconds = Math.max(5, Math.min(240, parseInt(p.goldRateWindowSeconds)));
                }
                if (!isNaN(parseInt(p.goldTopCount))) {
                    cfg.goldTopCount = Math.max(1, Math.min(20, parseInt(p.goldTopCount)));
                }
                if (!isNaN(parseFloat(p.guiOpacity))) {
                    cfg.guiOpacity = Math.max(0.1, Math.min(1, parseFloat(p.guiOpacity)));
                }
                if (!isNaN(parseFloat(p.overlayOpacity))) {
                    cfg.overlayOpacity = Math.max(0.1, Math.min(1, parseFloat(p.overlayOpacity)));
                }
                if (typeof p.guiColor === 'string' && p.guiColor.trim()) {
                    const normalized = normalizeHexColor(p.guiColor.trim());
                    if (normalized) cfg.guiColor = normalized;
                }
                if (!isNaN(parseInt(p.guiColorHue, 10))) {
                    cfg.guiColorHue = clamp(parseInt(p.guiColorHue, 10), 0, 360);
                }
                if (typeof p.overlayColor === 'string' && p.overlayColor.trim()) {
                    const normalized = normalizeHexColor(p.overlayColor.trim());
                    if (normalized) cfg.overlayColor = normalized;
                }
            }
        }
    } catch(e) {}
    const initialGuiColorHsl = rgbToHsl(...Object.values(hexToRgb(cfg.guiColor) || { r: 0, g: 255, b: 102 }));
    if (!Number.isFinite(cfg.guiColorHue) || cfg.guiColorHue < 0 || cfg.guiColorHue > 360) {
        cfg.guiColorHue = Math.round(initialGuiColorHsl.h || 150);
    }

    for (const key in cfg.hotkeyMap) {
        if (cfg.hotkeyMap[key] === 'atk_10') cfg.hotkeyMap[key] = 'atk_1';
        else if (cfg.hotkeyMap[key] === 'atk_25') cfg.hotkeyMap[key] = 'atk_2';
        else if (cfg.hotkeyMap[key] === 'atk_50') cfg.hotkeyMap[key] = 'atk_3';
        else if (cfg.hotkeyMap[key] === 'atk_100') cfg.hotkeyMap[key] = 'atk_4';
    }
// maybe add more hotkeys later if useful?
    const defaultCombatKeys = { 'atk_1': 'z', 'atk_2': 'x', 'atk_3': 'c', 'atk_4': 'v', 'boat_1': 'b' };
    for (const [atkId, defaultKey] of Object.entries(defaultCombatKeys)) {
        const alreadyBound = Object.values(cfg.hotkeyMap).includes(atkId);
        if (!alreadyBound) {
            delete cfg.hotkeyMap[defaultKey];
            cfg.hotkeyMap[defaultKey] = atkId;
        }
    }

    function saveCfg() {
        try { localStorage.setItem(STORAGE_KEY, JSON.stringify(cfg)); } catch(e) {}
    }

    // websocket stuff
    const NativeWS = window.WebSocket;
    const NativeSend = NativeWS.prototype.send;
    const WS_OPEN = NativeWS.OPEN || 1; // use native constant never undefined
    let activeSocket = null;

    // the game creates many sockets like lobby, analytics, CDN that are not the game socket
    window.WebSocket = function(url, protocols) {
        const sock = protocols !== undefined ? new NativeWS(url, protocols) : new NativeWS(url);
        sock.addEventListener('close', () => {
            if (activeSocket === sock) {
                activeSocket = null;
                setLinkStatus(false);
            }
        });
        return sock;
    };
    window.WebSocket.prototype = NativeWS.prototype;
    Object.getOwnPropertyNames(NativeWS).forEach(k => {
        try { window.WebSocket[k] = NativeWS[k]; } catch(_) {}
    });

    NativeWS.prototype.send = function(data) {
        if (typeof data === 'string') {
            try {
                const msg = JSON.parse(data);
                if (msg) {
                    // only see this socket as the game socket when it sends a game intent
                    // or a known game message type... prevents lobby sockets
                    // from stealing activeSocket and causing disconnect
                    // i hate websockets D:
                    const isGameMsg = (msg.type === 'intent' && msg.intent) ||
                                      msg.type === 'quick_chat' ||
                                      msg.type === 'ping';
                    if (isGameMsg && activeSocket !== this) {
                        activeSocket = this;
                        setLinkStatus(true);
                    }

                    const intent = msg.intent || msg;
                    if (typeof intent.tile === 'number') lastKnownTile = intent.tile;
                    if (typeof intent.dst  === 'number') lastKnownTile = intent.dst;
                    if (typeof intent.src  === 'number') lastKnownTile = intent.src;
                }
            } catch(e) {}
        }
        return NativeSend.apply(this, arguments);
    };

    function sendPacket(intentObj) {
        if (!activeSocket || activeSocket.readyState !== WS_OPEN) return false;
        try {
            NativeSend.call(activeSocket, JSON.stringify({ type: 'intent', intent: intentObj }));
            return true;
        } catch(e) {
            console.error('[Blon] Packet send failed:', e);
            return false;
        }
    }

    function setLinkStatus(connected) {
        const el = document.getElementById('blon-link');
        if (!el) return;
        el.textContent = connected ? 'CONNECTED' : 'DISCONNECTED';
        el.style.color = connected ? '#00ff66' : '#ff4444';
    }

    // mouse pos
    let mouseX = 0, mouseY = 0;
    window.addEventListener('mousemove', e => { mouseX = e.clientX; mouseY = e.clientY; });

    let lastKnownTile = null;

    function hookCanvasTileSniff() {
        const sniff = (clientX, clientY) => {
            try {
                const overlay = document.querySelector('player-info-overlay');
                if (overlay && overlay.game && overlay.transform) {
                    const worldCoord = overlay.transform.screenToWorldCoordinates(clientX, clientY);
                    if (worldCoord && typeof overlay.game.isValidCoord === 'function' && overlay.game.isValidCoord(worldCoord.x, worldCoord.y)) {
                        const tile = overlay.game.ref(worldCoord.x, worldCoord.y);
                        if (tile !== null && tile !== undefined) {
                            lastKnownTile = tile;
                        }
                    }
                }
            } catch(e) {}
        };

        window.addEventListener('mousemove', (e) => {
            sniff(e.clientX, e.clientY);
        }, { passive: true });

        window.addEventListener('pointerdown', (e) => {
            sniff(e.clientX, e.clientY);
        }, { passive: true });
    }
    hookCanvasTileSniff();

    const UNIT_TYPES = {
        'City':         'City',
        'Defense Post': 'Defense Post',
        'Port':         'Port',
        'SAM Launcher': 'SAM Launcher',
        'Missile Silo': 'Missile Silo',
        'Factory':      'Factory',
        'Warship':      'Warship',
        'Atom Bomb':    'Atom Bomb',
        'H-Bomb':       'Hydrogen Bomb',
        'MIRV':         'MIRV',
    };

    let spamInterval = null;
    let holdTimeout = null;
    let countdownInterval = null;
    let currentSpamKey = null;
    let isKeyDown = false;
    let toggleSpamActive = false;
    let internalRebindSlot = null;
    let internalRebindAction = null; // for hotkeys

    function getGameState() {
        try {
            const overlay = document.querySelector('player-info-overlay');
            let game = overlay && overlay.game ? overlay.game : null;

            if (!game) game = readCachedGameView();

            if (!game) return null;
            const myPlayer = game.myPlayer();
            if (!myPlayer) return null;
            return { game, myPlayer };
        } catch(e) { return null; }
    }

    function updateCombatStatus(text, color) {
        const el = document.getElementById('blon-combat-status');
        if (el) { el.textContent = text; el.style.color = color || '#00ff66'; }
    }

    function getSiloConstructionInfo(game) {
        try {
            if (typeof game.unitStates === 'function' && typeof game.unit === 'function') {
                for (const state of game.unitStates().values()) {
                    if (
                        state.unitType === 'Missile Silo' &&
                        state.underConstruction === true
                    ) {
                        const unit = game.unit(state.id);
                        if (unit) return unit;
                    }
                }
            }

            const silos = (typeof game.units === 'function' ? game.units('Missile Silo') : []) || [];
            return silos.filter(
                (u) => typeof u.isUnderConstruction === 'function' && u.isUnderConstruction(),
            )[0] || null;
        } catch (e) {
            return null;
        }
    }

    function getSiloTrackerUnits(game) {
        const units = [];
        const seen = new Set();
        const includeUnit = (unit) => {
            if (!unit) return;
            const id = typeof unit.id === 'function' ? unit.id() : null;
            if (id !== null && seen.has(id)) return;
            if (!cfg.combatSiloKeepAllPlaced && typeof unit.isUnderConstruction === 'function' && !unit.isUnderConstruction()) return;
            if (id !== null) seen.add(id);
            units.push(unit);
        };
        try {
            if (typeof game.unitStates === 'function' && typeof game.unit === 'function') {
                for (const state of game.unitStates().values()) {
                    if (state.unitType !== 'Missile Silo') continue;
                    if (state.isActive !== true) continue;
                    if (state.markedForDeletion !== false) continue;
                    if (!cfg.combatSiloKeepAllPlaced && state.underConstruction !== true) continue;
                    const unit = game.unit(state.id);
                    includeUnit(unit);
                }
            }
            if (typeof game.units === 'function') {
                const silos = game.units('Missile Silo');
                silos.forEach(includeUnit);
            }
        } catch (e) {
            return [];
        }
        return units;
    }

    function isAllySilo(unit, myPlayer) {
        if (!myPlayer || !unit || typeof unit.owner !== 'function') return false;
        try {
            const owner = unit.owner();
            if (!owner) return false;
            return isFriendlyPlayer(myPlayer, owner);
        } catch (e) {
            return false;
        }
    }

    function getSiloTileCoordinates(game, unit) {
        try {
            const tile = typeof unit.tile === 'function' ? unit.tile() : null;
            if (!tile) return null;
            if (typeof game.x !== 'function' || typeof game.y !== 'function') return null;
            return { x: game.x(tile), y: game.y(tile) };
        } catch (e) {
            return null;
        }
    }

    function updateSiloPanel() {
        if (!cfg.combatSiloIndicator || !cfg.combatSiloPanel) {
            removePopoutPanel('blon-popout-silos');
            return;
        }

        const panel = document.getElementById('blon-popout-silos') || createDraggablePopoutPanel('blon-popout-silos', 'Silo Tracker', () => {
            cfg.combatSiloPanel = false;
            saveCfg();
            const toggle = document.getElementById('cfg-combat-silo-panel');
            if (toggle) toggle.checked = false;
        });
        const content = document.getElementById('blon-popout-silos-content');
        if (!content) return;

        const state = getGameState();
        if (!state) {
            content.innerHTML = '<div style="color:#777;">No game state available.</div>';
            return;
        }

        const allSilos = getSiloTrackerUnits(state.game).filter((unit) => {
            if (!cfg.combatSiloOnlyAllies) return true;
            if (!state.myPlayer) return false;
return !isAllySilo(unit, state.myPlayer);
        });

        if (allSilos.length === 0) {
            content.innerHTML = '<div style="color:#777;">No matching missile silos found.</div>';
            return;
        }

        const items = allSilos
            .sort((a, b) => {
                const aUnder = typeof a.isUnderConstruction === 'function' && a.isUnderConstruction() ? 0 : 1;
                const bUnder = typeof b.isUnderConstruction === 'function' && b.isUnderConstruction() ? 0 : 1;
                return aUnder - bUnder;
            })
            .map((unit) => {
                const owner = typeof unit.owner === 'function' ? unit.owner() : null;
                const ownerName = owner ? formatPlayerLabel(owner, state.myPlayer) : 'Unknown';
                const coords = getSiloTileCoordinates(state.game, unit);
                const status = typeof unit.isUnderConstruction === 'function' && unit.isUnderConstruction() ? 'Building' : 'Placed';
                const posLabel = coords ? `@${coords.x},${coords.y}` : '@unknown';
                return { unit, ownerName, status, posLabel, coords };
            });

        content.innerHTML = '';
        items.forEach((item) => {
            const row = document.createElement('div');
            row.style.cssText = 'padding:6px 6px;border-bottom:1px solid rgba(255,255,255,0.08);cursor:pointer;';
            row.innerHTML = `
                <div style="display:flex;justify-content:space-between;gap:8px;align-items:center;">
                    <strong style="color:#fff;font-size:11px;">${item.ownerName}</strong>
                    <span style="color:${item.status === 'Building' ? '#00ccff' : '#ffcc66'};font-size:10px;">${item.status}</span>
                </div>
                <div style="display:flex;justify-content:space-between;gap:8px;align-items:center;margin-top:2px;font-size:10px;color:#aaa;">
                    <span>${item.posLabel}</span>
                    ${item.coords ? '<span style="color:#ccc;">center</span>' : ''}
                </div>
            `;
            row.addEventListener('click', () => {
                if (item.coords) centerCameraOnTile(item.coords.x, item.coords.y);
            });
            content.appendChild(row);
        });
    }

    function setSiloSubtoggleVisibility() {
        const subSection = document.getElementById('blon-silo-subtoggles');
        if (!subSection) return;
        subSection.style.display = cfg.combatSiloIndicator ? 'block' : 'none';
    }

    function formatPlayerLabel(player, myPlayer) {
        try {
            if (player && typeof player.smallID === 'function' && myPlayer && typeof myPlayer.smallID === 'function' && player.smallID() === myPlayer.smallID()) {
                return 'You';
            }
            if (player && typeof player.displayName === 'function') return player.displayName();
            if (player && typeof player.name === 'function') return player.name();
        } catch (e) {}
        return 'Unknown';
    }

    function centerCameraOnTile(x, y) {
        try {
            const overlay = document.querySelector('player-info-overlay');
            if (!overlay) return;
            const transform = overlay.transform;
            if (transform && typeof transform.onGoToPosition === 'function') {
                transform.onGoToPosition({ x, y });
            }
        } catch (e) {
            console.warn('Failed to center camera on silo', e);
        }
    }

    function updateSiloNotification() {
        const el = document.getElementById('blon-silo-notification');
        if (!el) return;
        if (!cfg.combatSiloIndicator) {
            el.style.display = 'none';
            return;
        }

        const state = getGameState();
        if (!state) {
            el.style.display = 'none';
            return;
        }

        const silo = getSiloConstructionInfo(state.game);
        if (!silo) {
            el.style.display = 'none';
            updateSiloPanel();
            return;
        }

        const owner = typeof silo.owner === 'function' ? silo.owner() : null;
        const ownerLabel = owner ? formatPlayerLabel(owner, state.myPlayer) : 'Unknown';
        const message = ownerLabel === 'You'
            ? 'You are placing a silo'
            : `${ownerLabel} is placing a silo`;
        el.textContent = message;
        el.style.display = 'block';
    }

    function onSiloNotificationClick() {
        const state = getGameState();
        if (!state) return;
        const silo = getSiloConstructionInfo(state.game);
        if (!silo) return;
        const tile = typeof silo.tile === 'function' ? silo.tile() : null;
        if (!tile) return;
        const x = typeof state.game.x === 'function' ? state.game.x(tile) : null;
        const y = typeof state.game.y === 'function' ? state.game.y(tile) : null;
        if (x === null || y === null) return;
        centerCameraOnTile(x, y);
    }

    const QUICK_CHAT_KEYS = [
        'help.troops','help.troops_frontlines','help.gold','help.no_attack','help.sorry_attack',
        'help.alliance','help.trade_partners',
        'attack.build_warships',
        'defend.build_posts',
        'greet.hello','greet.good_job','greet.good_luck','greet.have_fun','greet.gg',
        'greet.nice_to_meet','greet.well_played','greet.hi_again','greet.bye',
        'greet.thanks','greet.oops','greet.trust_me','greet.trust_broken',
        'greet.ruining_games','greet.dont_do_that','greet.same_team',
        'misc.go','misc.strategy','misc.fun','misc.pr','misc.build_closer','misc.coastline',
        'warnings.number1_warning','warnings.stalemate','warnings.stop_trading_all'
    ];
    function samePlayer(a, b) {
        if (!a || !b) return false;
        try {
            if (typeof a.smallID === 'function' && typeof b.smallID === 'function') return a.smallID() === b.smallID();
            if (typeof a.id === 'function' && typeof b.id === 'function') return a.id() === b.id();
        } catch (e) {}
        return false;
    }

    function isFriendlyPlayer(myPlayer, otherPlayer) {
        if (!myPlayer || !otherPlayer) return false;
        try {
            if (samePlayer(myPlayer, otherPlayer)) return true;
            if (typeof myPlayer.isFriendly === 'function' && myPlayer.isFriendly(otherPlayer)) return true;
            if (typeof myPlayer.isAlliedWith === 'function' && myPlayer.isAlliedWith(otherPlayer)) return true;
            if (typeof myPlayer.isOnSameTeam === 'function' && myPlayer.isOnSameTeam(otherPlayer)) return true;
            if (typeof otherPlayer.isFriendly === 'function' && otherPlayer.isFriendly(myPlayer)) return true;
            if (typeof otherPlayer.isAlliedWith === 'function' && otherPlayer.isAlliedWith(myPlayer)) return true;
            if (typeof otherPlayer.isOnSameTeam === 'function' && otherPlayer.isOnSameTeam(myPlayer)) return true;
            if (typeof myPlayer.team === 'function' && typeof otherPlayer.team === 'function') {
                const myTeam = myPlayer.team();
                const otherTeam = otherPlayer.team();
                if (myTeam != null && otherTeam != null && myTeam === otherTeam) return true;
            }
        } catch (e) {
            return false;
        }
        return false;
    }

    function getGamePlayers(game) {
        try {
            if (game && typeof game.players === 'function') return game.players() || [];
            if (game && typeof game.playerViews === 'function') return game.playerViews() || [];
        } catch (e) {}
        return [];
    }

    function sendQuickChat(key, targetMode) {
        const state = getGameState();
        if (!state) {
            updateDiploStatus('Game state not found', '#ff4444'); return;
        }
        if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
            updateDiploStatus('Not connected', '#ff4444'); return;
        }
        try {
            const allPlayers = getGamePlayers(state.game);
            const otherPlayers = (allPlayers || []).filter(p => {
                try {
                    return p && p.isPlayer && p.isPlayer() && !samePlayer(p, state.myPlayer) && p.isAlive();
                } catch(e) { return false; }
            });

            if (otherPlayers.length === 0) {
                updateDiploStatus('No other players found', '#ffaa00');
                return;
            }

            const mode = ['everyone', 'allies', 'enemies'].includes(targetMode) ? targetMode : 'enemies';
            const targetPlayers = otherPlayers.filter(p => {
                const friendly = isFriendlyPlayer(state.myPlayer, p);
                if (mode === 'allies') return friendly;
                if (mode === 'enemies') return !friendly;
                return true;
            });

            if (targetPlayers.length === 0) {
                const label = mode === 'allies' ? 'allies' : mode === 'enemies' ? 'enemies' : 'players';
                updateDiploStatus(`No ${label} found`, '#ffaa00');
                return;
            }

            let sent = 0;
            targetPlayers.forEach(p => {
                const ok = sendPacket({
                    type: 'quick_chat',
                    recipient: p.id(),
                    quickChatKey: key
                });
                if (ok) sent++;
            });

            const targetLabel = mode === 'allies' ? 'allies' : mode === 'enemies' ? 'enemies' : 'everyone';
            updateDiploStatus(`Sent to ${sent} ${targetLabel}`, '#00ff66');
        } catch(e) {
            updateDiploStatus('Send failed: ' + e.message, '#ff4444');
        }
        setTimeout(() => updateDiploStatus('Ready', '#00ff66'), 2000);
    }

    let autoAcceptTimer = null;
    function setAutoAccept(enabled) {
        if (enabled) {
            autoAcceptTimer = setInterval(() => {
                const state = getGameState();
                if (!state) return;
                try {
                    const allPlayers = state.game.players ? state.game.players() : [];
                    (allPlayers || []).forEach(p => {
                        try {
                            if (!p || !p.isPlayer || !p.isPlayer() || !p.isAlive()) return;
                            if (p.id() === state.myPlayer.id()) return;

                            // Check if they have an active alliance request sent to us
                            const outgoing = p.state.outgoingAllianceRequests || [];
                            const hasRequestedMe = outgoing.includes(state.myPlayer.id());

                            // Check if already allied
                            const isAllied = state.myPlayer.isAlliedWith && state.myPlayer.isAlliedWith(p);

                            if (hasRequestedMe && !isAllied) {
                                sendPacket({ type: 'allianceRequest', recipient: p.id() });
                            }
                        } catch(e) {}
                    });
                } catch(e) {}
            }, 2000);
        } else {
            clearInterval(autoAcceptTimer);
            autoAcceptTimer = null;
        }
    }

    function updateDiploStatus(text, color) {
        const el = document.getElementById('blon-diplo-status');
        if (el) { el.textContent = text; el.style.color = color || '#00ff66'; }
    }

    let lastGoldAmount = null;
    let lastGoldTime = null;
    let lastGoldRate = 0;
    let lastPlayerGoldSnapshot = new Map();
    const playerGoldHistory = new Map();

    function getGoldRateWindowMs() {
        return Math.max(5000, Math.min(240000, (cfg.goldRateWindowSeconds || 12) * 1000));
    }

    function recordPlayerGoldSample(id, gold) {
        if (id == null) return null;
        const now = Date.now();
        const numericGold = typeof gold === 'bigint' ? Number(gold) : Number(gold || 0);
        const windowMs = getGoldRateWindowMs();
        const history = playerGoldHistory.get(id) || [];
        history.push({ time: now, gold: numericGold });
        while (history.length > 1 && now - history[0].time > windowMs) {
            history.shift();
        }
        playerGoldHistory.set(id, history);
        return history;
    }

    function getPlayerMpsFromHistory(id) {
        const history = playerGoldHistory.get(id);
        if (!history || history.length < 2) return null;
        const oldest = history[0];
        const newest = history[history.length - 1];
        const deltaSeconds = Math.max((newest.time - oldest.time) / 1000, 0.001);
        return (newest.gold - oldest.gold) / deltaSeconds;
    }

    function getPlayerFocusCoordinates(game, player) {
        if (!game || !player || typeof player.smallID !== 'function') return null;
        const playerSmallId = player.smallID();
        if (typeof game.units !== 'function' || typeof game.x !== 'function' || typeof game.y !== 'function') {
            return null;
        }
        try {
            const units = game.units();
            for (const unit of units) {
                if (!unit || typeof unit.owner !== 'function' || typeof unit.tile !== 'function') continue;
                const owner = unit.owner();
                if (!owner || typeof owner.smallID !== 'function' || owner.smallID() !== playerSmallId) continue;
                const tile = unit.tile();
                if (!tile) continue;
                return { x: game.x(tile), y: game.y(tile) };
            }
        } catch (e) {
            return null;
        }
        return null;
    }

    function centerCameraOnPlayer(game, player) {
        const coords = getPlayerFocusCoordinates(game, player);
        if (!coords) return;
        centerCameraOnTile(coords.x, coords.y);
    }

    function getPlayerUniqueId(player) {
        if (!player) return null;
        if (player.id && typeof player.id === 'function') return player.id();
        if (player.smallID && typeof player.smallID === 'function') return player.smallID();
        return null;
    }

    function getPlayerLabel(player) {
        if (!player) return 'Unknown';
        if (player.name && typeof player.name === 'function') return player.name();
        const id = getPlayerUniqueId(player);
        return id != null ? `Player ${id}` : 'Unknown';
    }

    function getPlayerGoldValue(player) {
        const value = player.gold ? player.gold() : 0;
        return typeof value === 'bigint' ? Number(value) : Number(value || 0);
    }

    function getPlayerTeamKey(player) {
        if (!player) return 'No Team';
        let rawTeam = null;
        if (player.team && typeof player.team === 'function') rawTeam = player.team();
        else if (player.static && player.static.team != null) rawTeam = player.static.team;
        else if (player.team != null) rawTeam = player.team;
        if (rawTeam == null || rawTeam === 'No Team' || rawTeam === '') return 'No Team';
        return `Team ${rawTeam}`;
    }

    function fmtNum(n) {
        if (n == null) return '?';
        if (n >= 1e9) return (n/1e9).toFixed(2) + 'B';
        if (n >= 1e6) return (n/1e6).toFixed(2) + 'M';
        if (n >= 1e3) return (n/1e3).toFixed(1) + 'K';
        return String(Math.floor(n));
    }

    function createDraggablePopoutPanel(panelId, title, onClose) {
        const existing = document.getElementById(panelId);
        if (existing) return existing;
        const panel = document.createElement('div');
        panel.id = panelId;
        panel.style.cssText = `
            position:fixed; left:50%; top:50%; transform:translate(-50%, -50%); width:250px; min-width:200px; min-height:80px;
            background:rgba(0,0,0,0.95); color:#fff; border:1px solid #333;
            font-family:monospace; font-size:11px; border-radius:5px; z-index:999999;
            box-shadow:0 8px 30px rgba(0,0,0,0.7); overflow:hidden; cursor:default;
        `;
        panel.innerHTML = `
            <div class="blon-popout-drag" style="display:flex;justify-content:space-between;align-items:center;padding:8px 10px;background:#111;border-bottom:1px solid #222;cursor:move;user-select:none;">
                <span style="font-size:10px;color:#00ff66;font-weight:bold;">${title}</span>
                <div style="display:flex;gap:6px;align-items:center;">
                    <button id="${panelId}-minimize" style="background:transparent;border:none;color:#888;cursor:pointer;font-size:12px;line-height:1;">▾</button>
                    <button id="${panelId}-close" style="background:transparent;border:none;color:#888;cursor:pointer;font-size:12px;">×</button>
                </div>
            </div>
            <div id="${panelId}-content" style="padding:10px;line-height:1.4;color:#ddd;"></div>
            <div class="blon-popout-resize" style="position:absolute;right:4px;bottom:4px;width:12px;height:12px;cursor:nwse-resize;border-right:2px solid #444;border-bottom:2px solid #444;">
            </div>
        `;
        document.body.appendChild(panel);

        const dragHandle = panel.querySelector('.blon-popout-drag');
        let dragging = false;
        let offsetX = 0;
        let offsetY = 0;

        const contentEl = panel.querySelector(`#${panelId}-content`);
        const minimizeBtn = panel.querySelector(`#${panelId}-minimize`);
        const closeBtn = panel.querySelector(`#${panelId}-close`);
        const resizeHandle = panel.querySelector('.blon-popout-resize');
        let resizing = false;
        let startX = 0;
        let startY = 0;
        let startWidth = 0;
        let startHeight = 0;

        if (contentEl) {
            contentEl.style.fontSize = '11px';
            contentEl.style.overflowY = 'auto';
            contentEl.style.maxHeight = '300px';
            contentEl.style.boxSizing = 'border-box';
        }
        panel.dataset.minimized = 'false';
        panel.dataset.savedHeight = '';

        const setMinimizedState = minimized => {
            if (!contentEl) return;
            if (minimized) {
                panel.dataset.savedHeight = panel.style.height || `${panel.getBoundingClientRect().height}px`;
                contentEl.style.display = 'none';
                panel.style.height = `${dragHandle.getBoundingClientRect().height}px`;
                panel.style.minHeight = '0px';
                resizeHandle.style.display = 'none';
                minimizeBtn.textContent = '▴';
                panel.dataset.minimized = 'true';
            } else {
                contentEl.style.display = 'block';
                panel.style.height = panel.dataset.savedHeight || '';
                panel.style.minHeight = '80px';
                resizeHandle.style.display = 'block';
                minimizeBtn.textContent = '▾';
                panel.dataset.minimized = 'false';
            }
        };

        dragHandle.addEventListener('mousedown', e => {
            if (e.target === minimizeBtn || e.target === closeBtn) return;
            dragging = true;
            const rect = panel.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            panel.style.transform = 'none';
            panel.style.left = `${rect.left}px`;
            panel.style.top = `${rect.top}px`;
            dragHandle.style.color = '#00ffcc';
            e.preventDefault();
        });
        window.addEventListener('mousemove', e => {
            if (dragging) {
                panel.style.left = `${e.clientX - offsetX}px`;
                panel.style.top = `${e.clientY - offsetY}px`;
                panel.style.right = 'auto';
            }
            if (resizing) {
                const newWidth = Math.max(200, startWidth + e.clientX - startX);
                const newHeight = Math.max(80, startHeight + e.clientY - startY);
                panel.style.width = `${newWidth}px`;
                panel.style.height = `${newHeight}px`;
                panel.dataset.savedHeight = panel.style.height;
                if (contentEl) {
                    const fontSize = Math.min(16, Math.max(10, 11 * (newWidth / 250)));
                    contentEl.style.fontSize = `${fontSize.toFixed(1)}px`;
                }
            }
        });
        window.addEventListener('mouseup', () => {
            if (dragging) {
                dragging = false;
                dragHandle.style.color = '#00ff66';
            }
            resizing = false;
        });

        resizeHandle.addEventListener('mousedown', e => {
            resizing = true;
            const rect = panel.getBoundingClientRect();
            startX = e.clientX;
            startY = e.clientY;
            startWidth = rect.width;
            startHeight = rect.height;
            e.preventDefault();
            e.stopPropagation();
        });

        minimizeBtn.addEventListener('mousedown', e => e.stopPropagation());
        closeBtn.addEventListener('mousedown', e => e.stopPropagation());
        minimizeBtn.addEventListener('click', () => {
            const minimized = panel.dataset.minimized === 'true';
            setMinimizedState(!minimized);
        });

        closeBtn.addEventListener('click', () => {
            panel.remove();
            if (typeof onClose === 'function') onClose();
            updateMainGoldOverlayVisibility();
        });

        return panel;
    }

    function removePopoutPanel(panelId) {
        const panel = document.getElementById(panelId);
        if (panel) panel.remove();
        updateMainGoldOverlayVisibility();
    }

    function updateMainGoldOverlayVisibility() {
        const goldOverlay = document.getElementById('blon-gold-overlay');
        if (!goldOverlay) return;
        const popoutGold = !!document.getElementById('blon-popout-gold');
        const popoutMps = !!document.getElementById('blon-popout-mps');
        goldOverlay.style.display = (cfg.showGoldOverlay && !popoutGold && !popoutMps) ? 'block' : 'none';
    }

    function renderGoldOverlay(state) {
        const populateTop10 = (top10El, groupEl) => {
            if (!top10El || !groupEl) return;
            const allPlayers = state.game.players ? state.game.players() : [];
            const players = (allPlayers || []).filter(p => p && p.gold && typeof p.gold === 'function');
            const playerRows = players.map(p => {
                const value = getPlayerGoldValue(p);
                return { player: p, gold: value };
            }).sort((a, b) => b.gold - a.gold).slice(0, cfg.goldTopCount || 10);

            const myId = state.myPlayer.id ? state.myPlayer.id() : null;
            top10El.innerHTML = '';
            if (playerRows.length === 0) {
                top10El.innerHTML = '<div style="color:#777;grid-column:1/-1;">No player gold data available.</div>';
            } else {
                playerRows.forEach((row, index) => {
                    const isMe = myId !== null && row.player.id && row.player.id() === myId;
                    const rank = document.createElement('div');
                    rank.textContent = `${index + 1}.`;
                    rank.style.color = isMe ? '#fff' : '#aaa';
                    const name = document.createElement('div');
                    name.textContent = getPlayerLabel(row.player);
                    name.style.color = isMe ? '#00ff66' : '#ddd';
                    name.style.whiteSpace = 'nowrap';
                    name.style.cursor = 'pointer';
                    name.title = 'Click to center on player';
                    name.addEventListener('click', () => centerCameraOnPlayer(state.game, row.player));
                    const value = document.createElement('div');
                    value.textContent = fmtNum(row.gold);
                    value.style.color = '#ffcc00';
                    value.style.textAlign = 'right';
                    top10El.appendChild(rank);
                    top10El.appendChild(name);
                    top10El.appendChild(value);
                });
            }

            const grouped = new Map();
            let teamMode = false;
            players.forEach(p => {
                const teamKey = getPlayerTeamKey(p);
                if (teamKey !== 'No Team') teamMode = true;
                grouped.set(teamKey, (grouped.get(teamKey) || 0) + getPlayerGoldValue(p));
            });

            groupEl.innerHTML = '';
            if (!teamMode) {
                groupEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">Team totals unavailable in this game mode.</div>';
            } else {
                Array.from(grouped.entries()).sort((a, b) => b[1] - a[1]).forEach(([team, gold]) => {
                    const teamName = document.createElement('div');
                    teamName.textContent = team;
                    teamName.style.color = '#ddd';
                    const teamValue = document.createElement('div');
                    teamValue.textContent = fmtNum(gold);
                    teamValue.style.color = '#ffcc00';
                    teamValue.style.textAlign = 'right';
                    groupEl.appendChild(teamName);
                    groupEl.appendChild(teamValue);
                });
            }
        };

        const populateTop10Mps = (mpsEl) => {
            if (!mpsEl) return;
            const allPlayers = state.game.players ? state.game.players() : [];
            const players = (allPlayers || []).filter(p => p && p.gold && typeof p.gold === 'function');
            if (!players.length) {
                mpsEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">Income/sec data unavailable yet.</div>';
                return;
            }

            const rows = players.map(p => {
                const id = getPlayerUniqueId(p);
                const mps = id != null ? getPlayerMpsFromHistory(id) : null;
                return {
                    player: p,
                    mps
                };
            }).filter(row => row.mps !== null).sort((a, b) => b.mps - a.mps).slice(0, cfg.goldTopCount || 10);

            mpsEl.innerHTML = '';
            if (rows.length === 0) {
                mpsEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">Income/sec data unavailable yet.</div>';
                return;
            }
            const myId = state.myPlayer.id ? state.myPlayer.id() : null;
            rows.forEach((row, index) => {
                const isMe = myId !== null && row.player.id && row.player.id() === myId;
                const rank = document.createElement('div');
                rank.textContent = `${index + 1}.`;
                rank.style.color = isMe ? '#fff' : '#aaa';
                const name = document.createElement('div');
                name.textContent = getPlayerLabel(row.player);
                name.style.color = isMe ? '#00ff66' : '#ddd';
                name.style.whiteSpace = 'nowrap';
                name.style.cursor = 'pointer';
                name.title = 'Click to center on player';
                name.addEventListener('click', () => centerCameraOnPlayer(state.game, row.player));
                const value = document.createElement('div');
                value.textContent = `${fmtNum(row.mps)}/s`;
                value.style.color = '#ffcc00';
                value.style.textAlign = 'right';
                mpsEl.appendChild(rank);
                mpsEl.appendChild(name);
                mpsEl.appendChild(value);
            });
        };

        const allPlayers = state.game.players ? state.game.players() : [];
        const currentSnapshot = new Map();
        (allPlayers || []).forEach(p => {
            const id = getPlayerUniqueId(p);
            if (id != null) {
                const gold = getPlayerGoldValue(p);
                currentSnapshot.set(id, gold);
                recordPlayerGoldSample(id, gold);
            }
        });

        const top10El = document.getElementById('blon-gold-top10');
        const teamGoldEl = document.getElementById('blon-team-gold');
        populateTop10(top10El, teamGoldEl);
        const mpsTop10El = document.getElementById('blon-gold-mps-top10');
        populateTop10Mps(mpsTop10El);

        const popoutTop10El = document.getElementById('blon-popout-gold-top10');
        const popoutTeamGoldEl = document.getElementById('blon-popout-team-gold');
        if (popoutTop10El || popoutTeamGoldEl) {
            populateTop10(popoutTop10El, popoutTeamGoldEl);
        }
        const popoutMpsEl = document.getElementById('blon-popout-mps-top10');
        if (popoutMpsEl) {
            populateTop10Mps(popoutMpsEl);
        }

        updateMainGoldOverlayVisibility();
        lastPlayerGoldSnapshot = currentSnapshot;
    }

    function renderGoldRate(state) {
        const currentGold = state.myPlayer.gold ? state.myPlayer.gold() : 0;
        const numericGold = typeof currentGold === 'bigint' ? Number(currentGold) : currentGold;
        const myId = getPlayerUniqueId(state.myPlayer);
        const selfHistory = recordPlayerGoldSample(myId, numericGold);

        if (selfHistory && selfHistory.length > 1) {
            const oldest = selfHistory[0];
            const deltaGold = numericGold - oldest.gold;
            const deltaSeconds = Math.max((Date.now() - oldest.time) / 1000, 0.001);
            lastGoldRate = deltaGold / deltaSeconds;
        }

        lastGoldAmount = numericGold;
        lastGoldTime = Date.now();

        const rateEl = document.getElementById('blon-stat-gold-rate');
        if (rateEl) rateEl.textContent = `${fmtNum(lastGoldRate)}/s`;

        const popoutRateEl = document.getElementById('blon-popout-gold-rate');
        if (popoutRateEl) popoutRateEl.textContent = `${fmtNum(lastGoldRate)}/s`;

        const popoutValueEl = document.getElementById('blon-popout-gold-value');
        if (popoutValueEl) popoutValueEl.textContent = fmtNum(numericGold);
    }

    function updateStatsHUD() {
        const tEl = document.getElementById('blon-stat-troops');
        const gEl = document.getElementById('blon-stat-gold');
        const tiEl = document.getElementById('blon-stat-tiles');
        const aEl = document.getElementById('blon-stat-attacks');
        if (!tEl) return;
        const state = getGameState();
        if (!state) {
            [tEl,gEl,tiEl,aEl].forEach(el => { if(el) el.textContent = 'N/A'; });
            const rateEl = document.getElementById('blon-stat-gold-rate');
            if (rateEl) rateEl.textContent = 'N/A';
            const popoutRateEl = document.getElementById('blon-popout-gold-rate');
            if (popoutRateEl) popoutRateEl.textContent = 'N/A';
            const popoutValueEl = document.getElementById('blon-popout-gold-value');
            if (popoutValueEl) popoutValueEl.textContent = 'N/A';
            const top10El = document.getElementById('blon-gold-top10');
            const teamGoldEl = document.getElementById('blon-team-gold');
            if (top10El) top10El.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
            if (teamGoldEl) teamGoldEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
            const popoutTop10El = document.getElementById('blon-popout-gold-top10');
            const popoutTeamGoldEl = document.getElementById('blon-popout-team-gold');
            if (popoutTop10El) popoutTop10El.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
            if (popoutTeamGoldEl) popoutTeamGoldEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No game state available.</div>';
            updateMainGoldOverlayVisibility();
            return;
        }
        try {
            if (tEl) {
                const tr = state.myPlayer.troops ? state.myPlayer.troops() : 0;
                const numericTroops = typeof tr === 'bigint' ? Number(tr) : tr;
                tEl.textContent = fmtNum(numericTroops / 10);
            }
            if (gEl) {
                const g = state.myPlayer.gold ? state.myPlayer.gold() : 0;
                gEl.textContent = fmtNum(typeof g === 'bigint' ? Number(g) : g);
            }
            if (tiEl) tiEl.textContent = fmtNum(state.myPlayer.numTilesOwned ? state.myPlayer.numTilesOwned() : 0);
            const atks = state.myPlayer.outgoingAttacks ? state.myPlayer.outgoingAttacks() : [];
            if (aEl) aEl.textContent = atks.length;
            renderGoldRate(state);
            if (cfg.features && cfg.features.overlays === false) {
                const overlay = document.getElementById('blon-gold-overlay');
                if (overlay) overlay.style.display = 'none';
                const popTop10 = document.getElementById('blon-popout-gold-top10');
                const popTeam = document.getElementById('blon-popout-team-gold');
                if (popTop10) popTop10.innerHTML = '';
                if (popTeam) popTeam.innerHTML = '';
            } else if (cfg.showGoldOverlay || document.getElementById('blon-popout-gold-top10') || document.getElementById('blon-popout-team-gold')) {
                renderGoldOverlay(state);
            }
            updateSiloNotification();
            updateSiloPanel();
        } catch(e) {}
    }

    let lobbyWS = null;
    let lobbyReconnectTimeout = null;
    let lobbyShouldReconnect = true;
    let lobbyConnected = false;
    let lobbyLatestPayload = null;

    function getNumWorkers() {
        const bc = window.BOOTSTRAP_CONFIG;
        return bc && Number.isInteger(bc.numWorkers) && bc.numWorkers > 0 ? bc.numWorkers : 8;
    }

    function simpleHashForWorkerPath(str) {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = ((hash << 5) - hash) + str.charCodeAt(i);
            hash |= 0;
        }
        return Math.abs(hash);
    }

    function getCurrentWorkerPath() {
        const pathParts = window.location.pathname.split("/").filter(Boolean);
        const candidate = pathParts[0];
        return /^w\d+$/.test(candidate) ? candidate : null;
    }

    function getWorkerPath(gameID) {
        const currentPath = getCurrentWorkerPath();
        if (currentPath) {
            return currentPath;
        }
        return `w${simpleHashForWorkerPath(gameID) % getNumWorkers()}`;
    }

    function getLobbyUrl(gameID) {
        const path = getWorkerPath(gameID);
        return `${window.location.origin}/${path}/game/${encodeURIComponent(gameID)}`;
    }

    function formatDuration(ms) {
        if (ms <= 0) return 'now';
        const seconds = Math.ceil(ms / 1000);
        if (seconds < 60) return `${seconds}s`;
        const minutes = Math.floor(seconds / 60);
        if (minutes < 60) return `${minutes}m`;
        return `${Math.floor(minutes / 60)}h`;
    }

    function getLobbyModeLabel(game) {
        const cfg = game.gameConfig;
        const mode = cfg && typeof cfg.gameMode === 'string' ? cfg.gameMode : null;
        const totalPlayers = cfg?.maxPlayers ?? game.numClients ?? undefined;

        if (mode === 'Free For All') {
            return 'FFA';
        }

        if (mode === 'Team') {
            if (cfg?.playerTeams === 'Humans Vs Nations') {
                return totalPlayers
                    ? `Humans vs Nations (${totalPlayers} players)`
                    : 'Humans vs Nations';
            }

            const namedTeamSizes = {
                Duos: 2,
                Trios: 3,
                Quads: 4,
            };

            if (typeof cfg?.playerTeams === 'string' && namedTeamSizes[cfg.playerTeams]) {
                const playersPerTeam = namedTeamSizes[cfg.playerTeams];
                if (totalPlayers) {
                    const teamCount = Math.floor(totalPlayers / playersPerTeam);
                    return `${cfg.playerTeams} (${teamCount} teams of ${playersPerTeam})`;
                }
                return cfg.playerTeams;
            }

            if (typeof cfg?.playerTeams === 'number' && cfg.playerTeams > 0) {
                const teamCount = cfg.playerTeams;
                if (totalPlayers) {
                    const playersPerTeam = Math.floor(totalPlayers / teamCount);
                    return `${teamCount} teams of ${playersPerTeam}`;
                }
                return `${teamCount} teams`;
            }

            return 'Team';
        }

        if (game.publicGameType) {
            return String(game.publicGameType).toUpperCase();
        }

        return 'UNKNOWN';
    }

    function getLobbyMapDisplay(game) {
        try {
            if (!game || typeof game !== 'object') return 'Unknown';

            const resolveVal = (val) => {
                if (val == null) return null;
                if (typeof val === 'string' && val.trim()) return val.trim();
                if (typeof val === 'number') return String(val);
                if (typeof val === 'object') {
                    if (typeof val.name === 'string' && val.name.trim()) return val.name.trim();
                    if (typeof val.mapName === 'string' && val.mapName.trim()) return val.mapName.trim();
                    if (typeof val.gameMap === 'string' && val.gameMap.trim()) return val.gameMap.trim();
                    if (typeof val.id === 'string' && val.id.trim()) return val.id.trim();
                    if (typeof val.id === 'number') return String(val.id);
                }
                return null;
            };
            // try them im too lazy to check which ones are actually used across servers so might as well be thorough
            const topCandidates = [
                'gameMap', 'game_map', 'map', 'mapName', 'map_name', 'terrain', 'terrainName', 'terrain_name', 'mapId', 'map_id'
            ];
            for (const k of topCandidates) {
                if (Object.prototype.hasOwnProperty.call(game, k)) {
                    const v = resolveVal(game[k]);
                    if (v) return v;
                }
            }

            if (game.gameConfig && typeof game.gameConfig === 'object') {
                for (const k of topCandidates) {
                    if (Object.prototype.hasOwnProperty.call(game.gameConfig, k)) {
                        const v = resolveVal(game.gameConfig[k]);
                        if (v) return v;
                    }
                }
                if (Object.prototype.hasOwnProperty.call(game.gameConfig, 'gameMap')) {
                    const v = resolveVal(game.gameConfig.gameMap);
                    if (v) return v;
                }
            }

            for (const key of Object.keys(game)) {
                if (/map|terrain/i.test(key)) {
                    const v = resolveVal(game[key]);
                    if (v) return v;
                }
            }
        } catch (e) {}
        return 'Unknown';
    }

    function setMatchFeedStatus(text, color) {
        const statusEl = document.getElementById('blon-matches-status');
        if (statusEl) { statusEl.textContent = text; statusEl.style.color = color || '#ffcc00'; }
    }

    function updateMatchFeedButton() {
        const btn = document.getElementById('blon-matches-connect-btn');
        if (!btn) return;
        btn.textContent = lobbyWS ? 'Disconnect Feed' : 'Connect Feed';
    }

    function renderLobbyMatches() {
        const listEl = document.getElementById('blon-matches-list');
        if (!listEl) return;
        if (!lobbyLatestPayload || !lobbyLatestPayload.games) {
            listEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No lobby feed data yet.</div>';
            return;
        }
        const serverTime = typeof lobbyLatestPayload.serverTime === 'number' ? lobbyLatestPayload.serverTime : Date.now();
        const games = Object.values(lobbyLatestPayload.games).flat();
        if (!games.length) {
            listEl.innerHTML = '<div style="color:#777;grid-column:1/-1;">No upcoming matches available.</div>';
            return;
        }

        const sortedGames = games.slice().sort((a, b) => {
            const aStart = typeof a.startsAt === 'number' ? a.startsAt : serverTime;
            const bStart = typeof b.startsAt === 'number' ? b.startsAt : serverTime;
            if (aStart !== bStart) return aStart - bStart;
            return (a.numClients || 0) - (b.numClients || 0);
        }).slice(0, 18);

        listEl.innerHTML = '';
        sortedGames.forEach(game => {
            const startAt = typeof game.startsAt === 'number' ? game.startsAt : serverTime;
            const timeDelta = startAt - serverTime;
            const isLive = typeof game.startsAt !== 'number' || timeDelta <= 0;
            const status = isLive ? 'Open lobby' : `Starts in ${formatDuration(timeDelta)}`;
            const type = getLobbyModeLabel(game);
            const row = document.createElement('div');
            row.style.cssText = 'display:grid;grid-template-columns:1.7fr 1fr auto;gap:6px;align-items:center;padding:8px 0;border-bottom:1px solid #222;';
            row.innerHTML = `
                <div style="display:flex;flex-direction:column;gap:3px;">
                    <span style="color:#fff;font-size:12px;font-weight:600;">${type}</span>
<span style="color:#aaa;font-size:10px;">${status}</span>
                    <span style="color:#777;font-size:10px;">Map: ${getLobbyMapDisplay(game)}</span>
                </div>
                <div style="display:flex;flex-direction:column;gap:3px;text-align:right;">
                    <span style="color:#ddd;font-size:12px;">${game.numClients} players</span>
                    <span style="color:#777;font-size:10px;">ID ${game.gameID}</span>
                </div>
                <button type="button" style="padding:5px 8px;background:#111;border:1px solid #333;color:#aaa;border-radius:3px;font-size:10px;cursor:pointer;">Join</button>
            `;
            const button = row.querySelector('button');
            if (button) {
                button.addEventListener('click', () => {
                    const lobbyId = game.gameID;
                    try {
                        if (typeof window.showPage === 'function') {
                            window.showPage('page-join-lobby');
                        }

                        const joinEvent = new CustomEvent('join-lobby', {
                            detail: { gameID: lobbyId, source: 'public' },
                            bubbles: true,
                            composed: true,
                        });
                        document.dispatchEvent(joinEvent);

                        return;
                    } catch (e) {
                        console.warn('Join via event failed falling back to URL', e);
                    }

                    const joinModal = document.querySelector('join-lobby-modal');
                    if (joinModal && typeof joinModal.open === 'function') {
                        try { joinModal.open({ lobbyId }); return; } catch (e) { }
                    }

                    const url = getLobbyUrl(lobbyId);
                    window.location.href = url;
                });
            }
            listEl.appendChild(row);
        });
    }

    function scheduleLobbyReconnect() {
        if (!lobbyShouldReconnect || lobbyReconnectTimeout) return;
        lobbyReconnectTimeout = window.setTimeout(() => {
            lobbyReconnectTimeout = null;
            if (lobbyShouldReconnect) connectLobbyFeed();
        }, 3000);
    }

    function handleLobbySocketMessage(event) {
        try {
            const payload = JSON.parse(event.data);
            if (payload && typeof payload.serverTime === 'number' && payload.games) {
                lobbyLatestPayload = payload;
                renderLobbyMatches();
            }
        } catch(e) {
            console.error('[Blon] Lobby feed parse failed', e);
        }
    }

    function connectLobbyFeed() {
        if (!NativeWS) return;
        if (lobbyWS) return;
        lobbyShouldReconnect = true;
        const workerPath = `/w${Math.floor(Math.random() * getNumWorkers())}`;
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const url = `${protocol}//${window.location.host}${workerPath}/lobbies`;
        try {
            lobbyWS = new NativeWS(url);
            setMatchFeedStatus('Connecting…', '#ffaa00');
            updateMatchFeedButton();
            lobbyWS.addEventListener('open', () => {
                lobbyConnected = true;
                setMatchFeedStatus('Connected', '#00ff66');
                if (lobbyReconnectTimeout) { clearTimeout(lobbyReconnectTimeout); lobbyReconnectTimeout = null; }
                updateMatchFeedButton();
                renderLobbyMatches();
            });
            lobbyWS.addEventListener('message', handleLobbySocketMessage);
            lobbyWS.addEventListener('close', () => {
                lobbyConnected = false;
                lobbyWS = null;
                setMatchFeedStatus('Disconnected', '#ff4444');
                updateMatchFeedButton();
                scheduleLobbyReconnect();
            });
            lobbyWS.addEventListener('error', () => {
                lobbyConnected = false;
                setMatchFeedStatus('Error', '#ff4444');
                updateMatchFeedButton();
                scheduleLobbyReconnect();
            });
        } catch (e) {
            lobbyWS = null;
            setMatchFeedStatus('Connect failed', '#ff4444');
            updateMatchFeedButton();
            scheduleLobbyReconnect();
        }
    }

    function disconnectLobbyFeed() {
        lobbyShouldReconnect = false;
        if (lobbyReconnectTimeout) { clearTimeout(lobbyReconnectTimeout); lobbyReconnectTimeout = null; }
        if (lobbyWS) { lobbyWS.close(); lobbyWS = null; }
        lobbyConnected = false;
        setMatchFeedStatus('Disconnected', '#ff4444');
        updateMatchFeedButton();
    }

    let slotUnitMap = {
        '1': 'City', '2': 'Defense Post', '3': 'Port', '4': 'SAM Launcher',
        '5': 'Missile Silo', '6': 'Factory', '7': 'Warship',
        '8': 'Atom Bomb', '9': 'Hydrogen Bomb', '0': 'MIRV'
    };

    try {
        const storedSlots = localStorage.getItem('blon_v8_slots');
        if (storedSlots) slotUnitMap = Object.assign(slotUnitMap, JSON.parse(storedSlots));
    } catch(e) {}

    function saveSlots() {
        try { localStorage.setItem('blon_v8_slots', JSON.stringify(slotUnitMap)); } catch(e) {}
    }

    function spamBuildPacket(unitType) {
        if (!unitType) return; // undefined unit
        if (cfg.spamMethod === 'click') {
            fallbackClick();
            return;
        }
        if (lastKnownTile === null) {
            fallbackClick();
            return;
        }
        sendPacket({ type: 'build_unit', unit: unitType, tile: lastKnownTile });
    }



    function fallbackClick() {
        const target = document.elementFromPoint(mouseX, mouseY) || document.querySelector('canvas') || document.body;
        const props = { view: window, bubbles: true, cancelable: true, clientX: mouseX, clientY: mouseY, button: 0, buttons: 1 };
        target.dispatchEvent(new PointerEvent('pointerdown', props));
        target.dispatchEvent(new MouseEvent('mousedown', props));
        target.dispatchEvent(new PointerEvent('pointerup', props));
        target.dispatchEvent(new MouseEvent('mouseup', props));
        target.dispatchEvent(new MouseEvent('click', props));
    }

    function startSpam(slot) {
        const unitType = slotUnitMap[slot];
        if (!unitType) return;

        if (spamInterval !== null) { clearInterval(spamInterval); spamInterval = null; }

        const statusEl = document.getElementById('blon-spam-status');
        if (statusEl) { statusEl.textContent = `SPAMMING: ${unitType}`; statusEl.style.color = '#ff3333'; }

        spamBuildPacket(unitType);
        spamInterval = setInterval(() => {
            spamBuildPacket(unitType);
        }, cfg.spamInterval);
    }

    function stopSpam() {
        clearTimeout(holdTimeout);
        clearInterval(countdownInterval);
        clearInterval(spamInterval);
        holdTimeout = null; countdownInterval = null; spamInterval = null; currentSpamKey = null;
        toggleSpamActive = false;
        isKeyDown = false;
        const statusEl = document.getElementById('blon-spam-status');
        if (statusEl) { statusEl.textContent = 'IDLE'; statusEl.style.color = '#888'; }
    }

    let embargoOnCooldown = false;
    let embargoCooldownTimer = null;
    let embargoAutoRepeat = false;
    const EMBARGO_COOLDOWN_MS = 30000;

    function fireEmbargoAll() {
        if (embargoOnCooldown) { return; }
        if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
            updateEmbargoStatus('No socket join a game first', '#ff4444');
            return;
        }
        const ok = sendPacket({ type: 'embargo_all', action: 'start' });
        if (ok) {
            startEmbargoCooldown();
        }
    }

    function liftEmbargoAll() {
        if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
            updateEmbargoStatus('No socket join a game first', '#ff4444');
            return;
        }
        sendPacket({ type: 'embargo_all', action: 'stop' });
        updateEmbargoStatus('All embargoes lifted', '#00ff66');
        setTimeout(() => updateEmbargoStatus('Ready', '#00ff66'), 2000);
    }

    function getOnePercentAmount(value) {
        return getPercentAmount(value, 1);
    }

    function getPercentAmount(value, percent) {
        const n = typeof value === 'bigint' ? Number(value) : Number(value || 0);
        if (!Number.isFinite(n) || n <= 0) return 0;
        const pct = clamp(Number(percent || 0), 1, 100);
        return Math.max(1, Math.floor(n * pct / 100));
    }

    function getPlayerAtTile(game, tile) {
        if (!game || tile === null || tile === undefined) return null;
        try {
            if (typeof game.hasOwner === 'function' && !game.hasOwner(tile)) return null;
            const owner = typeof game.owner === 'function' ? game.owner(tile) : null;
            if (!owner || (typeof owner.isPlayer === 'function' && !owner.isPlayer())) return null;
            return owner;
        } catch (e) {
            return null;
        }
    }

    function getDonationTarget() {
        const state = getGameState();
        if (!state) return null;

        const hovered = getPlayerAtTile(state.game, lastKnownTile);
        if (
            hovered &&
            !samePlayer(hovered, state.myPlayer) &&
            isFriendlyPlayer(state.myPlayer, hovered) &&
            (!hovered.isAlive || hovered.isAlive())
        ) {
            return { state, target: hovered };
        }

        return { state, target: null };
    }

    function sendOnePercentBoat() {
        const state = getGameState();
        if (!state) {
            updateCombatStatus('Game state not found', '#ff4444');
            return;
        }
        if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
            updateCombatStatus('No socket join a game first', '#ff4444');
            return;
        }
        if (lastKnownTile === null) {
            updateCombatStatus('Hover a target tile first', '#ffaa00');
            return;
        }
        const troops = getOnePercentAmount(state.myPlayer.troops ? state.myPlayer.troops() : 0);
        if (troops <= 0) {
            updateCombatStatus('No troops available', '#ffaa00');
            return;
        }
        const ok = sendPacket({ type: 'boat', troops, dst: lastKnownTile });
        updateCombatStatus(ok ? `Sent 1% boat (${fmtNum(troops / 10)})` : 'Boat send failed', ok ? '#00ff66' : '#ff4444');
        if (ok) setTimeout(() => updateCombatStatus('Ready', '#00ff66'), 2000);
    }

    function sendDonationToAlly(kind, percent) {
        if (!activeSocket || activeSocket.readyState !== WS_OPEN) {
            updateEmbargoStatus('No socket join a game first', '#ff4444');
            return;
        }

        const result = getDonationTarget();
        if (!result || !result.state) {
            updateEmbargoStatus('Game state not found', '#ff4444');
            return;
        }
        if (!result.target) {
            updateEmbargoStatus('Hover an ally first', '#ffaa00');
            return;
        }

        const targetId = result.target.id && result.target.id();
        if (!targetId) {
            updateEmbargoStatus('Ally id not found', '#ff4444');
            return;
        }

        const amount = kind === 'gold'
            ? getPercentAmount(result.state.myPlayer.gold ? result.state.myPlayer.gold() : 0, percent)
            : getPercentAmount(result.state.myPlayer.troops ? result.state.myPlayer.troops() : 0, percent);
        if (amount <= 0) {
            updateEmbargoStatus(kind === 'gold' ? 'No gold available' : 'No troops available', '#ffaa00');
            return;
        }

        const intent = kind === 'gold'
            ? { type: 'donate_gold', recipient: targetId, gold: amount }
            : { type: 'donate_troops', recipient: targetId, troops: amount };
        const ok = sendPacket(intent);
        const label = kind === 'gold' ? 'gold' : 'troops';
        const targetName = getPlayerLabel(result.target);
        const formattedAmount = kind === 'gold' ? fmtNum(amount) : fmtNum(amount / 10);
        updateEmbargoStatus(ok ? `Sent ${percent}% ${label} (${formattedAmount}) to ${targetName}` : `Send ${label} failed`, ok ? '#00ff66' : '#ff4444');
        if (ok) setTimeout(() => updateEmbargoStatus('Ready', '#00ff66'), 2000);
    }

    function startEmbargoCooldown() {
        embargoOnCooldown = true;
        updateEmbargoStatus('Sent! Cooling down...', '#ffaa00');

        const fireBtn = document.getElementById('embargo-fire-btn');
        if (fireBtn) {
            fireBtn.disabled = true;
            fireBtn.style.opacity = '0.4';
            fireBtn.style.cursor = 'not-allowed';
        }

        let remaining = EMBARGO_COOLDOWN_MS / 1000;
        const tick = () => {
            remaining--;
            updateEmbargoStatus(`Cooldown: ${remaining}s`, '#888');
            if (remaining <= 0) {
                clearInterval(embargoCooldownTimer);
                embargoOnCooldown = false;
                updateEmbargoStatus('Ready', '#00ff66');
                if (fireBtn) { fireBtn.disabled = false; fireBtn.style.opacity = '1'; fireBtn.style.cursor = 'pointer'; }
                if (embargoAutoRepeat) { fireEmbargoAll(); }
            }
        };
        embargoCooldownTimer = setInterval(tick, 1000);
    }

    function updateEmbargoStatus(text, color) {
        const el = document.getElementById('embargo-status-text');
        if (el) { el.textContent = text; el.style.color = color; }
    }

    function fmtActionKey(actionId) {
        const binding = cfg.actionHotkeyMap[actionId];
        if (!binding || !binding.key) return 'NONE';
        const mod = binding.mod ? binding.mod.toUpperCase() + '+' : '';
        return mod + binding.key.toUpperCase();
    }

    const ui = document.createElement('div');
    ui.id = 'blon-root';
ui.style.cssText = `
        position:fixed; top:15px; right:15px; width:320px; min-width:260px; max-width:560px;

        background:rgba(0,0,0,${cfg.guiOpacity}); color:#fff;
        font-family:monospace; font-size:11px;
        border-radius:5px; border:1px solid #333;
        z-index:999999; user-select:none;
        box-shadow:0 6px 24px rgba(0,0,0,0.7);
        resize:both; overflow:hidden;
        display:flex; flex-direction:column;
        min-height:120px; max-height:85vh;
        opacity:1;
    `;

    function applyUiStyle() {
        ui.style.backgroundColor = `rgba(0,0,0,${cfg.guiOpacity})`;
        const drag = document.getElementById('blon-drag');
        if (drag) drag.style.color = cfg.guiColor;
        const link = document.getElementById('blon-link');
        if (link) link.style.color = cfg.guiColor;
    }

    function applyOverlayAppearance() {
        const overlay = document.getElementById('blon-gold-overlay');
        if (overlay) {
            overlay.style.opacity = String(cfg.overlayOpacity);
            overlay.style.setProperty('opacity', String(cfg.overlayOpacity), 'important');
        }

        const popTop10Overlay = document.getElementById('blon-popout-gold');
        const popMpsOverlay = document.getElementById('blon-popout-mps');
        [popTop10Overlay, popMpsOverlay].forEach(el => {
            if (!el) return;
            el.style.opacity = String(cfg.overlayOpacity);
            el.style.setProperty('opacity', String(cfg.overlayOpacity), 'important');
        });
    }

    ui.innerHTML = `
        <div id="blon-drag" style="display:flex;align-items:center;justify-content:space-between;padding:7px 12px;background:#111;color:${cfg.guiColor};font-weight:bold;letter-spacing:1px;cursor:move;border-radius:5px 5px 0 0;border-bottom:1px solid #222;flex-shrink:0;">
            <div style="display:flex;align-items:center;gap:7px;">
                <a href="https://github.com/blontd6/" target="_blank" id="blon-github" style="color:#00ff66;text-decoration:none;" title="GitHub">
                    <svg height="14" width="14" viewBox="0 0 16 16" fill="currentColor">
                        <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
                    </svg>
                </a>
                <span>PROJECT BLON v22</span>
            </div>
            <div style="display:flex;align-items:center;gap:8px;">
                <span id="blon-link" style="font-size:9px;color:#ff4444;font-weight:normal;">DISCONNECTED</span>
                <button id="blon-minimize-btn" title="Minimize Blon UI" style="background:transparent;border:none;color:#888;cursor:pointer;font-size:12px;line-height:1;padding:0 4px;">▾</button>
            </div>
        </div>

        <div style="display:flex;background:#161616;border-bottom:1px solid #222;font-size:10px;flex-shrink:0;">
            <div id="tab-main2-btn"  class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;border-right:1px solid #222;background:#000;font-weight:bold;color:#fff;">Main</div>
            <div id="tab-main-btn"    class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Spam</div>
            <div id="tab-stats-btn"   class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Stats</div>
            <div id="tab-matches-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Lob</div>
        <div id="tab-combat-btn"  class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Cmbt</div>


            <div id="tab-diplo-btn"   class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Diplo</div>
            <div id="tab-embargo-btn" class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;border-right:1px solid #222;">Trade</div>
            <div id="tab-cfg-btn"     class="blon-tab" style="flex:1;text-align:center;padding:6px 0;cursor:pointer;color:#888;">Cfg</div>
        </div>

        <div id="tab-main2-panel" style="padding:10px 12px;overflow-y:auto;flex:1;">
            <div style="margin-bottom:10px;border-top:1px solid #222;padding-top:8px;">
                <div style="color:#aaa;font-size:10px;margin-bottom:6px;">Disable Features</div>
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
                    <input type="checkbox" id="blon-feat-spam-hotkeys" ${cfg.features?.spamHotkeys ? 'checked' : ''} style="cursor:pointer;margin:0;"> Spam hotkeys
                </label>
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
                    <input type="checkbox" id="blon-feat-combat-hotkeys" ${cfg.features?.combatHotkeys ? 'checked' : ''} style="cursor:pointer;margin:0;"> Combat hotkeys
                </label>
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
                    <input type="checkbox" id="blon-feat-action-hotkeys" ${cfg.features?.actionHotkeys ? 'checked' : ''} style="cursor:pointer;margin:0;"> Action hotkeys (trade/embargo)
                </label>
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
                    <input type="checkbox" id="blon-feat-quick-chat" ${cfg.features?.quickChat ? 'checked' : ''} style="cursor:pointer;margin:0;"> Quick chat
                </label>
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 4px;">
                    <input type="checkbox" id="blon-feat-embargo" ${cfg.features?.embargo ? 'checked' : ''} style="cursor:pointer;margin:0;"> Embargo controls
                </label>
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin:0 0 0;">
                    <input type="checkbox" id="blon-feat-overlays" ${cfg.features?.overlays ? 'checked' : ''} style="cursor:pointer;margin:0;"> Overlays
                </label>
            </div>
        </div>

        <div id="tab-main-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
            <div style="color:#888;margin-bottom:7px;font-size:10px;" id="blon-mode-hint">Hold hotkey over map to spam build packets.</div>

            <label style="display:flex;align-items:center;gap:6px;margin-bottom:5px;cursor:pointer;color:#aaa;">
                <input type="checkbox" id="blon-toggle-mode" ${cfg.toggleMode ? 'checked' : ''} style="cursor:pointer;margin:0;"> Toggle Mode (press once to start/stop)
            </label>

            <div style="margin-bottom:8px;">
                <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Spam Method:</div>
                <div style="display:flex;gap:4px;">
                    <button id="blon-method-ws" style="flex:1;padding:4px 0;border-radius:3px;border:1px solid #444;font-family:monospace;font-size:10px;cursor:pointer;background:${cfg.spamMethod==='websocket'?'#00ff66':'#111'};color:${cfg.spamMethod==='websocket'?'#000':'#aaa'};font-weight:${cfg.spamMethod==='websocket'?'bold':'normal'};">WebSocket</button>
                    <button id="blon-method-click" style="flex:1;padding:4px 0;border-radius:3px;border:1px solid #444;font-family:monospace;font-size:10px;cursor:pointer;background:${cfg.spamMethod==='click'?'#ff9900':'#111'};color:${cfg.spamMethod==='click'?'#000':'#aaa'};font-weight:${cfg.spamMethod==='click'?'bold':'normal'};">Click</button>
                </div>
            </div>

            <label style="display:flex;align-items:center;gap:6px;margin-bottom:5px;cursor:pointer;color:#aaa;">
                <input type="checkbox" id="blon-toggle-charge" ${cfg.useChargeTime ? 'checked' : ''} style="cursor:pointer;margin:0;"> Enable Charge Delay
            </label>
            <label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
                <input type="checkbox" id="blon-toggle-passthrough" ${cfg.blockPassThrough ? 'checked' : ''} style="cursor:pointer;margin:0;"> Block Key Pass-Through
            </label>

            <div style="font-size:10px;color:#555;border-top:1px solid #222;padding-top:6px;">
                Spam Status: <span id="blon-spam-status" style="color:#888;">IDLE</span>
            </div>
            <div style="font-size:10px;color:#555;margin-top:3px;">
                Last Tile: <span id="blon-tile-display" style="color:#888;">none</span>
            </div>
        </div>

        <div id="tab-stats-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
            <div style="display:grid;grid-template-columns:auto 1fr;gap:4px 10px;font-size:11px;">
                <span style="color:#888;">Troops</span>   <span id="blon-stat-troops"  style="color:#00ff66;font-weight:bold;">N/A</span>
                <span style="color:#888;">Gold</span>     <span id="blon-stat-gold"    style="color:#ffcc00;font-weight:bold;">N/A</span>
                <span style="color:#888;">Tiles</span>    <span id="blon-stat-tiles"   style="color:#66aaff;font-weight:bold;">N/A</span>
                <span style="color:#888;">Attacks</span>  <span id="blon-stat-attacks" style="color:#ff9900;font-weight:bold;">N/A</span>
                <span style="color:#888;">Money/sec</span> <span id="blon-stat-gold-rate" style="color:#ffcc00;font-weight:bold;">N/A</span>
            </div>
            <div style="display:grid;grid-template-columns:auto 1fr;gap:6px 10px;align-items:center;font-size:11px;margin-top:6px;">
                <span style="color:#888;">Smoothing window</span>
                <div style="display:flex;align-items:center;gap:8px;">
                    <input id="blon-gold-rate-window" type="range" min="5" max="240" step="1" value="${cfg.goldRateWindowSeconds}" style="flex:1 1 100px;min-width:60px;max-width:100px;">
                    <span id="blon-gold-rate-window-value" style="color:#ffcc00;font-size:10px;min-width:40px;text-align:right;">${cfg.goldRateWindowSeconds}s</span>
                </div>
            </div>
            <div style="display:flex;justify-content:space-between;gap:8px;margin:12px 0 8px;flex-wrap:wrap;align-items:center;">
                <div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
                    <label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#aaa;margin:0;">
                        <input type="checkbox" id="blon-toggle-gold-overlay" ${cfg.showGoldOverlay ? 'checked' : ''} style="cursor:pointer;margin:0;"> Show top gold leaderboard and team totals
                    </label>
                    <label style="display:flex;align-items:center;gap:6px;cursor:pointer;color:#aaa;margin:0;">
                        <input type="checkbox" id="blon-toggle-troops-overlay" ${cfg.showTroopsOverlay ? 'checked' : ''} style="cursor:pointer;margin:0;"> Show Troops in Leaderboard
                    </label>
                    <div style="display:flex;align-items:center;gap:8px;color:#aaa;font-size:11px;">
                        <span>Leaderboard size</span>
                        <input id="blon-gold-top-count" type="range" min="1" max="20" step="1" value="${cfg.goldTopCount || 10}" style="flex:1 1 120px;min-width:80px;max-width:180px;">
                        <span id="blon-gold-top-count-value" style="color:#ffcc00;font-size:10px;min-width:20px;text-align:right;">${cfg.goldTopCount || 10}</span>
                    </div>
                    <div style="display:flex;align-items:center;gap:8px;color:#aaa;font-size:11px;">
                        <span>Troops Check</span>
                        <input id="blon-troops-check-interval" type="range" min="1" max="1000" step="1" value="${cfg.troopsCheckIntervalMs || 500}" style="flex:1 1 120px;min-width:80px;max-width:180px;">
                        <span id="blon-troops-check-interval-value" style="color:#00ff66;font-size:10px;min-width:40px;text-align:right;">${cfg.troopsCheckIntervalMs || 500}ms</span>
                    </div>
                </div>
                <div style="display:flex;gap:6px;flex-wrap:wrap;">
                    <button id="blon-popout-gold-btn" style="padding:4px 8px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Pop out Gold Leaderboard</button>
                    <button id="blon-popout-mps-btn" style="padding:4px 8px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Pop out Income/sec</button>
                </div>
            </div>
            <div id="blon-gold-overlay" style="display:${cfg.showGoldOverlay ? 'block' : 'none'};font-size:11px;line-height:1.4;color:#ddd;opacity:${cfg.overlayOpacity};">
                <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Top ${cfg.goldTopCount || 10} players by gold</div>
                <div id="blon-gold-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;margin-bottom:10px;"></div>
                <div style="color:#aaa;font-size:10px;margin:10px 0 4px;">Top ${cfg.goldTopCount || 10} players by income/sec</div>
                <div id="blon-gold-mps-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;margin-bottom:10px;"></div>
                <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Team gold totals</div>
                <div id="blon-team-gold" style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;"></div>
            </div>
        </div>

        <div id="tab-matches-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                <span style="color:#aaa;font-size:10px;">Future Match Detector</span>
                <span id="blon-matches-status" style="color:#ffcc00;font-weight:bold;font-size:10px;">Disconnected</span>
            </div>
            <div style="display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap;">
                <button id="blon-matches-connect-btn" style="flex:1;min-width:120px;padding:6px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Connect Feed</button>
                <button id="blon-matches-refresh-btn" style="flex:1;min-width:120px;padding:6px;background:#111;border:1px solid #333;color:#aaa;font-size:10px;cursor:pointer;border-radius:3px;">Refresh List</button>
            </div>
            <div id="blon-matches-list" style="display:flex;flex-direction:column;gap:8px;font-size:11px;min-height:120px;color:#ddd;">
                <div style="color:#777;font-size:10px;">Waiting for lobby feed...</div>
            </div>
        </div>

        <div id="tab-combat-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">





            <div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
                <span style="color:#aaa;font-size:10px;">Status:</span>
                <span id="blon-combat-status" style="color:#00ff66;font-weight:bold;font-size:10px;">Ready</span>
            </div>
            <div id="blon-silo-notification" style="color:#00ff66;font-size:10px;min-height:18px;cursor:pointer;text-decoration:underline;display:none;user-select:none;margin-bottom:8px;">Silo indicator disabled</div>
            <label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
                <input type="checkbox" id="cfg-combat-silo-indicator" ${cfg.combatSiloIndicator ? 'checked' : ''} style="cursor:pointer;margin:0;"> Silo build notifications
            </label>
            <div id="blon-silo-subtoggles" style="display:${cfg.combatSiloIndicator ? 'block' : 'none'};margin-left:14px;margin-bottom:8px;">
                <label style="display:flex;align-items:center;gap:6px;margin-bottom:6px;cursor:pointer;color:#aaa;">
                    <input type="checkbox" id="cfg-combat-silo-panel" ${cfg.combatSiloPanel ? 'checked' : ''} style="cursor:pointer;margin:0;"> Pop out silo tracker UI
                </label>
                <label style="display:flex;align-items:center;gap:6px;margin-bottom:6px;cursor:pointer;color:#aaa;">
                    <input type="checkbox" id="cfg-combat-silo-allies" ${cfg.combatSiloOnlyAllies ? 'checked' : ''} style="cursor:pointer;margin:0;"> Filter ally silos only
                </label>
                <label style="display:flex;align-items:center;gap:6px;margin-bottom:6px;cursor:pointer;color:#aaa;">
                    <input type="checkbox" id="cfg-combat-silo-keep-all" ${cfg.combatSiloKeepAllPlaced ? 'checked' : ''} style="cursor:pointer;margin:0;"> Keep every placed silo in the UI
                </label>
            </div>
            <label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
                <input type="checkbox" id="cfg-combat-priority" ${cfg.combatHotkeysPriority ? 'checked' : ''} style="cursor:pointer;margin:0;"> Combat hotkeys override game hotkeys
            </label>
            <label style="display:flex;align-items:center;gap:6px;margin-bottom:8px;cursor:pointer;color:#aaa;">
                <input type="checkbox" id="cfg-missile-predictor" ${cfg.features?.missilePredictor ? 'checked' : ''} style="cursor:pointer;margin:0;"> Missile predictor overlay
            </label>

            <div style="border-top:1px solid #222;padding-top:8px;margin-top:4px;">
                <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Combat hotkeys (sets attack %):</div>
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;font-size:10px;">
                    <div id="combat-atk1-label" style="color:#888;"></div><div id="combat-atk2-label" style="color:#888;"></div>
                    <div id="combat-atk3-label" style="color:#888;"></div><div id="combat-atk4-label" style="color:#888;"></div>
                    <div id="combat-boat1-label" style="color:#888;grid-column:1/-1;"></div>
                </div>
            </div>
        </div>

        <div id="tab-diplo-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
            <div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;">
                <span style="color:#aaa;font-size:10px;">Status:</span>
                <span id="blon-diplo-status" style="color:#00ff66;font-weight:bold;font-size:10px;">Ready</span>
            </div>

            <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;margin-bottom:8px;">
                <input type="checkbox" id="blon-auto-accept" style="cursor:pointer;margin:0;">
                <span style="font-size:11px;">Auto-Accept Incoming Alliances</span>
            </label>

            <div style="border-top:1px solid #222;padding-top:8px;margin-top:8px;">
                <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Quick Chat:</div>
                <select id="blon-qchat-target" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 4px;font-family:monospace;font-size:10px;border-radius:2px;margin-bottom:5px;box-sizing:border-box;">
                    <option value="enemies" selected>Send to enemies only</option>
                    <option value="allies">Send to allies only</option>
                    <option value="everyone">Send to everyone</option>
                </select>
                <select id="blon-qchat-select" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 4px;font-family:monospace;font-size:10px;border-radius:2px;margin-bottom:5px;box-sizing:border-box;"></select>
                <button id="blon-qchat-btn" style="width:100%;background:#111;color:#aaa;border:1px solid #444;padding:6px;border-radius:4px;font-weight:bold;font-size:11px;cursor:pointer;font-family:monospace;transition:background 0.15s;box-sizing:border-box;">
                    SEND QUICK CHAT
                </button>
            </div>
        </div>

        <div id="tab-embargo-panel" style="padding:12px;display:none;overflow-y:auto;flex:1;">
            <div style="margin-bottom:10px;display:flex;justify-content:space-between;align-items:center;">
                <span style="color:#aaa;">Status:</span>
                <span id="embargo-status-text" style="color:#00ff66;font-weight:bold;">Ready</span>
            </div>

            <button id="embargo-fire-btn" style="width:100%;background:#111;color:#aaa;border:1px solid #444;padding:6px;border-radius:4px;font-weight:bold;font-size:11px;cursor:pointer;margin-bottom:6px;font-family:monospace;transition:background 0.15s;box-sizing:border-box;">
                EMBARGO ALL [<span id="label-embargo_fire">${fmtActionKey('embargo_fire')}</span>]
            </button>

            <button id="embargo-lift-btn" style="width:100%;background:#111;color:#aaa;border:1px solid #444;padding:6px;border-radius:4px;font-weight:bold;font-size:11px;cursor:pointer;margin-bottom:10px;font-family:monospace;transition:background 0.15s;box-sizing:border-box;">
                LIFT ALL EMBARGOES [<span id="label-embargo_lift">${fmtActionKey('embargo_lift')}</span>]
            </button>

            <div style="border-top:1px solid #222;padding-top:8px;margin-top:8px;">
                <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Donation hotkeys target the hovered ally:</div>
                <div style="display:grid;grid-template-columns:1fr 1fr;gap:3px;font-size:10px;margin-bottom:8px;">
                    <div id="trade-troops1-label" style="color:#888;"></div><div id="trade-gold1-label" style="color:#888;"></div>
                    <div id="trade-troops2-label" style="color:#888;"></div><div id="trade-gold2-label" style="color:#888;"></div>
                    <div id="trade-troops3-label" style="color:#888;"></div><div id="trade-gold3-label" style="color:#888;"></div>
                </div>
            </div>

            <div style="border-top:1px solid #222;padding-top:8px;">
                <label style="display:flex;align-items:center;gap:7px;cursor:pointer;color:#aaa;">
                    <input type="checkbox" id="embargo-auto-repeat" style="cursor:pointer;margin:0;">
                    <span>Auto-Repeat after cooldown</span>
                </label>
            </div>
        </div>

        <div id="tab-cfg-panel" style="padding:10px 12px;display:none;overflow-y:auto;flex:1;">
            <div style="margin-bottom:8px;">
                <div style="color:#aaa;margin-bottom:3px;">Spam Interval (ms):</div>
                <input type="number" id="cfg-spam-rate" value="${cfg.spamInterval}" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
            </div>
            <div style="margin-bottom:10px;">
                <div style="color:#aaa;margin-bottom:3px;">Charge Time (ms):</div>
                <input type="number" id="cfg-charge-delay" value="${cfg.holdDelay}" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
            </div>

            <div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Combat Hotkey Percentages</div>
            <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;">
                <div>
                    <span style="color:#aaa;">Hotkey 1 (%):</span>
                    <input type="number" id="cfg-combat-pct-1" value="${cfg.combatPercentages.atk_1}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Hotkey 2 (%):</span>
                    <input type="number" id="cfg-combat-pct-2" value="${cfg.combatPercentages.atk_2}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Hotkey 3 (%):</span>
                    <input type="number" id="cfg-combat-pct-3" value="${cfg.combatPercentages.atk_3}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Hotkey 4 (%):</span>
                    <input type="number" id="cfg-combat-pct-4" value="${cfg.combatPercentages.atk_4}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
            </div>

            <div style="color:#00ff66;font-weight:bold;margin-bottom:6px;border-top:1px solid #222;padding-top:7px;">Key Bindings (click to rebind)</div>
            <div id="blon-bind-matrix" style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"></div>

            <div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Trade Hotkey Percentages</div>
            <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;">
                <div>
                    <span style="color:#aaa;">Troops 1 (%):</span>
                    <input type="number" id="cfg-trade-pct-troops-1" value="${cfg.tradePercentages.donate_troops_1}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Money 1 (%):</span>
                    <input type="number" id="cfg-trade-pct-gold-1" value="${cfg.tradePercentages.donate_gold_1}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Troops 2 (%):</span>
                    <input type="number" id="cfg-trade-pct-troops-2" value="${cfg.tradePercentages.donate_troops_2}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Money 2 (%):</span>
                    <input type="number" id="cfg-trade-pct-gold-2" value="${cfg.tradePercentages.donate_gold_2}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Troops 3 (%):</span>
                    <input type="number" id="cfg-trade-pct-troops-3" value="${cfg.tradePercentages.donate_troops_3}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Money 3 (%):</span>
                    <input type="number" id="cfg-trade-pct-gold-3" value="${cfg.tradePercentages.donate_gold_3}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Troops 4 (%):</span>
                    <input type="number" id="cfg-trade-pct-troops-4" value="${cfg.tradePercentages.donate_troops_4}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
                <div>
                    <span style="color:#aaa;">Money 4 (%):</span>
                    <input type="number" id="cfg-trade-pct-gold-4" value="${cfg.tradePercentages.donate_gold_4}" min="1" max="100" style="width:100%;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                </div>
            </div>

            <div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Action Hotkeys (click to rebind)</div>
            <div style="font-size:10px;color:#555;margin-bottom:6px;">Mod keys: Alt, Ctrl, Shift</div>
            <div id="blon-action-bind-matrix" style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"></div>

            <div style="color:#00ff66;font-weight:bold;margin:10px 0 6px;border-top:1px solid #222;padding-top:7px;">Slot to Unit Mapping (websocket spam only)</div>
            <div id="blon-slot-matrix" style="display:flex;flex-direction:column;gap:3px;"></div>

            <div style="color:#00ff66;font-weight:bold;margin:14px 0 6px;border-top:1px solid #222;padding-top:10px;">GUI & Overlay Appearance</div>

            <div style="display:grid;grid-template-columns:1fr;gap:8px;margin-top:6px;">
                <div>
                    <div style="color:#aaa;margin-bottom:3px;font-size:10px;">GUI background opacity (0.1 - 1):</div>
                    <input type="range" id="cfg-gui-opacity" min="0.1" max="1" step="0.01" value="${cfg.guiOpacity}" style="width:100%;">
                    <div style="display:flex;justify-content:space-between;gap:10px;font-size:10px;color:#888;">
                        <span>0.10</span><span id="cfg-gui-opacity-val" style="color:#ffcc00;font-weight:bold;">${cfg.guiOpacity.toFixed(2)}</span><span>1.00</span>
                    </div>
                </div>

                <div style="display:none;">
                    <div style="color:#aaa;margin-bottom:3px;font-size:10px;">GUI accent color:</div>
                    <div style="display:flex;gap:6px;align-items:center;">
                        <input type="color" id="cfg-gui-color-picker" value="${cfg.guiColor}" style="width:34px;height:28px;border:none;padding:0;background:#111;border-radius:4px;cursor:pointer;">
                        <input type="text" id="cfg-gui-color" value="${cfg.guiColor}" style="flex:1;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                    </div>
                    <div style="color:#aaa;margin:6px 0 3px;font-size:10px;">Accent hue:</div>
                    <input type="range" id="cfg-gui-color-hue" min="0" max="360" step="1" value="${cfg.guiColorHue}" style="width:100%;">
                    <div style="display:flex;justify-content:space-between;gap:10px;font-size:10px;color:#888;">
                        <span>0</span><span id="cfg-gui-color-hue-val" style="color:#ffcc00;font-weight:bold;">${cfg.guiColorHue}</span><span>360</span>
                    </div>
                </div>

                <div>
                    <div style="color:#aaa;margin-bottom:3px;font-size:10px;">Overlay opacity (0.1 - 1):</div>
                    <input type="range" id="cfg-overlay-opacity" min="0.1" max="1" step="0.01" value="${cfg.overlayOpacity}" style="width:100%;">
                    <div style="display:flex;justify-content:space-between;gap:10px;font-size:10px;color:#888;">
                        <span>0.10</span><span id="cfg-overlay-opacity-val" style="color:#ffcc00;font-weight:bold;">${cfg.overlayOpacity.toFixed(2)}</span><span>1.00</span>
                    </div>
                </div>

                <div style="display:none;">
                    <div style="color:#aaa;margin-bottom:3px;font-size:10px;">Overlay color:</div>
                    <div style="display:flex;gap:6px;align-items:center;">
                        <input type="color" id="cfg-overlay-color-picker" value="${cfg.overlayColor}" style="width:34px;height:28px;border:none;padding:0;background:#111;border-radius:4px;cursor:pointer;">
                        <input type="text" id="cfg-overlay-color" value="${cfg.overlayColor}" style="flex:1;background:#111;border:1px solid #444;color:#fff;padding:3px 5px;box-sizing:border-box;font-family:monospace;font-size:11px;border-radius:2px;">
                    </div>
                </div>
            </div>
        </div>
    `;

    function mountUI() {
        document.body.appendChild(ui);
        bindUIEvents();
        setSiloSubtoggleVisibility();
        updateSiloPanel();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', mountUI);
    } else {
        mountUI();
    }

    const ACTION_DEFS = [
        { id: 'embargo_fire',   label: 'Embargo All' },
        { id: 'embargo_lift',   label: 'Lift Embargo' },
        { id: 'donate_troops_1', label: 'Donate Troops 1' },
        { id: 'donate_troops_2', label: 'Donate Troops 2' },
        { id: 'donate_troops_3', label: 'Donate Troops 3' },
        { id: 'donate_troops_4', label: 'Donate Troops 4' },
        { id: 'donate_gold_1',   label: 'Donate Money 1' },
        { id: 'donate_gold_2',   label: 'Donate Money 2' },
        { id: 'donate_gold_3',   label: 'Donate Money 3' },
        { id: 'donate_gold_4',   label: 'Donate Money 4' }
    ];

    function refreshActionLabels() {
        ACTION_DEFS.forEach(def => {
            const label = fmtActionKey(def.id);
            ['label-' + def.id, 'label-' + def.id + '2'].forEach(elId => {
                const el = document.getElementById(elId);
                if (el) el.textContent = label;
            });
        });
        // refresh combat atk labels
        refreshAtkLabels();
        refreshTradeLabels();
    }

    function refreshTradeLabels() {
        const slots = [
            { id: 'donate_troops_1', elId: 'trade-troops1-label', label: 'Troops 1' },
            { id: 'donate_troops_2', elId: 'trade-troops2-label', label: 'Troops 2' },
            { id: 'donate_troops_3', elId: 'trade-troops3-label', label: 'Troops 3' },
            { id: 'donate_troops_4', elId: 'trade-troops4-label', label: 'Troops 4' },
            { id: 'donate_gold_1', elId: 'trade-gold1-label', label: 'Money 1' },
            { id: 'donate_gold_2', elId: 'trade-gold2-label', label: 'Money 2' },
            { id: 'donate_gold_3', elId: 'trade-gold3-label', label: 'Money 3' },
            { id: 'donate_gold_4', elId: 'trade-gold4-label', label: 'Money 4' },
        ];
        slots.forEach(s => {
            const el = document.getElementById(s.elId);
            if (!el) return;
            const keyLabel = fmtActionKey(s.id);
            const pct = cfg.tradePercentages[s.id] || 0;
            el.textContent = `${s.label}: ${keyLabel} = ${pct}%`;
        });
    }

    function refreshAtkLabels() {
        const slots = [
            { id: 'atk_1', elId: 'combat-atk1-label' },
            { id: 'atk_2', elId: 'combat-atk2-label' },
            { id: 'atk_3', elId: 'combat-atk3-label' },
            { id: 'atk_4', elId: 'combat-atk4-label' },
            { id: 'boat_1', elId: 'combat-boat1-label' },
        ];
        slots.forEach(s => {
            const boundKey = Object.keys(cfg.hotkeyMap).find(k => cfg.hotkeyMap[k] === s.id) || 'NONE';
            const el = document.getElementById(s.elId);
            const pct = cfg.combatPercentages[s.id] || 0;
            if (el) el.textContent = s.id === 'boat_1'
                ? boundKey.toUpperCase() + ' = 1% boat'
                : boundKey.toUpperCase() + ' = ' + pct + '%';
        });
    }

    function buildActionBindMatrix() {
        const container = document.getElementById('blon-action-bind-matrix');
        if (!container) return;
        container.innerHTML = '';
        ACTION_DEFS.forEach(def => {
            const binding = cfg.actionHotkeyMap[def.id] || { mod: '', key: '' };
            const box = document.createElement('div');
            box.style.cssText = 'background:#111;border:1px solid #333;padding:4px 6px;border-radius:2px;cursor:pointer;text-align:center;';
            const keyLabel = fmtActionKey(def.id);
            const pctLabel = cfg.tradePercentages && cfg.tradePercentages[def.id]
                ? ` (${cfg.tradePercentages[def.id]}%)`
                : '';
            box.innerHTML = `<span style="color:#888">${def.label}${pctLabel}:</span> <strong id="action-bind-${def.id}" style="color:#fff">${keyLabel}</strong>`;
            box.addEventListener('click', () => {
                if (internalRebindAction === def.id) { internalRebindAction = null; buildActionBindMatrix(); return; }
                internalRebindAction = def.id;
                internalRebindSlot = null;
                const strong = document.getElementById(`action-bind-${def.id}`);
                if (strong) { strong.textContent = 'PRESS KEY'; strong.style.color = '#ffaa00'; }
            });
            container.appendChild(box);
        });
    }

    function bindUIEvents() {
        const ghLink = document.getElementById('blon-github');
        ghLink.addEventListener('mousedown', e => e.stopPropagation());
        ghLink.addEventListener('mouseenter', () => ghLink.style.color = '#fff');
        ghLink.addEventListener('mouseleave', () => ghLink.style.color = '#00ff66');

        const allTabBtns = ['tab-main2-btn','tab-main-btn','tab-stats-btn','tab-matches-btn','tab-combat-btn','tab-diplo-btn','tab-embargo-btn','tab-cfg-btn'];

        const allTabPanels = ['tab-main2-panel','tab-main-panel','tab-stats-panel','tab-matches-panel','tab-combat-panel','tab-diplo-panel','tab-embargo-panel','tab-cfg-panel'];

        const blonRoot = document.getElementById('blon-root');
        const blonContentElements = Array.from(document.querySelectorAll('#blon-root > div:not(#blon-drag)'));
        const minimizeBtn = document.getElementById('blon-minimize-btn');
        if (minimizeBtn && blonRoot) {
            minimizeBtn.dataset.minimized = 'false';
            minimizeBtn.dataset.savedHeight = blonRoot.style.height || '';
            minimizeBtn.dataset.savedMinHeight = blonRoot.style.minHeight || '';
            minimizeBtn.addEventListener('mousedown', e => e.stopPropagation());
            minimizeBtn.addEventListener('click', () => {
                const isMinimized = minimizeBtn.dataset.minimized === 'true';
                if (isMinimized) {
                    blonRoot.style.minHeight = minimizeBtn.dataset.savedMinHeight || '';
                    blonRoot.style.height = minimizeBtn.dataset.savedHeight || '';
                    blonContentElements.forEach(el => {
                        el.style.display = el.dataset.origDisplay || 'flex';
                    });
                    minimizeBtn.textContent = '▾';
                    minimizeBtn.dataset.minimized = 'false';
                } else {
                    minimizeBtn.dataset.savedHeight = blonRoot.style.height || '';
                    minimizeBtn.dataset.savedMinHeight = blonRoot.style.minHeight || '';
                    const drag = document.getElementById('blon-drag');
                    const headerHeight = drag ? `${Math.ceil(drag.getBoundingClientRect().height)}px` : '36px';
                    blonRoot.style.minHeight = '0px';
                    blonRoot.style.height = headerHeight;
                    blonContentElements.forEach(el => {
                        el.dataset.origDisplay = el.style.display || window.getComputedStyle(el).display;
                        el.style.display = 'none';
                    });
                    minimizeBtn.textContent = '▴';
                    minimizeBtn.dataset.minimized = 'true';
                }
            });
        }

        const tabMap = {
            'tab-main2-btn':  'tab-main2-panel',
            'tab-main-btn':    'tab-main-panel',
            'tab-stats-btn':   'tab-stats-panel',
            'tab-matches-btn': 'tab-matches-panel',
            'tab-combat-btn':  'tab-combat-panel',


            'tab-diplo-btn':   'tab-diplo-panel',

            'tab-embargo-btn': 'tab-embargo-panel',
            'tab-cfg-btn':     'tab-cfg-panel',
        };

        allTabBtns.forEach(btnId => {
            document.getElementById(btnId).addEventListener('click', () => {
                const showPanel = tabMap[btnId];
                allTabPanels.forEach(p => {
                    const el = document.getElementById(p);
                    el.style.display = (p === showPanel) ? 'flex' : 'none';
                    el.style.flexDirection = 'column';
                });
                allTabBtns.forEach(id => {
                    const btn = document.getElementById(id);
                    if (id === btnId) {
                        btn.style.background = '#000'; btn.style.color = '#fff'; btn.style.fontWeight = 'bold';
                    } else {
                        btn.style.background = 'transparent'; btn.style.color = '#888'; btn.style.fontWeight = 'normal';
                    }
                });
                if (btnId === 'tab-cfg-btn') { buildBindMatrix(); buildSlotMatrix(); buildActionBindMatrix(); }
                if (btnId === 'tab-combat-btn') { refreshAtkLabels(); }
                if (btnId === 'tab-embargo-btn') { refreshTradeLabels(); }
                if (btnId === 'tab-main2-btn') { }




                if (btnId === 'tab-matches-btn') { renderLobbyMatches(); updateMatchFeedButton(); }
                if (btnId !== 'tab-cfg-btn') { internalRebindSlot = null; internalRebindAction = null; }
            });
        });

        // diplomacy tab
        document.getElementById('blon-auto-accept').addEventListener('change', e => {
            setAutoAccept(e.target.checked);
        });

        // quick chat select finally got this trash FULLY working
        const qchatTargetSel = document.getElementById('blon-qchat-target');
        const qchatSel = document.getElementById('blon-qchat-select');
        QUICK_CHAT_KEYS.forEach(k => {
            const opt = document.createElement('option');
            opt.value = k;
            opt.textContent = k.replace('.', ': ').replace(/_/g, ' ');
            qchatSel.appendChild(opt);
        });
        const qchatBtn = document.getElementById('blon-qchat-btn');
        qchatBtn.addEventListener('mouseenter', () => qchatBtn.style.background = '#1a1a1a');
        qchatBtn.addEventListener('mouseleave', () => qchatBtn.style.background = '#111');
        qchatBtn.addEventListener('click', () => {
            if (cfg.features && cfg.features.quickChat === false) return;
            sendQuickChat(qchatSel.value, qchatTargetSel.value);
        });

        document.getElementById('blon-toggle-mode').addEventListener('change', e => {
            cfg.toggleMode = e.target.checked;
            saveCfg();
            const hint = document.getElementById('blon-mode-hint');
            if (hint) hint.textContent = cfg.toggleMode
                ? 'Press hotkey once to start spamming, press again to stop.'
                : 'Hold hotkey over map to spam build packets.';
        });

        const goldOverlayToggle = document.getElementById('blon-toggle-gold-overlay');
        if (goldOverlayToggle) {
            goldOverlayToggle.addEventListener('change', e => {
                cfg.showGoldOverlay = e.target.checked;
                saveCfg();
                updateMainGoldOverlayVisibility();
                updateStatsHUD();
            });
        }

        const troopsOverlayToggle = document.getElementById('blon-toggle-troops-overlay');
        if (troopsOverlayToggle) {
            troopsOverlayToggle.addEventListener('change', e => {
                cfg.showTroopsOverlay = e.target.checked;
                saveCfg();
                patchLeaderboardDOM(document.querySelector('leader-board'));
            });
        }

        const goldRateSlider = document.getElementById('blon-gold-rate-window');
        const goldRateValue = document.getElementById('blon-gold-rate-window-value');
        const goldTopCountSlider = document.getElementById('blon-gold-top-count');
        const goldTopCountValue = document.getElementById('blon-gold-top-count-value');
        if (goldRateSlider) {
            goldRateSlider.addEventListener('input', e => {
                const sec = parseInt(e.target.value);
                if (!isNaN(sec)) {
                    cfg.goldRateWindowSeconds = Math.max(5, Math.min(240, sec));
                    if (goldRateValue) goldRateValue.textContent = `${cfg.goldRateWindowSeconds}s`;
                    saveCfg();
                }
            });
        }
        if (goldTopCountSlider) {
            goldTopCountSlider.addEventListener('input', e => {
                const count = parseInt(e.target.value);
                if (!isNaN(count)) {
                    cfg.goldTopCount = Math.max(1, Math.min(20, count));
                    if (goldTopCountValue) goldTopCountValue.textContent = `${cfg.goldTopCount}`;
                    saveCfg();
                    updateStatsHUD();
                }
            });
        }

        const troopsIntervalSlider = document.getElementById('blon-troops-check-interval');
        const troopsIntervalValue = document.getElementById('blon-troops-check-interval-value');
        if (troopsIntervalSlider) {
            troopsIntervalSlider.addEventListener('input', e => {
                const ms = parseInt(e.target.value);
                if (!isNaN(ms)) {
                    cfg.troopsCheckIntervalMs = Math.max(1, Math.min(1000, ms));
                    if (troopsIntervalValue) troopsIntervalValue.textContent = `${cfg.troopsCheckIntervalMs}ms`;
                    saveCfg();
                    startLeaderboardLoop();
                }
            });
        }

        const popoutGoldBtn = document.getElementById('blon-popout-gold-btn');
        if (popoutGoldBtn) {
            popoutGoldBtn.addEventListener('click', () => {
                const panel = createDraggablePopoutPanel('blon-popout-gold', 'Gold Leaderboard');
                const content = document.getElementById('blon-popout-gold-content');
                if (content) {
                    content.innerHTML = `
                        <div style="color:#aaa;font-size:10px;margin-bottom:6px;">Top ${cfg.goldTopCount || 10} players by gold</div>
                        <div id="blon-popout-gold-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;margin-bottom:10px;"></div>
                        <div style="color:#aaa;font-size:10px;margin-bottom:6px;">Team gold totals</div>
                        <div id="blon-popout-team-gold" style="display:grid;grid-template-columns:auto 1fr;gap:3px 8px;"></div>
                    `;
                    updateStatsHUD();
                    updateMainGoldOverlayVisibility();
                }
            });
        }

        const popoutMpsBtn = document.getElementById('blon-popout-mps-btn');
        if (popoutMpsBtn) {
            popoutMpsBtn.addEventListener('click', () => {
                const panel = createDraggablePopoutPanel('blon-popout-mps', 'Gold Income/sec');
                const content = document.getElementById('blon-popout-mps-content');
                if (content) {
                    content.innerHTML = `
                        <div style="display:grid;grid-template-columns:auto 1fr;gap:4px 8px;font-size:11px;margin-bottom:10px;">
                            <div style="color:#888;">Gold</div><div id="blon-popout-gold-value" style="color:#ffcc00;font-weight:bold;">N/A</div>
                            <div style="color:#888;">Income/sec</div><div id="blon-popout-gold-rate" style="color:#ffcc00;font-weight:bold;">N/A</div>
                        </div>
                        <div style="color:#aaa;font-size:10px;margin-bottom:4px;">Top ${cfg.goldTopCount || 10} players by income/sec</div>
                        <div id="blon-popout-mps-top10" style="display:grid;grid-template-columns:auto auto 1fr;gap:3px 8px;"></div>
                    `;
                    updateStatsHUD();
                    updateMainGoldOverlayVisibility();
                }
            });
        }

        const matchesConnectBtn = document.getElementById('blon-matches-connect-btn');
        if (matchesConnectBtn) {
            matchesConnectBtn.addEventListener('click', () => {
                if (lobbyWS) {
                    disconnectLobbyFeed();
                } else {
                    connectLobbyFeed();
                }
            });
        }

        const matchesRefreshBtn = document.getElementById('blon-matches-refresh-btn');
        if (matchesRefreshBtn) {
            matchesRefreshBtn.addEventListener('click', renderLobbyMatches);
        }

        updateMatchFeedButton();
        connectLobbyFeed();

        function updateMethodButtons() {
            const wsBtn = document.getElementById('blon-method-ws');
            const clBtn = document.getElementById('blon-method-click');
            if (!wsBtn || !clBtn) return;
            wsBtn.style.background = cfg.spamMethod === 'websocket' ? '#00ff66' : '#111';
            wsBtn.style.color     = cfg.spamMethod === 'websocket' ? '#000' : '#aaa';
            wsBtn.style.fontWeight = cfg.spamMethod === 'websocket' ? 'bold' : 'normal';
            clBtn.style.background = cfg.spamMethod === 'click' ? '#ff9900' : '#111';
            clBtn.style.color     = cfg.spamMethod === 'click' ? '#000' : '#aaa';
            clBtn.style.fontWeight = cfg.spamMethod === 'click' ? 'bold' : 'normal';
        }
        document.getElementById('blon-method-ws').addEventListener('click', () => {
            cfg.spamMethod = 'websocket'; saveCfg(); updateMethodButtons();
        });
        document.getElementById('blon-method-click').addEventListener('click', () => {
            cfg.spamMethod = 'click'; saveCfg(); updateMethodButtons();
        });

        document.getElementById('blon-toggle-charge').addEventListener('change', e => {
            cfg.useChargeTime = e.target.checked; saveCfg();
        });
        document.getElementById('blon-toggle-passthrough').addEventListener('change', e => {
            cfg.blockPassThrough = e.target.checked; saveCfg();
        });

        const featureIds = [
            ['blon-feat-spam-hotkeys', 'spamHotkeys', true],
            ['blon-feat-combat-hotkeys', 'combatHotkeys', true],
            ['blon-feat-action-hotkeys', 'actionHotkeys', true],
            ['blon-feat-quick-chat', 'quickChat', true],
            ['blon-feat-embargo', 'embargo', true],
            ['blon-feat-overlays', 'overlays', true],
            ['cfg-missile-predictor', 'missilePredictor', true],
        ];
        featureIds.forEach(([domId, featKey, defaultVal]) => {
            const el = document.getElementById(domId);
            if (!el) return;
            if (typeof cfg.features?.[featKey] !== 'boolean') cfg.features = cfg.features || {};
            el.checked = cfg.features?.[featKey] ?? defaultVal;
            el.addEventListener('change', ev => {
                cfg.features[featKey] = ev.target.checked;
                saveCfg();
                if (!cfg.features.spamHotkeys) stopSpam();
                if (!cfg.features.overlays) updateMainGoldOverlayVisibility();
            });
        });

        document.getElementById('cfg-combat-silo-indicator').addEventListener('change', e => {
            cfg.combatSiloIndicator = e.target.checked; saveCfg(); updateSiloNotification(); setSiloSubtoggleVisibility(); updateSiloPanel();
        });
        document.getElementById('cfg-combat-silo-panel').addEventListener('change', e => {
            cfg.combatSiloPanel = e.target.checked; saveCfg(); updateSiloPanel();
        });
        document.getElementById('cfg-combat-silo-allies').addEventListener('change', e => {
            cfg.combatSiloOnlyAllies = e.target.checked; saveCfg(); updateSiloPanel();
        });
        document.getElementById('cfg-combat-silo-keep-all').addEventListener('change', e => {
            cfg.combatSiloKeepAllPlaced = e.target.checked; saveCfg(); updateSiloPanel();
        });
        document.getElementById('cfg-combat-priority').addEventListener('change', e => {
            cfg.combatHotkeysPriority = e.target.checked; saveCfg();
        });
        const siloNotificationEl = document.getElementById('blon-silo-notification');
        if (siloNotificationEl) {
            siloNotificationEl.addEventListener('click', onSiloNotificationClick);
        }
        document.getElementById('cfg-spam-rate').addEventListener('input', e => {
            const v = parseInt(e.target.value);
            if (!isNaN(v) && v >= 5) { cfg.spamInterval = v; saveCfg(); }
        });
        document.getElementById('cfg-charge-delay').addEventListener('input', e => {
            const v = parseInt(e.target.value);
            if (!isNaN(v) && v >= 0) { cfg.holdDelay = v; saveCfg(); }
        });
        document.getElementById('cfg-combat-pct-1').addEventListener('input', e => {
            const v = parseInt(e.target.value);
            if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_1 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
        });
        document.getElementById('cfg-combat-pct-2').addEventListener('input', e => {
            const v = parseInt(e.target.value);
            if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_2 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
        });
        document.getElementById('cfg-combat-pct-3').addEventListener('input', e => {
            const v = parseInt(e.target.value);
            if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_3 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
        });
        document.getElementById('cfg-combat-pct-4').addEventListener('input', e => {
            const v = parseInt(e.target.value);
            if (!isNaN(v) && v >= 1 && v <= 100) { cfg.combatPercentages.atk_4 = v; saveCfg(); refreshAtkLabels(); buildBindMatrix(); }
        });
        [
            ['cfg-trade-pct-troops-1', 'donate_troops_1'],
            ['cfg-trade-pct-troops-2', 'donate_troops_2'],
            ['cfg-trade-pct-troops-3', 'donate_troops_3'],
            ['cfg-trade-pct-troops-4', 'donate_troops_4'],
            ['cfg-trade-pct-gold-1', 'donate_gold_1'],
            ['cfg-trade-pct-gold-2', 'donate_gold_2'],
            ['cfg-trade-pct-gold-3', 'donate_gold_3'],
            ['cfg-trade-pct-gold-4', 'donate_gold_4'],
        ].forEach(([domId, pctId]) => {
            const input = document.getElementById(domId);
            if (!input) return;
            input.addEventListener('input', e => {
                const v = parseInt(e.target.value);
                if (!isNaN(v) && v >= 1 && v <= 100) {
                    cfg.tradePercentages[pctId] = v;
                    saveCfg();
                    refreshTradeLabels();
                    buildActionBindMatrix();
                }
            });
        });
        const fireBtn = document.getElementById('embargo-fire-btn');
        const liftBtn = document.getElementById('embargo-lift-btn');
        fireBtn.addEventListener('mouseenter', () => { if (!embargoOnCooldown) fireBtn.style.background = '#1a1a1a'; });
        fireBtn.addEventListener('mouseleave', () => { if (!embargoOnCooldown) fireBtn.style.background = '#111'; });
        fireBtn.addEventListener('click', () => {
            if (cfg.features && cfg.features.embargo === false) return;
            fireEmbargoAll();
        });
        liftBtn.addEventListener('mouseenter', () => liftBtn.style.background = '#1a1a1a');
        liftBtn.addEventListener('mouseleave', () => liftBtn.style.background = '#111');
        liftBtn.addEventListener('click', liftEmbargoAll);

        document.getElementById('embargo-auto-repeat').addEventListener('change', e => {
            embargoAutoRepeat = e.target.checked;
        });

        const dragHandle = document.getElementById('blon-drag');
        let dragging = false, ox, oy;
        dragHandle.addEventListener('mousedown', e => {
            dragging = true;
            ox = e.clientX - ui.getBoundingClientRect().left;
            oy = e.clientY - ui.getBoundingClientRect().top;
            dragHandle.style.color = '#00ffcc';
        });
        window.addEventListener('mousemove', e => {
            if (!dragging) return;
            ui.style.left = (e.clientX - ox) + 'px';
            ui.style.top  = (e.clientY - oy) + 'px';
            ui.style.right = 'auto';
        });
        window.addEventListener('mouseup', () => {
            dragging = false;
            dragHandle.style.color = '#00ff66';
        });

        // lbl refresh
        refreshAtkLabels();
        refreshTradeLabels();

        const guiOpacity = document.getElementById('cfg-gui-opacity');
        const guiOpacityVal = document.getElementById('cfg-gui-opacity-val');
        if (guiOpacity) {
            guiOpacity.addEventListener('input', e => {
                const v = parseFloat(e.target.value);
                if (!isNaN(v)) {
                    cfg.guiOpacity = Math.max(0.1, Math.min(1, v));
                    if (guiOpacityVal) guiOpacityVal.textContent = cfg.guiOpacity.toFixed(2);
                    saveCfg();
                    applyUiStyle();
                }
            });
        }

        const guiColor = document.getElementById('cfg-gui-color');
        const guiColorPicker = document.getElementById('cfg-gui-color-picker');
        const guiColorHue = document.getElementById('cfg-gui-color-hue');
        const guiColorHueVal = document.getElementById('cfg-gui-color-hue-val');
        if (guiColorPicker) {
            guiColorPicker.addEventListener('input', e => {
                const v = String(e.target.value || '').trim();
                const normalized = syncGuiColorFromHex(v);
                if (normalized) {
                    if (guiColor) guiColor.value = normalized;
                    if (guiColorHue) guiColorHue.value = String(cfg.guiColorHue);
                    if (guiColorHueVal) guiColorHueVal.textContent = String(cfg.guiColorHue);
                    saveCfg();
                    applyUiStyle();
                }
            });
        }
        if (guiColor) {
            guiColor.addEventListener('input', e => {
                const v = String(e.target.value || '').trim();
                const normalized = syncGuiColorFromHex(v);
                if (normalized) {
                    if (guiColorPicker) guiColorPicker.value = normalized;
                    if (guiColorHue) guiColorHue.value = String(cfg.guiColorHue);
                    if (guiColorHueVal) guiColorHueVal.textContent = String(cfg.guiColorHue);
                    saveCfg();
                    applyUiStyle();
                }
            });
        }
        if (guiColorHue) {
            guiColorHue.addEventListener('input', e => {
                const v = parseInt(e.target.value, 10);
                if (!isNaN(v)) {
                    const normalized = syncGuiColorFromHue(v);
                    if (guiColor) guiColor.value = normalized;
                    if (guiColorPicker) guiColorPicker.value = normalized;
                    if (guiColorHueVal) guiColorHueVal.textContent = String(cfg.guiColorHue);
                    saveCfg();
                    applyUiStyle();
                }
            });
        }

        // disabled for now because i am not rewriting more stuff
        const overlayColor = document.getElementById('cfg-overlay-color');
        const overlayColorPicker = document.getElementById('cfg-overlay-color-picker');
        if (overlayColor) overlayColor.disabled = true;
        if (overlayColorPicker) overlayColorPicker.disabled = true;

        const overlayOpacity = document.getElementById('cfg-overlay-opacity');
        const overlayOpacityVal = document.getElementById('cfg-overlay-opacity-val');
        if (overlayOpacity) {
            overlayOpacity.addEventListener('input', e => {
                const v = parseFloat(e.target.value);
                if (!isNaN(v)) {
                    cfg.overlayOpacity = Math.max(0.1, Math.min(1, v));
                    if (overlayOpacityVal) overlayOpacityVal.textContent = cfg.overlayOpacity.toFixed(2);
                    saveCfg();
                    applyOverlayAppearance();
                }
            });
        }

        applyOverlayAppearance();
    }

    const BINDABLE_ACTIONS = [
        { id: '1', label: 'Slot 1' },
        { id: '2', label: 'Slot 2' },
        { id: '3', label: 'Slot 3' },
        { id: '4', label: 'Slot 4' },
        { id: '5', label: 'Slot 5' },
        { id: '6', label: 'Slot 6' },
        { id: '7', label: 'Slot 7' },
        { id: '8', label: 'Slot 8' },
        { id: '9', label: 'Slot 9' },
        { id: '0', label: 'Slot 0' },
        { id: 'atk_1', label: 'Combat 1' },
        { id: 'atk_2', label: 'Combat 2' },
        { id: 'atk_3', label: 'Combat 3' },
        { id: 'atk_4', label: 'Combat 4' },
        { id: 'boat_1', label: 'Boat 1%' }
    ];

    function buildBindMatrix() {
        const container = document.getElementById('blon-bind-matrix');
        if (!container) return;
        container.innerHTML = '';
        BINDABLE_ACTIONS.forEach(action => {
            const boundKey = Object.keys(cfg.hotkeyMap).find(k => cfg.hotkeyMap[k] === action.id) || 'NONE';
            const box = document.createElement('div');
            box.style.cssText = 'background:#111;border:1px solid #333;padding:4px 6px;border-radius:2px;cursor:pointer;text-align:center;';

            let displayLabel = action.label;
            if (action.id.startsWith('atk_')) {
                const pct = cfg.combatPercentages[action.id] || 0;
                displayLabel = `Combat ${action.id.replace('atk_', '')} (${pct}%)`;
            } else if (action.id === 'boat_1') {
                displayLabel = 'Boat (1%)';
            }

            box.innerHTML = `<span style="color:#888">${displayLabel}:</span> <strong id="bind-slot-${action.id}" style="color:#fff">${boundKey.toUpperCase()}</strong>`;
            box.addEventListener('click', () => {
                if (internalRebindSlot === action.id) { internalRebindSlot = null; buildBindMatrix(); return; }
                internalRebindSlot = action.id;
                internalRebindAction = null;
                document.getElementById(`bind-slot-${action.id}`).textContent = 'PRESS KEY';
                document.getElementById(`bind-slot-${action.id}`).style.color = '#ffaa00';
            });
            container.appendChild(box);
        });
    }

    function buildSlotMatrix() {
        const container = document.getElementById('blon-slot-matrix');
        if (!container) return;
        container.innerHTML = '';
        ['1','2','3','4','5','6','7','8','9','0'].forEach(slot => {
            const row = document.createElement('div');
            row.style.cssText = 'display:flex;align-items:center;gap:6px;';
            row.innerHTML = `
                <span style="color:#888;min-width:44px;">Slot ${slot}:</span>
                <select id="slot-unit-${slot}" style="flex:1;background:#111;border:1px solid #444;color:#fff;padding:2px 4px;font-family:monospace;font-size:10px;border-radius:2px;">
                    ${Object.entries(UNIT_TYPES).map(([label, val]) =>
                        `<option value="${val}" ${slotUnitMap[slot] === val ? 'selected' : ''}>${label}</option>`
                    ).join('')}
                </select>
            `;
            const sel = row.querySelector(`#slot-unit-${slot}`);
            sel.addEventListener('change', e => {
                slotUnitMap[slot] = e.target.value;
                saveSlots();
            });
            container.appendChild(row);
        });
    }

    window.addEventListener('keydown', e => {
        if (!internalRebindSlot && !internalRebindAction) return;
        e.preventDefault(); e.stopPropagation();
        const key = e.key.toLowerCase();
        if (key === 'escape') {
            if (internalRebindSlot) {
                for (const k in cfg.hotkeyMap) {
                    if (cfg.hotkeyMap[k] === internalRebindSlot) delete cfg.hotkeyMap[k];
                }
            } else if (internalRebindAction) {
                cfg.actionHotkeyMap[internalRebindAction] = { mod: '', key: '' };
            }
            internalRebindSlot = null; internalRebindAction = null;
            saveCfg(); buildBindMatrix(); buildActionBindMatrix(); refreshAtkLabels(); refreshActionLabels();
            return;
        }
        if (['tab','enter'].includes(key)) {
            internalRebindSlot = null; internalRebindAction = null;
            buildBindMatrix(); buildActionBindMatrix();
            return;
        }
        if (['shift','control','alt','meta'].includes(key)) {
            // ignore standalone modifier presses while waiting for a combo key
            return;
        }

        if (internalRebindSlot) {
            for (const k in cfg.hotkeyMap) {
                if (cfg.hotkeyMap[k] === internalRebindSlot || k === key) delete cfg.hotkeyMap[k];
            }
            cfg.hotkeyMap[key] = internalRebindSlot;
            internalRebindSlot = null;
            saveCfg(); buildBindMatrix(); refreshAtkLabels();
        } else if (internalRebindAction) {
            const mod = e.altKey ? 'alt' : e.ctrlKey ? 'ctrl' : e.shiftKey ? 'shift' : '';
            cfg.actionHotkeyMap[internalRebindAction] = { mod, key };
            internalRebindAction = null;
            saveCfg(); buildActionBindMatrix(); refreshActionLabels();
        }
    }, true);

    window.addEventListener('keydown', e => {
        if (internalRebindSlot || internalRebindAction) return;
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;

        const key = e.key.toLowerCase();
        const action = cfg.hotkeyMap[key];

        if (typeof action === 'string' && action.startsWith('atk_')) {
            if (!cfg.features || !cfg.features.combatHotkeys) return;
            // always stop propagation so the game never steals atk_ keys
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            if (e.repeat) return;
            const pct = cfg.combatPercentages[action] || 10;
            const sliders = document.querySelectorAll('control-panel input[type="range"]');
            if (sliders.length > 0) {
                sliders.forEach(slider => {
                    slider.value = String(pct);
                    slider.valueAsNumber = pct;
                    slider.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
                    slider.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
                });
                updateCombatStatus(`Set attack ratio to ${pct}%`, '#00ff66');
            } else {
                const panels = document.querySelectorAll('control-panel');
                let panelUpdated = false;
                panels.forEach(panel => {
                    try {
                        const slider = panel.querySelector && panel.querySelector('input[type="range"]');
                        if (slider) {
                            slider.value = String(pct);
                            slider.valueAsNumber = pct;
                            slider.dispatchEvent(new InputEvent('input', { bubbles: true, composed: true }));
                            slider.dispatchEvent(new Event('change', { bubbles: true, composed: true }));
                            panelUpdated = true;
                            return;
                        }
                        if (typeof panel.onAttackRatioChange === 'function') {
                            panel.onAttackRatioChange(pct / 100);
                            panelUpdated = true;
                            return;
                        }
                        if (typeof panel.attackRatio !== 'undefined') {
                            panel.attackRatio = pct / 100;
                            if (typeof panel.requestUpdate === 'function') panel.requestUpdate();
                            panelUpdated = true;
                        }
                    } catch (err) {
                        // ignore if component API is different
                    }
                });
                if (panelUpdated) {
                    updateCombatStatus(`Set attack ratio to ${pct}%`, '#00ff66');
                } else {
                    updateCombatStatus('Control panel sliders not found', '#ff4444');
                }
            }
            return;
        }

        if (action === 'boat_1') {
            if (!cfg.features || !cfg.features.combatHotkeys) return;
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
            if (e.repeat) return;
            sendOnePercentBoat();
            return;
        }

        if (!e.repeat) {
            if (!cfg.features || !cfg.features.actionHotkeys) return;
            for (const actionId in cfg.actionHotkeyMap) {
                const binding = cfg.actionHotkeyMap[actionId];
                if (!binding || !binding.key) continue;
                const modMatch = (binding.mod === 'alt'   && e.altKey)   ||
                                 (binding.mod === 'ctrl'  && e.ctrlKey)  ||
                                 (binding.mod === 'shift' && e.shiftKey) ||
                                 (!binding.mod && !e.altKey && !e.ctrlKey && !e.shiftKey);
                if (modMatch && e.key.toLowerCase() === binding.key.toLowerCase()) {
                    e.preventDefault();
                    switch(actionId) {
                        case 'embargo_fire':    fireEmbargoAll(); break;
                        case 'embargo_lift':    liftEmbargoAll(); break;
                        case 'donate_troops_1':
                        case 'donate_troops_2':
                        case 'donate_troops_3':
                        case 'donate_troops_4':
                            sendDonationToAlly('troops', cfg.tradePercentages[actionId] || 1);
                            break;
                        case 'donate_gold_1':
                        case 'donate_gold_2':
                        case 'donate_gold_3':
                        case 'donate_gold_4':
                            sendDonationToAlly('gold', cfg.tradePercentages[actionId] || 1);
                            break;
                    }
                    return;
                }
            }
        }

        if (!action) return;

        if (!cfg.features || !cfg.features.spamHotkeys) return;

        if (cfg.toggleMode) {
            if (e.repeat) return;
            if (toggleSpamActive && e.key === currentSpamKey) {
                stopSpam();
                return;
            }
            stopSpam();
            currentSpamKey = e.key;
            toggleSpamActive = true;
            const slot = cfg.hotkeyMap[key];
            const unitType = slotUnitMap[slot];
            if (!unitType) return; // invalid slot, bail out
            const statusEl = document.getElementById('blon-spam-status');
            if (cfg.useChargeTime && cfg.holdDelay > 0) {
                let timeLeft = cfg.holdDelay / 1000;
                if (statusEl) { statusEl.textContent = `CHARGING ${unitType} (${timeLeft.toFixed(1)}s)`; statusEl.style.color = '#ffaa00'; }
                countdownInterval = setInterval(() => {
                    timeLeft -= 0.1;
                    if (statusEl && timeLeft > 0) statusEl.textContent = `CHARGING ${unitType} (${Math.max(0,timeLeft).toFixed(1)}s)`;
                    if (timeLeft <= 0) clearInterval(countdownInterval);
                }, 100);
                holdTimeout = setTimeout(() => startSpam(slot), cfg.holdDelay);
            } else {
                // immediate
                startSpam(slot);
            }
            return;
        }

        if (isKeyDown && e.key === currentSpamKey) return;

        stopSpam();
        isKeyDown = true;
        currentSpamKey = e.key;
        const slot = cfg.hotkeyMap[key];
        const unitType = slotUnitMap[slot];
        if (!unitType) return; // invalid slot bail

        const statusEl = document.getElementById('blon-spam-status');

        if (cfg.useChargeTime && cfg.holdDelay > 0) {
            let timeLeft = cfg.holdDelay / 1000;
            if (statusEl) { statusEl.textContent = `CHARGING ${unitType} (${timeLeft.toFixed(1)}s)`; statusEl.style.color = '#ffaa00'; }
            countdownInterval = setInterval(() => {
                timeLeft -= 0.1;
                if (statusEl && timeLeft > 0) statusEl.textContent = `CHARGING ${unitType} (${Math.max(0,timeLeft).toFixed(1)}s)`;
                if (timeLeft <= 0) clearInterval(countdownInterval);
            }, 100);
            // fires the first packet itself once the charge completes (fix)
            holdTimeout = setTimeout(() => startSpam(slot), cfg.holdDelay);
        } else {
            startSpam(slot);
        }
    }, true);

    window.addEventListener('keyup', e => {
        if (cfg.toggleMode) return;
        if (!cfg.features || !cfg.features.spamHotkeys) return;
        if (e.key === currentSpamKey) {
            if (cfg.blockPassThrough) { e.preventDefault(); e.stopPropagation(); }
            isKeyDown = false;
            stopSpam();
        }
    }, true);

    setInterval(() => {
        const el = document.getElementById('blon-tile-display');
        if (el) el.textContent = lastKnownTile !== null ? `#${lastKnownTile}` : 'none';
    }, 500);

    // interval for updating stats maybe lower it prolly wont lag even at like 20ms
    setInterval(updateStatsHUD, 500);

    startLeaderboardLoop();

    // MISSILE PREDICTOR finally working!
    (function blonMissilePredictor() {
        const NUKE_DATA = {
            'Atom Bomb':      { inner: 12, outer: 30, color: '#ff9900', label: 'ATOM' },
            'Hydrogen Bomb':  { inner: 80, outer: 100, color: '#ff0000', label: 'H-BOMB' },
            'MIRV Warhead':   { inner: 12, outer: 18, color: '#ffff00', label: 'MIRV' },
        };
        const MISSILE_SPEED = 80; // tiles per second
        const MISSILE_TYPES = ['Atom Bomb', 'Hydrogen Bomb', 'MIRV Warhead'];
        const trackedMissiles = new Map();
        let overlayCanvas = null;
        let overlayCtx = null;
        let animFrameId = null;
        let scanIntervalId = null;

        function ensureCanvas() {
            if (overlayCanvas) return;
            overlayCanvas = document.createElement('canvas');
            overlayCanvas.style.cssText = 'position:fixed;top:0;left:0;width:100vw;height:100vh;pointer-events:none;z-index:999998;';
            document.body.appendChild(overlayCanvas);
            overlayCtx = overlayCanvas.getContext('2d');
            resizeCanvas();
            window.addEventListener('resize', resizeCanvas);
        }

        function resizeCanvas() {
            if (!overlayCanvas) return;
            overlayCanvas.width = window.innerWidth;
            overlayCanvas.height = window.innerHeight;
        }

        function getOverlay() {
            try {
                const overlay = document.querySelector('player-info-overlay');
                if (overlay && overlay.game && overlay.transform) return overlay;
            } catch(e) {}
            return null;
        }

        function scanMissiles() {
            const overlay = getOverlay();
            if (!overlay) return;
            const game = overlay.game;
            const now = performance.now();
            const seen = new Set();

            for (const type of MISSILE_TYPES) {
                let units;
                try { units = game.units(type); } catch(e) { continue; }
                if (!units) continue;

                for (const missile of units) {
                    try {
                        if (!missile.isActive()) continue;
                        const id = missile.id();
                        seen.add(id);

                        if (!trackedMissiles.has(id)) {
                            const targetTile = missile.targetTile();
                            if (!targetTile) continue;
                            trackedMissiles.set(id, {
                                id: id,
                                type: missile.type(),
                                currentTile: missile.tile(),
                                targetTile: targetTile,
                                firstSeen: now,
                                lastSeen: now,
                            });
                        } else {
                            const record = trackedMissiles.get(id);
                            record.currentTile = missile.tile();
                            record.lastSeen = now;
                        }
                    } catch(e) {}
                }
            }

            // delete missiles not seen for 3s... (Maybe shorten cuz ofc sams and i dont want hanging overlays??)
            for (const [id, record] of trackedMissiles) {
                if (!seen.has(id) && now - record.lastSeen > 3000) {
                    trackedMissiles.delete(id);
                }
            }
        }

        function worldToScreen(game, transform, worldX, worldY) {
            try {
                return transform.worldToScreenCoordinates({ x: worldX, y: worldY });
            } catch(e) { return null; }
        }

        function calculateRadiusPx(transform, worldX, worldY, radius) {
            try {
                const p1 = transform.worldToScreenCoordinates({ x: worldX, y: worldY });
                const p2 = transform.worldToScreenCoordinates({ x: worldX + radius, y: worldY });
                return Math.abs(p2.x - p1.x);
            } catch(e) {
                return radius * (transform.scale || 1);
            }
        }

        function render() {
            animFrameId = requestAnimationFrame(render);

            if (!cfg.features || !cfg.features.missilePredictor) return;
            if (!overlayCtx || !overlayCanvas) return;

            const overlay = getOverlay();
            if (!overlay) return;

            const game = overlay.game;
            const transform = overlay.transform;
            const ctx = overlayCtx;
            const w = overlayCanvas.width;
            const h = overlayCanvas.height;

            ctx.clearRect(0, 0, w, h);

            for (const [id, record] of trackedMissiles) {
                const data = NUKE_DATA[record.type];
                if (!data) continue;

                try {
                    const targetWorldX = game.x(record.targetTile);
                    const targetWorldY = game.y(record.targetTile);
                    const currentWorldX = game.x(record.currentTile);
                    const currentWorldY = game.y(record.currentTile);

                    const screen = worldToScreen(game, transform, targetWorldX, targetWorldY);
                    if (!screen) continue;

                    const outerRadiusPx = calculateRadiusPx(transform, targetWorldX, targetWorldY, data.outer);
                    const innerRadiusPx = (data.inner !== data.outer)
                        ? calculateRadiusPx(transform, targetWorldX, targetWorldY, data.inner)
                        : outerRadiusPx;

                    // ETA
                    const dx = targetWorldX - currentWorldX;
                    const dy = targetWorldY - currentWorldY;
                    const distance = Math.hypot(dx, dy);
                    const eta = Math.max(0, distance / MISSILE_SPEED).toFixed(1);

                    // Trajectory line
                    const currentScreen = worldToScreen(game, transform, currentWorldX, currentWorldY);
                    if (currentScreen) {
                        ctx.beginPath();
                        ctx.moveTo(currentScreen.x, currentScreen.y);
                        ctx.lineTo(screen.x, screen.y);
                        ctx.strokeStyle = data.color;
                        ctx.globalAlpha = 0.25;
                        ctx.lineWidth = 2;
                        ctx.setLineDash([8, 5]);
                        ctx.stroke();
                        ctx.setLineDash([]);
                        ctx.globalAlpha = 1;

                        // Draw current missile position dot
                        ctx.beginPath();
                        ctx.arc(currentScreen.x, currentScreen.y, 4, 0, Math.PI * 2);
                        ctx.fillStyle = data.color;
                        ctx.globalAlpha = 0.9;
                        ctx.fill();
                        ctx.globalAlpha = 1;
                    }

                    const margin = outerRadiusPx + 30;
                    const onScreen = (
                        screen.x + margin > 0 && screen.x - margin < w &&
                        screen.y + margin > 0 && screen.y - margin < h
                    );

                    if (onScreen) {
                        // Outer blast radius
                        ctx.beginPath();
                        ctx.arc(screen.x, screen.y, outerRadiusPx, 0, Math.PI * 2);
                        ctx.fillStyle = data.color;
                        ctx.globalAlpha = 0.1;
                        ctx.fill();
                        ctx.globalAlpha = 0.6;
                        ctx.strokeStyle = data.color;
                        ctx.lineWidth = 2;
                        ctx.stroke();
                        ctx.globalAlpha = 1;

                        // Inner blast radius
                        if (data.inner !== data.outer) {
                            ctx.beginPath();
                            ctx.arc(screen.x, screen.y, innerRadiusPx, 0, Math.PI * 2);
                            ctx.fillStyle = data.color;
                            ctx.globalAlpha = 0.18;
                            ctx.fill();
                            ctx.globalAlpha = 0.8;
                            ctx.strokeStyle = data.color;
                            ctx.lineWidth = 1.5;
                            ctx.setLineDash([4, 3]);
                            ctx.stroke();
                            ctx.setLineDash([]);
                            ctx.globalAlpha = 1;
                        }

                        // plus sign thingy
                        const crossSize = Math.min(outerRadiusPx * 0.3, 12);
                        ctx.beginPath();
                        ctx.moveTo(screen.x - crossSize, screen.y);
                        ctx.lineTo(screen.x + crossSize, screen.y);
                        ctx.moveTo(screen.x, screen.y - crossSize);
                        ctx.lineTo(screen.x, screen.y + crossSize);
                        ctx.strokeStyle = data.color;
                        ctx.globalAlpha = 0.7;
                        ctx.lineWidth = 1.5;
                        ctx.stroke();
                        ctx.globalAlpha = 1;

                        // ETA label
                        ctx.font = 'bold 12px monospace';
                        ctx.textAlign = 'center';
                        ctx.textBaseline = 'middle';
                        const labelText = data.label + ' ' + eta + 's';
                        const textWidth = ctx.measureText(labelText).width;
                        ctx.fillStyle = 'rgba(0,0,0,0.75)';
                        const labelY = screen.y - outerRadiusPx - 14;
                        ctx.fillRect(screen.x - textWidth/2 - 5, labelY - 8, textWidth + 10, 16);
                        ctx.fillStyle = data.color;
                        ctx.fillText(labelText, screen.x, labelY);
                    }
                } catch(e) {}
            }
        }


        function start() {
            ensureCanvas();
            if (!animFrameId) animFrameId = requestAnimationFrame(render);
            if (!scanIntervalId) scanIntervalId = setInterval(scanMissiles, 100);
        }

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', start);
        } else {
            start();
        }
    })();

})();