Chess.com Cheat Engine

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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 });
    })();

})();