Chess.com Cheat Engine

Chess.com cheat engine — K-EXPERT edition (ELO + Stream-Hide + PlayStyle)

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      16.0
// @description  Chess.com cheat engine — K-EXPERT edition (ELO + Stream-Hide + PlayStyle)
// @author       kliel
// @license      MIT
// @match        https://www.chess.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      unpkg.com
// @connect      lichess.org
// @connect      explorer.lichess.ovh
// @run-at       document-idle
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    // ─── PAGE GUARD ─────────────────────────────────────────────────────────────
    const INACTIVE_PATHS = [/\/analysis/, /\/learn/, /\/puzzles/, /\/lessons/, /\/drills/, /\/courses/, /\/practice/, /\/openings/];
    const isInactivePage = () => INACTIVE_PATHS.some(p => p.test(location.pathname));
    let _paused = isInactivePage();

    (['pushState', 'replaceState']).forEach(m => {
        const orig = history[m];
        history[m] = function (...a) { const r = orig.apply(this, a); window.dispatchEvent(new Event('_chess_nav')); return r; };
    });
    window.addEventListener('popstate', () => window.dispatchEvent(new Event('_chess_nav')));
    window.addEventListener('_chess_nav', () => {
        const was = _paused;
        _paused = isInactivePage();
        if (_paused && !was) {
            try { SF.worker && SF.worker.postMessage('stop'); } catch (_) {}
            UI.clearArrows();
        }
        if (!_paused && was) {
            State.lastFen = null;
            State.boardCache = null;
            setTimeout(Loop.tick, 200);
        }
    });

    // ─── CONFIG ──────────────────────────────────────────────────────────────────
    const CFG = {
        active: false,
        autoPlay: false,
        useBook: true,
        showThreats: true,
        depth: 14,
        multiPV: 5,
        humanization: true,
        suboptimalRate: 0.35,
        correlation: 0.60,
        timeControl: 'blitz',
        eloMode: false,
        targetElo: 1500,
        eloTc: 'blitz',
        style: 'balanced',          // ← NEW: playing style
        timing: {
            bullet: { min: 200,   max: 700,   depth: 8  },
            blitz:  { min: 1000,  max: 4000,  depth: 12 },
            rapid:  { min: 4000,  max: 12000, depth: 16 },
        },
    };

    // ─── STATE ───────────────────────────────────────────────────────────────────
    const State = {
        lastFen: null,
        playerColor: null,
        moveCount: 0,
        boardCache: null,
        candidates: {},
        currentEval: null,
        opponentMove: null,
        bestCount: 0,
        totalCount: 0,
        perfectStreak: 0,
        sloppyStreak: 0,
    };

    // ─── UTILS ───────────────────────────────────────────────────────────────────
    const $ = (sel, root = document) => {
        try {
            if (Array.isArray(sel)) { for (const s of sel) { const e = root.querySelector(s); if (e) return e; } return null; }
            return root.querySelector(sel);
        } catch { return null; }
    };
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    const rnd = (a, b) => Math.random() * (b - a) + a;
    const gauss = (m, s) => { const u = Math.random(), v = Math.random(); return m + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * s; };
    const humanMs = (min, max) => {
        const r = (Math.random() + Math.random() + Math.random()) / 3;
        return Math.round(min + r * (max - min));
    };
    const log = (m, t) => console.log('%c[KE] ' + m, 'color:' + ({info:'#4caf50',warn:'#ffcc00',error:'#f44336',debug:'#90caf9'})[t||'info'] + ';font-weight:bold');

    // ─── ELO STRENGTH ENGINE ─────────────────────────────────────────────────────
    const EloStrength = {
        toUciElo: (elo) => Math.max(1320, Math.min(3190, elo)),
        toSkill: (elo) => {
            if (elo <  800) return 0;
            if (elo <  900) return 1;
            if (elo < 1000) return 2;
            if (elo < 1100) return 3;
            if (elo < 1200) return 4;
            if (elo < 1300) return 5;
            if (elo < 1400) return 6;
            if (elo < 1500) return 8;
            if (elo < 1600) return 10;
            if (elo < 1700) return 11;
            if (elo < 1800) return 12;
            if (elo < 1900) return 13;
            if (elo < 2000) return 14;
            if (elo < 2100) return 15;
            if (elo < 2200) return 16;
            if (elo < 2400) return 17;
            if (elo < 2600) return 18;
            if (elo < 2800) return 19;
            return 20;
        },
        toDepth: (elo) => {
            if (elo <  700) return 1;
            if (elo <  900) return 2;
            if (elo < 1100) return 3;
            if (elo < 1300) return 5;
            if (elo < 1500) return 7;
            if (elo < 1700) return 9;
            if (elo < 1900) return 11;
            if (elo < 2100) return 13;
            if (elo < 2300) return 15;
            if (elo < 2500) return 17;
            if (elo < 2700) return 19;
            return 20;
        },
        toSuboptimalRate: (elo) => {
            if (elo <  600) return 0.85;
            if (elo <  800) return 0.70;
            if (elo <  900) return 0.58;
            if (elo < 1000) return 0.50;
            if (elo < 1100) return 0.43;
            if (elo < 1200) return 0.37;
            if (elo < 1300) return 0.32;
            if (elo < 1400) return 0.27;
            if (elo < 1500) return 0.23;
            if (elo < 1600) return 0.19;
            if (elo < 1700) return 0.15;
            if (elo < 1800) return 0.12;
            if (elo < 1900) return 0.09;
            if (elo < 2000) return 0.07;
            if (elo < 2100) return 0.055;
            if (elo < 2200) return 0.04;
            if (elo < 2400) return 0.025;
            if (elo < 2600) return 0.015;
            return 0.008;
        },
        toMaxLoss: (elo) => {
            if (elo <  800) return 600;
            if (elo <  900) return 450;
            if (elo < 1000) return 350;
            if (elo < 1100) return 280;
            if (elo < 1200) return 220;
            if (elo < 1300) return 170;
            if (elo < 1400) return 130;
            if (elo < 1500) return 100;
            if (elo < 1600) return 75;
            if (elo < 1700) return 55;
            if (elo < 1800) return 40;
            if (elo < 2000) return 28;
            if (elo < 2200) return 18;
            if (elo < 2400) return 10;
            return 6;
        },
        toTiming: (elo, tc) => {
            const base = {
                bullet: [
                    [0,    500,  100,  400],
                    [500,  1000, 200,  600],
                    [1000, 1500, 300,  700],
                    [1500, 2000, 250,  650],
                    [2000, 9999, 200,  550],
                ],
                blitz: [
                    [0,    500,  500,  2000],
                    [500,  1000, 800,  3000],
                    [1000, 1500, 1000, 4000],
                    [1500, 2000, 1500, 5000],
                    [2000, 2500, 2000, 6000],
                    [2500, 9999, 2500, 7000],
                ],
                rapid: [
                    [0,    500,  1000, 5000],
                    [500,  1000, 2000, 8000],
                    [1000, 1500, 3000, 10000],
                    [1500, 2000, 4000, 13000],
                    [2000, 2500, 5000, 16000],
                    [2500, 9999, 6000, 20000],
                ],
            };
            const rows = base[tc] || base.blitz;
            for (const [lo, hi, mn, mx] of rows) {
                if (elo >= lo && elo < hi) return { min: mn, max: mx };
            }
            return { min: 2000, max: 8000 };
        },
        label: (elo) => {
            if (elo <  800) return 'Beginner';
            if (elo < 1000) return 'Novice';
            if (elo < 1200) return 'Class D';
            if (elo < 1400) return 'Class C';
            if (elo < 1600) return 'Class B';
            if (elo < 1800) return 'Class A';
            if (elo < 2000) return 'Candidate';
            if (elo < 2200) return 'Expert';
            if (elo < 2400) return 'NM / FM';
            if (elo < 2500) return 'IM';
            if (elo < 2700) return 'GM';
            return 'Super-GM';
        },
        apply: (elo) => {
            if (!SF.ready || !SF.worker) return;
            const skill = EloStrength.toSkill(elo);
            const uciElo = EloStrength.toUciElo(elo);
            SF.worker.postMessage('setoption name UCI_LimitStrength value true');
            SF.worker.postMessage('setoption name UCI_Elo value ' + uciElo);
            SF.worker.postMessage('setoption name Skill Level value ' + skill);
            log('ELO ' + elo + ' → Skill ' + skill + ', UCI_Elo ' + uciElo + ', Depth ' + EloStrength.toDepth(elo));
        },
        reset: () => {
            if (!SF.ready || !SF.worker) return;
            SF.worker.postMessage('setoption name UCI_LimitStrength value false');
            SF.worker.postMessage('setoption name Skill Level value 20');
            log('ELO mode OFF — full strength');
        },
    };

    // ─── PLAY STYLE ENGINE ───────────────────────────────────────────────────────
    //
    //  Each style physically changes WHICH move is selected from the MultiPV
    //  candidate list — not just a label. Logic:
    //
    //   • boardMap()   — parse FEN into a square→piece lookup
    //   • classify()   — tag each candidate move (capture? sacrifice? equal trade?)
    //   • pickMove()   — per-style weighted selection from classified candidates
    //
    //  Styles:
    //    balanced   — default randomised humanization (no override)
    //    aggressive — prefers captures of high-value pieces; willing to sac slightly
    //    defensive  — avoids captures; never sacrifices; plays solid non-tactical moves
    //    brilliant  — actively seeks sacrifices (give up higher-value piece);
    //                 if no sac available, tries the most forcing capture
    //    endgame    — trades pieces toward a favourable endgame; in endgame phase
    //                 always plays the single best move (no randomisation)
    //    positional — avoids all captures when quiet alternatives exist; prefers
    //                 long-term manoeuvring over tactics
    //
    const PlayStyle = {

        // ── Meta-info used by UI ──────────────────────────────────────────────
        LABELS: {
            balanced:   { icon: '⚖️', label: 'Balanced',   color: '#9e9e9e' },
            aggressive: { icon: '🔥', label: 'Aggressive',  color: '#f44336' },
            defensive:  { icon: '🛡️', label: 'Defensive',  color: '#2196f3' },
            brilliant:  { icon: '✨', label: 'Brilliant',   color: '#9c27b0' },
            endgame:    { icon: '👑', label: 'Endgame',     color: '#ffc107' },
            positional: { icon: '♟️', label: 'Positional', color: '#00bcd4' },
        },

        PIECE_VAL: { p: 1, n: 3, b: 3, r: 5, q: 9, k: 100 },

        // Parse FEN board section into { square: { piece, color, val } }
        boardMap: (fen) => {
            const map = {};
            const rows = (fen || '').split(' ')[0].split('/');
            for (let r = 0; r < 8; r++) {
                let file = 0;
                for (const ch of (rows[r] || '')) {
                    if (/\d/.test(ch)) { file += parseInt(ch); }
                    else {
                        const sq = String.fromCharCode(97 + file) + (8 - r);
                        const lower = ch.toLowerCase();
                        map[sq] = { piece: lower, color: ch === ch.toUpperCase() ? 'w' : 'b', val: PlayStyle.PIECE_VAL[lower] || 0 };
                        file++;
                    }
                }
            }
            return map;
        },

        // Classify a move given the board map and whose turn it is
        classify: (move, map, myColor) => {
            const from   = move.slice(0, 2);
            const to     = move.slice(2, 4);
            const mover  = map[from];
            const target = map[to];
            const isCapture    = !!(target && target.color !== myColor);
            const isSacrifice  = isCapture && !!mover && !!target && mover.val > target.val;
            const isEqualTrade = isCapture && !!mover && !!target && mover.val === target.val;
            return {
                isCapture,
                isSacrifice,
                isEqualTrade,
                moverVal:  mover  ? mover.val  : 0,
                targetVal: target ? target.val : 0,
            };
        },

        // Main entry point — returns { move, best } or null (null = use default logic)
        pickMove: (bestMove, fen) => {
            const style = CFG.style;
            if (!style || style === 'balanced') return null;

            const best = State.candidates[1];
            if (!best) return null;

            const sideToMove = (fen || '').split(' ')[1] || 'w';
            const map = PlayStyle.boardMap(fen);

            // Build annotated candidate list
            const allC = [];
            for (let i = 1; i <= CFG.multiPV; i++) {
                const c = State.candidates[i];
                if (!c || !c.move) continue;
                const cl   = PlayStyle.classify(c.move, map, sideToMove);
                // centipawn loss vs best (always ≥ 0)
                const loss = (c.eval.type === 'cp' && best.eval.type === 'cp')
                    ? Math.max(0, (best.eval.val - c.eval.val) * 100)
                    : 0;
                allC.push({ ...c, rank: i, loss, ...cl });
            }
            if (!allC.length) return null;

            switch (style) {

                // ── AGGRESSIVE ─────────────────────────────────────────────────
                // Heavily weight safe captures of high-value pieces.
                // Accepts equal-value trades freely. Will take a small loss (≤80cp)
                // for an exciting capture. Sacrifices are considered if loss ≤ 80cp.
                case 'aggressive': {
                    const pool = allC.filter(c => c.loss <= 130);
                    if (!pool.length) return null;
                    const w = pool.map(c => {
                        if (c.isSacrifice && c.loss <= 80) return 2.5;
                        if (c.isCapture && !c.isSacrifice) return 4 + c.targetVal * 0.8;
                        return 1;
                    });
                    const picked = PlayStyle._wPick(pool, w);
                    log('AGGRESSIVE pick: ' + (picked && picked.move));
                    return picked;
                }

                // ── DEFENSIVE ──────────────────────────────────────────────────
                // Strongly prefers quiet non-capturing moves.
                // Never sacrifices. Accepts captures only when winning clear material.
                // Very consistent — essentially no randomisation beyond style weighting.
                case 'defensive': {
                    const pool = allC.filter(c => c.loss <= 45 && !c.isSacrifice);
                    if (!pool.length) return { move: bestMove, best: true };
                    const w = pool.map(c => {
                        if (!c.isCapture) return 4;                // strongly prefer quiet
                        if (c.targetVal > c.moverVal) return 2;    // winning capture — ok
                        if (c.isEqualTrade) return 0.8;            // equal trade — reluctant
                        return 0.3;                                // anything else — avoid
                    });
                    const picked = PlayStyle._wPick(pool, w);
                    log('DEFENSIVE pick: ' + (picked && picked.move));
                    return picked;
                }

                // ── BRILLIANT ──────────────────────────────────────────────────
                // Actively hunts for sacrifices every move.
                // Prefers giving up the highest-value piece possible within 200cp of best.
                // If no sacrifice is available, goes for the most forcing capture.
                // Falls back to best move only when position is completely forced/quiet.
                case 'brilliant': {
                    const sacrifices = allC.filter(c => c.isSacrifice && c.loss <= 200);
                    if (sacrifices.length) {
                        // Prefer bigger piece sacrificed, then smallest eval loss
                        sacrifices.sort((a, b) =>
                            b.moverVal !== a.moverVal ? b.moverVal - a.moverVal : a.loss - b.loss
                        );
                        const s = sacrifices[0];
                        log('BRILLIANT sacrifice: ' + s.move + ' (gives up ' + s.moverVal + ' for ' + s.targetVal + ')');
                        return { move: s.move, best: s.rank === 1 };
                    }
                    // No sac available — play the most valuable capture (forcing)
                    const captures = allC.filter(c => c.isCapture && c.loss <= 100);
                    if (captures.length) {
                        captures.sort((a, b) => b.targetVal - a.targetVal || a.loss - b.loss);
                        log('BRILLIANT best capture: ' + captures[0].move);
                        return { move: captures[0].move, best: captures[0].rank === 1 };
                    }
                    // No captures at all — play best move (positional position)
                    return { move: bestMove, best: true };
                }

                // ── ENDGAME SPECIALIST ─────────────────────────────────────────
                // In opening/middlegame: steers toward piece trades to reach a
                // favourable endgame. Prefers equal trades, then winning captures.
                // In endgame phase: bypasses ALL randomisation and always plays
                // the single engine-best move.
                case 'endgame': {
                    const phase = Human.phase(fen);
                    if (phase === 'end') {
                        log('ENDGAME specialist — endgame phase, playing best');
                        return { move: bestMove, best: true };
                    }
                    // Middlegame: prefer equal trades (simplify)
                    const equalTrades = allC.filter(c => c.isEqualTrade && c.loss <= 70);
                    if (equalTrades.length) {
                        equalTrades.sort((a, b) => b.targetVal - a.targetVal);
                        log('ENDGAME trade: ' + equalTrades[0].move + ' (trading ' + equalTrades[0].targetVal + ')');
                        return { move: equalTrades[0].move, best: equalTrades[0].rank === 1 };
                    }
                    // Accept winning captures (simplification)
                    const winCaps = allC.filter(c => c.isCapture && !c.isSacrifice && c.loss <= 55);
                    if (winCaps.length) {
                        winCaps.sort((a, b) => b.targetVal - a.targetVal);
                        log('ENDGAME winning cap: ' + winCaps[0].move);
                        return { move: winCaps[0].move, best: winCaps[0].rank === 1 };
                    }
                    return null; // let normal logic handle it
                }

                // ── POSITIONAL ─────────────────────────────────────────────────
                // Prefers quiet, strategic non-capturing moves (long-term thinking).
                // Avoids all tactical complications unless forced.
                // Weights quiet moves by engine rank (best quiet move gets most weight).
                case 'positional': {
                    const quietPool = allC.filter(c => !c.isCapture && c.loss <= 80);
                    if (quietPool.length) {
                        // Weight by inverse rank: rank 1 gets highest weight
                        const w = quietPool.map(c => 1 / c.rank);
                        const picked = PlayStyle._wPick(quietPool, w);
                        log('POSITIONAL quiet pick: ' + (picked && picked.move));
                        return picked;
                    }
                    // All moves are captures — play best (forced recapture etc.)
                    return { move: bestMove, best: true };
                }
            }

            return null;
        },

        // Weighted random selection from a candidate array
        _wPick: (candidates, weights) => {
            if (!candidates.length) return null;
            const total = weights.reduce((s, w) => s + w, 0);
            if (total <= 0) return { move: candidates[0].move, best: candidates[0].rank === 1 };
            let r = Math.random() * total;
            for (let i = 0; i < candidates.length; i++) {
                r -= weights[i];
                if (r <= 0) return { move: candidates[i].move, best: candidates[i].rank === 1 };
            }
            return { move: candidates[0].move, best: candidates[0].rank === 1 };
        },

        // Short description of the active style shown in the UI
        describe: (style) => {
            const d = {
                balanced:   'Normal humanised engine play',
                aggressive: 'Hunts captures & high-value pieces',
                defensive:  'Avoids captures, plays solid & safe',
                brilliant:  'Sacrifices material every chance it gets',
                endgame:    'Trades into endgame, then plays perfectly',
                positional: 'Quiet strategic moves, avoids tactics',
            };
            return d[style] || '';
        },
    };

    // ─── STREAM HIDE — FLOATING MINI HUD ────────────────────────────────────────
    const StreamHide = {
        active: false,
        _el:    null,
        _raf:   null,

        toggle: () => {
            StreamHide.active ? StreamHide.stop() : StreamHide.start();
        },

        start: () => {
            StreamHide._buildHUD();
            StreamHide.active = true;
            if (UI.panel) UI.panel.classList.add('ke-sh-hidden');
            document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = '0');
            StreamHide._loop();
            UI._refreshStreamBtn();
            log('Stream-Hide ON — drag the mini HUD outside your capture region');
        },

        stop: () => {
            StreamHide.active = false;
            if (StreamHide._raf) { cancelAnimationFrame(StreamHide._raf); StreamHide._raf = null; }
            if (StreamHide._el)  { StreamHide._el.remove(); StreamHide._el = null; }
            if (UI.panel) UI.panel.classList.remove('ke-sh-hidden');
            document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = '');
            UI._refreshStreamBtn();
            log('Stream-Hide OFF');
        },

        _loop: () => {
            if (!StreamHide.active || !StreamHide._el) return;
            StreamHide._update();
            StreamHide._raf = requestAnimationFrame(StreamHide._loop);
        },

        _buildHUD: () => {
            if (StreamHide._el) { StreamHide._el.remove(); StreamHide._el = null; }
            const el = document.createElement('div');
            el.id = 'ke-float-hud';
            Object.assign(el.style, {
                position:'fixed', top:'16px', right:'16px',
                width:'320px', background:'#0d0d10',
                border:'1px solid rgba(255,255,255,0.10)',
                borderRadius:'10px',
                fontFamily:'Inter,system-ui,sans-serif',
                boxShadow:'0 16px 48px rgba(0,0,0,0.8)',
                zIndex:'2147483647',
                userSelect:'none', webkitUserSelect:'none',
                touchAction:'none', overflow:'hidden', cursor:'grab',
            });
            el.innerHTML = [
                '<div id="ke-fhud-bar" style="padding:8px 10px 7px;background:rgba(255,255,255,0.03);border-bottom:1px solid rgba(255,255,255,0.06);display:flex;align-items:center;gap:8px;">',
                  '<span style="font-size:10px;font-weight:700;letter-spacing:0.09em;background:linear-gradient(120deg,#69f0ae,#4caf50);-webkit-background-clip:text;-webkit-text-fill-color:transparent;flex:1">K-EXPERT</span>',
                  '<span id="ke-fhud-mode-lbl" style="font-size:9px;font-weight:600;color:#555;letter-spacing:0.06em">STANDBY</span>',
                  '<span id="ke-fhud-dot" style="width:6px;height:6px;border-radius:50%;background:#444;flex-shrink:0;margin-left:4px"></span>',
                  '<span id="ke-fhud-close" style="margin-left:4px;width:18px;height:18px;border-radius:4px;background:rgba(255,255,255,0.06);display:flex;align-items:center;justify-content:center;font-size:11px;cursor:pointer;flex-shrink:0;color:#666;-webkit-text-fill-color:#666;line-height:18px;text-align:center">&#x2715;</span>',
                '</div>',
                '<div style="padding:10px 10px 7px;display:grid;grid-template-columns:1fr 1fr;gap:7px;">',
                  '<div style="background:#111115;border:1px solid rgba(255,255,255,0.06);border-radius:7px;padding:8px 10px;">',
                    '<div style="font-size:8px;color:#444;letter-spacing:0.09em;margin-bottom:4px">EVAL</div>',
                    '<div id="ke-fhud-eval" style="font-family:JetBrains Mono,Menlo,monospace;font-size:30px;font-weight:700;color:#444;line-height:1">&#x2014;</div>',
                  '</div>',
                  '<div style="background:#111115;border:1px solid rgba(255,255,255,0.06);border-radius:7px;padding:8px 10px;">',
                    '<div style="font-size:8px;color:#444;letter-spacing:0.09em;margin-bottom:4px">BEST MOVE</div>',
                    '<div id="ke-fhud-move" style="font-family:JetBrains Mono,Menlo,monospace;font-size:20px;font-weight:700;color:#444;line-height:1;padding:4px 0;border-radius:5px">&#x2014;</div>',
                  '</div>',
                '</div>',
                '<div id="ke-fhud-threat-row" style="display:none;margin:0 10px 7px;background:rgba(244,67,54,0.07);border:1px solid rgba(244,67,54,0.18);border-radius:6px;padding:6px 10px;font-family:JetBrains Mono,Menlo,monospace;font-size:12px;font-weight:700;color:#f44336">',
                  '<span style="font-family:Inter,system-ui,sans-serif;font-size:8px;opacity:0.7;letter-spacing:0.09em;font-weight:600;-webkit-text-fill-color:#f44336">OPP THREAT&nbsp;&nbsp;</span>',
                  '<span id="ke-fhud-threat"></span>',
                '</div>',
                '<div id="ke-fhud-elo-strip" style="margin:0 10px 7px;background:rgba(76,175,80,0.07);border:1px solid rgba(76,175,80,0.12);border-radius:6px;padding:6px 10px;font-size:9px;font-weight:600;color:#69f0ae;letter-spacing:0.04em">ENGINE STANDBY</div>',
                // Style strip — shown in HUD
                '<div id="ke-fhud-style-strip" style="margin:0 10px 8px;border-radius:6px;padding:5px 10px;font-size:9px;font-weight:700;letter-spacing:0.05em;border:1px solid rgba(255,255,255,0.06);color:#555;background:#111115">⚖️ BALANCED</div>',
                '<div style="padding:0 10px 9px;display:flex;gap:5px;">',
                  ...[['MOVES','ke-fhud-v1','0'],['BEST','ke-fhud-v2','0'],['CORR','ke-fhud-v3','&#x2014;'],['TC','ke-fhud-v4','&#x2014;']].map(([l,id,d]) =>
                    `<div style="flex:1;background:#111115;border:1px solid rgba(255,255,255,0.05);border-radius:5px;padding:6px 8px;"><div style="font-size:7px;color:#444;letter-spacing:0.08em;margin-bottom:2px">${l}</div><div id="${id}" style="font-family:JetBrains Mono,Menlo,monospace;font-size:15px;font-weight:700;color:#ccc">${d}</div></div>`
                  ),
                '</div>',
                '<div style="padding:5px 10px 6px;border-top:1px solid rgba(255,255,255,0.04);font-size:8px;color:#2a2a35;text-align:center">Drag to reposition &nbsp;&#183;&nbsp; &#x2715; to close</div>',
            ].join('');

            document.body.appendChild(el);
            StreamHide._el = el;

            // ── Drag ─────────────────────────────────────────────────────────
            let drag=false, ox=0, oy=0, sl=0, st=0;
            const ds = (cx,cy) => {
                drag=true; ox=cx; oy=cy;
                const r=el.getBoundingClientRect(); sl=r.left; st=r.top;
                el.style.cursor='grabbing'; el.style.right='auto';
                el.style.left=sl+'px'; el.style.top=st+'px';
            };
            const dm = (cx,cy) => { if(!drag)return; el.style.left=(sl+cx-ox)+'px'; el.style.top=(st+cy-oy)+'px'; };
            const de = () => { drag=false; el.style.cursor='grab'; };

            el.addEventListener('mousedown', e => { if(e.target.id==='ke-fhud-close')return; ds(e.clientX,e.clientY); });
            document.addEventListener('mousemove', e => dm(e.clientX,e.clientY));
            document.addEventListener('mouseup', de);
            el.addEventListener('touchstart', e => {
                if(e.target.id==='ke-fhud-close')return;
                const t=e.touches[0]; ds(t.clientX,t.clientY);
            }, {passive:true});
            document.addEventListener('touchmove', e => {
                if(!drag)return; e.preventDefault();
                const t=e.touches[0]; dm(t.clientX,t.clientY);
            }, {passive:false});
            document.addEventListener('touchend', de);
            el.querySelector('#ke-fhud-close').addEventListener('click', e => { e.stopPropagation(); StreamHide.stop(); });
        },

        _update: () => {
            const el = StreamHide._el;
            if (!el) return;

            // Eval
            const ev = State.currentEval;
            const evEl = el.querySelector('#ke-fhud-eval');
            if (evEl) {
                if (!ev) { evEl.textContent='—'; evEl.style.color='#444'; }
                else if (ev.type==='mate') { evEl.textContent='M'+Math.abs(ev.val); evEl.style.color=ev.val>0?'#4caf50':'#f44336'; }
                else { evEl.textContent=(ev.val>0?'+':'')+ev.val.toFixed(2); evEl.style.color=ev.val>0.4?'#4caf50':ev.val<-0.4?'#f44336':'#e0e0e0'; }
            }

            // Move
            const srcMv = UI.panel && UI.panel.querySelector('#ke-move');
            const mvEl  = el.querySelector('#ke-fhud-move');
            if (mvEl && srcMv) {
                const txt=srcMv.textContent||'—';
                const best=!srcMv.classList.contains('sub');
                const idle=txt==='—'||txt==='...';
                mvEl.textContent=txt;
                mvEl.style.color=idle?'#444':best?'#4caf50':'#ffc107';
                mvEl.style.background=idle?'transparent':best?'rgba(76,175,80,0.12)':'rgba(255,193,7,0.12)';
                mvEl.style.padding=idle?'4px 0':'4px 8px';
            }

            // Threat
            const tRow=el.querySelector('#ke-fhud-threat-row');
            const tEl=el.querySelector('#ke-fhud-threat');
            if (tRow&&tEl) {
                const show=CFG.showThreats&&State.opponentMove&&State.opponentMove!=='—';
                tRow.style.display=show?'block':'none';
                if(show) tEl.textContent=State.opponentMove;
            }

            // ELO / mode strip
            const strip=el.querySelector('#ke-fhud-elo-strip');
            const modeLbl=el.querySelector('#ke-fhud-mode-lbl');
            if (strip) {
                let txt,col,bg,bd;
                if (!CFG.active) { txt='ENGINE STANDBY'; col='#555'; bg='rgba(255,255,255,0.03)'; bd='rgba(255,255,255,0.06)'; }
                else if (CFG.eloMode) { txt='ELO '+CFG.targetElo+'  '+EloStrength.label(CFG.targetElo)+'  Sk'+EloStrength.toSkill(CFG.targetElo)+'  D'+EloStrength.toDepth(CFG.targetElo); col='#ce93d8'; bg='rgba(156,39,176,0.10)'; bd='rgba(156,39,176,0.22)'; }
                else { txt='FULL STRENGTH  D'+CFG.depth+'  '+(CFG.useBook?'Book ON':'Book OFF'); col='#69f0ae'; bg='rgba(76,175,80,0.07)'; bd='rgba(76,175,80,0.12)'; }
                strip.textContent=txt; strip.style.color=col; strip.style.background=bg; strip.style.borderColor=bd;
                if (modeLbl) { modeLbl.textContent=CFG.active?(CFG.eloMode?'ELO '+CFG.targetElo:'ACTIVE'):'STANDBY'; modeLbl.style.color=col; }
            }

            // Style strip (NEW)
            const styleStrip = el.querySelector('#ke-fhud-style-strip');
            if (styleStrip) {
                const sl = PlayStyle.LABELS[CFG.style] || PlayStyle.LABELS.balanced;
                styleStrip.textContent = sl.icon + ' ' + sl.label.toUpperCase();
                styleStrip.style.color = CFG.active ? sl.color : '#555';
                styleStrip.style.background = CFG.active ? 'rgba(0,0,0,0.3)' : '#111115';
                styleStrip.style.borderColor = CFG.active ? sl.color + '44' : 'rgba(255,255,255,0.06)';
            }

            // Dot
            const dot=el.querySelector('#ke-fhud-dot');
            if (dot) {
                const c=!CFG.active?'#444':CFG.eloMode?'#9c27b0':'#4caf50';
                dot.style.background=c; dot.style.boxShadow=CFG.active?'0 0 6px '+c:'none';
            }

            // Stats
            const corr=State.totalCount>0?Math.round(State.bestCount/State.totalCount*100)+'%':'—';
            [String(State.totalCount),String(State.bestCount),corr,CFG.timeControl.toUpperCase()].forEach((v,i)=>{
                const e=el.querySelector('#ke-fhud-v'+(i+1)); if(e) e.textContent=v;
            });
        },
    };

    // ─── BOARD / GAME ─────────────────────────────────────────────────────────────
    const Board = {
        el: () => {
            if (State.boardCache && State.boardCache.isConnected) return State.boardCache;
            State.boardCache = $(['wc-chess-board', 'chess-board']);
            return State.boardCache;
        },
        color: () => {
            try {
                const b = Board.el();
                return (b && (b.classList.contains('flipped') || b.getAttribute('flipped') === 'true')) ? 'b' : 'w';
            } catch (e) { return 'w'; }
        },
        fen: () => {
            try {
                const b = Board.el();
                if (!b) return null;
                if (b.game && b.game.getFEN) return b.game.getFEN();
                const rk = Object.keys(b).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternal'));
                if (rk) {
                    let cur = b[rk], d = 0;
                    while (cur && d++ < 150) {
                        if (cur.memoizedProps && cur.memoizedProps.game && cur.memoizedProps.game.fen) return cur.memoizedProps.game.fen;
                        if (typeof cur.memoizedProps === 'object' && cur.memoizedProps !== null && typeof cur.memoizedProps.fen === 'string') return cur.memoizedProps.fen;
                        cur = cur.return;
                    }
                }
                return null;
            } catch (e) { return null; }
        },
        myTurn: (fen) => fen && State.playerColor && fen.split(' ')[1] === State.playerColor,
        coords: (sq) => {
            try {
                const b = Board.el();
                if (!b) return null;
                const r = b.getBoundingClientRect();
                if (!r || !r.width) return null;
                const sqSz = r.width / 8;
                const flip = State.playerColor === 'b';
                const f = sq.charCodeAt(0) - 97;
                const rk = parseInt(sq[1]) - 1;
                return {
                    x: r.left + (flip ? 7 - f : f) * sqSz + sqSz / 2,
                    y: r.top + (flip ? rk : 7 - rk) * sqSz + sqSz / 2
                };
            } catch (e) { return null; }
        },
    };

    // ─── OPENING BOOK ─────────────────────────────────────────────────────────────
    const Book = {
        fetch: (fen) => {
            if (!CFG.useBook) return Promise.resolve(null);
            if (CFG.eloMode && CFG.targetElo < 1200) return Promise.resolve(null);
            return new Promise(res => {
                const t = setTimeout(() => res(null), 2000);
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://explorer.lichess.ovh/masters?fen=' + encodeURIComponent(fen),
                    onload: r => {
                        clearTimeout(t);
                        try {
                            const d = JSON.parse(r.responseText);
                            if (!d.moves || !d.moves.length) return res(null);
                            const top = d.moves.slice(0, 3);
                            const total = top.reduce((s, m) => s + m.white + m.draw + m.black, 0);
                            let x = Math.random() * total;
                            for (const m of top) {
                                x -= m.white + m.draw + m.black;
                                if (x <= 0) return res(m.uci);
                            }
                            res(top[0].uci);
                        } catch (e) { res(null); }
                    },
                    onerror: () => { clearTimeout(t); res(null); }
                });
            });
        },
    };

    // ─── STOCKFISH ───────────────────────────────────────────────────────────────
    const SF = {
        worker: null,
        ready: false,
        _analyzing: false,
        _pendingFen: null,
        _resolveInit: null,

        init: () => new Promise(resolve => {
            if (SF.ready) return resolve(true);
            log('Loading Stockfish...');
            UI.setStatus('loading');
            GM_xmlhttpRequest({
                method: 'GET',
                url: 'https://unpkg.com/[email protected]/stockfish.js',
                onload: res => {
                    try {
                        const blob = new Blob([res.responseText], { type: 'application/javascript' });
                        SF.worker = new Worker(URL.createObjectURL(blob));
                        SF.worker.onerror = err => { log('SF worker error: ' + (err && err.message), 'error'); UI.setStatus('error'); };
                        SF.worker.onmessage = e => SF._msg(e.data);
                        SF._resolveInit = resolve;
                        SF.worker.postMessage('uci');
                    } catch (e) { log('SF init failed: ' + e, 'error'); UI.setStatus('error'); resolve(false); }
                },
                onerror: () => { log('SF download failed', 'error'); UI.setStatus('error'); resolve(false); }
            });
        }),

        _msg: (msg) => {
            if (msg === 'uciok') {
                SF.worker.postMessage('setoption name MultiPV value ' + CFG.multiPV);
                SF.worker.postMessage('isready');
                return;
            }
            if (msg === 'readyok') {
                SF.ready = true;
                log('Stockfish ready');
                UI.setStatus('ready');
                if (CFG.eloMode) EloStrength.apply(CFG.targetElo);
                if (SF._resolveInit) { SF._resolveInit(true); SF._resolveInit = null; }
                if (CFG.active && !_paused) { State.lastFen = null; Loop.tick(); }
                return;
            }
            if (msg.startsWith('info') && msg.includes(' score ') && SF._analyzing) {
                SF._parseInfo(msg);
                return;
            }
            if (msg.startsWith('bestmove') && SF._analyzing) {
                SF._analyzing = false;
                const parts = msg.split(' ');
                const move = parts[1];
                if (move && move !== '(none)') Engine.onBestMove(move);
                if (SF._pendingFen) {
                    const fen = SF._pendingFen;
                    SF._pendingFen = null;
                    SF._go(fen);
                }
            }
        },

        _parseInfo: (msg) => {
            try {
                const score = msg.match(/score (cp|mate) (-?\d+)/);
                const pv    = msg.match(/multipv (\d+)/);
                const mv    = msg.match(/ pv ([a-h][1-8][a-h][1-8]\w*)/);
                const dep   = msg.match(/depth (\d+)/);
                if (!score || !pv || !mv) return;
                const mpv  = parseInt(pv[1]);
                const type = score[1];
                let val    = parseInt(score[2]);
                if (type === 'cp') val = val / 100;
                State.candidates[mpv] = { move: mv[1], eval: { type: type, val: val }, depth: parseInt((dep && dep[1]) || 0) };
                if (mpv === 1) {
                    State.currentEval = { type: type, val: val };
                    const pvStr = msg.split(' pv ')[1];
                    if (pvStr) { const pvMoves = pvStr.split(' '); State.opponentMove = pvMoves[1] || null; }
                    UI.updateEval(type, val);
                }
            } catch (e) { /* ignore */ }
        },

        _go: (fen) => {
            if (!SF.ready || !SF.worker) return;
            SF._analyzing = true;
            State.candidates = {};
            State.currentEval = null;
            SF.worker.postMessage('stop');
            SF.worker.postMessage('position fen ' + fen);
            SF.worker.postMessage('setoption name MultiPV value ' + CFG.multiPV);
            if (CFG.eloMode) {
                const skill = EloStrength.toSkill(CFG.targetElo);
                const uciElo = Math.max(1320, Math.min(3190, CFG.targetElo));
                SF.worker.postMessage('setoption name UCI_LimitStrength value true');
                SF.worker.postMessage('setoption name UCI_Elo value ' + uciElo);
                SF.worker.postMessage('setoption name Skill Level value ' + skill);
                SF.worker.postMessage('go depth ' + EloStrength.toDepth(CFG.targetElo));
            } else {
                SF.worker.postMessage('setoption name UCI_LimitStrength value false');
                SF.worker.postMessage('setoption name Skill Level value 20');
                SF.worker.postMessage('go depth ' + CFG.depth);
            }
        },

        analyze: (fen) => {
            if (!SF.ready || !SF.worker) return;
            if (SF._analyzing) {
                SF._pendingFen = fen;
                SF.worker.postMessage('stop');
            } else {
                SF._go(fen);
            }
        },

        stop: () => {
            SF._pendingFen = null;
            SF._analyzing = false;
            try { SF.worker && SF.worker.postMessage('stop'); } catch (e) { /* ignore */ }
        },
    };

    // ─── HUMANIZATION ────────────────────────────────────────────────────────────
    const Human = {
        phase: (fen) => {
            if (!fen) return 'mid';
            const mn = parseInt(fen.split(' ')[5] || 1);
            const pieces = (fen.split(' ')[0].match(/[rnbqRNBQ]/g) || []).length;
            if (mn <= 10) return 'open';
            if (pieces <= 6) return 'end';
            return 'mid';
        },
        pickMove: (bestMove) => {
            if (!CFG.humanization) return { move: bestMove, best: true };

            // ── STYLE OVERRIDE (runs before normal humanization) ───────────────
            // PlayStyle.pickMove() returns null when style='balanced' or has no
            // style-appropriate candidate — in that case we fall through.
            if (CFG.style && CFG.style !== 'balanced') {
                const stylePick = PlayStyle.pickMove(bestMove, State.lastFen || '');
                if (stylePick) return stylePick;
            }

            // ── DEFAULT HUMANIZATION (unchanged) ──────────────────────────────
            const best = State.candidates[1];
            if (!best) return { move: bestMove, best: true };
            if (State.perfectStreak >= 4) { const s = Human._subopt(); if (s) return s; }
            if (State.sloppyStreak >= 3) return { move: bestMove, best: true };
            let rate;
            if (CFG.eloMode) {
                rate = EloStrength.toSuboptimalRate(CFG.targetElo);
            } else {
                rate = CFG.suboptimalRate;
                if (State.totalCount >= 6) {
                    const corr = State.bestCount / State.totalCount;
                    if (corr > CFG.correlation + 0.08) rate += 0.15;
                    else if (corr < CFG.correlation - 0.12) rate *= 0.2;
                }
                const ev = (State.currentEval && State.currentEval.val) || 0;
                if (ev > 2)  rate += 0.12;
                if (ev > 4)  rate += 0.15;
                if (ev > 6)  rate += 0.20;
                if (ev < -1) rate *= 0.3;
                rate = Math.max(0.05, Math.min(0.65, rate));
            }
            if (Math.random() < rate) { const s = Human._subopt(); if (s) return s; }
            return { move: bestMove, best: true };
        },
        _subopt: () => {
            const best = State.candidates[1];
            if (!best || Object.keys(State.candidates).length < 2) return null;
            const defaultMax = ({ open: 50, mid: 100, end: 60 })[Human.phase(State.lastFen)] || 80;
            const maxLoss = CFG.eloMode ? EloStrength.toMaxLoss(CFG.targetElo) : defaultMax;
            const alts = [];
            for (let i = 2; i <= CFG.multiPV; i++) {
                const c = State.candidates[i];
                if (!c || !c.eval) continue;
                let loss;
                if (best.eval.type === 'mate') { loss = 999; }
                else if (c.eval.type === 'mate' && c.eval.val > 0) { loss = 0; }
                else { loss = (best.eval.val - c.eval.val) * 100; }
                if (loss >= 0 && loss <= maxLoss) alts.push({ move: c.move, loss: loss });
            }
            if (!alts.length) return null;
            const w = alts.map(a => 1 / (1 + a.loss / 25));
            const tot = w.reduce((s, x) => s + x, 0);
            let r = Math.random() * tot;
            for (let i = 0; i < alts.length; i++) { r -= w[i]; if (r <= 0) return { move: alts[i].move, best: false }; }
            return { move: alts[0].move, best: false };
        },
        track: (best) => {
            State.totalCount++;
            if (best) { State.bestCount++; State.perfectStreak++; State.sloppyStreak = 0; }
            else { State.perfectStreak = 0; State.sloppyStreak++; }
        },
        delay: () => {
            if (CFG.eloMode) {
                const t = EloStrength.toTiming(CFG.targetElo, CFG.eloTc);
                return humanMs(t.min, t.max);
            }
            const tc = CFG.timing[CFG.timeControl] || CFG.timing.blitz;
            if (tc.depth && CFG.depth !== tc.depth) CFG.depth = tc.depth;
            return humanMs(tc.min, tc.max);
        },
    };

    // ─── ENGINE COORDINATOR ──────────────────────────────────────────────────────
    const Engine = {
        onBestMove: async (bestMove, isBook) => {
            if (_paused || !CFG.active) return;
            State.moveCount++;
            const picked = isBook ? { move: bestMove, best: true } : Human.pickMove(bestMove);
            Human.track(picked.best);
            if (!StreamHide.active) UI.drawArrows(bestMove, picked.move);
            UI.updateMove(picked.move, picked.best);
            if (CFG.autoPlay && Board.myTurn(State.lastFen)) {
                const delay = Human.delay();
                log('Auto-play in ' + Math.round(delay) + 'ms');
                await sleep(delay);
                const liveFen = Board.fen();
                if (!_paused && CFG.active && liveFen && Board.myTurn(liveFen)) {
                    await Mover.exec(picked.move);
                }
            }
        },
    };

    // ─── MAIN LOOP ───────────────────────────────────────────────────────────────
    const Loop = {
        _lastAnalyzedFen: null,
        tick: () => {
            if (_paused || !CFG.active) return;
            try {
                const fen = Board.fen();
                if (!fen) return;
                State.playerColor = Board.color();
                const isStart = fen.startsWith('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR');
                if (isStart && State.moveCount > 2) {
                    State.moveCount = 0; State.bestCount = 0; State.totalCount = 0;
                    State.perfectStreak = 0; State.sloppyStreak = 0;
                    Loop._lastAnalyzedFen = null;
                }
                if (fen === State.lastFen) return;
                State.lastFen = fen;
                if (!StreamHide.active) UI.clearArrows();
                UI.updateMove('...', true);
                if ($(['.game-over-modal-container', '.modal-game-over-component', '[data-cy="game-over-modal"]'])) return;
                if (Board.myTurn(fen) && fen !== Loop._lastAnalyzedFen) {
                    Loop._lastAnalyzedFen = fen;
                    if (CFG.useBook) {
                        Book.fetch(fen).then(bm => {
                            if (!CFG.active || _paused || fen !== State.lastFen) return;
                            if (bm) { log('Book: ' + bm); Engine.onBestMove(bm, true); }
                            else SF.analyze(fen);
                        });
                    } else {
                        SF.analyze(fen);
                    }
                }
            } catch (e) { log('loop error: ' + e.message, 'warn'); }
        },
        start: () => {
            const target = $(['vertical-move-list', 'wc-move-list', '.move-list-component']) || document.body;
            new MutationObserver(() => {
                if (!_paused && CFG.active) requestAnimationFrame(Loop.tick);
            }).observe(target, { childList: true, subtree: true, characterData: true });
            setInterval(() => { if (!_paused && CFG.active) Loop.tick(); }, 600);
        },
    };

    // ─── MOVE EXECUTOR ───────────────────────────────────────────────────────────
    const Mover = {
        exec: async (move) => {
            try {
                const liveFen = Board.fen();
                if (!liveFen || !Board.myTurn(liveFen)) return;
                const from = move.slice(0, 2);
                const to   = move.slice(2, 4);
                const promo = move[4] || 'q';
                await Mover.drag(from, to);
                if ((to[1] === '8' || to[1] === '1') && Mover.isPawn(from)) await Mover.promo(promo);
            } catch (e) { log('exec error: ' + e.message, 'warn'); }
        },
        isPawn: (sq) => {
            try {
                const b = Board.el();
                if (!b) return false;
                const coord = (sq.charCodeAt(0) - 96) + '' + sq[1];
                const f = b.querySelector('.piece.square-' + coord);
                if (f) return f.className.includes('wp') || f.className.includes('bp');
                return sq[1] === '2' || sq[1] === '7';
            } catch (e) { return false; }
        },
        drag: async (from, to) => {
            const p1 = Board.coords(from);
            const p2 = Board.coords(to);
            if (!p1 || !p2) return;
            const b = Board.el();
            const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };
            const pe = (t, x, y) => new PointerEvent(t, Object.assign({}, opts, { clientX: x, clientY: y }));
            const me = (t, x, y) => new MouseEvent(t, Object.assign({}, opts, { clientX: x, clientY: y }));
            const src = document.elementFromPoint(p1.x, p1.y) || b;
            src.dispatchEvent(pe('pointerdown', p1.x, p1.y));
            src.dispatchEvent(me('mousedown',   p1.x, p1.y));
            await sleep(rnd(25, 65));
            const steps = 10 + Math.round(rnd(0, 6));
            const cpx = (p1.x + p2.x) / 2 + gauss(0, 18);
            const cpy = (p1.y + p2.y) / 2 + gauss(0, 18);
            for (let i = 1; i <= steps; i++) {
                const t = i / steps;
                const x = (1-t)*(1-t)*p1.x + 2*(1-t)*t*cpx + t*t*p2.x + gauss(0, 1.2);
                const y = (1-t)*(1-t)*p1.y + 2*(1-t)*t*cpy + t*t*p2.y + gauss(0, 1.2);
                document.dispatchEvent(pe('pointermove', x, y));
                document.dispatchEvent(me('mousemove',   x, y));
                if (Math.random() < 0.35) await sleep(rnd(2, 8));
            }
            const dst = document.elementFromPoint(p2.x, p2.y) || b;
            dst.dispatchEvent(pe('pointerup', p2.x, p2.y));
            dst.dispatchEvent(me('mouseup',   p2.x, p2.y));
            dst.dispatchEvent(pe('click',     p2.x, p2.y));
        },
        promo: async (piece) => {
            piece = piece || 'q';
            await sleep(200);
            const idx = { q: 0, r: 1, b: 2, n: 3 }[piece] || 0;
            const sels = ['.promotion-piece[data-piece="' + piece + '"]','.promotion-window .promotion-piece:nth-child(' + (idx+1) + ')','.promotion-piece'];
            let el = null;
            for (let i = 0; i < 15 && !el; i++) {
                await sleep(80);
                for (const s of sels) { try { el = document.querySelector(s); } catch(e){} if (el) break; }
            }
            if (el) { el.click(); log('Promoted to ' + piece); }
        },
    };

    // ─── UI ──────────────────────────────────────────────────────────────────────
    const UI = {
        panel: null,
        _stealth: false,

        init: () => { UI._css(); UI._build(); },

        _css: () => {
            GM_addStyle(`
                @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');

                .ke-panel {
                    position: fixed; top: 60px; left: 60px; z-index: 99999;
                    width: 272px;
                    background: #0d0d10;
                    border: 1px solid rgba(255,255,255,0.07);
                    border-radius: 12px;
                    font-family: 'Inter', sans-serif;
                    box-shadow: 0 24px 64px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.03);
                    color: #e0e0e0;
                    touch-action: none; user-select: none;
                    transition: opacity 0.2s;
                }
                .ke-panel.stealth        { opacity: 0 !important; pointer-events: none; }
                .ke-panel.ke-sh-hidden   { opacity: 0 !important; pointer-events: none; }

                .ke-hdr {
                    padding: 13px 14px 11px;
                    display: flex; align-items: center; gap: 9px;
                    cursor: grab; border-bottom: 1px solid rgba(255,255,255,0.06);
                    background: linear-gradient(180deg,rgba(255,255,255,0.025) 0%,transparent 100%);
                    border-radius: 12px 12px 0 0;
                }
                .ke-hdr:active { cursor: grabbing; }
                .ke-logo {
                    font-weight: 800; font-size: 12.5px; letter-spacing: 0.1em; flex: 1;
                    background: linear-gradient(120deg,#69f0ae,#4caf50); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
                }
                .ke-logo em { -webkit-text-fill-color: rgba(255,255,255,0.35); font-style: normal; font-weight: 400; }
                .ke-dot { width: 6px; height: 6px; border-radius: 50%; background: #333; transition: background 0.3s, box-shadow 0.3s; flex-shrink: 0; }
                .ke-dot.loading { background: #ffc107; animation: ke-pulse 0.7s ease-in-out infinite; }
                .ke-dot.ready   { background: #444; }
                .ke-dot.active  { background: #4caf50; box-shadow: 0 0 7px #4caf50; animation: ke-pulse 1.8s ease-in-out infinite; }
                .ke-dot.elo     { background: #9c27b0; box-shadow: 0 0 7px #9c27b0; animation: ke-pulse 1.8s ease-in-out infinite; }
                .ke-dot.stream  { background: #2196f3; box-shadow: 0 0 7px #2196f3; animation: ke-pulse 0.9s ease-in-out infinite; }
                .ke-dot.error   { background: #f44336; }
                .ke-colbtn {
                    width: 22px; height: 22px; border-radius: 5px; background: rgba(255,255,255,0.05);
                    border: none; color: #555; cursor: pointer; font-size: 13px;
                    display: flex; align-items: center; justify-content: center; transition: 0.15s; flex-shrink: 0; line-height: 1;
                }
                .ke-colbtn:hover { background: rgba(255,255,255,0.1); color: #ccc; }
                .ke-body { overflow: hidden; }
                .ke-body.col { max-height: 0 !important; overflow: hidden; }

                .ke-master {
                    margin: 12px 12px 10px; padding: 11px 13px; border-radius: 8px;
                    border: 1px solid rgba(255,255,255,0.07); background: #141418;
                    cursor: pointer; display: flex; align-items: center; gap: 10px;
                    transition: border-color 0.2s, box-shadow 0.2s; position: relative; overflow: hidden;
                }
                .ke-master-bg { position: absolute; inset: 0; background: linear-gradient(120deg,#2e7d32,#43a047); opacity: 0; transition: opacity 0.2s; pointer-events: none; }
                .ke-master.on .ke-master-bg { opacity: 1; }
                .ke-master.on { border-color: #4caf50; box-shadow: 0 0 18px rgba(76,175,80,0.28); }
                .ke-master:hover { border-color: rgba(255,255,255,0.13); }
                .ke-mic { width: 30px; height: 30px; border-radius: 50%; background: rgba(255,255,255,0.07); display: flex; align-items: center; justify-content: center; font-size: 13px; position: relative; z-index: 1; flex-shrink: 0; }
                .ke-master.on .ke-mic { background: rgba(255,255,255,0.14); }
                .ke-mtxt { position: relative; z-index: 1; }
                .ke-mtitle { font-size: 11.5px; font-weight: 700; letter-spacing: 0.07em; color: #555; transition: color 0.2s; }
                .ke-master.on .ke-mtitle { color: #fff; }
                .ke-msub { font-size: 10px; color: #444; margin-top: 1px; transition: color 0.2s; }
                .ke-master.on .ke-msub { color: rgba(255,255,255,0.65); }

                .ke-eval-row { margin: 0 12px 10px; background: #111115; border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 11px 13px; display: flex; align-items: center; justify-content: space-between; }
                .ke-eval { font-family: 'JetBrains Mono',monospace; font-size: 28px; font-weight: 600; color: #333; transition: color 0.2s; line-height: 1; }
                .ke-eval.pos { color: #4caf50; } .ke-eval.neg { color: #f44336; } .ke-eval.neu { color: #e0e0e0; }
                .ke-movebox { text-align: right; }
                .ke-movelbl { font-size: 9px; color: #444; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 4px; }
                .ke-move { font-family: 'JetBrains Mono',monospace; font-size: 15px; font-weight: 600; color: #4caf50; background: rgba(76,175,80,0.1); padding: 3px 8px; border-radius: 5px; letter-spacing: 0.05em; transition: color 0.15s, background 0.15s; }
                .ke-move.sub  { color: #ffc107; background: rgba(255,193,7,0.1); }
                .ke-move.idle { color: #333; background: transparent; }

                .ke-tabbar { display: flex; margin: 0 12px 10px; background: #111115; padding: 3px; border-radius: 7px; border: 1px solid rgba(255,255,255,0.05); gap: 3px; }
                .ke-tab { flex: 1; padding: 6px 0; font-size: 9.5px; font-weight: 600; letter-spacing: 0.07em; color: #444; cursor: pointer; border-radius: 5px; text-align: center; transition: 0.15s; }
                .ke-tab:hover { color: #999; }
                .ke-tab.act { background: #1a1a20; color: #e0e0e0; box-shadow: 0 1px 4px rgba(0,0,0,0.5); }
                .ke-page { display: none; padding: 0 12px 12px; }
                .ke-page.act { display: block; }
                .ke-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
                .ke-rlbl { font-size: 11px; color: #888; }

                .ke-sw { width: 32px; height: 17px; border-radius: 9px; background: #1e1e24; border: 1px solid rgba(255,255,255,0.07); cursor: pointer; position: relative; transition: 0.18s; flex-shrink: 0; }
                .ke-sw.on { background: #4caf50; border-color: #4caf50; }
                .ke-sw::after { content:''; position: absolute; top: 2px; left: 2px; width: 11px; height: 11px; border-radius: 50%; background: #fff; transition: transform 0.18s; box-shadow: 0 1px 3px rgba(0,0,0,0.4); }
                .ke-sw.on::after { transform: translateX(15px); }
                .ke-sw.elo-sw.on    { background: #9c27b0; border-color: #9c27b0; }

                .ke-tc-row { display: flex; gap: 5px; margin-bottom: 10px; }
                .ke-tc { flex: 1; padding: 5px 0; font-size: 9.5px; font-weight: 600; border-radius: 5px; cursor: pointer; text-align: center; background: #141418; border: 1px solid rgba(255,255,255,0.06); color: #444; transition: 0.15s; }
                .ke-tc:hover { color: #ccc; border-color: rgba(255,255,255,0.13); }
                .ke-tc.act { color: #111; font-weight: 700; border-color: transparent; }
                .ke-tc[data-tc=bullet].act { background: #ef5350; }
                .ke-tc[data-tc=blitz].act  { background: #ffc107; }
                .ke-tc[data-tc=rapid].act  { background: #4caf50; }

                .ke-slwrap { margin-bottom: 11px; }
                .ke-slhdr { display: flex; justify-content: space-between; margin-bottom: 4px; }
                .ke-slhdr span:first-child { font-size: 10.5px; color: #777; }
                .ke-slhdr span:last-child  { font-size: 10px; color: #4caf50; font-family: 'JetBrains Mono',monospace; }
                .ke-sl { -webkit-appearance: none; width: 100%; height: 3px; border-radius: 2px; outline: none; cursor: pointer; background: linear-gradient(90deg,#4caf50 var(--v,50%),#1e1e24 var(--v,50%)); }
                .ke-sl::-webkit-slider-thumb { -webkit-appearance: none; width: 13px; height: 13px; background: #fff; border-radius: 50%; cursor: pointer; box-shadow: 0 0 0 3px rgba(76,175,80,0.25), 0 2px 5px rgba(0,0,0,0.4); }

                /* ── Stream-Hide button ───────────────────────────────────────── */
                .ke-stream-btn {
                    margin: 0 12px 10px; padding: 9px 13px; border-radius: 8px;
                    border: 1px solid rgba(255,255,255,0.07); background: #141418;
                    cursor: pointer; display: flex; align-items: center; gap: 9px;
                    transition: border-color 0.2s, box-shadow 0.2s; position: relative; overflow: hidden;
                }
                .ke-stream-btn-bg { position: absolute; inset: 0; background: linear-gradient(120deg,#0d47a1,#1565c0); opacity: 0; transition: opacity 0.2s; pointer-events: none; }
                .ke-stream-btn.on .ke-stream-btn-bg { opacity: 1; }
                .ke-stream-btn.on { border-color: #2196f3; box-shadow: 0 0 16px rgba(33,150,243,0.3); }
                .ke-stream-btn:hover { border-color: rgba(255,255,255,0.14); }
                .ke-stream-icon { width: 28px; height: 28px; border-radius: 50%; background: rgba(255,255,255,0.07); display: flex; align-items: center; justify-content: center; font-size: 13px; position: relative; z-index: 1; flex-shrink: 0; }
                .ke-stream-btn.on .ke-stream-icon { background: rgba(255,255,255,0.14); }
                .ke-stream-txt { position: relative; z-index: 1; }
                .ke-stream-title { font-size: 11px; font-weight: 700; letter-spacing: 0.07em; color: #555; transition: color 0.2s; }
                .ke-stream-btn.on .ke-stream-title { color: #fff; }
                .ke-stream-sub { font-size: 9.5px; color: #444; margin-top: 1px; transition: color 0.2s; }
                .ke-stream-btn.on .ke-stream-sub { color: rgba(255,255,255,0.6); }
                .ke-stream-badge {
                    margin-left: auto; position: relative; z-index: 1;
                    font-size: 8px; font-weight: 700; letter-spacing: 0.06em;
                    background: rgba(33,150,243,0.2); color: #64b5f6;
                    padding: 2px 6px; border-radius: 3px; border: 1px solid rgba(33,150,243,0.3);
                }
                .ke-stream-btn.on .ke-stream-badge { background: rgba(255,255,255,0.15); color: #fff; border-color: rgba(255,255,255,0.2); }

                /* ── ELO block ───────────────────────────────────────────────── */
                .ke-elo-block { background: #0f0f14; border: 1px solid rgba(255,255,255,0.06); border-radius: 8px; padding: 10px 11px 11px; margin-bottom: 10px; transition: border-color 0.2s; }
                .ke-elo-block.elo-on { border-color: rgba(156,39,176,0.5); box-shadow: 0 0 14px rgba(156,39,176,0.15); }
                .ke-elo-hdr { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
                .ke-elo-title { font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; color: #777; }
                .ke-elo-block.elo-on .ke-elo-title { color: #ce93d8; }
                .ke-elo-input-row { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
                .ke-elo-input { flex: 1; background: #1a1a22; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; color: #e0e0e0; font-family: 'JetBrains Mono',monospace; font-size: 18px; font-weight: 600; text-align: center; padding: 6px 8px; outline: none; transition: border-color 0.15s; -moz-appearance: textfield; }
                .ke-elo-input::-webkit-inner-spin-button,.ke-elo-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
                .ke-elo-input:focus { border-color: #9c27b0; }
                .ke-elo-block.elo-on .ke-elo-input { border-color: rgba(156,39,176,0.4); }
                .ke-elo-badge { font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em; background: rgba(156,39,176,0.15); color: #ce93d8; padding: 3px 7px; border-radius: 4px; white-space: nowrap; min-width: 70px; text-align: center; }
                .ke-elo-bar-wrap { position: relative; height: 4px; border-radius: 2px; background: #1e1e24; overflow: hidden; }
                .ke-elo-bar { height: 100%; border-radius: 2px; background: linear-gradient(90deg,#4caf50,#ffc107,#f44336,#9c27b0); background-size: 400% 100%; transition: width 0.3s; }
                .ke-elo-ticks { display: flex; justify-content: space-between; margin-top: 4px; font-size: 8px; color: #333; }
                .ke-elo-presets { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; }
                .ke-elo-preset { flex: 1; min-width: 0; padding: 4px 0; font-size: 9px; font-weight: 600; border-radius: 4px; cursor: pointer; text-align: center; border: 1px solid rgba(255,255,255,0.06); color: #444; background: #141418; transition: 0.15s; white-space: nowrap; overflow: hidden; }
                .ke-elo-preset:hover { color: #ccc; border-color: rgba(255,255,255,0.15); }
                .ke-elo-preset.act { background: rgba(156,39,176,0.25); color: #ce93d8; border-color: rgba(156,39,176,0.5); }

                /* ── Playing Style selector ──────────────────────────────────── */
                .ke-style-section-lbl {
                    font-size: 9.5px; color: #444; letter-spacing: 0.09em; text-transform: uppercase;
                    margin-bottom: 6px; margin-top: 2px;
                }
                .ke-style-grid {
                    display: grid; grid-template-columns: 1fr 1fr 1fr;
                    gap: 4px; margin-bottom: 10px;
                }
                .ke-style-btn {
                    padding: 7px 4px 6px; border-radius: 6px; cursor: pointer; text-align: center;
                    border: 1px solid rgba(255,255,255,0.07); background: #141418;
                    transition: border-color 0.15s, background 0.15s, box-shadow 0.15s;
                    display: flex; flex-direction: column; align-items: center; gap: 3px;
                }
                .ke-style-btn:hover { border-color: rgba(255,255,255,0.18); background: #1a1a20; }
                .ke-style-btn .ke-si  { font-size: 14px; line-height: 1; }
                .ke-style-btn .ke-sl  { font-size: 8px; font-weight: 700; letter-spacing: 0.06em; color: #444; transition: color 0.15s; }
                /* Active states per style */
                .ke-style-btn.act[data-style=balanced]   { background: rgba(158,158,158,0.12); border-color: rgba(158,158,158,0.45); box-shadow: 0 0 10px rgba(158,158,158,0.12); }
                .ke-style-btn.act[data-style=balanced]   .ke-sl { color: #9e9e9e; }
                .ke-style-btn.act[data-style=aggressive] { background: rgba(244,67,54,0.12);   border-color: rgba(244,67,54,0.50);   box-shadow: 0 0 10px rgba(244,67,54,0.15); }
                .ke-style-btn.act[data-style=aggressive] .ke-sl { color: #f44336; }
                .ke-style-btn.act[data-style=defensive]  { background: rgba(33,150,243,0.12);  border-color: rgba(33,150,243,0.50);  box-shadow: 0 0 10px rgba(33,150,243,0.15); }
                .ke-style-btn.act[data-style=defensive]  .ke-sl { color: #64b5f6; }
                .ke-style-btn.act[data-style=brilliant]  { background: rgba(156,39,176,0.14);  border-color: rgba(156,39,176,0.55);  box-shadow: 0 0 10px rgba(156,39,176,0.18); }
                .ke-style-btn.act[data-style=brilliant]  .ke-sl { color: #ce93d8; }
                .ke-style-btn.act[data-style=endgame]    { background: rgba(255,193,7,0.12);   border-color: rgba(255,193,7,0.50);   box-shadow: 0 0 10px rgba(255,193,7,0.15); }
                .ke-style-btn.act[data-style=endgame]    .ke-sl { color: #ffc107; }
                .ke-style-btn.act[data-style=positional] { background: rgba(0,188,212,0.12);   border-color: rgba(0,188,212,0.50);   box-shadow: 0 0 10px rgba(0,188,212,0.15); }
                .ke-style-btn.act[data-style=positional] .ke-sl { color: #00bcd4; }
                /* Description bar below grid */
                .ke-style-desc {
                    font-size: 9px; color: #555; margin-bottom: 10px;
                    padding: 5px 8px; background: #111115;
                    border: 1px solid rgba(255,255,255,0.05); border-radius: 5px;
                    min-height: 26px; line-height: 1.4;
                    transition: color 0.2s;
                }

                .ke-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 2px; }
                .ke-stat { background: #111115; border: 1px solid rgba(255,255,255,0.05); border-radius: 7px; padding: 8px 10px; }
                .ke-stat-l { font-size: 9px; color: #444; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 3px; }
                .ke-stat-v { font-family: 'JetBrains Mono',monospace; font-size: 17px; font-weight: 600; color: #ddd; }

                .ke-footer { padding: 7px 12px; border-top: 1px solid rgba(255,255,255,0.05); display: flex; gap: 8px; font-size: 9.5px; color: #444; flex-wrap: wrap; }
                .ke-k { background: #141418; border: 1px solid rgba(255,255,255,0.09); border-radius: 3px; padding: 1px 5px; font-family: 'JetBrains Mono',monospace; font-size: 9px; }

                .ke-overlay { position: fixed; top: 0; left: 0; pointer-events: none; z-index: 9998; transition: opacity 0.2s; }

                @keyframes ke-pulse { 0%,100%{opacity:1} 50%{opacity:0.35} }
            `);
        },

        _build: () => {
            if (UI.panel) return;
            const p = document.createElement('div');
            p.className = 'ke-panel';
            p.innerHTML = `
                <div class="ke-hdr">
                    <div class="ke-logo">K-EXPERT<em>.MENU</em></div>
                    <div class="ke-dot ready" id="ke-dot"></div>
                    <button class="ke-colbtn" id="ke-col">−</button>
                </div>
                <div class="ke-body" id="ke-body">

                    <div class="ke-master" id="ke-master">
                        <div class="ke-master-bg"></div>
                        <div class="ke-mic">⚡</div>
                        <div class="ke-mtxt">
                            <div class="ke-mtitle" id="ke-mtitle">CLICK TO ACTIVATE</div>
                            <div class="ke-msub"   id="ke-msub">Engine loading...</div>
                        </div>
                    </div>

                    <div class="ke-stream-btn" id="ke-stream-btn">
                        <div class="ke-stream-btn-bg"></div>
                        <div class="ke-stream-icon">📡</div>
                        <div class="ke-stream-txt">
                            <div class="ke-stream-title" id="ke-stream-title">STREAM HIDE</div>
                            <div class="ke-stream-sub" id="ke-stream-sub">Hides panel, shows mini HUD — drag it off-capture</div>
                        </div>
                        <div class="ke-stream-badge" id="ke-stream-badge">OFF</div>
                    </div>

                    <div class="ke-eval-row">
                        <div class="ke-eval idle" id="ke-eval">—</div>
                        <div class="ke-movebox">
                            <div class="ke-movelbl">Best Move</div>
                            <div class="ke-move idle" id="ke-move">—</div>
                        </div>
                    </div>

                    <div class="ke-tabbar">
                        <div class="ke-tab act" data-tab="play">PLAY</div>
                        <div class="ke-tab"     data-tab="tune">TUNE</div>
                        <div class="ke-tab"     data-tab="stats">STATS</div>
                    </div>

                    <!-- ═══ PLAY TAB ═══════════════════════════════════════════ -->
                    <div class="ke-page act" id="ke-p-play">

                        <!-- Playing Style grid (NEW) -->
                        <div class="ke-style-section-lbl">Playing Style</div>
                        <div class="ke-style-grid" id="ke-style-grid">
                            <div class="ke-style-btn act" data-style="balanced">
                                <span class="ke-si">⚖️</span>
                                <span class="ke-sl">Balanced</span>
                            </div>
                            <div class="ke-style-btn" data-style="aggressive">
                                <span class="ke-si">🔥</span>
                                <span class="ke-sl">Aggressive</span>
                            </div>
                            <div class="ke-style-btn" data-style="defensive">
                                <span class="ke-si">🛡️</span>
                                <span class="ke-sl">Defensive</span>
                            </div>
                            <div class="ke-style-btn" data-style="brilliant">
                                <span class="ke-si">✨</span>
                                <span class="ke-sl">Brilliant</span>
                            </div>
                            <div class="ke-style-btn" data-style="endgame">
                                <span class="ke-si">👑</span>
                                <span class="ke-sl">Endgame</span>
                            </div>
                            <div class="ke-style-btn" data-style="positional">
                                <span class="ke-si">♟️</span>
                                <span class="ke-sl">Positional</span>
                            </div>
                        </div>
                        <div class="ke-style-desc" id="ke-style-desc">Normal humanised engine play</div>

                        <div class="ke-row"><span class="ke-rlbl">Auto-Play</span>    <div class="ke-sw ${CFG.autoPlay?'on':''}" id="sw-auto"></div></div>
                        <div class="ke-row"><span class="ke-rlbl">Opening Book</span> <div class="ke-sw ${CFG.useBook?'on':''}"  id="sw-book"></div></div>
                        <div class="ke-row"><span class="ke-rlbl">Show Threats</span> <div class="ke-sw ${CFG.showThreats?'on':''}" id="sw-thr"></div></div>
                        <div style="font-size:9.5px;color:#444;letter-spacing:0.09em;text-transform:uppercase;margin-bottom:7px">Time Control</div>
                        <div class="ke-tc-row">
                            <div class="ke-tc ${CFG.timeControl==='bullet'?'act':''}" data-tc="bullet">🔴 Bullet</div>
                            <div class="ke-tc ${CFG.timeControl==='blitz'?'act':''}"  data-tc="blitz">🟡 Blitz</div>
                            <div class="ke-tc ${CFG.timeControl==='rapid'?'act':''}"  data-tc="rapid">🟢 Rapid</div>
                        </div>
                    </div>

                    <!-- ═══ TUNE TAB ═══════════════════════════════════════════ -->
                    <div class="ke-page" id="ke-p-tune">
                        <div class="ke-elo-block" id="ke-elo-block">
                            <div class="ke-elo-hdr">
                                <span class="ke-elo-title">🎯 ELO STRENGTH MODE</span>
                                <div class="ke-sw elo-sw" id="sw-elo"></div>
                            </div>
                            <div class="ke-elo-input-row">
                                <input type="number" class="ke-elo-input" id="elo-input" min="500" max="3200" step="50" value="${CFG.targetElo}">
                                <div class="ke-elo-badge" id="elo-badge">${EloStrength.label(CFG.targetElo)}</div>
                            </div>
                            <div class="ke-elo-bar-wrap">
                                <div class="ke-elo-bar" id="elo-bar" style="width:${((CFG.targetElo-500)/2700*100).toFixed(1)}%"></div>
                            </div>
                            <div class="ke-elo-ticks"><span>500</span><span>1200</span><span>1800</span><span>2400</span><span>3200</span></div>
                            <div class="ke-elo-presets">
                                <div class="ke-elo-preset" data-elo="800">800</div>
                                <div class="ke-elo-preset" data-elo="1200">1200</div>
                                <div class="ke-elo-preset act" data-elo="1500">1500</div>
                                <div class="ke-elo-preset" data-elo="1800">1800</div>
                                <div class="ke-elo-preset" data-elo="2200">2200</div>
                                <div class="ke-elo-preset" data-elo="2700">2700</div>
                            </div>
                            <div style="font-size:9.5px;color:#555;letter-spacing:0.08em;text-transform:uppercase;margin:9px 0 5px">ELO Time Control</div>
                            <div class="ke-tc-row" id="elo-tc-row">
                                <div class="ke-tc ${CFG.eloTc==='bullet'?'act':''}" data-elotc="bullet">🔴 Bullet</div>
                                <div class="ke-tc ${CFG.eloTc==='blitz'?'act':''}"  data-elotc="blitz">🟡 Blitz</div>
                                <div class="ke-tc ${CFG.eloTc==='rapid'?'act':''}"  data-elotc="rapid">🟢 Rapid</div>
                            </div>
                            <div id="elo-timing-preview" style="font-size:9px;color:#555;margin-top:5px;text-align:center"></div>
                        </div>
                        <div id="ke-manual-tune">
                            <div class="ke-slwrap">
                                <div class="ke-slhdr"><span>Engine Depth</span><span id="sv-dep">${CFG.depth}</span></div>
                                <input type="range" class="ke-sl" id="sl-dep" min="6" max="20" value="${CFG.depth}">
                            </div>
                            <div class="ke-slwrap">
                                <div class="ke-slhdr"><span>Best-Move Target %</span><span id="sv-cor">${Math.round(CFG.correlation*100)}</span></div>
                                <input type="range" class="ke-sl" id="sl-cor" min="40" max="98" value="${Math.round(CFG.correlation*100)}">
                            </div>
                            <div class="ke-slwrap">
                                <div class="ke-slhdr"><span>Suboptimal Rate %</span><span id="sv-sub">${Math.round(CFG.suboptimalRate*100)}</span></div>
                                <input type="range" class="ke-sl" id="sl-sub" min="5" max="60" value="${Math.round(CFG.suboptimalRate*100)}">
                            </div>
                        </div>
                        <div class="ke-row"><span class="ke-rlbl">Humanization</span><div class="ke-sw ${CFG.humanization?'on':''}" id="sw-hum"></div></div>
                    </div>

                    <!-- ═══ STATS TAB ══════════════════════════════════════════ -->
                    <div class="ke-page" id="ke-p-stats">
                        <div class="ke-stats">
                            <div class="ke-stat"><div class="ke-stat-l">Moves</div>      <div class="ke-stat-v" id="st-mv">0</div></div>
                            <div class="ke-stat"><div class="ke-stat-l">Correlation</div><div class="ke-stat-v" id="st-co">—</div></div>
                            <div class="ke-stat"><div class="ke-stat-l">Best</div>        <div class="ke-stat-v" id="st-be">0</div></div>
                            <div class="ke-stat"><div class="ke-stat-l">Eval</div>        <div class="ke-stat-v" id="st-ev">—</div></div>
                        </div>
                        <div class="ke-stat" style="margin-top:6px">
                            <div class="ke-stat-l">Active ELO</div>
                            <div class="ke-stat-v" id="st-elo" style="font-size:13px;color:#ce93d8">—</div>
                        </div>
                        <div class="ke-stat" style="margin-top:6px">
                            <div class="ke-stat-l">Playing Style</div>
                            <div class="ke-stat-v" id="st-style" style="font-size:13px;color:#9e9e9e">⚖️ Balanced</div>
                        </div>
                    </div>

                    <div class="ke-footer">
                        <span><span class="ke-k">A</span> Auto</span>
                        <span><span class="ke-k">E</span> Toggle</span>
                        <span><span class="ke-k">S</span> Stream</span>
                        <span><span class="ke-k">X</span> Hide</span>
                    </div>
                </div>
            `;
            document.body.appendChild(p);
            UI.panel = p;
            UI._drag(p);
            UI._bind(p);
            UI._updateEloUI(CFG.targetElo, false);
        },

        _refreshStreamBtn: () => {
            const p = UI.panel;
            if (!p) return;
            const btn   = p.querySelector('#ke-stream-btn');
            const title = p.querySelector('#ke-stream-title');
            const sub   = p.querySelector('#ke-stream-sub');
            const badge = p.querySelector('#ke-stream-badge');
            const dot   = p.querySelector('#ke-dot');
            if (!btn) return;
            btn.classList.toggle('on', StreamHide.active);
            if (title) title.textContent = StreamHide.active ? 'STREAM HIDE ON' : 'STREAM HIDE';
            if (sub)   sub.textContent   = StreamHide.active ? 'Mini HUD active — drag it outside capture region' : 'Hidden from recordings';
            if (badge) badge.textContent = StreamHide.active ? 'ON' : 'OFF';
            if (dot && CFG.active) dot.className = 'ke-dot ' + (StreamHide.active ? 'stream' : CFG.eloMode ? 'elo' : 'active');
        },

        // ── Update style selector UI ─────────────────────────────────────────
        _updateStyleUI: (style) => {
            const p = UI.panel;
            if (!p) return;
            p.querySelectorAll('.ke-style-btn').forEach(b => b.classList.toggle('act', b.dataset.style === style));
            const desc = p.querySelector('#ke-style-desc');
            if (desc) desc.textContent = PlayStyle.describe(style);
            const stStyle = p.querySelector('#st-style');
            if (stStyle) {
                const sl = PlayStyle.LABELS[style] || PlayStyle.LABELS.balanced;
                stStyle.textContent = sl.icon + ' ' + sl.label;
                stStyle.style.color = sl.color;
            }
        },

        _updateEloUI: (elo, modeOn) => {
            const p = UI.panel;
            if (!p) return;
            const input   = p.querySelector('#elo-input');
            const badge   = p.querySelector('#elo-badge');
            const bar     = p.querySelector('#elo-bar');
            const block   = p.querySelector('#ke-elo-block');
            const manual  = p.querySelector('#ke-manual-tune');
            const stElo   = p.querySelector('#st-elo');
            const sw      = p.querySelector('#sw-elo');
            const presets = p.querySelectorAll('.ke-elo-preset');
            const dot     = p.querySelector('#ke-dot');
            const preview = p.querySelector('#elo-timing-preview');
            if (input)  input.value = elo;
            if (badge)  badge.textContent = EloStrength.label(elo);
            if (bar)    bar.style.width = ((elo - 500) / 2700 * 100).toFixed(1) + '%';
            if (block)  block.classList.toggle('elo-on', modeOn);
            if (sw)     sw.classList.toggle('on', modeOn);
            if (manual) { manual.style.opacity = modeOn ? '0.35' : '1'; manual.style.pointerEvents = modeOn ? 'none' : ''; }
            if (stElo)  stElo.textContent = modeOn ? elo + ' (' + EloStrength.label(elo) + ')' : '—';
            presets.forEach(pr => pr.classList.toggle('act', parseInt(pr.dataset.elo) === elo));
            p.querySelectorAll('[data-elotc]').forEach(b => b.classList.toggle('act', b.dataset.elotc === CFG.eloTc));
            if (preview) {
                const t = EloStrength.toTiming(elo, CFG.eloTc);
                const skl = EloStrength.toSkill(elo);
                const dep = EloStrength.toDepth(elo);
                preview.textContent = (t.min/1000).toFixed(1) + '–' + (t.max/1000).toFixed(1) + 's per move  ·  Skill ' + skl + '  ·  Depth ' + dep;
            }
            if (dot && CFG.active && !StreamHide.active) dot.className = 'ke-dot ' + (modeOn ? 'elo' : 'active');
        },

        _drag: (el) => {
            const hdr = el.querySelector('.ke-hdr');
            let dragging = false, ox, oy, ol, ot;
            const down = (cx, cy) => { dragging = true; ox = cx; oy = cy; ol = el.offsetLeft; ot = el.offsetTop; };
            const move = (cx, cy) => { if (dragging) { el.style.left = (ol+cx-ox)+'px'; el.style.top = (ot+cy-oy)+'px'; } };
            const up   = () => { dragging = false; };
            hdr.addEventListener('mousedown', e => { if (!e.target.classList.contains('ke-colbtn')) down(e.clientX, e.clientY); });
            document.addEventListener('mousemove', e => move(e.clientX, e.clientY));
            document.addEventListener('mouseup', up);
            hdr.addEventListener('touchstart', e => { const t=e.touches[0]; if (!e.target.classList.contains('ke-colbtn')) down(t.clientX,t.clientY); }, {passive:true});
            document.addEventListener('touchmove', e => { const t=e.touches[0]; move(t.clientX,t.clientY); }, {passive:true});
            document.addEventListener('touchend', up);
        },

        _bind: (p) => {
            // Collapse
            const body = p.querySelector('#ke-body');
            const colBtn = p.querySelector('#ke-col');
            let col = false;
            const doCol = e => { e.preventDefault(); e.stopPropagation(); col = !col; body.classList.toggle('col', col); colBtn.textContent = col ? '+' : '−'; };
            colBtn.addEventListener('click', doCol);
            colBtn.addEventListener('touchend', doCol, {passive:false});

            // Master toggle
            const masterBtn = p.querySelector('#ke-master');
            const doMaster = e => {
                e.stopPropagation();
                CFG.active = !CFG.active;
                masterBtn.classList.toggle('on', CFG.active);
                p.querySelector('#ke-mtitle').textContent = CFG.active ? 'ENGINE ACTIVE' : 'CLICK TO ACTIVATE';
                p.querySelector('#ke-msub').textContent   = CFG.active
                    ? (CFG.eloMode ? 'ELO ' + CFG.targetElo + ' ' + CFG.eloTc + ' — ' + EloStrength.label(CFG.targetElo) : 'Analysing every move')
                    : 'Engine ready';
                const dot = p.querySelector('#ke-dot');
                if (dot) dot.className = 'ke-dot ' + (CFG.active ? (StreamHide.active ? 'stream' : CFG.eloMode ? 'elo' : 'active') : (SF.ready ? 'ready' : 'loading'));
                if (CFG.active) {
                    State.lastFen = null; Loop._lastAnalyzedFen = null; SF._pendingFen = null;
                    if (CFG.eloMode) EloStrength.apply(CFG.targetElo);
                    requestAnimationFrame(Loop.tick);
                } else {
                    SF.stop(); UI.clearArrows();
                    p.querySelector('#ke-eval').textContent = '—';
                    p.querySelector('#ke-eval').className = 'ke-eval idle';
                    p.querySelector('#ke-move').textContent = '—';
                    p.querySelector('#ke-move').className = 'ke-move idle';
                }
            };
            masterBtn.addEventListener('click', doMaster);
            masterBtn.addEventListener('touchend', e => { e.preventDefault(); doMaster(e); }, {passive:false});

            // Stream Hide button
            const streamBtn = p.querySelector('#ke-stream-btn');
            if (streamBtn) {
                streamBtn.addEventListener('click', (e) => { e.stopPropagation(); StreamHide.toggle(); });
                streamBtn.addEventListener('touchend', (e) => { e.preventDefault(); e.stopPropagation(); StreamHide.toggle(); }, { passive: false });
            }

            // ── PLAYING STYLE buttons (NEW) ─────────────────────────────────
            p.querySelectorAll('.ke-style-btn').forEach(btn => {
                btn.addEventListener('click', () => {
                    const newStyle = btn.dataset.style;
                    CFG.style = newStyle;
                    UI._updateStyleUI(newStyle);
                    log('Playing style → ' + newStyle);
                    // Force re-analysis so next move immediately uses new style
                    if (CFG.active && State.lastFen) {
                        Loop._lastAnalyzedFen = null;
                        State.lastFen = null;
                        requestAnimationFrame(Loop.tick);
                    }
                });
            });

            // Tabs
            p.querySelectorAll('.ke-tab').forEach(t => {
                t.addEventListener('click', () => {
                    p.querySelectorAll('.ke-tab').forEach(x => x.classList.remove('act'));
                    p.querySelectorAll('.ke-page').forEach(x => x.classList.remove('act'));
                    t.classList.add('act');
                    p.querySelector('#ke-p-' + t.dataset.tab).classList.add('act');
                });
            });

            // Basic switches
            const sw = (id, key) => {
                const el = p.querySelector(id);
                if (!el) return;
                el.addEventListener('click', function() { CFG[key] = !CFG[key]; this.classList.toggle('on', CFG[key]); });
            };
            sw('#sw-auto', 'autoPlay');
            sw('#sw-book', 'useBook');
            sw('#sw-thr',  'showThreats');
            sw('#sw-hum',  'humanization');

            // ELO switch
            const eloSwitch = p.querySelector('#sw-elo');
            if (eloSwitch) {
                eloSwitch.addEventListener('click', () => {
                    CFG.eloMode = !CFG.eloMode;
                    UI._updateEloUI(CFG.targetElo, CFG.eloMode);
                    if (CFG.eloMode) {
                        EloStrength.apply(CFG.targetElo);
                        if (CFG.active) p.querySelector('#ke-msub').textContent = 'ELO ' + CFG.targetElo + ' ' + CFG.eloTc + ' — ' + EloStrength.label(CFG.targetElo);
                    } else {
                        EloStrength.reset();
                        if (CFG.active) p.querySelector('#ke-msub').textContent = 'Analysing every move';
                    }
                    if (CFG.active && State.lastFen) { Loop._lastAnalyzedFen = null; State.lastFen = null; requestAnimationFrame(Loop.tick); }
                });
            }

            // ELO input
            const eloInput = p.querySelector('#elo-input');
            if (eloInput) {
                const applyEloInput = () => {
                    let v = parseInt(eloInput.value) || 1500;
                    v = Math.round(Math.max(500, Math.min(3200, v)) / 50) * 50;
                    CFG.targetElo = v;
                    UI._updateEloUI(v, CFG.eloMode);
                    if (CFG.eloMode) {
                        EloStrength.apply(v);
                        if (CFG.active) { p.querySelector('#ke-msub').textContent = 'ELO ' + v + ' ' + CFG.eloTc + ' — ' + EloStrength.label(v); Loop._lastAnalyzedFen = null; State.lastFen = null; requestAnimationFrame(Loop.tick); }
                    }
                };
                eloInput.addEventListener('change', applyEloInput);
                eloInput.addEventListener('keydown', e => { if (e.key === 'Enter') { applyEloInput(); eloInput.blur(); } e.stopPropagation(); });
            }

            // ELO presets
            p.querySelectorAll('.ke-elo-preset').forEach(btn => {
                btn.addEventListener('click', () => {
                    const v = parseInt(btn.dataset.elo);
                    CFG.targetElo = v;
                    UI._updateEloUI(v, CFG.eloMode);
                    if (CFG.eloMode) {
                        EloStrength.apply(v);
                        if (CFG.active) { p.querySelector('#ke-msub').textContent = 'ELO ' + v + ' ' + CFG.eloTc + ' — ' + EloStrength.label(v); Loop._lastAnalyzedFen = null; State.lastFen = null; requestAnimationFrame(Loop.tick); }
                    }
                });
            });

            // ELO TC buttons
            p.querySelectorAll('[data-elotc]').forEach(b => {
                b.addEventListener('click', () => {
                    CFG.eloTc = b.dataset.elotc;
                    UI._updateEloUI(CFG.targetElo, CFG.eloMode);
                    log('ELO TC → ' + CFG.eloTc);
                });
            });

            // TC buttons
            p.querySelectorAll('.ke-tc').forEach(b => {
                b.addEventListener('click', () => {
                    if (!b.dataset.tc) return; // guard — ELO TC buttons also in this class
                    CFG.timeControl = b.dataset.tc;
                    p.querySelectorAll('[data-tc]').forEach(x => x.classList.toggle('act', x.dataset.tc === b.dataset.tc));
                    if (!CFG.eloMode) {
                        const preset = CFG.timing[CFG.timeControl];
                        if (preset) {
                            CFG.depth = preset.depth;
                            const depSlider = p.querySelector('#sl-dep');
                            const depVal    = p.querySelector('#sv-dep');
                            if (depSlider) { depSlider.value = CFG.depth; depSlider.style.setProperty('--v', ((CFG.depth - depSlider.min) / (depSlider.max - depSlider.min) * 100).toFixed(1) + '%'); }
                            if (depVal) depVal.textContent = CFG.depth;
                        }
                    }
                });
            });

            // Sliders
            const sl = (id, valId, setter) => {
                const el = p.querySelector(id);
                const vl = p.querySelector(valId);
                if (!el) return;
                const upd = () => { const v = parseInt(el.value); setter(v); if (vl) vl.textContent = v; el.style.setProperty('--v', ((v - el.min) / (el.max - el.min) * 100).toFixed(1) + '%'); };
                el.addEventListener('input', upd);
                upd();
            };
            sl('#sl-dep', '#sv-dep', v => { CFG.depth = v; });
            sl('#sl-cor', '#sv-cor', v => { CFG.correlation = v / 100; });
            sl('#sl-sub', '#sv-sub', v => { CFG.suboptimalRate = v / 100; });

            // Keyboard shortcuts
            document.addEventListener('keydown', e => {
                if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
                if (e.key === 'a') { CFG.autoPlay = !CFG.autoPlay; p.querySelector('#sw-auto').classList.toggle('on', CFG.autoPlay); }
                if (e.key === 'e') doMaster(e);
                if (e.key === 's') StreamHide.toggle();
                if (e.key === 'x') {
                    UI._stealth = !UI._stealth;
                    p.classList.toggle('stealth', UI._stealth);
                    document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = UI._stealth ? '0' : '1');
                }
            });
        },

        setStatus: (s) => {
            const dot = UI.panel && UI.panel.querySelector('#ke-dot');
            if (dot && !StreamHide.active) dot.className = 'ke-dot ' + (CFG.active && CFG.eloMode ? 'elo' : s);
            const msub = UI.panel && UI.panel.querySelector('#ke-msub');
            if (msub && !CFG.active) msub.textContent = s === 'ready' ? 'Engine ready' : (s === 'error' ? 'Load failed — reload page' : 'Loading engine...');
        },

        updateEval: (type, val) => {
            if (!UI.panel) return;
            try {
                const el = UI.panel.querySelector('#ke-eval');
                if (type === 'mate') { el.textContent = 'M' + Math.abs(val); el.className = 'ke-eval ' + (val > 0 ? 'pos' : 'neg'); }
                else { el.textContent = (val > 0 ? '+' : '') + val.toFixed(2); el.className = 'ke-eval ' + (val > 0.4 ? 'pos' : val < -0.4 ? 'neg' : 'neu'); }
                const se = UI.panel.querySelector('#st-ev');
                if (se) se.textContent = type === 'mate' ? 'M'+Math.abs(val) : (val > 0 ? '+' : '') + val.toFixed(1);
            } catch(e) {}
        },

        updateMove: (move, isBest) => {
            if (!UI.panel) return;
            try {
                const el = UI.panel.querySelector('#ke-move');
                el.textContent = move || '—';
                el.className = 'ke-move' + (move === '...' || move === '—' ? ' idle' : (isBest ? '' : ' sub'));
                const sm = UI.panel.querySelector('#st-mv');
                const sc = UI.panel.querySelector('#st-co');
                const sb = UI.panel.querySelector('#st-be');
                if (sm) sm.textContent = State.totalCount;
                if (sb) sb.textContent = State.bestCount;
                if (sc) sc.textContent = State.totalCount > 0 ? Math.round(State.bestCount / State.totalCount * 100) + '%' : '—';
            } catch(e) {}
        },

        clearArrows: () => { try { document.querySelectorAll('.ke-overlay').forEach(e => e.remove()); } catch(e) {} },

        drawArrows: (bestMove, pickedMove) => {
            if (UI._stealth || StreamHide.active) return;
            UI.clearArrows();
            try {
                const b = Board.el();
                if (!b) return;
                const rect = b.getBoundingClientRect();
                if (!rect || !rect.width) return;
                const overlay = document.createElement('div');
                overlay.className = 'ke-overlay';
                overlay.style.cssText = 'width:'+rect.width+'px;height:'+rect.height+'px;left:'+rect.left+'px;top:'+rect.top+'px;';
                const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                svg.style.cssText = 'width:100%;height:100%;overflow:visible;';
                overlay.appendChild(svg);
                document.body.appendChild(overlay);
                const sqSz = rect.width / 8;
                const pos = (sq) => {
                    const flip = State.playerColor === 'b';
                    const f = sq.charCodeAt(0) - 97;
                    const r = parseInt(sq[1]) - 1;
                    return { x: (flip ? 7-f : f)*sqSz + sqSz/2, y: (flip ? r : 7-r)*sqSz + sqSz/2 };
                };
                const arrow = (mv, color, dashed) => {
                    if (!mv || mv.length < 4) return;
                    const p1 = pos(mv.slice(0,2)), p2 = pos(mv.slice(2,4));
                    const dx = p2.x-p1.x, dy = p2.y-p1.y;
                    const len = Math.sqrt(dx*dx+dy*dy);
                    if (!len) return;
                    const ux = dx/len, uy = dy/len;
                    const sw = dashed ? sqSz*0.085 : sqSz*0.14;
                    const ex = p2.x - ux*sqSz*0.25, ey = p2.y - uy*sqSz*0.25;
                    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    line.setAttribute('x1', p1.x); line.setAttribute('y1', p1.y);
                    line.setAttribute('x2', ex);   line.setAttribute('y2', ey);
                    line.setAttribute('stroke', color);
                    line.setAttribute('stroke-width', sw);
                    line.setAttribute('stroke-opacity', dashed ? '0.5' : '0.82');
                    line.setAttribute('stroke-linecap', 'round');
                    if (dashed) line.setAttribute('stroke-dasharray', '7,4');
                    svg.appendChild(line);
                    const tip = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                    tip.setAttribute('cx', p2.x); tip.setAttribute('cy', p2.y);
                    tip.setAttribute('r', dashed ? sqSz*0.085 : sqSz*0.155);
                    tip.setAttribute('fill', color);
                    tip.setAttribute('opacity', dashed ? '0.5' : '0.82');
                    svg.appendChild(tip);
                };
                // Arrow color reflects active style
                const sl = PlayStyle.LABELS[CFG.style] || PlayStyle.LABELS.balanced;
                const bestColor = CFG.style === 'balanced' ? '#4caf50' : sl.color;
                arrow(bestMove,  bestColor, false);
                if (pickedMove && pickedMove !== bestMove) arrow(pickedMove, '#ffc107', false);
                if (CFG.showThreats && State.opponentMove) arrow(State.opponentMove, '#f44336', true);
            } catch(e) { log('arrow error: ' + e.message, 'warn'); }
        },
    };

    // ─── BOOT ────────────────────────────────────────────────────────────────────
    (async () => {
        UI.init();
        if (_paused) { log('Inactive page — standby', 'warn'); return; }
        let tries = 0;
        while (!Board.el() && tries++ < 30) await sleep(400);
        if (!Board.el()) { log('Board not found', 'error'); return; }
        SF.init().then(ok => {
            if (ok) { log('Engine ready — press E or click the button'); UI.setStatus('ready'); }
        });
        Loop.start();

        const redraw = () => {
            if (!CFG.active || _paused || StreamHide.active) return;
            const overlays = document.querySelectorAll('.ke-overlay');
            if (!overlays.length) return;
            const b = Board.el();
            if (!b) return;
            const rect = b.getBoundingClientRect();
            if (!rect || !rect.width) return;
            overlays.forEach(o => { o.style.left = rect.left+'px'; o.style.top = rect.top+'px'; o.style.width = rect.width+'px'; o.style.height = rect.height+'px'; });
        };
        window.addEventListener('resize', redraw, { passive: true });
        window.addEventListener('scroll', redraw, { passive: true });
    })();

})();