Greasy Fork is available in English.

Project Blon Openfront Cheats

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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

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

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

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();