♟Super-chess-Bot

Super chess Bot is a tournament level bullet bot

// ==UserScript==
// @name          ♟Super-chess-Bot
// @namespace     http://tampermonkey.net/
// @version       8.1
// @description   Super chess Bot is a tournament level bullet bot
// @author        quantavil
// @match         https://www.chess.com/play/computer*
// @match         https://www.chess.com/game/*
// @match         https://www.chess.com/play/online*
// @license       MIT
// @icon          https://www.google.com/s2/favicons?sz=64&domain=chess.com
// @grant         none
// @antifeature   membership
// ==/UserScript==

(async function () {
    'use strict';

    // Single-instance guard to prevent duplicate UI/intervals on SPA navigations
    if (window.__GABIBOT_RUNNING__) {
        console.log('GabiBot: Already running, skipping init.');
        return;
    }
    window.__GABIBOT_RUNNING__ = true;

    // Engine + logic constants
    const API_URL = 'https://stockfish.online/api/s/v2.php';
    const MULTIPV = 1;
    const ANALYZE_TIMEOUT_MS = 3000;      // ⚡ 8000 → 3000ms for bullet
    const AUTO_MOVE_BASE = 800;            // ⚡ 5000 → 800ms for bullet
    const AUTO_MOVE_STEP = 300;            // ⚡ 500 → 300ms for bullet
    const RANDOM_JITTER_MIN = 50;          // ⚡ 120 → 50ms for bullet

    console.log('GabiBot: Script loaded, waiting for board...');

    // Debounce helper
    function debounce(fn, wait = 150) {
        let t = null;
        return (...args) => {
            clearTimeout(t);
            t = setTimeout(() => fn(...args), wait);
        };
    }

    // Position cache system
    const PositionCache = {};

    function getRandomDepth() {
        const minDepth = 5;
        const maxDepth = Math.max(BotState.botPower || 10, minDepth);
        return Math.floor(Math.random() * (maxDepth - minDepth + 1)) + minDepth;
    }
    function getHumanDelay(baseDelay, randomDelay) {
        return baseDelay + Math.floor(Math.random() * randomDelay);
    }

    // Helpers
    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    const qs = (sel, root = document) => root.querySelector(sel);
    const qsa = (sel, root = document) => Array.from(root.querySelectorAll(sel));

    async function waitForElement(selector, timeout = 15000) {
        return new Promise((resolve, reject) => {
            const existing = qs(selector);
            if (existing) return resolve(existing);

            let timeoutId;
            const obs = new MutationObserver(() => {
                const el = qs(selector);
                if (el) {
                    clearTimeout(timeoutId);
                    obs.disconnect();
                    resolve(el);
                }
            });

            obs.observe(document.body, { childList: true, subtree: true });
            timeoutId = setTimeout(() => {
                obs.disconnect();
                reject(new Error(`Element ${selector} not found within ${timeout}ms`));
            }, timeout);
        });
    }

    // Encapsulated state (avoid global pollution)
    const BotState = {
        hackEnabled: 0,
        botPower: 8,
        updateSpeed: 10,
        autoMove: 1,
        autoMoveSpeed: 8,
        randomDelay: 300,
        currentEvaluation: '-',
        bestMove: '-',
        principalVariation: '-',
        statusInfo: 'Ready',
        premoveEnabled: 0,
        premoveMode: 'every',
        premovePieces: { q: 1, r: 1, b: 1, n: 1, k: 1, p: 1 },
        premoveChance: 85,
        autoRematch: 0
    };

    // Global state
    let ui = null;
    let boardCtx = null; // { boardEl, drawingBoard, ctx, evalBarWrap, resizeObserver, cancelPendingOnUserAction, touchOpts, detachListeners }
    let domObserver = null;
    let tickTimer = null;
    let gameStartInterval = null;
    let gameEndInterval = null;

    // Analysis queue
    let analysisQueue = Promise.resolve();
    let currentAnalysisId = 0;

    // Tick state
    let lastFenProcessedMain = '';
    let lastFenProcessedPremove = '';
    let lastFenSeen = '';
    let pendingMoveTimeoutId = null;

    // Premove state
    let lastPremoveFen = '';
    let lastPremoveUci = '';

    // Cached DOM queries (reduce overhead)
    let cachedGame = null;
    let cachedGameTimestamp = 0;
    const GAME_CACHE_TTL = 500; // Cache game object for 500ms

    // Cache board flip state (changes infrequently)
    let cachedBoardFlipped = false;
    let cachedFlipTimestamp = 0;

    // Main init
    async function init() {
        try {
            const board = await waitForElement('.board, chess-board, .board-layout-vertical, .board-layout-horizontal').catch(() => null);
            await buildUI();
            attachToBoard(board || qs('chess-board') || qs('.board') || qs('[class*="board"]'));
            startDomBoardWatcher(); // observe board replacements (SPA safe)
            startAutoWatchers();    // game start/end watchers
            console.log('GabiBot: Initialized.');
        } catch (error) {
            console.error('GabiBot Error:', error);
            alert('GabiBot: Could not find chess board. Please refresh or check console.');
        }
    }

    // Build UI and bind settings
    async function buildUI() {
        // Create menu
        const menuWrap = document.createElement('div');
        menuWrap.id = 'menuWrap';
        const menuWrapStyle = document.createElement('style');

        menuWrap.innerHTML = `
      <div id="topText">
        <a id="modTitle">♟ GabiBot</a>
        <button id="minimizeBtn" title="Minimize (Ctrl+B)">─</button>
      </div>
      <div id="itemsList">
        <div name="enableHack" class="listItem">
          <input class="checkboxMod" type="checkbox">
          <a class="itemDescription">Enable Bot</a>
          <a class="itemState">Off</a>
        </div>
        <div name="autoMove" class="listItem">
          <input class="checkboxMod" type="checkbox">
          <a class="itemDescription">Auto Move</a>
          <a class="itemState">Off</a>
        </div>

        <div class="divider"></div>

        <div name="premoveEnabled" class="listItem">
          <input class="checkboxMod" type="checkbox">
          <a class="itemDescription">Premove System</a>
          <a class="itemState">Off</a>
        </div>
        <div name="premoveMode" class="listItem select-row">
          <a class="itemDescription">Premove Mode</a>
          <select class="selectMod">
            <option value="every">Every next move</option>
            <option value="capture">Only if capture</option>
            <option value="filter">By piece filters</option>
          </select>
        </div>
        <div name="premoveChance" class="listItem info-item">
          <a class="itemDescription">Premove Chance:</a>
          <a class="itemState">0%</a>
        </div>
        <div name="premovePieces" class="listItem">
          <div class="pieceFilters">
            <label class="chip"><input type="checkbox" data-piece="q" checked><span>Q</span></label>
            <label class="chip"><input type="checkbox" data-piece="r" checked><span>R</span></label>
            <label class="chip"><input type="checkbox" data-piece="b" checked><span>B</span></label>
            <label class="chip"><input type="checkbox" data-piece="n" checked><span>N</span></label>
            <label class="chip"><input type="checkbox" data-piece="k" checked><span>K</span></label>
            <label class="chip"><input type="checkbox" data-piece="p" checked><span>P</span></label>
          </div>
          <a class="itemDescription">Pieces</a>
          <a class="itemState">-</a>
        </div>

        <div class="divider"></div>

        <div name="autoRematch" class="listItem">
          <input class="checkboxMod" type="checkbox">
          <a class="itemDescription">Auto Rematch</a>
          <a class="itemState">Off</a>
        </div>

        <div class="divider"></div>

        <div name="botPower" class="listItem">
          <input min="1" max="15" value="10" class="rangeSlider" type="range">
          <a class="itemDescription">Depth</a>
          <a class="itemState">12</a>
        </div>
        <div name="autoMoveSpeed" class="listItem">
          <input min="1" max="10" value="8" class="rangeSlider" type="range">
          <a class="itemDescription">Move Speed</a>
          <a class="itemState">4</a>
        </div>
        <div name="randomDelay" class="listItem">
          <input min="120" max="2000" value="300" class="rangeSlider" type="range">
          <a class="itemDescription">Random Delay (ms)</a>
          <a class="itemState">1000</a>
        </div>
        <div name="updateSpeed" class="listItem">
          <input min="1" max="10" value="8" class="rangeSlider" type="range">
          <a class="itemDescription">Update Rate</a>
          <a class="itemState">8</a>
        </div>

        <div class="divider"></div>

        <div name="currentEvaluation" class="listItem info-item">
          <a class="itemDescription">Eval:</a>
          <a class="itemState eval-value">-</a>
        </div>
        <div name="bestMove" class="listItem info-item">
          <a class="itemDescription">Best:</a>
          <a class="itemState">-</a>
        </div>
        <div name="pvDisplay" class="listItem info-item">
          <a class="itemDescription">PV:</a>
          <a class="itemState pv-text-state" title="Principal Variation">-</a>
        </div>
        <div name="statusInfo" class="listItem info-item">
          <a class="itemDescription">Status:</a>
          <a class="itemState status-text">Ready</a>
        </div>
      </div>
    `;

        menuWrapStyle.innerHTML = `
      #menuWrap {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
        border-radius: 8px;
        z-index: 9999999;
        display: grid;
        grid-template-rows: auto 1fr;
        width: 300px; max-height: 550px;
        position: fixed;
        border: 1px solid rgba(255, 255, 255, 0.1);
        background: rgba(20, 20, 20, 0.95);
        backdrop-filter: blur(10px);
        box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
        user-select: none;
        top: 20px; right: 20px;
        transition: opacity 0.3s ease, transform 0.3s ease;
      }
      #menuWrap.minimized { grid-template-rows: auto 0fr; max-height: 50px; }
      #menuWrap.minimized #itemsList { overflow: hidden; opacity: 0; }
      #menuWrap.grabbing { cursor: grabbing !important; opacity: 0.9; }
      .divider { height: 1px; background: rgba(255, 255, 255, 0.1); margin: 10px 0; }
      .pv-text-state { color: #aaa !important; font-size: 11px; max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .eval-value { font-weight: bold; font-size: 14px; }
      .status-text { color: #4CAF50 !important; font-size: 11px; }
      .info-item { opacity: 0.8; margin-bottom: 8px !important; }
      #evaluationBarWrap {
        position: absolute;
        height: 100%;
        width: 20px;
        left: -28px;
        top: 0;
        background: #000;
        z-index: 50;
        border-radius: 6px;
        overflow: hidden;
        border: 1px solid rgba(255, 255, 255, 0.2);
      }
      #evaluationBarWhite { position: absolute; top: 0; left: 0; right: 0; background: #f0d9b5; transition: height 0.3s ease; }
      #evaluationBarBlack { position: absolute; bottom: 0; left: 0; right: 0; background: #000; transition: height 0.3s ease; }
      #topText { display: flex; justify-content: space-between; align-items: center; padding: 12px 16px;
        background: rgba(255, 255, 255, 0.05); border-bottom: 1px solid rgba(255, 255, 255, 0.1); cursor: move; }
      #modTitle { color: #fff; font-size: 16px; font-weight: 600; letter-spacing: 0.5px; }
      #minimizeBtn { background: rgba(255, 255, 255, 0.1); border: none; color: #fff; width: 24px; height: 24px;
        border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.2s; }
      #minimizeBtn:hover { background: rgba(255, 255, 255, 0.2); }
      #itemsList { overflow-y: auto; overflow-x: hidden; padding: 12px 16px; transition: opacity 0.3s ease; }
      ::-webkit-scrollbar { width: 6px; }
      ::-webkit-scrollbar-track { background: rgba(255, 255, 255, 0.05); }
      ::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 3px; }
      ::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.3); }
      .listItem { display: flex; align-items: center; margin-bottom: 12px; gap: 8px; }
      .listItem.select-row { display: grid; grid-template-columns: 1fr 1.2fr; gap: 10px; align-items: center; }
      .listItem.select-row .itemDescription { color: rgba(255, 255, 255, 0.85); font-weight: 500; }
      .checkboxMod { appearance: none; width: 18px; height: 18px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 4px;
        background: rgba(255, 255, 255, 0.05); cursor: pointer; position: relative; transition: all 0.2s; flex-shrink: 0; }
      .checkboxMod:checked { background: #4CAF50; border-color: #4CAF50; }
      .checkboxMod:checked::after { content: "✓"; position: absolute; color: white; font-size: 12px; top: 50%; left: 50%; transform: translate(-50%, -50%); }
      .rangeSlider { -webkit-appearance: none; flex: 1; height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.1); outline: none; }
      .rangeSlider::-webkit-slider-thumb { -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%; background: #4CAF50; cursor: pointer; transition: transform 0.2s; }
      .rangeSlider::-webkit-slider-thumb:hover { transform: scale(1.2); }
      .itemDescription { color: rgba(255, 255, 255, 0.7); font-size: 12px; flex: 1; }
      .itemState { color: #fff; font-size: 12px; min-width: 35px; text-align: right; font-weight: 500; }
      #arrowCanvas { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; pointer-events: none !important; z-index: 100 !important; }
      .selectMod {
        appearance: none;
        background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.2);
        color: #fff;
        border-radius: 6px;
        padding: 6px 28px 6px 10px;
        flex: 1;
        outline: none;
        cursor: pointer;
        font-size: 12px;
        font-family: inherit;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23fff' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
        background-repeat: no-repeat;
        background-position: right 8px center;
        transition: all 0.2s ease;
      }
      .selectMod:hover { background-color: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.3); }
      .selectMod:focus { background-color: rgba(255,255,255,0.1); border-color: #4CAF50; box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); }
      .selectMod option { background: #1a1a1a; color: #fff; padding: 8px; }
      .pieceFilters { display: flex; flex-wrap: wrap; gap: 6px; }
      .pieceFilters .chip {
        user-select: none; display: inline-flex; align-items: center; gap: 6px;
        padding: 5px 10px; background: rgba(255,255,255,0.08);
        border: 1px solid rgba(255,255,255,0.2); border-radius: 999px; cursor: pointer;
        color: rgba(255,255,255,0.7); transition: all 0.2s ease;
        font-size: 11px; font-weight: 500;
      }
      .pieceFilters .chip:hover { background: rgba(255,255,255,0.12); border-color: rgba(255,255,255,0.3); }
      .pieceFilters .chip input { appearance: none; width: 14px; height: 14px; border-radius: 3px; border: 2px solid rgba(255,255,255,0.4); background: rgba(255,255,255,0.05); transition: all 0.2s ease; }
      .pieceFilters .chip input:checked { background: #4CAF50; border-color: #4CAF50; }
      .pieceFilters .chip input:checked::after { content: "✓"; color: white; font-size: 9px; display: flex; align-items: center; justify-content: center; height: 100%; }
      .pieceFilters .chip input:checked + span { color: #fff; font-weight: 600; }
    `;

        document.body.appendChild(menuWrap);
        document.body.appendChild(menuWrapStyle);

        // Settings persistence
        const Settings = {
            save: debounce(() => {
                try {
                    const settings = {
                        hackEnabled: BotState.hackEnabled,
                        botPower: BotState.botPower,
                        updateSpeed: BotState.updateSpeed,
                        autoMove: BotState.autoMove,
                        autoMoveSpeed: BotState.autoMoveSpeed,
                        randomDelay: Math.max(RANDOM_JITTER_MIN, BotState.randomDelay),
                        premoveEnabled: BotState.premoveEnabled,
                        premoveMode: BotState.premoveMode,
                        premovePieces: BotState.premovePieces,
                        autoRematch: BotState.autoRematch,
                        menuPosition: { top: menuWrap.style.top, left: menuWrap.style.left }
                    };
                    localStorage.setItem('gabibot_settings', JSON.stringify(settings));
                } catch (e) { console.warn('Failed to save settings:', e); }
            }, 200),
            load() {
                try {
                    const saved = localStorage.getItem('gabibot_settings');
                    if (!saved) return null;
                    const s = JSON.parse(saved);
                    BotState.hackEnabled = s.hackEnabled ?? 0;
                    BotState.botPower = s.botPower ?? 8;
                    BotState.updateSpeed = s.updateSpeed ?? 10;
                    BotState.autoMove = s.autoMove ?? 1;
                    BotState.autoMoveSpeed = s.autoMoveSpeed ?? 8;
                    BotState.randomDelay = Math.max(RANDOM_JITTER_MIN, s.randomDelay ?? 300);
                    BotState.premoveEnabled = s.premoveEnabled ?? 0;
                    BotState.premoveMode = s.premoveMode ?? 'every';
                    BotState.premovePieces = s.premovePieces ?? { q: 1, r: 1, b: 1, n: 1, k: 1, p: 1 };
                    BotState.autoRematch = s.autoRematch ?? 0;
                    return s;
                } catch (e) { console.error('Failed to load settings:', e); return null; }
            }
        };
        const saved = Settings.load();
        if (saved?.menuPosition) {
            menuWrap.style.top = saved.menuPosition.top || '20px';
            menuWrap.style.left = saved.menuPosition.left || '';
            menuWrap.style.right = saved.menuPosition.left ? 'auto' : '20px';
        }

        // Control binding helpers
        const getElementByName = (name, el) => el.querySelector(`[name="${name}"]`);
        const getInputElement = (el) => el.children[0];
        const getStateElement = (el) => el.children[el.children.length - 1];

        function bindControl(name, type, variable) {
            const modElement = getElementByName(name, menuWrap);
            if (!modElement) return;
            const modState = getStateElement(modElement);
            const modInput = getInputElement(modElement);
            const key = variable.replace('BotState.', '');
            if (type === 'checkbox') {
                modInput.checked = !!BotState[key];
                modState.textContent = BotState[key] ? 'On' : 'Off';
                modInput.addEventListener('input', () => {
                    BotState[key] = modInput.checked ? 1 : 0;
                    modState.textContent = BotState[key] ? 'On' : 'Off';
                    Settings.save();
                });
            } else if (type === 'range') {
                modInput.value = BotState[key];
                modState.textContent = BotState[key];
                modInput.addEventListener('input', () => {
                    let value = parseInt(modInput.value, 10);
                    const min = parseInt(modInput.min, 10);
                    const max = parseInt(modInput.max, 10);
                    value = Math.max(min, Math.min(max, value));
                    BotState[key] = value;
                    modInput.value = value;
                    modState.textContent = value;
                    Settings.save();
                });
            }
        }
        function bindSelect(name, variable) {
            const el = getElementByName(name, menuWrap);
            if (!el) return;
            const select = el.querySelector('select');
            const key = variable.replace('BotState.', '');
            select.value = BotState[key];
            select.addEventListener('change', () => {
                BotState[key] = select.value;
                refreshPremoveUIVisibility();
                Settings.save();
            });
        }
        function bindPieceFilters() {
            const el = getElementByName('premovePieces', menuWrap);
            if (!el) return;
            const checks = qsa('.pieceFilters input[type="checkbox"]', el);
            checks.forEach(chk => {
                const p = String(chk.dataset.piece || '').toLowerCase();
                chk.checked = !!BotState.premovePieces[p];
            });
            checks.forEach(chk => {
                chk.addEventListener('input', () => {
                    const p = String(chk.dataset.piece || '').toLowerCase();
                    BotState.premovePieces[p] = chk.checked ? 1 : 0;
                    Settings.save();
                });
            });
        }
        function refreshPremoveUIVisibility() {
            const row = getElementByName('premovePieces', menuWrap);
            if (row) row.style.display = (BotState.premoveMode === 'filter') ? 'flex' : 'none';
        }

        bindControl('enableHack', 'checkbox', 'BotState.hackEnabled');
        bindControl('autoMove', 'checkbox', 'BotState.autoMove');
        bindControl('botPower', 'range', 'BotState.botPower');
        bindControl('autoMoveSpeed', 'range', 'BotState.autoMoveSpeed');
        bindControl('updateSpeed', 'range', 'BotState.updateSpeed');
        bindControl('randomDelay', 'range', 'BotState.randomDelay');

        bindControl('premoveEnabled', 'checkbox', 'BotState.premoveEnabled');
        bindSelect('premoveMode', 'BotState.premoveMode');
        bindPieceFilters();
        refreshPremoveUIVisibility();

        bindControl('autoRematch', 'checkbox', 'BotState.autoRematch');

        // Drag/move panel
        makePanelDraggable(menuWrap);

        // Minimize
        document.getElementById('minimizeBtn').addEventListener('click', () => menuWrap.classList.toggle('minimized'));
        document.addEventListener('keydown', (e) => {
            if (e.key === 'b' && e.ctrlKey) {
                e.preventDefault();
                menuWrap.classList.toggle('minimized');
            }
        });

        ui = {
            menuWrap,
            setText(name, value, title) {
                const el = getElementByName(name, menuWrap);
                if (!el) return;
                const state = getStateElement(el);
                state.textContent = value ?? '-';
                if (title) state.title = title;
            },
            updateDisplay(playingAs) {
                this.setText('currentEvaluation', BotState.currentEvaluation);
                this.setText('bestMove', BotState.bestMove);
                this.setText('pvDisplay', BotState.principalVariation, BotState.principalVariation);
                this.setText('statusInfo', BotState.statusInfo);
                updateEvaluationBar(BotState.currentEvaluation, playingAs);
            },
            Settings
        };

        // React to enable/disable or speed changes
        let lastHackEnabled = BotState.hackEnabled;
        let lastUpdateSpeed = BotState.updateSpeed;
        let lastPremoveEnabled = BotState.premoveEnabled;

        setInterval(() => {
            if (BotState.hackEnabled !== lastHackEnabled) {
                lastHackEnabled = BotState.hackEnabled;
                if (BotState.hackEnabled) {
                    BotState.statusInfo = 'Ready';
                    ui.updateDisplay(pa());
                    startTickLoop();
                } else {
                    stopTickLoop();
                    Object.keys(PositionCache).forEach(key => delete PositionCache[key]);
                    clearArrows();
                    cancelPendingMove();
                    BotState.statusInfo = 'Bot disabled';
                    BotState.currentEvaluation = '-';
                    BotState.bestMove = '-';
                    ui.updateDisplay(pa());
                }
                ui.Settings.save();
            }
            if (BotState.updateSpeed !== lastUpdateSpeed) {
                lastUpdateSpeed = BotState.updateSpeed;
                if (BotState.hackEnabled) startTickLoop();
            }
            if (BotState.premoveEnabled !== lastPremoveEnabled) {
                lastPremoveEnabled = BotState.premoveEnabled;
                if (BotState.hackEnabled) startTickLoop();
            }
        }, 200);
    }

    // Board attach/detach
    function attachToBoard(boardEl) {
        // Invalidate cached game when board changes
        cachedGame = null;
        cachedGameTimestamp = 0;

        detachFromBoard(); // cleanup any previous

        if (!boardEl) {
            console.warn('GabiBot: No board element to attach.');
            return;
        }
        // Ensure relative for overlay
        if (getComputedStyle(boardEl).position === 'static') boardEl.style.position = 'relative';

        const drawingBoard = document.createElement('canvas');
        drawingBoard.id = 'arrowCanvas';
        drawingBoard.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:100;';
        const ctx = drawingBoard.getContext('2d');

        const evalBarWrap = document.createElement('div');
        evalBarWrap.id = 'evaluationBarWrap';
        const whiteBar = document.createElement('div');
        whiteBar.id = 'evaluationBarWhite';
        const blackBar = document.createElement('div');
        blackBar.id = 'evaluationBarBlack';
        evalBarWrap.appendChild(whiteBar);
        evalBarWrap.appendChild(blackBar);

        boardEl.appendChild(evalBarWrap);
        boardEl.appendChild(drawingBoard);

        const resizeCanvas = () => {
            const rect = boardEl.getBoundingClientRect();
            drawingBoard.width = rect.width;
            drawingBoard.height = rect.height;
        };
        resizeCanvas();
        const ro = new ResizeObserver(resizeCanvas);
        ro.observe(boardEl);

        const cancelPendingOnUserAction = () => {
            if (pendingMoveTimeoutId) {
                clearTimeout(pendingMoveTimeoutId);
                pendingMoveTimeoutId = null;
                BotState.statusInfo = 'Manual move in progress...';
                ui.updateDisplay(pa());
            }
        };
        const touchOpts = { passive: true, capture: true };
        boardEl.addEventListener('mousedown', cancelPendingOnUserAction, true);
        boardEl.addEventListener('touchstart', cancelPendingOnUserAction, touchOpts);

        boardCtx = {
            boardEl,
            drawingBoard,
            ctx,
            evalBarWrap,
            resizeObserver: ro,
            cancelPendingOnUserAction,
            touchOpts,
            detachListeners() {
                try { boardEl.removeEventListener('mousedown', cancelPendingOnUserAction, true); } catch { }
                try { boardEl.removeEventListener('touchstart', cancelPendingOnUserAction, touchOpts); } catch { }
                try { ro.disconnect(); } catch { }
                try { drawingBoard.remove(); } catch { }
                try { evalBarWrap.remove(); } catch { }
            }
        };

        // Show ready
        ui.updateDisplay(pa());
        if (BotState.hackEnabled) startTickLoop();
    }

    function detachFromBoard() {
        if (!boardCtx) return;
        try { boardCtx.detachListeners(); } catch { }
        boardCtx = null;
    }

    function startDomBoardWatcher() {
        if (domObserver) try { domObserver.disconnect(); } catch { }
        domObserver = new MutationObserver(debounce(() => {
            // Look for a current board element
            const newBoard = qs('chess-board') || qs('.board') || qs('[class*="board"]');
            if (!newBoard) return;
            if (!boardCtx || boardCtx.boardEl !== newBoard) {
                console.log('GabiBot: Board element changed, re-attaching.');
                attachToBoard(newBoard);
            }
        }, 200));
        domObserver.observe(document.body, { childList: true, subtree: true });
    }

    // Game helpers
    const getBoard = () => boardCtx?.boardEl || qs('chess-board') || qs('.board');
    const getGame = () => {
        const now = Date.now();
        if (cachedGame && (now - cachedGameTimestamp) < GAME_CACHE_TTL) {
            return cachedGame;
        }
        cachedGame = getBoard()?.game || null;
        cachedGameTimestamp = now;
        return cachedGame;
    };
    const getFen = (g) => { try { return g?.getFEN ? g.getFEN() : null; } catch { return null; } };
    const getPlayerColor = (g) => { try { const v = g?.getPlayingAs?.(); return v === 2 ? 'b' : 'w'; } catch { return 'w'; } };
    const getSideToMove = (g) => { const fen = getFen(g); return fen ? (fen.split(' ')[1] || null) : null; };
    const isPlayersTurn = (g) => { const me = getPlayerColor(g), stm = getSideToMove(g); return !!me && !!stm && me === stm; };
    const pa = () => (getGame()?.getPlayingAs ? getGame().getPlayingAs() : 1);

    function isBoardFlipped() {
        const now = Date.now();
        if ((now - cachedFlipTimestamp) < 1000) return cachedBoardFlipped;

        const el = getBoard();
        let flipped = false;

        try {
            const attr = el?.getAttribute?.('orientation');
            if (attr === 'black') flipped = true;
            else if (attr === 'white') flipped = false;
            else if (el?.classList?.contains('flipped')) flipped = true;
            else if (getGame()?.getPlayingAs?.() === 2) flipped = true;
        } catch { }

        cachedBoardFlipped = flipped;
        cachedFlipTimestamp = now;
        return flipped;
    }

    // Arrow drawing
    function clearArrows() {
        if (!boardCtx) return;
        const { drawingBoard, ctx } = boardCtx;
        ctx.clearRect(0, 0, drawingBoard.width, drawingBoard.height);
    }
    function drawArrow(uciFrom, uciTo, color, thickness) {
        if (!boardCtx || !uciFrom || !uciTo || uciFrom.length < 2 || uciTo.length < 2) return;
        const { drawingBoard, ctx } = boardCtx;

        const a = getSquareCenterCanvasXY(uciFrom);
        const b = getSquareCenterCanvasXY(uciTo);
        if (!a || !b) return;

        const size = Math.min(drawingBoard.width, drawingBoard.height);
        const tile = size / 8;

        ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
        ctx.lineWidth = thickness; ctx.strokeStyle = color; ctx.lineCap = 'round'; ctx.stroke();

        ctx.beginPath(); ctx.arc(a.x, a.y, tile / 7, 0, 2 * Math.PI);
        ctx.fillStyle = color.replace('0.7', '0.3'); ctx.fill(); ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();

        ctx.beginPath(); ctx.arc(b.x, b.y, tile / 5, 0, 2 * Math.PI);
        ctx.fillStyle = color.replace('0.7', '0.5'); ctx.fill(); ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.stroke();
    }

    // Square to client XY
    function getSquareCenterClientXY(square) {
        if (!boardCtx || !square || square.length < 2) return null;
        const file = 'abcdefgh'.indexOf(square[0]);
        const rank = parseInt(square[1], 10);
        if (file < 0 || isNaN(rank)) return null;
        const el = getBoard();
        const rect = el.getBoundingClientRect();
        const size = Math.min(rect.width, rect.height);
        const tile = size / 8;
        const offsetX = rect.left + (rect.width - size) / 2;
        const offsetY = rect.top + (rect.height - size) / 2;
        let x = file, y = 8 - rank;
        if (isBoardFlipped()) { x = 7 - x; y = 7 - y; }
        return { x: offsetX + (x + 0.5) * tile, y: offsetY + (y + 0.5) * tile };
    }

    function getSquareCenterCanvasXY(square) {
        if (!boardCtx || !square || square.length < 2) return null;
        const p = getSquareCenterClientXY(square);
        if (!p) return null;
        const rect = boardCtx.boardEl.getBoundingClientRect();
        return { x: p.x - rect.left, y: p.y - rect.top };
    }

    // Minimal event sequences (fix: reduce excessive event firing)
    function dispatchPointerOrMouse(el, type, opts, usePointer) {
        if (!el) return;
        if (usePointer) {
            try { el.dispatchEvent(new PointerEvent(type, { bubbles: true, cancelable: true, composed: true, ...opts })); return; } catch { /* fallthrough */ }
        }
        el.dispatchEvent(new MouseEvent(type.replace('pointer', 'mouse'), { bubbles: true, cancelable: true, composed: true, ...opts }));
    }

    function getTargetAt(x, y) {
        return document.elementFromPoint(x, y) || getBoard() || document.body;
    }

    async function simulateClickMove(from, to) {
        const a = getSquareCenterClientXY(from), b = getSquareCenterClientXY(to);
        if (!a || !b) return false;
        const usePointer = !!window.PointerEvent;
        const startEl = getTargetAt(a.x, a.y);
        const endEl = getTargetAt(b.x, b.y);

        const downStart = { clientX: a.x, clientY: a.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 1 };
        const upStart = { clientX: a.x, clientY: a.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 };
        const downEnd = { clientX: b.x, clientY: b.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 1 };
        const upEnd = { clientX: b.x, clientY: b.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 };

        // ⚡ Reduced delays for bullet
        dispatchPointerOrMouse(startEl, usePointer ? 'pointerdown' : 'mousedown', downStart, usePointer);
        await sleep(10); // ⚡ 20 → 10ms
        dispatchPointerOrMouse(startEl, usePointer ? 'pointerup' : 'mouseup', upStart, usePointer);
        startEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, composed: true, clientX: a.x, clientY: a.y }));

        await sleep(20); // ⚡ 40 → 20ms

        dispatchPointerOrMouse(endEl, usePointer ? 'pointerdown' : 'mousedown', downEnd, usePointer);
        await sleep(10); // ⚡ 20 → 10ms
        dispatchPointerOrMouse(endEl, usePointer ? 'pointerup' : 'mouseup', upEnd, usePointer);
        endEl.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, composed: true, clientX: b.x, clientY: b.y }));

        return true;
    }
    async function simulateDragMove(from, to) {
        const a = getSquareCenterClientXY(from), b = getSquareCenterClientXY(to);
        if (!a || !b) return false;
        const usePointer = !!window.PointerEvent;
        const startEl = getTargetAt(a.x, a.y);
        const endEl = getTargetAt(b.x, b.y);
        const down = { clientX: a.x, clientY: a.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 1 };
        const up = { clientX: b.x, clientY: b.y, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 };

        dispatchPointerOrMouse(startEl, usePointer ? 'pointerdown' : 'mousedown', down, usePointer);
        // Fewer move steps to avoid excessive firing
        const steps = 3;
        for (let i = 1; i <= steps; i++) {
            const t = i / steps;
            const mx = a.x + (b.x - a.x) * t;
            const my = a.y + (b.y - a.y) * t;
            dispatchPointerOrMouse(endEl, usePointer ? 'pointermove' : 'mousemove', { clientX: mx, clientY: my, buttons: 1 }, usePointer);
            await sleep(12);
        }
        dispatchPointerOrMouse(endEl, usePointer ? 'pointerup' : 'mouseup', up, usePointer);
        return true;
    }

    async function waitForFenChange(prevFen, timeout = 1000) {
        const start = Date.now();
        while (Date.now() - start < timeout) {
            const g = getGame();
            const fen = g?.getFEN ? g.getFEN() : null;
            if (fen && fen !== prevFen) return true;
            await sleep(40);
        }
        return false;
    }

    // Promotion handling
    async function maybeSelectPromotion(prefer = 'q') {
        const preferList = (prefer ? [prefer] : ['q', 'r', 'b', 'n']).map(c => c.toLowerCase());
        const getCandidates = () => qsa('[data-test-element*="promotion"], [class*="promotion"] [class*="piece"], [class*="promotion"] button, .promotion-piece, .promotion-card');
        const tryClick = (el) => {
            try {
                el.click?.();
                el.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
                el.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
                el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
                return true;
            } catch { return false; }
        };
        const start = Date.now();
        while (Date.now() - start < 1000) {
            const nodes = getCandidates();
            if (nodes.length) {
                for (const pref of preferList) {
                    const match = nodes.find(n =>
                        (n.dataset?.piece?.toLowerCase?.() || '') === pref ||
                        (n.getAttribute?.('data-piece') || '').toLowerCase() === pref ||
                        (n.getAttribute?.('aria-label') || '').toLowerCase().includes(pref) ||
                        (n.className || '').toLowerCase().includes(pref) ||
                        (n.textContent || '').toLowerCase().includes(pref)
                    );
                    if (match && tryClick(match)) return true;
                }
                if (tryClick(nodes[0])) return true;
            }
            await sleep(60);
        }
        return false;
    }

    function cancelPendingMove() {
        if (pendingMoveTimeoutId) {
            clearTimeout(pendingMoveTimeoutId);
            pendingMoveTimeoutId = null;
        }
    }

    async function makeMove(from, to, expectedFen, promotionChar) {
        const game = getGame();
        if (!game || !BotState.autoMove) return false;

        const beforeFen = getFen(game);
        if (!beforeFen || beforeFen !== expectedFen || !isPlayersTurn(game)) return false;

        await simulateClickMove(from, to);
        if (promotionChar) await maybeSelectPromotion(String(promotionChar).toLowerCase());

        // Only treat as success if the real board FEN changed
        const changed = await waitForFenChange(beforeFen, 1000);
        return !!changed;
    }

    // Engine integration
    function scoreFrom(obj) {
        if (!obj) return {};
        if (typeof obj === 'object') {
            if ('mate' in obj && obj.mate !== 0) return { mate: parseInt(obj.mate, 10) };
            if ('cp' in obj) return { cp: parseInt(obj.cp, 10) };
        }
        if (typeof obj === 'string') {
            if (obj.toUpperCase().includes('M')) {
                const m = parseInt(obj.replace(/[^-0-9]/g, ''), 10);
                if (!isNaN(m)) return { mate: m };
            }
            const cpFloat = parseFloat(obj);
            if (!isNaN(cpFloat)) return { cp: Math.round(cpFloat * 100) };
        }
        if (typeof obj === 'number') return { cp: Math.round(obj * 100) };
        return {};
    }
    function scoreToDisplay(score) {
        if (score && typeof score.mate === 'number' && score.mate !== 0) return `M${score.mate}`;
        if (score && typeof score.cp === 'number') return (score.cp / 100).toFixed(2);
        return '-';
    }
    function scoreNumeric(s) {
        if (!s) return -Infinity;
        if (typeof s.mate === 'number') return s.mate > 0 ? 100000 - s.mate : -100000 - s.mate;
        if (typeof s.cp === 'number') return s.cp;
        return -Infinity;
    }

    async function fetchEngineData(fen, depth, signal) {
        const startTime = performance.now();
        console.log(`GabiBot: 📡 API request STARTED for FEN: ${fen.substring(0, 20)}... | Depth: ${depth}`);

        const call = async (params) => {
            const url = `${API_URL}?fen=${encodeURIComponent(fen)}&depth=${depth}&${params}`;
            const ctrl = new AbortController();
            const onAbort = () => ctrl.abort('external-abort');
            if (signal?.aborted) {
                ctrl.abort('already-aborted');
                throw new DOMException('Aborted', 'AbortError');
            }
            signal?.addEventListener('abort', onAbort, { once: true });
            const to = setTimeout(() => ctrl.abort('timeout'), ANALYZE_TIMEOUT_MS);
            try {
                const res = await fetch(url, {
                    method: 'GET',
                    headers: { Accept: 'application/json' },
                    signal: ctrl.signal
                });
                const endTime = performance.now();
                const duration = endTime - startTime;
                if (!res.ok) {
                    console.warn(`GabiBot: ❌ API failed (${res.status}) after ${duration.toFixed(0)}ms`);
                    throw new Error(`API error ${res.status}`);
                }
                const data = await res.json();
                if (data.success === false) {
                    console.warn(`GabiBot: ❌ API success=false after ${duration.toFixed(0)}ms`);
                    throw new Error('API success=false');
                }
                console.log(`GabiBot: ✅ API success in ${duration.toFixed(0)}ms | FEN: ${fen.substring(0, 20)}...`);
                return data;
            } finally {
                clearTimeout(to);
                signal?.removeEventListener('abort', onAbort);
            }
        };
        try { return await call(`multipv=${MULTIPV}&mode=analysis`); }
        catch {
            try { return await call(`multipv=${MULTIPV}&mode=bestmove`); }
            catch { return await call('mode=bestmove'); }
        }
    }

    async function fetchEngineDataWithRetry(fen, depth, signal, maxRetries = 1) {
        // Simple cache check
        if (PositionCache[fen]) {
            console.log('GabiBot: 🗃️ Using cached analysis');
            return PositionCache[fen];
        }

        let lastError;
        for (let attempt = 0; attempt <= maxRetries; attempt++) {
            if (signal?.aborted || !BotState.hackEnabled) {
                console.log('GabiBot: ⏹️ Analysis aborted before attempt', attempt + 1);
                throw new DOMException('Aborted', 'AbortError');
            }

            if (attempt > 0) {
                const backoff = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
                console.log(`GabiBot: 🔁 Retry attempt #${attempt} for FEN: ${fen.substring(0, 20)}... (backoff: ${backoff}ms)`);
                await sleep(backoff);
            }

            try {
                const data = await fetchEngineData(fen, depth, signal);
                PositionCache[fen] = data;
                if (attempt > 0) {
                    console.log(`GabiBot: 🎯 Retry succeeded on attempt #${attempt + 1}`);
                }
                return data;
            } catch (error) {
                lastError = error;
                console.warn(`GabiBot: ⚠️ Attempt #${attempt + 1} failed:`, error.message || error);
                if (attempt >= maxRetries) break;
            }
        }
        console.error(`GabiBot: 💥 All ${maxRetries + 1} attempts failed for FEN: ${fen.substring(0, 20)}...`);
        throw lastError;
    }

    function parseBestLine(data) {
        const lines = [];
        const pushLine = (uci, pv, score) => {
            if (!uci || uci.length < 4) return;
            lines.push({ uci: uci.trim(), pv: (pv || '').trim(), score: score || {} });
        };
        const addFromArray = (arr) => arr.forEach(item => {
            const pv = item.pv || item.line || item.moves || '';
            const uci = item.uci || (pv ? pv.split(' ')[0] : '');
            const score = scoreFrom(item.score || item.evaluation || item.eval);
            pushLine(uci, pv, score);
        });

        if (Array.isArray(data.analysis)) addFromArray(data.analysis);
        else if (Array.isArray(data.lines)) addFromArray(data.lines);
        else if (Array.isArray(data.pvs)) addFromArray(data.pvs);

        if (!lines.length && typeof data.bestmove === 'string') {
            const parts = data.bestmove.split(' ');
            let uci = parts.length > 1 ? parts[1] : parts[0];
            if (uci === 'bestmove' && parts[1]) uci = parts[1];
            const pv = data.pv || data.continuation || uci;
            const score = scoreFrom(data.evaluation);
            pushLine(uci, pv, score);
        }
        lines.sort((a, b) => scoreNumeric(b.score) - scoreNumeric(a.score));
        return lines[0] || null;
    }

    function updateEvaluationBar(evaluation, playingAs) {
        if (!boardCtx) return;
        const whiteBar = boardCtx.evalBarWrap.querySelector('#evaluationBarWhite');
        const blackBar = boardCtx.evalBarWrap.querySelector('#evaluationBarBlack');
        if (!whiteBar || !blackBar) return;

        let score = 0;
        if (typeof evaluation === 'string') {
            if (evaluation === '-' || evaluation === 'Error') {
                // Neutral position when no eval
                whiteBar.style.height = '50%';
                blackBar.style.height = '50%';
                return;
            }

            if (evaluation.includes('M')) {
                const m = parseInt(evaluation.replace('M', '').replace('+', ''), 10);
                // Mate scores: positive = White mating, negative = Black mating
                score = m > 0 ? 10 : -10; // Cap at ±10 for mate
            } else {
                score = parseFloat(evaluation);
            }
        } else {
            score = parseFloat(evaluation);
        }

        if (isNaN(score)) {
            whiteBar.style.height = '50%';
            blackBar.style.height = '50%';
            return;
        }

        // Clamp score for visual representation
        const maxScore = 5;
        const clampedScore = Math.max(-maxScore, Math.min(maxScore, score));

        // Calculate percentages (eval is ALWAYS from White's perspective)
        // +score = White winning, -score = Black winning
        const whitePercent = 50 + (clampedScore / maxScore) * 50;
        const blackPercent = 100 - whitePercent;

        // NO FLIPPING! The evaluation meaning stays constant.
        // White winning = more white, regardless of board orientation
        whiteBar.style.height = `${whitePercent}%`;
        blackBar.style.height = `${blackPercent}%`;

        const ourColor = getPlayerColor(getGame());
        const ourEval = ourColor === 'w' ? score : -score;

        if (ourEval < -2) {
            boardCtx.evalBarWrap.style.borderColor = 'rgba(255, 100, 100, 0.5)';
        } else if (ourEval > 2) {
            boardCtx.evalBarWrap.style.borderColor = 'rgba(100, 255, 100, 0.5)';
        } else {
            boardCtx.evalBarWrap.style.borderColor = 'rgba(255, 255, 255, 0.2)';
        }
    }

    // FEN helpers for piece info
    function fenCharAtSquare(fen, square) {
        if (!fen || !square) return null;
        const placement = fen.split(' ')[0];
        const ranks = placement.split('/');
        const file = 'abcdefgh'.indexOf(square[0]);
        const rankNum = parseInt(square[1], 10);
        if (file < 0 || rankNum < 1 || rankNum > 8 || ranks.length !== 8) return null;
        const row = 8 - rankNum;
        const rowStr = ranks[row];
        let col = 0;
        for (const ch of rowStr) {
            if (/\d/.test(ch)) {
                col += parseInt(ch, 10);
                if (col > file) return null;
            } else {
                if (col === file) return ch;
                col++;
            }
        }
        return null;
    }
    function pieceFromFenChar(ch) {
        if (!ch) return null;
        const isUpper = ch === ch.toUpperCase();
        return { color: isUpper ? 'w' : 'b', type: ch.toLowerCase() };
    }

    // En passant detection for premove capture mode
    function isEnPassantCapture(fen, from, to, ourColor) {
        const parts = fen.split(' ');
        const ep = parts[3];
        const fromPiece = pieceFromFenChar(fenCharAtSquare(fen, from));
        if (!fromPiece || fromPiece.color !== ourColor || fromPiece.type !== 'p') return false;
        return ep && ep !== '-' && to === ep && from[0] !== to[0];
    }

    // Determine our planned move from PV given side-to-move
    function getOurMoveFromPV(pv, ourColor, sideToMove) {
        if (!pv) return null;
        const moves = pv.trim().split(/\s+/).filter(Boolean);
        if (!moves.length) return null;
        const idx = (sideToMove === ourColor) ? 0 : 1;
        return moves[idx] || null;
    }

    // Execute best move (draw + optional auto-move)
    async function executeAction(selectedUci, analysisFen) {
        try {
            clearArrows();
            if (!selectedUci || selectedUci.length < 4) return;
            const from = selectedUci.substring(0, 2);
            const to = selectedUci.substring(2, 4);
            const promotionChar = selectedUci.length >= 5 ? selectedUci[4] : null;

            drawArrow(from, to, 'rgba(100, 255, 100, 0.7)', 4);

            if (BotState.hackEnabled && BotState.autoMove) {
                const game = getGame();
                if (!game || !isPlayersTurn(game)) {
                    BotState.statusInfo = 'Waiting for opponent...'; ui.updateDisplay(pa()); return;
                }

                cancelPendingMove();

                const baseDelay = Math.max(0, AUTO_MOVE_BASE - BotState.autoMoveSpeed * AUTO_MOVE_STEP);
                const totalDelay = getHumanDelay(baseDelay, BotState.randomDelay);

                console.log(`GabiBot: Delay ${totalDelay}ms`);
                BotState.statusInfo = `Moving in ${(totalDelay / 1000).toFixed(1)}s`; ui.updateDisplay(pa());

                pendingMoveTimeoutId = setTimeout(async () => {
                    const g = getGame(); if (!g) return;
                    if (!isPlayersTurn(g)) { BotState.statusInfo = 'Move canceled (not our turn)'; ui.updateDisplay(pa()); return; }
                    if (getFen(g) !== analysisFen) { BotState.statusInfo = 'Move canceled (position changed)'; ui.updateDisplay(pa()); return; }

                    BotState.statusInfo = 'Making move...'; ui.updateDisplay(pa());
                    const success = await makeMove(from, to, analysisFen, promotionChar);
                    BotState.statusInfo = success ? '✓ Move made!' : '❌ Move failed';
                    ui.updateDisplay(pa());

                    //  Retry on failure
                    if (!success) {
                        setTimeout(() => {
                            if (BotState.hackEnabled && isPlayersTurn(getGame())) {
                                lastFenProcessedMain = '';
                                tick();
                            }
                        }, 800);
                    }

                }, totalDelay);
            } else {
                BotState.statusInfo = 'Ready (manual)'; ui.updateDisplay(pa());
            }
        } catch (error) {
            console.error('Error in executeAction:', error);
        }
    }

    // Single queued analysis runner for both main and premove
    function scheduleAnalysis(kind, fen) {
        const analysisId = ++currentAnalysisId;
        analysisQueue = analysisQueue.then(async () => {
            if (analysisId !== currentAnalysisId) return;
            if (!BotState.hackEnabled) return;

            const game = getGame();
            if (!game) return;

            if (kind === 'main') {
                if (lastFenProcessedMain === fen) return;
            } else {
                if (lastFenProcessedPremove === fen) return;
            }

            const ctrl = new AbortController(); // Local controller per analysis

            try {
                BotState.statusInfo = kind === 'main' ? '🔄 Analyzing...' : '🔄 Analyzing (premove)...';
                ui.updateDisplay(pa());

                const randomDepth = getRandomDepth();

                // Check if newer analysis superseded this one
                if (analysisId !== currentAnalysisId) {
                    ctrl.abort('superseded');
                    return;
                }

                const data = await fetchEngineDataWithRetry(fen, randomDepth, ctrl.signal);

                // Double-check still current after async operation
                if (analysisId !== currentAnalysisId) return;

                const best = parseBestLine(data);

                if (kind === 'main') {
                    BotState.bestMove = best?.uci || '-';
                    BotState.currentEvaluation = scoreToDisplay(best?.score);
                    BotState.principalVariation = best?.pv || 'Not available';
                    BotState.statusInfo = '✓ Ready';
                    ui.updateDisplay(pa());

                    if (best) await executeAction(best.uci, fen);
                    lastFenProcessedMain = fen;
                } else {
                    // Premove analysis
                    const ourColor = getPlayerColor(game);
                    const stm = getSideToMove(game);
                    const ourUci = getOurMoveFromPV(best?.pv || '', ourColor, stm) ||
                        ((stm === ourColor) ? (best?.uci || null) : null);

                    const premoveEvalDisplay = scoreToDisplay(best?.score);

                    if (!ourUci) {
                        BotState.statusInfo = 'Premove unavailable (no PV)';
                        ui.updateDisplay(pa());
                        lastFenProcessedPremove = fen;
                        return;
                    }

                    if (!shouldPremove(ourUci, fen)) {
                        BotState.statusInfo = `Premove skipped (${BotState.premoveMode})`;
                        ui.updateDisplay(pa());
                        lastFenProcessedPremove = fen;
                        return;
                    }

                    // 🛡️ SAFETY CHECK: Threat detection
                    const safetyCheck = checkPremoveSafety(fen, ourUci, ourColor);
                    if (!safetyCheck.safe) {
                        BotState.statusInfo = `🛡️ Premove blocked: ${safetyCheck.reason}`;
                        ui.updateDisplay(pa());
                        lastFenProcessedPremove = fen;
                        return;
                    }

                    let currentChance = getEvalBasedPremoveChance(premoveEvalDisplay, ourColor);

                    // 🛡️ Reduce confidence based on risk level
                    if (safetyCheck.riskLevel > 0) {
                        const riskPenalty = safetyCheck.riskLevel * 0.5; // 50% reduction at max risk
                        currentChance = Math.max(5, currentChance - riskPenalty);
                        console.log(`GabiBot: Risk detected (${safetyCheck.riskLevel}%), reducing confidence: ${currentChance.toFixed(0)}%`);
                    }
                    const chanceEl = qs('[name="premoveChance"] .itemState');
                    if (chanceEl) chanceEl.textContent = `${Math.round(currentChance)}%`;

                    const roll = Math.random() * 100;
                    if (roll > currentChance) {
                        const skipReason = safetyCheck.riskLevel > 0
                            ? `${safetyCheck.reason} (${roll.toFixed(0)}% > ${currentChance.toFixed(0)}%)`
                            : `eval: ${premoveEvalDisplay}, ${roll.toFixed(0)}% > ${currentChance.toFixed(0)}%`;
                        BotState.statusInfo = `Premove skipped: ${skipReason}`;
                        ui.updateDisplay(pa());
                        lastFenProcessedPremove = fen;
                        return;
                    }

                    const from = ourUci.substring(0, 2);
                    const to = ourUci.substring(2, 4);

                    clearArrows();
                    drawArrow(from, to, 'rgba(80, 180, 255, 0.7)', 3);

                    await simulateClickMove(from, to);
                    await sleep(80);

                    lastPremoveFen = fen;
                    lastPremoveUci = ourUci;
                    const safetyEmoji = safetyCheck.riskLevel === 0 ? '✅' : safetyCheck.riskLevel < 25 ? '⚠️' : '🔶';
                    BotState.statusInfo = `${safetyEmoji} Premove: ${ourUci} (${Math.round(currentChance)}% confidence)`;
                    ui.updateDisplay(pa());
                    lastFenProcessedPremove = fen;
                }
            } catch (error) {
                if (String(error?.name || error).toLowerCase().includes('abort') ||
                    String(error?.message || error).toLowerCase().includes('superseded')) {
                    BotState.statusInfo = '⏸ Analysis canceled';
                } else {
                    console.error('GabiBot API Error:', error);
                    BotState.statusInfo = '❌ API Error';
                    BotState.currentEvaluation = 'Error';
                }
                ui.updateDisplay(pa());
            }
        });
    }
    // ═══════════════════════════════════════════════════════════
    // PREMOVE SAFETY: HEURISTIC THREAT DETECTION
    // ═══════════════════════════════════════════════════════════

    // Piece values for safety checks
    const PIECE_VALUES = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 };

    // Get all squares attacking a given square (fast heuristic)
    function getAttackersOfSquare(fen, targetSquare, attackerColor) {
        const attackers = [];
        const placement = fen.split(' ')[0];
        const ranks = placement.split('/');

        const tFile = 'abcdefgh'.indexOf(targetSquare[0]);
        const tRank = parseInt(targetSquare[1], 10);

        if (tFile < 0 || tRank < 1 || tRank > 8) return attackers;

        // Helper: check square and add if it contains attacker piece
        const checkSquare = (file, rank, pieceTypes) => {
            if (file < 0 || file > 7 || rank < 1 || rank > 8) return;
            const sq = 'abcdefgh'[file] + rank;
            const ch = fenCharAtSquare(fen, sq);
            const piece = pieceFromFenChar(ch);
            if (piece && piece.color === attackerColor && pieceTypes.includes(piece.type)) {
                attackers.push({ square: sq, piece: piece.type });
            }
        };

        // Pawn attacks (diagonal)
        const pawnDir = attackerColor === 'w' ? 1 : -1;
        checkSquare(tFile - 1, tRank - pawnDir, ['p']);
        checkSquare(tFile + 1, tRank - pawnDir, ['p']);

        // Knight attacks
        const knightMoves = [
            [2, 1], [2, -1], [-2, 1], [-2, -1],
            [1, 2], [1, -2], [-1, 2], [-1, -2]
        ];
        knightMoves.forEach(([df, dr]) => checkSquare(tFile + df, tRank + dr, ['n']));

        // King attacks
        for (let df = -1; df <= 1; df++) {
            for (let dr = -1; dr <= 1; dr++) {
                if (df === 0 && dr === 0) continue;
                checkSquare(tFile + df, tRank + dr, ['k']);
            }
        }

        // Sliding pieces (rook, bishop, queen)
        const directions = [
            { dx: 1, dy: 0, pieces: ['r', 'q'] },   // right
            { dx: -1, dy: 0, pieces: ['r', 'q'] },  // left
            { dx: 0, dy: 1, pieces: ['r', 'q'] },   // up
            { dx: 0, dy: -1, pieces: ['r', 'q'] },  // down
            { dx: 1, dy: 1, pieces: ['b', 'q'] },   // diagonal
            { dx: 1, dy: -1, pieces: ['b', 'q'] },
            { dx: -1, dy: 1, pieces: ['b', 'q'] },
            { dx: -1, dy: -1, pieces: ['b', 'q'] }
        ];

        directions.forEach(({ dx, dy, pieces }) => {
            let f = tFile + dx;
            let r = tRank + dy;
            while (f >= 0 && f <= 7 && r >= 1 && r <= 8) {
                const sq = 'abcdefgh'[f] + r;
                const ch = fenCharAtSquare(fen, sq);
                if (ch) {
                    const piece = pieceFromFenChar(ch);
                    if (piece && piece.color === attackerColor && pieces.includes(piece.type)) {
                        attackers.push({ square: sq, piece: piece.type });
                    }
                    break; // Blocked
                }
                f += dx;
                r += dy;
            }
        });

        return attackers;
    }

    // Check if square is attacked by opponent
    function isSquareAttackedBy(fen, square, attackerColor) {
        return getAttackersOfSquare(fen, square, attackerColor).length > 0;
    }

    // Find king position
    function findKing(fen, color) {
        const placement = fen.split(' ')[0];
        const ranks = placement.split('/');
        const kingChar = color === 'w' ? 'K' : 'k';

        for (let rankIdx = 0; rankIdx < 8; rankIdx++) {
            const rank = 8 - rankIdx;
            let file = 0;
            for (const ch of ranks[rankIdx]) {
                if (/\d/.test(ch)) {
                    file += parseInt(ch, 10);
                } else {
                    if (ch === kingChar) {
                        return 'abcdefgh'[file] + rank;
                    }
                    file++;
                }
            }
        }
        return null;
    }

    // Simple FEN after move simulation (for king safety check)
    function makeSimpleMove(fen, from, to) {
        const parts = fen.split(' ');
        const placement = parts[0];
        const ranks = placement.split('/');

        const fromFile = 'abcdefgh'.indexOf(from[0]);
        const fromRank = parseInt(from[1], 10);
        const toFile = 'abcdefgh'.indexOf(to[0]);
        const toRank = parseInt(to[1], 10);

        if (fromFile < 0 || toFile < 0 || fromRank < 1 || toRank < 1) return fen;

        const fromRowIdx = 8 - fromRank;
        const toRowIdx = 8 - toRank;

        const movingPiece = fenCharAtSquare(fen, from);
        if (!movingPiece) return fen;

        // Remove piece from 'from' square
        let fromRow = ranks[fromRowIdx];
        let fromCol = 0;
        let newFromRow = '';
        let emptyCount = 0;

        for (const ch of fromRow) {
            if (/\d/.test(ch)) {
                const spaces = parseInt(ch, 10);
                if (fromCol + spaces > fromFile) {
                    // Our square is in this empty span
                    const before = fromFile - fromCol;
                    const after = spaces - before - 1;
                    if (before > 0) newFromRow += before;
                    emptyCount = 1; // This square is now empty
                    if (after > 0) newFromRow += after;
                    fromCol += spaces;
                } else {
                    newFromRow += ch;
                    fromCol += spaces;
                }
            } else {
                if (fromCol === fromFile) {
                    emptyCount = 1; // Replace piece with empty
                } else {
                    if (emptyCount > 0) {
                        newFromRow += emptyCount;
                        emptyCount = 0;
                    }
                    newFromRow += ch;
                }
                fromCol++;
            }
        }
        if (emptyCount > 0) newFromRow += emptyCount;

        // Compress consecutive digits
        newFromRow = newFromRow.replace(/(\d)(\d)/g, (m, a, b) => parseInt(a) + parseInt(b));
        ranks[fromRowIdx] = newFromRow;

        // Place piece on 'to' square
        let toRow = ranks[toRowIdx];
        let toCol = 0;
        let newToRow = '';

        for (const ch of toRow) {
            if (/\d/.test(ch)) {
                const spaces = parseInt(ch, 10);
                if (toCol + spaces > toFile) {
                    const before = toFile - toCol;
                    const after = spaces - before - 1;
                    if (before > 0) newToRow += before;
                    newToRow += movingPiece; // Place moving piece
                    if (after > 0) newToRow += after;
                    toCol += spaces;
                } else {
                    newToRow += ch;
                    toCol += spaces;
                }
            } else {
                if (toCol === toFile) {
                    newToRow += movingPiece; // Replace whatever was there
                } else {
                    newToRow += ch;
                }
                toCol++;
            }
        }

        ranks[toRowIdx] = newToRow;
        parts[0] = ranks.join('/');

        return parts.join(' ');
    }

    // Main premove safety check
    function checkPremoveSafety(fen, uci, ourColor) {
        if (!fen || !uci || uci.length < 4) {
            return { safe: false, reason: 'Invalid move', riskLevel: 100 };
        }

        const from = uci.substring(0, 2);
        const to = uci.substring(2, 4);
        const oppColor = ourColor === 'w' ? 'b' : 'w';

        const movingCh = fenCharAtSquare(fen, from);
        const movingPiece = pieceFromFenChar(movingCh);

        if (!movingPiece || movingPiece.color !== ourColor) {
            return { safe: false, reason: 'Not our piece', riskLevel: 100 };
        }

        const destCh = fenCharAtSquare(fen, to);
        const destPiece = pieceFromFenChar(destCh);

        let riskLevel = 0;
        const reasons = [];

        // Check 1: King safety (critical)
        if (movingPiece.type === 'k') {
            if (isSquareAttackedBy(fen, to, oppColor)) {
                return { safe: false, reason: 'King moves into check', riskLevel: 100 };
            }
        } else {
            // Check if move exposes our king
            const newFen = makeSimpleMove(fen, from, to);
            const kingPos = findKing(newFen, ourColor);
            if (kingPos && isSquareAttackedBy(newFen, kingPos, oppColor)) {
                return { safe: false, reason: 'Exposes king to check', riskLevel: 100 };
            }
        }

        // Check 2: Don't hang the queen
        if (movingPiece.type === 'q') {
            const attackers = getAttackersOfSquare(fen, to, oppColor);
            if (attackers.length > 0) {
                // Queen moving to attacked square
                const hasDefender = getAttackersOfSquare(fen, to, ourColor).length > 1; // >1 because queen itself
                if (!hasDefender || !destPiece) {
                    return { safe: false, reason: 'Hangs queen', riskLevel: 90 };
                }
            }
        }

        // Check 3: Don't hang rook for nothing
        if (movingPiece.type === 'r') {
            const attackers = getAttackersOfSquare(fen, to, oppColor);
            if (attackers.length > 0) {
                const captureValue = destPiece ? PIECE_VALUES[destPiece.type] : 0;
                if (captureValue < PIECE_VALUES.r) {
                    const hasDefender = getAttackersOfSquare(fen, to, ourColor).length > 1;
                    if (!hasDefender) {
                        reasons.push('Hangs rook');
                        riskLevel += 60;
                    }
                }
            }
        }

        // Check 4: Destination square safety
        const destAttackers = getAttackersOfSquare(fen, to, oppColor);
        if (destAttackers.length > 0 && !destPiece) {
            // Moving to attacked empty square
            const defenders = getAttackersOfSquare(fen, to, ourColor).length;
            if (defenders === 0) {
                reasons.push('Moves to undefended attacked square');
                riskLevel += 30;
            } else if (destAttackers.length > defenders) {
                reasons.push('Moves to heavily attacked square');
                riskLevel += 20;
            }
        }

        // Check 5: Unfavorable trades
        if (destPiece && destPiece.color === oppColor) {
            const ourValue = PIECE_VALUES[movingPiece.type];
            const theirValue = PIECE_VALUES[destPiece.type];
            const destAttackers = getAttackersOfSquare(fen, to, oppColor);

            if (destAttackers.length > 0 && ourValue > theirValue) {
                reasons.push(`Bad trade: ${movingPiece.type} for ${destPiece.type}`);
                riskLevel += 25;
            }
        }

        const safe = riskLevel < 50;
        const reason = reasons.length > 0 ? reasons.join(', ') : (safe ? 'Move appears safe' : 'Move risky');

        return { safe, reason, riskLevel };
    }
    // Should we premove this UCI for the given FEN (mode-aware)
    function shouldPremove(uci, fen) {
        if (!uci || uci.length < 4) return false;
        const game = getGame();
        const ourColor = getPlayerColor(game);
        const from = uci.substring(0, 2);
        const to = uci.substring(2, 4);
        const fromCh = fenCharAtSquare(fen, from);
        const toCh = fenCharAtSquare(fen, to);
        const fromPiece = pieceFromFenChar(fromCh);
        const toPiece = pieceFromFenChar(toCh);

        if (!fromPiece || fromPiece.color !== ourColor) return false;

        if (BotState.premoveMode === 'every') return true;

        if (BotState.premoveMode === 'capture') {
            return !!(toPiece && toPiece.color !== ourColor) || isEnPassantCapture(fen, from, to, ourColor);
        }

        if (BotState.premoveMode === 'filter') {
            return !!BotState.premovePieces[fromPiece.type];
        }

        return false;
    }

    // Calculate premove confidence based on position evaluation
    function getEvalBasedPremoveChance(evaluation, ourColor) {
        if (!BotState.premoveEnabled) return 0;

        const game = getGame();
        if (!game || isPlayersTurn(game)) return 0;

        let evalScore = 0;

        if (typeof evaluation === 'string') {
            if (evaluation === '-' || evaluation === 'Error') return 0;

            if (evaluation.includes('M')) {
                const mateNum = parseInt(evaluation.replace('M', '').replace('+', ''), 10);
                if (!isNaN(mateNum)) {
                    // Mate is always from White's perspective from engine
                    // Positive mate = White winning, Negative = Black winning
                    const ourMate = ourColor === 'w' ? mateNum : -mateNum;
                    return ourMate > 0 ? 100 : 20;
                }
            }

            evalScore = parseFloat(evaluation);
        } else {
            evalScore = parseFloat(evaluation);
        }

        if (isNaN(evalScore)) return 0;

        // Engine eval is from White's perspective
        // Convert to our perspective
        const ourEval = ourColor === 'w' ? evalScore : -evalScore;

        if (ourEval >= 3.0) return 90;
        if (ourEval >= 2.0) return 75;
        if (ourEval >= 1.0) return 50;
        if (ourEval >= 0.5) return 35;
        if (ourEval >= 0) return 25;
        return 20;
    }

    // Tick loop
    function tick() {
        if (!BotState.hackEnabled) return;

        const game = getGame();
        if (!game) return;

        if (game.isGameOver && game.isGameOver()) {
            BotState.currentEvaluation = 'GAME OVER';
            BotState.bestMove = '-';
            BotState.principalVariation = 'Game ended';
            BotState.statusInfo = 'Game finished';
            clearArrows();
            ui.updateDisplay(pa());
            return;
        }

        const fen = getFen(game);
        if (!fen) return;

        if (fen !== lastFenSeen) {
            lastFenSeen = fen;
            cancelPendingMove();
            clearArrows();
            lastPremoveFen = '';
            lastPremoveUci = '';
        }

        if (isPlayersTurn(game)) {
            if (lastFenProcessedMain !== fen) {
                scheduleAnalysis('main', fen);
            }
        } else {
            if (BotState.premoveEnabled) {
                if (lastFenProcessedPremove !== fen) {
                    scheduleAnalysis('premove', fen);
                } else {
                    //  Pass ourColor when updating premove chance display
                    const chanceEl = qs('[name="premoveChance"] .itemState');
                    if (chanceEl && BotState.currentEvaluation && BotState.currentEvaluation !== '-') {
                        const ourColor = getPlayerColor(game);
                        const currentChance = getEvalBasedPremoveChance(BotState.currentEvaluation, ourColor);
                        chanceEl.textContent = `${Math.round(currentChance)}%`;
                    }

                    BotState.statusInfo = (lastPremoveUci && lastPremoveFen === fen) ? 'Waiting (premove ready)...' : 'Waiting for opponent...';
                    ui.updateDisplay(pa());
                }
            } else {
                const chanceEl = qs('[name="premoveChance"] .itemState');
                if (chanceEl) chanceEl.textContent = '0%';

                BotState.statusInfo = 'Waiting for opponent...';
                ui.updateDisplay(pa());
            }
        }
    }

    function startTickLoop() {
        stopTickLoop();
        const interval = Math.max(150, 1100 - (Number(BotState.updateSpeed) || 8) * 100);
        tickTimer = setInterval(tick, interval);
        tick();
    }

    function stopTickLoop() {
        if (tickTimer) clearInterval(tickTimer);
        tickTimer = null;
    }

    // Game auto-end detection and auto-rematch watchers
    function startAutoWatchers() {
        if (gameStartInterval) clearInterval(gameStartInterval);
        if (gameEndInterval) clearInterval(gameEndInterval);

        let gameEndDetected = false;

        gameEndInterval = setInterval(() => {
            const gameOverModal = qs('.game-over-modal-content');

            if (gameOverModal && !gameEndDetected) {
                console.log('GabiBot: Game over detected');

                clearArrows();
                cancelPendingMove();

                BotState.statusInfo = 'Game ended, preparing new game...';
                BotState.currentEvaluation = '-';
                BotState.bestMove = '-';
                ui?.updateDisplay(pa());

                gameEndDetected = true;

                if (BotState.autoRematch) {
                    console.log('GabiBot: Auto-rematch enabled');

                    setTimeout(() => {
                        const modal = qs('.game-over-modal-content');
                        if (!modal) return console.log('GabiBot: [2s] Modal closed, game started');

                        const btn = qsa('button', modal).find(b =>
                            /rematch/i.test((b.textContent || '').trim()) ||
                            /rematch/i.test((b.getAttribute?.('aria-label') || '').trim())
                        );

                        if (btn) {
                            console.log('GabiBot: [2s] ✅ Clicking Rematch');
                            btn.click();
                        } else {
                            console.log('GabiBot: [2s] No Rematch button');
                        }
                    }, 2000);

                    setTimeout(() => {
                        const modal = qs('.game-over-modal-content');
                        if (!modal) return console.log('GabiBot: [12s] Modal closed, game started');

                        const btn = qsa('button', modal).find(b => {
                            const text = (b.textContent || '').replace(/\s+/g, ' ').trim();
                            return /new.*\d+.*min/i.test(text);
                        });

                        if (btn) {
                            console.log(`GabiBot: [12s] ✅ Clicking "${btn.textContent.trim()}"`);
                            btn.click();
                        } else {
                            console.log('GabiBot: [12s] No "New X min" button');
                        }
                    }, 12000);

                    setTimeout(async () => {
                        const modal = qs('.game-over-modal-content');
                        if (!modal) return console.log('GabiBot: [22s] Modal closed, game started');

                        console.log('GabiBot: [22s] Using New Game tab fallback');

                        const closeBtn = qs('[aria-label="Close"]', modal);
                        if (closeBtn) {
                            closeBtn.click();
                            await sleep(500);
                        }

                        const tab = qs('[data-tab="newGame"]') ||
                            qsa('.tabs-tab').find(t => /new.*game/i.test(t.textContent || ''));

                        if (tab) {
                            console.log('GabiBot: [22s] Clicking New Game tab');
                            tab.click();
                            await sleep(400);

                            const startBtn = qsa('button').find(b =>
                                /start.*game/i.test((b.textContent || '').trim())
                            );

                            if (startBtn) {
                                console.log('GabiBot: [22s] ✅ Clicking Start Game');
                                startBtn.click();
                            } else {
                                console.log('GabiBot: [22s] ❌ Start Game not found');
                            }
                        } else {
                            console.log('GabiBot: [22s] ❌ New Game tab not found');
                        }
                    }, 22000);
                }
            }

            if (!gameOverModal && gameEndDetected) {
                console.log('GabiBot: New game started, bot analyzing...');
                gameEndDetected = false;

                if (BotState.hackEnabled) {
                    BotState.statusInfo = 'Ready';
                    ui?.updateDisplay(pa());
                    setTimeout(() => {
                        if (BotState.hackEnabled) tick();
                    }, 500);
                }
            }
        }, 1000);
    }

    // Draggable panel
    function makePanelDraggable(panel) {
        function clampToViewport() {
            const rect = panel.getBoundingClientRect();
            const vw = window.innerWidth;
            const vh = window.innerHeight;
            const margin = 8;

            panel.style.right = 'auto';

            let left = parseFloat(panel.style.left || rect.left);
            let top = parseFloat(panel.style.top || rect.top);

            left = Math.max(margin, Math.min(left, vw - rect.width - margin));
            top = Math.max(margin, Math.min(top, vh - rect.height - margin));

            panel.style.left = left + 'px';
            panel.style.top = top + 'px';
        }

        function allowDragFromTarget(target, e) {
            if (e.altKey) return true;

            const rect = panel.getBoundingClientRect();
            const m = 14;
            const nearEdge =
                e.clientX <= rect.left + m ||
                e.clientX >= rect.right - m ||
                e.clientY <= rect.top + m ||
                e.clientY >= rect.bottom - m;

            if (nearEdge) return true;

            if (target.closest('input, select, textarea, button, label, a')) return false;

            return true;
        }

        function startDrag(e) {
            e.preventDefault();
            const startRect = panel.getBoundingClientRect();

            panel.classList.add('grabbing');
            panel.style.right = 'auto';
            panel.style.left = startRect.left + 'px';
            panel.style.top = startRect.top + 'px';

            const startX = e.clientX;
            const startY = e.clientY;

            const move = (ev) => {
                const dx = ev.clientX - startX;
                const dy = ev.clientY - startY;
                const vw = window.innerWidth;
                const vh = window.innerHeight;

                let newLeft = startRect.left + dx;
                let newTop = startRect.top + dy;

                const margin = 8;
                const maxLeft = Math.max(margin, vw - startRect.width - margin);
                const maxTop = Math.max(margin, vh - startRect.height - margin);
                newLeft = Math.min(Math.max(newLeft, margin), maxLeft);
                newTop = Math.min(Math.max(newTop, margin), maxTop);

                panel.style.left = newLeft + 'px';
                panel.style.top = newTop + 'px';
            };

            const up = () => {
                document.removeEventListener('mousemove', move);
                document.removeEventListener('mouseup', up);
                panel.classList.remove('grabbing');
                try { ui?.Settings.save?.(); } catch { }
            };

            document.addEventListener('mousemove', move);
            document.addEventListener('mouseup', up);
        }

        panel.addEventListener('mousedown', (e) => {
            if (e.button !== 0) return;
            if (!allowDragFromTarget(e.target, e)) return;
            startDrag(e);
        });

        window.addEventListener('resize', clampToViewport);
        setTimeout(clampToViewport, 50);
    }

    // Kick off
    setTimeout(init, 3000);
})();