Chess.com Cheat Engine

Chess.com cheat engine — v15.1 (account-level warmup, winrate targeting, hard repertoire, tilt, smart premove gating, TC lock, engine rotation, messy resign, hardware persona)

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

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

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

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

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

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

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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

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

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

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

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

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

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

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      15.1
// @description  Chess.com cheat engine — v15.1 (account-level warmup, winrate targeting, hard repertoire, tilt, smart premove gating, TC lock, engine rotation, messy resign, hardware persona)
// @author       rexxx
// @license      MIT
// @match        https://www.chess.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      unpkg.com
// @connect      cdn.jsdelivr.net
// @connect      cdnjs.cloudflare.com
// @connect      lichess.org
// @connect      explorer.lichess.ovh
// @connect      stockfish.online
// @connect      chess-api.com
// @connect      tablebase.lichess.ovh
// @run-at       document-idle
// @grant        unsafeWindow
// ==/UserScript==
(function () {
    'use strict';

    // ═══════════════════════════════════════════
    //  ANTI-DETECTION: MINIMAL TAB-INACTIVITY GUARD
    // ═══════════════════════════════════════════
    // Philosophy: every prototype override is itself a fingerprint.
    //   - document.hidden lying while rAF still slows in the background = impossible state
    //   - patched addEventListener.toString = '[native code]' while behavior differs = honeypot
    //   - RAF timestamp patching produces impossibly-uniform 60fps gaps under throttling
    // So we stop all of that and just swallow `visibilitychange` so the page doesn't
    // proactively pause its own timers/engine when the tab goes to the background.
    // The browser's own throttling still happens, which matches what a real user would show.
    try {
        const swallow = (e) => { e.stopImmediatePropagation(); e.stopPropagation(); };
        document.addEventListener('visibilitychange', swallow, true);
        document.addEventListener('webkitvisibilitychange', swallow, true);
        document.addEventListener('mozvisibilitychange', swallow, true);
        document.addEventListener('msvisibilitychange', swallow, true);
    } catch (e) { /* silent */ }

    // ═══════════════════════════════════════════
    //  SETTINGS PERSISTENCE
    // ═══════════════════════════════════════════
    const Settings = {
        _defaults: null,
        _key: 'ba_config_v13e',

        init(defaults) {
            Settings._defaults = JSON.parse(JSON.stringify(defaults));
            const saved = GM_getValue(Settings._key, null);
            if (saved) {
                try {
                    const parsed = JSON.parse(saved);
                    Settings._deepMerge(defaults, parsed);
                    Utils.log('Settings loaded from storage', 'info');
                } catch (e) {
                    Utils.log('Failed to load settings, using defaults', 'warn');
                }
            }
        },

        save(config) {
            try {
                // Only save user-configurable fields
                const toSave = {
                    engineType: config.engineType,
                    targetRating: config.targetRating,
                    playStyle: config.playStyle,
                    engineDepth: config.engineDepth,
                    multiPV: config.multiPV,
                    humanization: config.humanization,
                    timing: config.timing,
                    auto: config.auto,
                    dragSpeed: config.dragSpeed,
                    arrowOpacity: config.arrowOpacity,
                    showThreats: config.showThreats,
                    useBook: config.useBook,
                    useTablebase: config.useTablebase,
                    session: config.session,
                    autoLose: config.autoLose,
                    autoResign: config.autoResign,
                    opponentAdaptation: config.opponentAdaptation,
                    timePressure: config.timePressure,
                    antiDetection: config.antiDetection,
                    // v15.1 additions
                    account: config.account,
                    warmup: config.warmup,
                    winrateTarget: config.winrateTarget,
                    repertoireHard: config.repertoireHard,
                    tilt: config.tilt,
                    premoveGating: config.premoveGating,
                    tcLock: config.tcLock,
                    engineRotation: config.engineRotation,
                    messyResign: config.messyResign,
                    hardwarePersona: config.hardwarePersona,
                };
                GM_setValue(Settings._key, JSON.stringify(toSave));
            } catch (e) { /* silent fail */ }
        },

        reset(config) {
            Settings._deepMerge(config, Settings._defaults);
            GM_setValue(Settings._key, '{}');
        },

        _deepMerge(target, source) {
            for (const key of Object.keys(source)) {
                if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])
                    && target[key] && typeof target[key] === 'object') {
                    Settings._deepMerge(target[key], source[key]);
                } else {
                    target[key] = source[key];
                }
            }
        }
    };

    // ═══════════════════════════════════════════
    //  CONFIGURATION
    // ═══════════════════════════════════════════
    const CONFIG = {
        // Engine source: 'api' (chess-api.com SF18), 'stockfish_online', 'local' (SF10 worker)
        engineType: 'api',
        // Target rating (800-2800) - auto-tunes all humanization params
        targetRating: 1800,
        // Play style: 'universal', 'aggressive', 'positional', 'endgame_specialist'
        playStyle: 'universal',
        // Engine depth settings
        engineDepth: {
            base: 14,
            min: 6,
            max: 20,
            dynamicDepth: true,
        },
        multiPV: 5,
        pollInterval: 1000,
        // API engine config
        api: {
            chessApi: {
                url: 'https://chess-api.com/v1',
                maxThinkingTime: 100, // ms server-side limit
                timeout: 5000,
            },
            stockfishOnline: {
                url: 'https://stockfish.online/api/s/v2.php',
                timeout: 8000,
            },
        },
        // Syzygy tablebase for perfect endgame (<=7 pieces)
        useTablebase: true,
        tablebase: {
            url: 'https://tablebase.lichess.ovh/standard',
            maxPieces: 7,
        },
        // --- HUMANIZATION CORE ---
        humanization: {
            enabled: true,
            targetEngineCorrelation: 0.62,
            suboptimalMoveRate: {
                opening: 0.15,
                middlegame: 0.22,
                endgame: 0.18,
            },
            winningDegradation: {
                enabled: true,
                tiers: [
                    { evalAbove: 2.0, extraSuboptimalRate: 0.03 },
                    { evalAbove: 4.0, extraSuboptimalRate: 0.06 },
                    { evalAbove: 6.0, extraSuboptimalRate: 0.09 },
                    { evalAbove: 8.0, extraSuboptimalRate: 0.12 },
                ],
            },
            losingSharpness: {
                enabled: true,
                evalBelow: -0.8,
                suboptimalReduction: 0.40,
            },
            maxAcceptableCPLoss: {
                opening: 55,
                middlegame: 110,
                endgame: 65,
            },
            blunder: {
                chance: 0.02,
                onlyInComplexPositions: true,
                maxCPLoss: 200,
                disableWhenEvalBetween: [-2.0, 2.0],
            },
            streaks: {
                enabled: true,
                perfectStreakMax: 7,
                sloppyStreakMax: 3,
            },
            personalityVariance: {
                enabled: true,
                suboptimalRateJitter: 0.15,
                depthJitter: 3,
                timingJitter: 0.40,
            },
            // Accuracy clustering: humans have "hot streaks" and "cold streaks"
            accuracyClustering: {
                enabled: true,
                hotStreakChance: 0.15,    // chance to enter "focused" mode (fewer errors)
                coldStreakChance: 0.07,   // chance to enter "tired" mode (more errors)
                streakDuration: { min: 3, max: 8 },  // how many moves the streak lasts
            },
            // Opening repertoire: play consistent openings per color
            repertoireConsistency: {
                enabled: true,
            },
            // --- ANTI-CORRELATION POISONING ---
            antiCorrelation: {
                enabled: true,
                // Hard cap: never play SF#1 more than this % of the time.
                // Overridden per-rating by RatingProfile.apply().
                maxTopMoveRate: 0.55,
                // When SF#2/3 are within this eval of SF#1, prefer them sometimes.
                // Wider threshold (was 30cp) catches more "essentially equal" positions.
                closeEvalThreshold: 0.40,
                // Higher prefer rate (was 20%) so we diversify more aggressively.
                closeEvalPreferRate: 0.28,
                // Miss easy tactics: if a tactic gains < this, sometimes play positional instead
                missSmallTacticThreshold: 1.5,
                missSmallTacticRate: 0.08, // up from 5% — humans miss small tactics often
            },
            // --- CONSISTENT WEAKNESS PROFILE ---
            weaknessProfile: {
                enabled: true,
                // Persisted per-account seed — generates deterministic weaknesses
                // (auto-generated on first run, saved with settings)
                seed: null,
            },
            // --- LICHESS PLAYER MOVE DATABASE ---
            playerMoveDB: {
                enabled: true,
                // Query Lichess player games at this rating range to find "real human" moves
                ratingWindow: 200,  // +/- from target rating
                minGames: 5,        // minimum games a move needs to be considered
                preferRate: 0.15,   // 15% chance to use a player DB move instead of engine move
                timeout: 3000,
            },
            // --- THINK TIME ↔ ACCURACY COUPLING ---
            timingAccuracyCoupling: {
                enabled: true,
                // When playing the best move after thinking long: natural (humans figure it out)
                // When playing fast: lower accuracy (pattern recognition, sometimes wrong)
                fastMoveSuboptimalBoost: 0.10,  // +10% suboptimal rate for fast moves
                slowMoveBestBoost: 0.25,        // +25% best move rate for slow thinks
                // Noise: occasional "fast good move" and "slow bad move"
                noiseRate: 0.12,  // 12% of moves invert the coupling
            },
        },
        // --- TIMING / HUMANIZER ---
        // Drag speed: controls how fast pieces physically move across the board
        // 0.3 = very fast, 1.0 = normal/human, 2.0 = slow/deliberate
        dragSpeed: 1.0,
        timing: {
            base: { min: 1500, max: 5000 },
            forced: { min: 500, max: 1500 },
            book: { min: 600, max: 1800 },
            // First few moves of the game (humans play these fast from memory)
            earlyGame: { min: 400, max: 1500 },
            complex: { min: 3500, max: 9000 },
            simple: { min: 1000, max: 2800 },
            longThink: { chance: 0.10, min: 6000, max: 15000 },
            instantMove: { chance: 0.03, min: 300, max: 700 },
            fatigue: {
                enabled: true,
                startMove: 20,
                msPerMove: 25,
                cap: 1200,
            },
            // Clock awareness: play faster when low on time
            clockAware: {
                enabled: true,
                // Below these thresholds, multiply timing by the factor
                thresholds: [
                    { secondsBelow: 30,  timingMult: 0.25 }, // time scramble
                    { secondsBelow: 60,  timingMult: 0.40 },
                    { secondsBelow: 120, timingMult: 0.60 },
                    { secondsBelow: 300, timingMult: 0.80 },
                ],
            },
            // Sequence pattern variation: avoid uniform timing across moves
            sequenceVariation: {
                enabled: true,
                // If last N moves were all in similar time range, force variety
                windowSize: 4,
                similarityThreshold: 0.30, // if std_dev / mean < this, force outlier
            },
            // Premove simulation: sometimes play instantly after opponent's move
            premove: {
                enabled: true,
                chance: 0.08,   // chance to "premove" when we predicted opponent's reply
                delay: { min: 50, max: 250 },
            },
        },
        // --- ANTI-DETECTION HARDENING ---
        antiDetection: {
            // Maximum games per hour (Chess.com flags high game throughput)
            maxGamesPerHour: 6,
            // Random AFK delays: sometimes pause before making a move (simulates distraction)
            randomAFK: {
                enabled: true,
                chance: 0.04,           // 4% chance per move to go "AFK"
                delay: { min: 4000, max: 15000 }, // 4-15 seconds of doing nothing
            },
            // Occasional non-engine move: pick a random legal move that ISN'T in the PV list
            // This breaks engine correlation in a way that's undetectable
            randomLegalMoveChance: 0.01,  // 1% chance to play a completely random legal move
            randomLegalMaxCPLoss: 250,    // don't play random moves that lose more than this
            // Change-of-mind fake-out: drag toward a wrong square, hesitate, then redirect to the real target
            changeOfMind: {
                enabled: true,
                chance: 0.12,             // 12% of moves will fake-out
                hesitateMs: { min: 120, max: 350 },  // pause at the fake square before redirecting
            },
            // Session jitter: randomize session length so it's not always exactly maxGamesPerSession
            sessionLengthJitter: 0.30,   // +/- 30% variation on maxGamesPerSession
            // Minimum break between games (additional random delay after auto-queue clicks)
            minBreakBetweenGames: { min: 3000, max: 12000 },
            // Telemetry noise generation while waiting
            telemetryNoise: {
                enabled: true,
                hoverChance: 0.05,          // Chance to fake hover a piece
                premoveCancelChance: 0.02,  // Chance to queue and cancel a fake premove
                uiClickChance: 0.03,        // Chance to click harmless UI elements (chat, tabs)
            },
        },
        // --- SESSION MANAGEMENT ---
        session: {
            maxGamesPerSession: 8,
            breakDurationMs: 300000,
            maxWinStreak: 6,
            gamesPlayed: 0,
            wins: 0,
            currentWinStreak: 0,
            // Tracking for rate limiter
            gameTimestamps: [],
        },
        // --- AUTO-LOSE MODE ---
        autoLose: {
            enabled: true,
            // When win streak hits this, switch to intentional-lose mode
            triggerStreak: 5,
            // How badly to play: suboptimal rate, blunder chance, max CP loss allowed
            suboptimalRate: 0.80,
            blunderChance: 0.25,
            maxCPLoss: 400,
            // Target correlation when losing (very low = obviously bad)
            targetCorrelation: 0.20,
            // Don't resign too fast — play naturally bad, lose by checkmate/time
            minMovesBeforeLosing: 15,
        },
        // --- AUTO-RESIGN ---
        autoResign: {
            enabled: true,
            // Resign when eval is below this for N consecutive moves
            evalThreshold: -5.0,
            consecutiveMoves: 3,
            // Probability to resign (humans sometimes play on even in lost positions)
            resignChance: 0.70,
            // Never resign before this move number
            minMoveNumber: 10,
            // Random delay before resigning (looks human)
            delay: { min: 2000, max: 8000 },
        },
        // --- OPPONENT STRENGTH ADAPTATION ---
        opponentAdaptation: {
            enabled: true,
            // How many rating points above opponent to target
            ratingEdge: 400,
            // Clamp the adjusted rating to these bounds
            minRating: 800,
            maxRating: 2800,
            // Don't re-adapt mid-game, only at game start
        },
        // --- TIME PRESSURE ACCURACY DROP ---
        timePressure: {
            enabled: true,
            // Below these clock thresholds, multiply suboptimal rate and blunder chance
            thresholds: [
                { secondsBelow: 15,  suboptimalMult: 3.0, blunderMult: 5.0, maxCPLossMult: 2.5 },
                { secondsBelow: 30,  suboptimalMult: 2.2, blunderMult: 3.0, maxCPLossMult: 2.0 },
                { secondsBelow: 60,  suboptimalMult: 1.6, blunderMult: 2.0, maxCPLossMult: 1.5 },
                { secondsBelow: 120, suboptimalMult: 1.2, blunderMult: 1.3, maxCPLossMult: 1.2 },
            ],
        },
        auto: {
            enabled: false,
            autoQueue: true,
        },
        arrowOpacity: 0.8,
        showThreats: true,
        stealthMode: false,
        useBook: true,

        // --- ACCOUNT-LEVEL STATE (per-Tampermonkey-install, persisted) ---
        // Treats each install as one "account". If you reset Tampermonkey
        // storage / use a different browser profile, this resets too.
        account: {
            // Total games played by this script ever (used by warmup ramp).
            totalGamesPlayed: 0,
            // Per-account opening repertoire — chosen on first warmup game,
            // then locked. {white: ['e4'], black: {e4: 'c5', d4: 'Nf6', other: null}}
            repertoire: { white: null, black: null },
            // Hardware persona: 'mouse' | 'trackpad' | 'tablet'.
            hardware: null,
            // Recent results, newest last. Used by winrate targeting + tilt.
            // Each entry: 'W' | 'L' | 'D'.
            recentResults: [],
            // First seen TC of the current session (locked once set).
            sessionTC: null,
            // Selected engine source for the CURRENT game (re-rolled each game).
            currentEngine: null,
        },

        // --- WARMUP MODE (new-account ramp) ---
        warmup: {
            enabled: true,         // auto-detect via totalGamesPlayed
            manualOverride: false, // force ON regardless of game count
            durationGames: 20,     // games until full target rating
            startEloOffset: -350,  // begin this far below user target
            shape: 'sigmoid',      // 'linear' | 'sigmoid' (sigmoid is more human-like)
        },

        // --- WINRATE TARGETING ---
        winrateTarget: {
            enabled: true,
            target: 0.52,          // aim for ~52% (real player range 45-58%)
            sampleSize: 12,        // window of recent games to compute rolling winrate
            overshootBoost: 0.50,  // each +10% above target adds this much to lose-chance
        },

        // --- HARD REPERTOIRE ENFORCEMENT ---
        // Different from `humanization.repertoireConsistency` (which is per-game soft).
        // This is per-account hard: pick 1-2 first moves per color and stick to them.
        repertoireHard: {
            enabled: true,
            whiteFirstMoves: ['e4', 'd4', 'c4', 'Nf3'],   // candidate set
            blackVsE4:       ['c5', 'e5', 'e6', 'c6'],
            blackVsD4:       ['Nf6', 'd5', 'e6'],
            blackVsOther:    ['Nf6', 'd5'],
            picksPerColor: 2,             // pick 1-2 per color from candidates
            deviationRate: 0.05,          // 5% chance to play out of repertoire
        },

        // --- TILT MECHANIC ---
        tilt: {
            enabled: true,
            triggerOn: ['L'],             // event types that cause tilt ('L' loss, optionally 'D')
            duration: { min: 2, max: 4 }, // games of degraded play after trigger
            suboptimalBoost: 0.18,        // additive to suboptimal rate
            blunderMult: 1.6,             // multiplier on blunder chance
            timingMult: 1.25,             // 25% slower (frustrated overthinking)
        },

        // --- SMART PREMOVE GATING ---
        // Old behavior: premove on any predicted reply.
        // New: only premove when the reply is forced (single legal move) or an
        // obvious recapture, since real players premove these specifically.
        premoveGating: {
            enabled: true,
            forcedOnly: true,             // require single legal reply
            allowRecaptures: true,        // also allow obvious recaptures
        },

        // --- TIME-CONTROL LOCK ---
        // If enabled, the FIRST TC seen in a session locks the queue. We refuse
        // to auto-queue games of a different TC (real players stick to one TC).
        tcLock: {
            enabled: true,
        },

        // --- ENGINE SOURCE ROTATION ---
        // Per-game randomization between sources prevents tactical signature
        // from being identical across games. Weights normalized.
        engineRotation: {
            enabled: true,
            weights: { api: 0.55, local_full: 0.30, local_shallow: 0.15 },
            shallowDepth: 11,             // depth used when 'local_shallow' picked
        },

        // --- MESSY RESIGNATION ---
        // Replace the surgical "resign at -5.0 immediately" pattern with
        // human-style behavior: hesitate, sometimes blunder once first,
        // sometimes hold completely losing positions for a while.
        messyResign: {
            enabled: true,
            blunderBeforeChance: 0.22,    // chance to play one bad move then resign
            holdLostChance: 0.18,         // chance to refuse to resign even past threshold
            extraMovesBeforeResign: { min: 0, max: 3 }, // play 0-3 more moves after threshold met
        },

        // --- HARDWARE PERSONA ---
        // Different input devices produce different mouse fingerprints. Choose
        // one per account at first run, then keep it stable.
        hardwarePersona: {
            enabled: true,
            // Multipliers applied per-persona:
            //   jitterScale: how noisy the bezier path is (trackpad > mouse > tablet)
            //   clickHoldMs: how long mousedown before mouseup
            //   speedScale:  overall drag-speed multiplier
            personas: {
                mouse:    { jitterScale: 1.00, clickHoldMs: { min: 50,  max: 110 }, speedScale: 1.00 },
                trackpad: { jitterScale: 1.45, clickHoldMs: { min: 70,  max: 150 }, speedScale: 0.85 },
                tablet:   { jitterScale: 1.20, clickHoldMs: { min: 90,  max: 180 }, speedScale: 0.95 },
            },
        },
    };

    // ═══════════════════════════════════════════
    //  ACCOUNT-LEVEL POLICY MODULE
    // ═══════════════════════════════════════════
    // Owns "between-games" decisions: warmup ramp, winrate targeting, hard
    // repertoire, tilt, hardware persona, engine rotation, TC lock.
    // Called once per game from Main.gameLoop on new-game detection.
    const Account = {
        // ---------- WARMUP ----------
        isInWarmup: () => {
            const w = CONFIG.warmup;
            if (!w.enabled && !w.manualOverride) return false;
            if (w.manualOverride) return true;
            return CONFIG.account.totalGamesPlayed < w.durationGames;
        },

        // Returns the effective target rating for THIS game (warmup-adjusted).
        effectiveTargetRating: () => {
            const baseTarget = CONFIG.targetRating;
            if (!Account.isInWarmup()) return baseTarget;

            const w = CONFIG.warmup;
            const n = CONFIG.account.totalGamesPlayed;
            // Progress 0..1 across the warmup window
            const p = Math.min(1, Math.max(0, n / Math.max(1, w.durationGames)));
            // Sigmoid: slow ramp at start, accelerate, then plateau (more human)
            const easeFn = (p) => {
                if (w.shape === 'linear') return p;
                // Smoothstep-like sigmoid centered on 0.5
                return p * p * (3 - 2 * p);
            };
            const offset = w.startEloOffset * (1 - easeFn(p));
            return Math.max(800, Math.round(baseTarget + offset));
        },

        // ---------- WINRATE TARGETING ----------
        recentWinrate: () => {
            const wt = CONFIG.winrateTarget;
            const r = CONFIG.account.recentResults || [];
            if (r.length === 0) return null;
            const window = r.slice(-wt.sampleSize);
            const wins = window.filter(x => x === 'W').length;
            const games = window.length;
            return games > 0 ? wins / games : null;
        },

        // Returns extra lose-chance to apply on top of streak-based logic.
        // 0 = no boost; 1 = guaranteed lose this game.
        winrateOvershootBoost: () => {
            const wt = CONFIG.winrateTarget;
            if (!wt.enabled) return 0;
            const wr = Account.recentWinrate();
            if (wr == null) return 0;
            const overshoot = wr - wt.target;
            if (overshoot <= 0.05) return 0; // within tolerance
            // Each +10% above target adds wt.overshootBoost worth of lose probability
            return Math.min(0.85, (overshoot / 0.10) * wt.overshootBoost);
        },

        recordResult: (result) => {
            // result: 'W' | 'L' | 'D'
            if (!result || !'WLD'.includes(result)) return;
            CONFIG.account.recentResults = (CONFIG.account.recentResults || []).slice(-50);
            CONFIG.account.recentResults.push(result);
            CONFIG.account.totalGamesPlayed = (CONFIG.account.totalGamesPlayed || 0) + 1;
            Settings.save(CONFIG);
        },

        // ---------- TILT ----------
        // _tiltGamesLeft is in-memory state; we don't persist it (sessions reset OK).
        _tiltGamesLeft: 0,

        maybeStartTilt: (lastResult) => {
            const t = CONFIG.tilt;
            if (!t.enabled) return;
            if (!t.triggerOn.includes(lastResult)) return;
            const games = Math.round(Utils.randomRange(t.duration.min, t.duration.max));
            Account._tiltGamesLeft = games;
            Utils.log(`Tilt: triggered by ${lastResult}, ${games} games of degraded play`, 'warn');
            UI.toast('Tilt', `${lastResult === 'L' ? 'Frustrated' : 'Off-balance'} after last game — playing worse for ${games} games`, 'warn', 5000);
        },

        consumeTiltTick: () => {
            // Called once per new game. Returns the active tilt config or null.
            if (Account._tiltGamesLeft <= 0) return null;
            Account._tiltGamesLeft--;
            return CONFIG.tilt;
        },

        // ---------- HARDWARE PERSONA ----------
        ensureHardwarePersona: () => {
            if (!CONFIG.hardwarePersona.enabled) return null;
            if (!CONFIG.account.hardware) {
                const choices = ['mouse', 'trackpad', 'tablet'];
                // Bias toward mouse since most desktop chess players use one
                const weights = [0.62, 0.30, 0.08];
                let r = Math.random(), acc = 0;
                for (let i = 0; i < choices.length; i++) {
                    acc += weights[i];
                    if (r < acc) { CONFIG.account.hardware = choices[i]; break; }
                }
                Utils.log(`Hardware persona chosen for this account: ${CONFIG.account.hardware}`, 'info');
                Settings.save(CONFIG);
            }
            return CONFIG.hardwarePersona.personas[CONFIG.account.hardware] || null;
        },

        currentPersona: () => {
            if (!CONFIG.hardwarePersona.enabled || !CONFIG.account.hardware) return null;
            return CONFIG.hardwarePersona.personas[CONFIG.account.hardware] || null;
        },

        // ---------- ENGINE ROTATION ----------
        rollEngineForGame: () => {
            const er = CONFIG.engineRotation;
            if (!er.enabled) {
                State.gameEngineOverride = null;
                return CONFIG.engineType;
            }
            const w = er.weights;
            const total = (w.api || 0) + (w.local_full || 0) + (w.local_shallow || 0);
            if (total <= 0) {
                State.gameEngineOverride = null;
                return CONFIG.engineType;
            }
            const r = Math.random() * total;
            let pick;
            if (r < w.api) pick = 'api';
            else if (r < w.api + w.local_full) pick = 'local_full';
            else pick = 'local_shallow';
            State.gameEngineOverride = pick;
            Utils.log(`Engine rotation: this game uses ${pick}`, 'debug');
            return pick;
        },

        // What engine should the analysis path use right now?
        // Returns one of: 'api' | 'stockfish_online' | 'local'.
        // Also returns a depth override (or null) if rotation forces shallow play.
        currentEngine: () => {
            const ovr = State.gameEngineOverride;
            if (!ovr) return { type: CONFIG.engineType, depthCap: null };
            if (ovr === 'api') return { type: 'api', depthCap: null };
            if (ovr === 'local_full') return { type: 'local', depthCap: null };
            if (ovr === 'local_shallow') return { type: 'local', depthCap: CONFIG.engineRotation.shallowDepth };
            return { type: ovr, depthCap: null };
        },

        // ---------- HARD REPERTOIRE ----------
        ensureRepertoire: () => {
            const rh = CONFIG.repertoireHard;
            if (!rh.enabled) return;
            const acct = CONFIG.account;
            if (acct.repertoire?.white && acct.repertoire?.black) return;

            const pickN = (arr, n) => {
                const copy = [...arr];
                const out = [];
                while (out.length < n && copy.length > 0) {
                    const idx = Math.floor(Math.random() * copy.length);
                    out.push(copy.splice(idx, 1)[0]);
                }
                return out;
            };

            acct.repertoire = acct.repertoire || { white: null, black: null };
            if (!acct.repertoire.white) {
                acct.repertoire.white = pickN(rh.whiteFirstMoves, rh.picksPerColor);
            }
            if (!acct.repertoire.black) {
                acct.repertoire.black = {
                    e4: pickN(rh.blackVsE4, 1)[0],
                    d4: pickN(rh.blackVsD4, 1)[0],
                    other: pickN(rh.blackVsOther, 1)[0],
                };
            }
            Utils.log(`Repertoire chosen: W=${JSON.stringify(acct.repertoire.white)} B=${JSON.stringify(acct.repertoire.black)}`, 'info');
            Settings.save(CONFIG);
        },

        // Returns repertoire-preferred move for the current position, or null.
        // Only invoked within the first 1-2 ply of a game.
        repertoireMove: (fen, legalMoves) => {
            const rh = CONFIG.repertoireHard;
            if (!rh.enabled) return null;
            if (Math.random() < rh.deviationRate) return null; // deviate

            const acct = CONFIG.account;
            if (!acct.repertoire) return null;
            const fenParts = fen.split(' ');
            const stm = fenParts[1]; // 'w' or 'b'
            const fullmove = parseInt(fenParts[5] || '1');

            // White move 1: pick from chosen white openings
            if (stm === 'w' && fullmove === 1) {
                const candidates = acct.repertoire.white || [];
                // Translate first-move SAN to a UCI move that exists in legalMoves.
                // We just match by piece destination — e.g. 'e4' = pawn to e4 = 'e2e4'
                const sanToUci = { 'e4': 'e2e4', 'd4': 'd2d4', 'c4': 'c2c4', 'Nf3': 'g1f3' };
                const ucis = candidates.map(s => sanToUci[s]).filter(Boolean);
                const found = ucis.find(u => legalMoves.includes(u));
                return found || null;
            }

            // Black move 1: pick response based on white's first move
            if (stm === 'b' && fullmove === 1) {
                const board = fenParts[0];
                const ranks = board.split('/');
                // ranks[0] = rank 8, ranks[6] = rank 2, ranks[4] = rank 4
                const expandRank = (r) => {
                    let out = '';
                    for (const ch of r) out += /\d/.test(ch) ? ' '.repeat(parseInt(ch)) : ch;
                    return out;
                };
                const rank2 = expandRank(ranks[6]); // white's original pawn row
                const rank4 = expandRank(ranks[4]); // two-up destination row
                const rank3 = expandRank(ranks[5]); // one-up destination row (1.e3, 1.d3, 1.Nf3 etc)
                let whiteFirst = 'other';
                for (let f = 0; f < 8; f++) {
                    if (rank2[f] !== 'P') {
                        const file = 'abcdefgh'[f];
                        // Either pawn went 2 up (rank4) or 1 up (rank3); only care about e/d files
                        if ((rank4[f] === 'P' || rank3[f] === 'P') && (file === 'e' || file === 'd')) {
                            whiteFirst = file + '4'; // treat 1.e3 same as 1.e4 for repertoire purposes
                        }
                        break;
                    }
                }
                const blackChoice = acct.repertoire.black?.[whiteFirst] || acct.repertoire.black?.other;
                if (!blackChoice) return null;
                const sanToUci = {
                    'c5': 'c7c5', 'e5': 'e7e5', 'e6': 'e7e6', 'c6': 'c7c6',
                    'Nf6': 'g8f6', 'd5': 'd7d5',
                };
                const uci = sanToUci[blackChoice];
                return (uci && legalMoves.includes(uci)) ? uci : null;
            }

            return null;
        },

        // ---------- TC LOCK ----------
        // Reads TC label like "3 min", "10 | 5", "Bullet" from the page header.
        readTimeControl: () => {
            try {
                const sels = [
                    '.cc-game-header-time-control',
                    '[data-test-element="game-info-time-control"]',
                    '.game-info-section-time-control',
                    '.live-game-time-control',
                ];
                for (const s of sels) {
                    const el = document.querySelector(s);
                    if (el && el.textContent.trim()) return el.textContent.trim();
                }
                // Fallback: grab anything that smells like "X min" or "X | Y"
                const candidates = document.querySelectorAll('span, div');
                for (const c of candidates) {
                    if (c.children.length) continue;
                    const t = (c.textContent || '').trim();
                    if (/^\d{1,3}\s*(min|m)$/i.test(t)) return t;
                    if (/^\d{1,3}\s*\|\s*\d{1,3}$/.test(t)) return t;
                }
            } catch (e) {}
            return null;
        },

        // Called from auto-queue. Returns true if it's OK to queue another game.
        tcLockAllowsQueue: () => {
            if (!CONFIG.tcLock.enabled) return true;
            const current = Account.readTimeControl();
            if (!current) return true; // can't read → don't block
            if (!CONFIG.account.sessionTC) {
                CONFIG.account.sessionTC = current;
                Settings.save(CONFIG);
                Utils.log(`TC lock: session locked to "${current}"`, 'info');
                return true;
            }
            if (current !== CONFIG.account.sessionTC) {
                Utils.log(`TC lock: skipping queue (current "${current}" != session "${CONFIG.account.sessionTC}")`, 'warn');
                return false;
            }
            return true;
        },

        // Called when starting a new session (after break, fresh page load)
        clearSessionTC: () => {
            CONFIG.account.sessionTC = null;
            Settings.save(CONFIG);
        },
    };

    // ═══════════════════════════════════════════
    //  RATING PROFILE SYSTEM
    // ═══════════════════════════════════════════
    const RatingProfile = {
        // Play style modifiers (multiply suboptimal rates per phase)
        styles: {
            universal:   { opening: 1.0, middlegame: 1.0, endgame: 1.0, preferTactical: 0.5 },
            aggressive:  { opening: 0.7, middlegame: 0.8, endgame: 1.3, preferTactical: 0.8 },
            positional:  { opening: 1.2, middlegame: 1.1, endgame: 0.8, preferTactical: 0.2 },
            endgame_specialist: { opening: 1.5, middlegame: 1.3, endgame: 0.4, preferTactical: 0.5 },
        },

        // Map target rating to all humanization parameters.
        //
        // Correlation targets are calibrated against Lichess / chess.com public data:
        //   800  ELO real players match SF top-1 ~35-45% of the time
        //   1500 ELO real players match SF top-1 ~45-55%
        //   2000 ELO real players match SF top-1 ~55-62%
        //   2400 ELO real players match SF top-1 ~62-68%
        //   2800 ELO (Magnus) matches SF top-1 ~65-72% peak, rarely above
        //
        // The previous profile targeted 92% correlation at 2800 which is IMPOSSIBLE
        // for a human and guaranteed chess.com fair-play review within ~10 games.
        apply(targetRating) {
            const r = Math.max(800, Math.min(2800, targetRating));
            const t = (r - 800) / 2000; // 0.0 (800) to 1.0 (2800)

            const lerp = (a, b, t) => a + (b - a) * t;
            const style = RatingProfile.styles[CONFIG.playStyle] || RatingProfile.styles.universal;

            // Engine depth: keep this moderate even at high rating. We don't need
            // depth 20 to pick a 2200-quality move — depth 14-16 is plenty and
            // avoids the telltale "all top-n moves are SF depth 20 favorites" pattern.
            CONFIG.engineDepth.base = Math.round(lerp(8, 16, t));
            CONFIG.engineDepth.min = Math.max(6, CONFIG.engineDepth.base - 3);
            CONFIG.engineDepth.max = Math.min(20, CONFIG.engineDepth.base + 3);

            // Target engine correlation: 42% at 800 → 68% at 2800 (was 40% → 92%)
            CONFIG.humanization.targetEngineCorrelation = lerp(0.42, 0.68, t);

            // Suboptimal rates — floor raised so we never play sub-8% suboptimal,
            // even at 2800. Real 2400+ players still play suboptimal 15-20% of the time
            // against Stockfish 18 benchmark.
            CONFIG.humanization.suboptimalMoveRate.opening    = lerp(0.42, 0.12, t) * style.opening;
            CONFIG.humanization.suboptimalMoveRate.middlegame = lerp(0.48, 0.18, t) * style.middlegame;
            CONFIG.humanization.suboptimalMoveRate.endgame    = lerp(0.38, 0.10, t) * style.endgame;

            // Max CP loss — tight throughout. Hanging pieces IS the ban signal.
            CONFIG.humanization.maxAcceptableCPLoss.opening    = Math.round(lerp(80, 25, t));
            CONFIG.humanization.maxAcceptableCPLoss.middlegame = Math.round(lerp(110, 35, t));
            CONFIG.humanization.maxAcceptableCPLoss.endgame    = Math.round(lerp(75, 20, t));

            // Blunder chance: 3% at 800 → 0.3% at 2800 (was 4% → 0.1%).
            // Floor raised to 0.3% so even GM-profile has occasional slips.
            CONFIG.humanization.blunder.chance = lerp(0.03, 0.003, t);
            CONFIG.humanization.blunder.maxCPLoss = Math.round(lerp(180, 80, t));

            // Streaks: cap perfect run lower so we don't chain 12 best moves.
            CONFIG.humanization.streaks.perfectStreakMax = Math.round(lerp(3, 7, t));
            CONFIG.humanization.streaks.sloppyStreakMax = Math.round(lerp(3, 1, t));

            // maxTopMoveRate: HARD cap. Was 0.50→0.95, now 0.45→0.68.
            // This is the single most important anti-ban parameter — it bounds
            // how often we can EVER play SF#1 regardless of any other logic.
            if (CONFIG.humanization.antiCorrelation) {
                CONFIG.humanization.antiCorrelation.maxTopMoveRate = lerp(0.45, 0.68, t);
            }

            // Timing: slower at low rating, but keep a human floor (800 elo players
            // still sometimes blitz out moves in ~1s).
            CONFIG.timing.base.min = Math.round(lerp(2000, 900, t));
            CONFIG.timing.base.max = Math.round(lerp(6000, 3200, t));
            CONFIG.timing.complex.min = Math.round(lerp(4200, 2200, t));
            CONFIG.timing.complex.max = Math.round(lerp(11000, 5800, t));

            CONFIG.timing.longThink.chance = lerp(0.14, 0.07, t);

            Utils.log(`Rating profile applied: ${r} ELO (style: ${CONFIG.playStyle}) | target corr ${(CONFIG.humanization.targetEngineCorrelation*100).toFixed(0)}% | SF#1 cap ${(CONFIG.humanization.antiCorrelation.maxTopMoveRate*100).toFixed(0)}%`, 'info');
            Settings.save(CONFIG);
        }
    };

    // ═══════════════════════════════════════════
    //  SELECTORS
    // ═══════════════════════════════════════════
    const SELECTORS = {
        board: ['wc-chess-board', 'chess-board'],
        chat: '.chat-scroll-area-component',
        moves: 'vertical-move-list, wc-move-list, .move-list-component',
        clocks: '.clock-component, .clock-time-monospace',
        gameOver: [
            '[data-cy="game-over-modal-content"]',
            '.game-over-modal-shell-content',
            '.game-over-modal-container',
            '.modal-game-over-component',
            '[data-cy="game-over-modal"]',
            '.game-over-modal-buttons',
            '.game-over-buttons-component',
            '.game-result-header'
        ],
        gameOverResult: {
            win: [
                '.game-over-modal-header-userWon',
                '[data-cy="header-title-component"]:not(:empty)',
            ],
            loss: [
                '.game-over-modal-header-userLost',
            ],
            draw: [
                '.game-over-modal-header-draw',
            ],
        },
        drawOffer: '.draw-offer-component',
        promotion: {
            dialog: '.promotion-window, .promotion-piece',
            items: '.promotion-piece'
        }
    };

    // ═══════════════════════════════════════════
    //  STATE
    // ═══════════════════════════════════════════
    const State = {
        engineReady: false,
        apiAvailable: true,        // assume API works until proven otherwise
        apiFailCount: 0,
        // Per-game engine override (set by Account.rollEngineForGame).
        // 'api' | 'stockfish_online' | 'local_full' | 'local_shallow' | null
        gameEngineOverride: null,
        isThinking: false,
        lastFen: null,
        playerColor: null,
        gameId: null,
        moveCount: 0,
        boredomLevel: 0,
        personality: null,
        ui: {
            overlay: null,
            panel: null,
            statusDot: null,
            autoIndicator: null
        },
        workers: {
            stockfish: null
        },
        cache: {
            fen: new Map(),
            board: null,
            openingMoves: [],   // track opening moves for repertoire consistency
        },
        candidates: {},
        currentEval: null,
        currentBestMove: null,
        opponentResponse: null,
        apiResult: null,           // result from API engine
        // Clock tracking
        clock: {
            myTime: null,          // seconds remaining
            oppTime: null,
        },
        // Recent move timings for sequence variation
        recentTimings: [],
        // Humanization tracking
        human: {
            perfectStreak: 0,
            sloppyStreak: 0,
            bestMoveCount: 0,
            totalMoveCount: 0,
            gamePersonality: null,
            lastMoveWasBest: true,
            // Accuracy cluster state
            clusterMode: 'normal',  // 'normal', 'hot', 'cold'
            clusterMovesLeft: 0,
            // Predicted opponent reply (for premove simulation)
            predictedReply: null,
            predictedReplyFen: null,
            // Auto-resign: track consecutive losing evals
            consecutiveLosingEvals: 0,
            // Auto-lose mode active flag
            autoLoseActive: false,
            // Opponent rating (read from DOM)
            opponentRating: null,
            // Time pressure accuracy multipliers (live)
            timePressureMult: { suboptimal: 1, blunder: 1, maxCPLoss: 1 },
            // Anti-correlation: track how often we play SF#1
            topMoveCount: 0,
            // Weakness profile (generated from seed)
            weaknesses: null,
            // Timing-accuracy coupling: pre-decided think time category for current move
            thinkCategory: 'normal', // 'fast', 'normal', 'slow'
            // Opponent-move surprise: was the opponent's last move expected?
            opponentMoveSurprise: 0, // 0 = expected, 1 = surprising, set each move
            // Think momentum: how long we thought last move (ms)
            lastThinkTime: 0,
            // Track the eval BEFORE opponent moved, to measure surprise
            evalBeforeOpponentMove: null,
            // Persistent player tempo (seeded per account, set once)
            playerTempo: 1.0,  // multiplier: <1 = fast player, >1 = slow player
            playerAccuracyBand: 0, // small offset to base suboptimal rate
        },
        // Player move DB cache (FEN -> moves array)
        playerDBCache: new Map(),
        // Track if last move was a capture (for recapture detection)
        lastMoveWasCapture: false,
    };

    // ═══════════════════════════════════════════
    //  UTILITIES
    // ═══════════════════════════════════════════
    const Utils = {
        sleep: (ms) => new Promise(r => setTimeout(r, ms)),

        log: (msg, type = 'info') => {
            const colors = {
                info: '#3eff3e',
                warn: '#ffcc00',
                error: '#ff4444',
                debug: '#aaaaff'
            };
            console.log(`%c[BA] ${msg}`, `color: ${colors[type]}; font-weight: bold;`);
        },

        randomRange: (min, max) => Math.random() * (max - min) + min,

        gaussianRandom: (mean = 0, stdDev = 1) => {
            const u1 = Math.random();
            const u2 = Math.random();
            const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
            return mean + z * stdDev;
        },

        humanDelay: (min, max) => {
            const median = (min + max) / 2;
            const sigma = 0.6;
            const logNormal = Math.exp(Utils.gaussianRandom(Math.log(median), sigma));
            return Math.max(min * 0.7, Math.min(max * 1.4, logNormal));
        },

        isTabActive: () => !document.hidden,

        query: (selector, root = document) => {
            if (Array.isArray(selector)) {
                for (const s of selector) {
                    const el = root.querySelector(s);
                    if (el) return el;
                }
                return null;
            }
            return root.querySelector(selector);
        },

        countPieces: (fen) => {
            if (!fen) return 32;
            const board = fen.split(' ')[0];
            return (board.match(/[rnbqkpRNBQKP]/g) || []).length;
        },
    };

    // ═══════════════════════════════════════════
    //  WEAKNESS PROFILE — persistent per-account human identity
    // ═══════════════════════════════════════════
    const WeaknessProfile = {
        // Seeded PRNG (mulberry32) for deterministic weakness generation
        _prng: (seed) => {
            let s = seed | 0;
            return () => {
                s = (s + 0x6D2B79F5) | 0;
                let t = Math.imul(s ^ (s >>> 15), 1 | s);
                t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
                return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
            };
        },

        // All possible weakness dimensions a human can have
        _dimensions: {
            // Piece-type weaknesses: worse at using/defending specific pieces
            pieces: ['knight', 'bishop', 'rook', 'queen'],
            // Phase weaknesses: worse in specific game phases
            phases: ['opening', 'middlegame', 'endgame'],
            // Endgame-type weaknesses
            endgames: ['rook_endgame', 'bishop_endgame', 'knight_endgame', 'pawn_endgame', 'queen_endgame'],
            // Tactical motif blind spots
            tactics: ['fork', 'pin', 'skewer', 'discovery', 'back_rank', 'deflection'],
            // Positional blind spots
            positional: ['pawn_structure', 'king_safety', 'piece_activity', 'space', 'weak_squares'],
        },

        init: () => {
            const wp = CONFIG.humanization.weaknessProfile;
            if (!wp.enabled) {
                State.human.weaknesses = { pieces: [], phases: {}, endgames: [], tactics: [], positional: [], extraErrorRate: {} };
                return;
            }

            // Generate or load seed
            if (!wp.seed) {
                wp.seed = Math.floor(Math.random() * 2147483647);
                Settings.save(CONFIG);
                Utils.log(`WeaknessProfile: Generated new seed ${wp.seed}`, 'info');
            }

            const rng = WeaknessProfile._prng(wp.seed);
            const pick = (arr, count) => {
                const shuffled = [...arr].sort(() => rng() - 0.5);
                return shuffled.slice(0, count);
            };

            // Each account gets 1-2 piece weaknesses, 1 phase weakness, 1-2 endgame weaknesses,
            // 1-2 tactical blind spots, 1 positional weakness
            const d = WeaknessProfile._dimensions;
            const weakPieces = pick(d.pieces, 1 + (rng() < 0.4 ? 1 : 0));
            const weakEndgames = pick(d.endgames, 1 + (rng() < 0.5 ? 1 : 0));
            const weakTactics = pick(d.tactics, 1 + (rng() < 0.35 ? 1 : 0));
            const weakPositional = pick(d.positional, 1);

            // Phase weakness: one phase is noticeably worse
            const phaseWeights = {};
            for (const p of d.phases) {
                phaseWeights[p] = 1.0; // baseline
            }
            const worstPhase = pick(d.phases, 1)[0];
            phaseWeights[worstPhase] = 1.15 + rng() * 0.25; // 1.15-1.40x more errors in weak phase

            // Generate extra error rates for each weakness (how much worse they are)
            const extraError = {};
            for (const p of weakPieces) extraError[`piece_${p}`] = 0.04 + rng() * 0.05;
            for (const e of weakEndgames) extraError[`endgame_${e}`] = 0.05 + rng() * 0.06;
            for (const t of weakTactics) extraError[`tactic_${t}`] = 0.05 + rng() * 0.05;
            for (const p of weakPositional) extraError[`positional_${p}`] = 0.03 + rng() * 0.04;

            State.human.weaknesses = {
                pieces: weakPieces,
                phases: phaseWeights,
                endgames: weakEndgames,
                tactics: weakTactics,
                positional: weakPositional,
                extraErrorRate: extraError,
            };

            // --- Multi-game behavioral consistency (item 5) ---
            // Generate persistent player tempo and accuracy band from the same seed
            // These make the account feel like a consistent person across many games
            State.human.playerTempo = 0.80 + rng() * 0.40; // 0.80-1.20 (fast player vs slow player)
            State.human.playerAccuracyBand = -0.04 + rng() * 0.08; // -0.04 to +0.04 shift on suboptimal rate

            Utils.log(`WeaknessProfile [seed=${wp.seed}]: pieces=${weakPieces}, phase=${worstPhase}(x${phaseWeights[worstPhase].toFixed(2)}), endgames=${weakEndgames}, tactics=${weakTactics}, positional=${weakPositional}, tempo=${State.human.playerTempo.toFixed(2)}, accuracyBand=${State.human.playerAccuracyBand.toFixed(3)}`, 'info');
        },

        // Returns extra suboptimal rate for the current position based on weaknesses
        getExtraErrorRate: (fen, move) => {
            const w = State.human.weaknesses;
            if (!w || !CONFIG.humanization.weaknessProfile.enabled) return 0;

            let extra = 0;
            const phase = HumanStrategy.getGamePhase(fen);

            // Phase weakness multiplier (applied as a multiplier to total rate externally)
            // Here we return additive bonus
            const phaseBonus = (w.phases[phase] || 1.0) - 1.0;
            extra += phaseBonus * 0.06; // convert multiplier to small additive rate

            // Piece involvement: check if the moving piece matches a weakness
            if (move && move.length >= 4) {
                const fromSq = move.substring(0, 2);
                const movingPiece = WeaknessProfile._identifyPiece(fen, fromSq);
                if (movingPiece && w.pieces.includes(movingPiece)) {
                    extra += w.extraErrorRate[`piece_${movingPiece}`] || 0;
                }
            }

            // Endgame type weakness
            if (phase === 'endgame') {
                const pieces = Utils.countPieces(fen);
                const egType = WeaknessProfile._classifyEndgame(fen);
                if (egType && w.endgames.includes(egType)) {
                    extra += w.extraErrorRate[`endgame_${egType}`] || 0;
                }
            }

            return Math.min(extra, 0.15); // cap total extra at 15%
        },

        _identifyPiece: (fen, sq) => {
            const board = fen.split(' ')[0];
            const file = sq.charCodeAt(0) - 97;
            const rank = parseInt(sq[1]) - 1;
            const rows = board.split('/').reverse();
            if (!rows[rank]) return null;
            let col = 0;
            for (const ch of rows[rank]) {
                if (/\d/.test(ch)) { col += parseInt(ch); continue; }
                if (col === file) {
                    const map = { n: 'knight', b: 'bishop', r: 'rook', q: 'queen', k: 'king', p: 'pawn' };
                    return map[ch.toLowerCase()] || null;
                }
                col++;
            }
            return null;
        },

        _classifyEndgame: (fen) => {
            const board = fen.split(' ')[0].toLowerCase();
            const hasQ = board.includes('q');
            const hasR = board.includes('r');
            const hasB = board.includes('b');
            const hasN = board.includes('n');
            if (hasQ && !hasR && !hasB && !hasN) return 'queen_endgame';
            if (hasR && !hasQ && !hasB && !hasN) return 'rook_endgame';
            if (hasB && !hasQ && !hasR && !hasN) return 'bishop_endgame';
            if (hasN && !hasQ && !hasR && !hasB) return 'knight_endgame';
            if (!hasQ && !hasR && !hasB && !hasN) return 'pawn_endgame';
            return null; // mixed — no specific type
        },
    };

    // ═══════════════════════════════════════════
    //  PLAYER MOVE DATABASE — Lichess rating-filtered human moves
    // ═══════════════════════════════════════════
    const PlayerMoveDB = {
        fetch: (fen) => {
            const cfg = CONFIG.humanization.playerMoveDB;
            if (!cfg.enabled) return Promise.resolve(null);

            const cacheKey = fen.split(' ').slice(0, 4).join(' ');
            if (State.playerDBCache.has(cacheKey)) {
                return Promise.resolve(State.playerDBCache.get(cacheKey));
            }

            const rating = CONFIG.targetRating;
            const minR = Math.max(400, rating - cfg.ratingWindow);
            const maxR = Math.min(2800, rating + cfg.ratingWindow);
            // Lichess explorer API: player rating-filtered games
            const ratingStr = `${minR},${maxR}`;
            const url = `https://explorer.lichess.ovh/lichess?fen=${encodeURIComponent(fen)}&ratings=${encodeURIComponent(ratingStr)}&speeds=blitz,rapid,classical&topGames=0&recentGames=0`;

            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    timeout: cfg.timeout,
                    onload: (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.moves && data.moves.length > 0) {
                                const validMoves = data.moves
                                    .map(m => ({
                                        uci: m.uci,
                                        games: m.white + m.draws + m.black,
                                        winRate: m.white / Math.max(1, m.white + m.draws + m.black),
                                    }))
                                    .filter(m => m.games >= cfg.minGames);

                                if (validMoves.length > 0) {
                                    State.playerDBCache.set(cacheKey, validMoves);
                                    Utils.log(`PlayerMoveDB: ${validMoves.length} moves at ${minR}-${maxR} ELO`, 'debug');
                                    resolve(validMoves);
                                    return;
                                }
                            }
                            State.playerDBCache.set(cacheKey, null);
                            resolve(null);
                        } catch (e) {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null),
                    ontimeout: () => resolve(null),
                });
            });
        },

        // Pick a move from the player DB weighted by game count
        pickMove: (moves) => {
            if (!moves || moves.length === 0) return null;
            const totalGames = moves.reduce((s, m) => s + m.games, 0);
            let r = Math.random() * totalGames;
            for (const m of moves) {
                r -= m.games;
                if (r <= 0) return m.uci;
            }
            return moves[0].uci;
        },
    };

    // ═══════════════════════════════════════════
    //  UI
    // ═══════════════════════════════════════════
    const UI = {
        _styleAccents: {
            universal:           '#4caf50',
            aggressive:          '#ff5722',
            positional:          '#2196f3',
            endgame_specialist:  '#9c27b0',
        },

        setAccent: () => {
            const color = UI._styleAccents[CONFIG.playStyle] || '#4caf50';
            document.documentElement.style.setProperty('--ba-accent', color);
        },

        injectStyles: () => {
            GM_addStyle(`
                @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;700&display=swap');
                .ba-overlay { pointer-events: none; z-index: 10000; position: absolute; top: 0; left: 0; transition: opacity 0.2s; }
                .ba-stealth { opacity: 0 !important; pointer-events: none !important; }
                :root { --ba-accent: #4caf50; }
                .ba-panel {
                    position: fixed; top: 50px; left: 50px; z-index: 10001;
                    width: 320px;
                    background: rgba(10, 10, 12, 0.95);
                    color: #e0e0e0;
                    border: 1px solid #333;
                    border-left: 2px solid var(--ba-accent);
                    border-radius: 8px;
                    font-family: 'Inter', sans-serif;
                    box-shadow: 0 10px 40px rgba(0,0,0,0.6);
                    overflow: hidden;
                    display: flex; flex-direction: column;
                }
                .ba-header {
                    padding: 12px 16px;
                    background: linear-gradient(90deg, color-mix(in srgb, var(--ba-accent) 10%, transparent), transparent);
                    border-bottom: 1px solid rgba(255,255,255,0.05);
                    display: flex; justify-content: space-between; align-items: center;
                    cursor: grab; user-select: none;
                }
                .ba-logo { font-weight: 800; font-size: 14px; letter-spacing: 1px; color: var(--ba-accent); }
                .ba-logo span { color: #fff; opacity: 0.7; font-weight: 400; }
                .ba-minimize { cursor: pointer; opacity: 0.5; transition: 0.2s; font-size: 16px; }
                .ba-minimize:hover { opacity: 1; color: #fff; }
                .ba-tabs { display: flex; background: rgba(0,0,0,0.2); flex-wrap: wrap; }
                .ba-tab {
                    flex: 1; text-align: center; padding: 8px 0;
                    font-size: 10px; font-weight: 600; color: #666;
                    cursor: pointer; transition: 0.2s;
                    border-bottom: 2px solid transparent;
                    min-width: 60px;
                }
                .ba-tab:hover { color: #aaa; background: rgba(255,255,255,0.02); }
                .ba-tab.active { color: #e0e0e0; border-bottom: 2px solid var(--ba-accent); background: color-mix(in srgb, var(--ba-accent) 5%, transparent); }
                .ba-content { padding: 14px; min-height: 150px; max-height: 420px; overflow-y: auto; }
                .ba-page { display: none; }
                .ba-page.active { display: block; animation: fadeIn 0.2s; }
                .ba-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
                .ba-label { font-size: 11px; color: #aaa; }
                .ba-value { font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--ba-accent); }
                .ba-slider-container { margin-bottom: 14px; }
                .ba-slider-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
                .ba-slider {
                    -webkit-appearance: none; width: 100%; height: 4px;
                    background: #333; border-radius: 2px; outline: none;
                }
                .ba-slider::-webkit-slider-thumb {
                    -webkit-appearance: none; width: 12px; height: 12px;
                    background: var(--ba-accent); border-radius: 50%; cursor: pointer;
                    box-shadow: 0 0 10px color-mix(in srgb, var(--ba-accent) 40%, transparent);
                }
                .ba-checkbox {
                    width: 16px; height: 16px; border: 1px solid #444;
                    background: #111; border-radius: 3px; cursor: pointer;
                    display: flex; align-items: center; justify-content: center;
                    flex-shrink: 0;
                }
                .ba-checkbox.checked { background: var(--ba-accent); border-color: var(--ba-accent); }
                .ba-checkbox.checked::after { content: '✓'; font-size: 10px; color: #000; font-weight: bold; }
                .ba-status-box {
                    background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
                    border-radius: 6px; padding: 12px; margin-bottom: 14px; text-align: center;
                }
                .ba-eval-large { font-family: 'JetBrains Mono'; font-size: 24px; font-weight: 700; color: #fff; margin-bottom: 4px; display: block; }
                .ba-best-move-large { font-family: 'JetBrains Mono'; font-size: 14px; color: var(--ba-accent); background: color-mix(in srgb, var(--ba-accent) 10%, transparent); padding: 4px 8px; border-radius: 4px; display: inline-block; }
                .ba-engine-badge {
                    font-size: 9px; padding: 2px 6px; border-radius: 3px; margin-top: 6px; display: inline-block;
                    background: color-mix(in srgb, var(--ba-accent) 15%, transparent); color: var(--ba-accent); font-family: 'JetBrains Mono';
                }
                .ba-stats-row { display: flex; justify-content: space-between; margin-bottom: 6px; padding: 4px 8px; background: rgba(255,255,255,0.02); border-radius: 4px; }
                .ba-stats-label { font-size: 10px; color: #888; }
                .ba-stats-value { font-size: 10px; color: #ccc; font-family: 'JetBrains Mono'; }
                .ba-select {
                    background: #1a1a1a; color: #e0e0e0; border: 1px solid #444;
                    border-radius: 4px; padding: 4px 8px; font-size: 11px;
                    font-family: 'Inter', sans-serif; cursor: pointer; outline: none;
                }
                .ba-select:focus { border-color: var(--ba-accent); }
                input[type="color"] { -webkit-appearance: none; border: none; }
                input[type="color"]::-webkit-color-swatch-wrapper { padding: 2px; }
                input[type="color"]::-webkit-color-swatch { border: none; border-radius: 3px; }
                .ba-section-title { font-size: 10px; font-weight: 600; color: #666; text-transform: uppercase; letter-spacing: 1px; margin: 12px 0 8px 0; }
                .ba-footer {
                    padding: 8px 16px; font-size: 10px; color: #555;
                    border-top: 1px solid rgba(255,255,255,0.05);
                    display: flex; gap: 12px;
                }
                .ba-key { color: #888; background: #222; padding: 1px 4px; border-radius: 3px; font-family: monospace; border: 1px solid #333; }
                .ba-minimized .ba-tabs, .ba-minimized .ba-content, .ba-minimized .ba-footer { display: none; }
                @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
                .ba-arrow { stroke-linecap: round; filter: drop-shadow(0 0 3px rgba(0,0,0,0.4)); }
                @keyframes ba-dash-flow { from { stroke-dashoffset: 16; } to { stroke-dashoffset: 0; } }
                @keyframes ba-pulse-ring { 0%,100% { opacity: 0.5; } 50% { opacity: 0.9; } }
                @keyframes ba-fade-in { from { opacity: 0; } to { opacity: 1; } }
                .ba-arrow-group { animation: ba-fade-in 0.25s ease-out; }
                .ba-dash-anim { animation: ba-dash-flow 0.6s linear infinite; }
                .ba-pulse { animation: ba-pulse-ring 1.8s ease-in-out infinite; }
                .ba-toast-container {
                    position: fixed; bottom: 16px; right: 16px; z-index: 100001;
                    display: flex; flex-direction: column-reverse; gap: 8px; pointer-events: none;
                    max-height: 50vh; overflow: hidden;
                }
                .ba-toast {
                    pointer-events: auto;
                    font-family: 'Inter', 'Segoe UI', sans-serif;
                    font-size: 12px; line-height: 1.4;
                    padding: 10px 14px 10px 12px;
                    border-radius: 8px;
                    color: #e0e0e0;
                    background: #1a1a2eee;
                    border-left: 3px solid var(--ba-accent);
                    box-shadow: 0 4px 20px rgba(0,0,0,0.5);
                    backdrop-filter: blur(8px);
                    display: flex; align-items: flex-start; gap: 8px;
                    max-width: 320px; min-width: 200px;
                    animation: ba-toast-in 0.3s ease-out;
                    position: relative; overflow: hidden;
                    cursor: pointer;
                }
                .ba-toast:hover { opacity: 0.85; }
                .ba-toast-icon { font-size: 14px; flex-shrink: 0; margin-top: 1px; }
                .ba-toast-body { flex: 1; }
                .ba-toast-title { font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 2px; }
                .ba-toast-msg { color: #bbb; font-size: 11.5px; }
                .ba-toast-progress {
                    position: absolute; bottom: 0; left: 0; height: 2px;
                    background: currentColor; opacity: 0.4;
                    animation: ba-toast-timer linear forwards;
                }
                .ba-toast.ba-toast-info { border-left-color: #42a5f5; }
                .ba-toast.ba-toast-warn { border-left-color: #ffb74d; }
                .ba-toast.ba-toast-error { border-left-color: #ef5350; }
                .ba-toast.ba-toast-success { border-left-color: #66bb6a; }
                .ba-toast.ba-toast-move { border-left-color: #ab47bc; }
                .ba-toast-info .ba-toast-title { color: #42a5f5; }
                .ba-toast-warn .ba-toast-title { color: #ffb74d; }
                .ba-toast-error .ba-toast-title { color: #ef5350; }
                .ba-toast-success .ba-toast-title { color: #66bb6a; }
                .ba-toast-move .ba-toast-title { color: #ab47bc; }
                @keyframes ba-toast-in { from { opacity: 0; transform: translateX(60px); } to { opacity: 1; transform: translateX(0); } }
                @keyframes ba-toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(60px); } }
                @keyframes ba-toast-timer { from { width: 100%; } to { width: 0%; } }
            `);
        },


        createInterface: () => {
            if (State.ui.panel) return;
            const panel = document.createElement('div');
            panel.className = 'ba-panel';
            panel.innerHTML = `
                <div class="ba-header">
                    <div class="ba-logo">REXXX<span>.MENU v15.1</span></div>
                    <div class="ba-minimize">_</div>
                </div>
                <div class="ba-tabs">
                    <div class="ba-tab active" data-tab="main">MAIN</div>
                    <div class="ba-tab" data-tab="engine">ENGINE</div>
                    <div class="ba-tab" data-tab="timings">TIMINGS</div>
                    <div class="ba-tab" data-tab="safety">SAFETY</div>
                    <div class="ba-tab" data-tab="visuals">VISUALS</div>
                    <div class="ba-tab" data-tab="stats">STATS</div>
                </div>
                <div class="ba-content">
                    <!-- MAIN TAB -->
                    <div id="tab-main" class="ba-page active">
                        <div class="ba-status-box">
                            <span class="ba-eval-large" id="eval-display">0.00</span>
                            <span class="ba-best-move-large" id="move-display">Waiting...</span>
                            <div class="ba-engine-badge" id="engine-badge">LOCAL SF10</div>
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Auto-Play</span>
                            <div class="ba-checkbox ${CONFIG.auto.enabled ? 'checked' : ''}" id="toggle-auto"></div>
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Opening Book</span>
                            <div class="ba-checkbox ${CONFIG.useBook ? 'checked' : ''}" id="toggle-book"></div>
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Tablebase (Endgame)</span>
                            <div class="ba-checkbox ${CONFIG.useTablebase ? 'checked' : ''}" id="toggle-tablebase"></div>
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Target Rating</span>
                                <span class="ba-value" id="val-rating">${CONFIG.targetRating}</span>
                            </div>
                            <input type="range" class="ba-slider" min="800" max="2800" step="50" value="${CONFIG.targetRating}" id="slide-rating">
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Play Style</span>
                            <select class="ba-select" id="select-style">
                                <option value="universal" ${CONFIG.playStyle === 'universal' ? 'selected' : ''}>Universal</option>
                                <option value="aggressive" ${CONFIG.playStyle === 'aggressive' ? 'selected' : ''}>Aggressive</option>
                                <option value="positional" ${CONFIG.playStyle === 'positional' ? 'selected' : ''}>Positional</option>
                                <option value="endgame_specialist" ${CONFIG.playStyle === 'endgame_specialist' ? 'selected' : ''}>Endgame Specialist</option>
                            </select>
                        </div>
                    </div>
                    <!-- ENGINE TAB -->
                    <div id="tab-engine" class="ba-page">
                        <div class="ba-section-title">Engine Source</div>
                        <div class="ba-row">
                            <span class="ba-label">Primary Engine</span>
                            <select class="ba-select" id="select-engine">
                                <option value="api" ${CONFIG.engineType === 'api' ? 'selected' : ''}>chess-api.com (SF18)</option>
                                <option value="stockfish_online" ${CONFIG.engineType === 'stockfish_online' ? 'selected' : ''}>stockfish.online (SF16)</option>
                                <option value="local" ${CONFIG.engineType === 'local' ? 'selected' : ''}>Local SF10 (Fallback)</option>
                            </select>
                        </div>
                        <div class="ba-section-title">Depth</div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Base Depth</span>
                                <span class="ba-value" id="val-depth">${CONFIG.engineDepth.base}</span>
                            </div>
                            <input type="range" class="ba-slider" min="4" max="22" value="${CONFIG.engineDepth.base}" id="slide-depth">
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Dynamic Depth</span>
                            <div class="ba-checkbox ${CONFIG.engineDepth.dynamicDepth ? 'checked' : ''}" id="toggle-dynamic-depth"></div>
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Multi-PV Lines</span>
                                <span class="ba-value" id="val-mpv">${CONFIG.multiPV}</span>
                            </div>
                            <input type="range" class="ba-slider" min="1" max="8" value="${CONFIG.multiPV}" id="slide-mpv">
                        </div>
                    </div>
                    <!-- TIMINGS TAB -->
                    <div id="tab-timings" class="ba-page">
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Engine Correlation %</span>
                                <span class="ba-value" id="val-corr">${Math.round(CONFIG.humanization.targetEngineCorrelation * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="30" max="95" value="${Math.round(CONFIG.humanization.targetEngineCorrelation * 100)}" id="slide-corr">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Base Min Delay (ms)</span>
                                <span class="ba-value" id="val-min">${CONFIG.timing.base.min}</span>
                            </div>
                            <input type="range" class="ba-slider" min="200" max="5000" value="${CONFIG.timing.base.min}" id="slide-min">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Base Max Delay (ms)</span>
                                <span class="ba-value" id="val-max">${CONFIG.timing.base.max}</span>
                            </div>
                            <input type="range" class="ba-slider" min="500" max="12000" value="${CONFIG.timing.base.max}" id="slide-max">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Middlegame Suboptimal %</span>
                                <span class="ba-value" id="val-subopt">${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="5" max="70" value="${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}" id="slide-subopt">
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Clock-Aware Timing</span>
                            <div class="ba-checkbox ${CONFIG.timing.clockAware.enabled ? 'checked' : ''}" id="toggle-clock-aware"></div>
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Premove Simulation</span>
                            <div class="ba-checkbox ${CONFIG.timing.premove.enabled ? 'checked' : ''}" id="toggle-premove"></div>
                        </div>
                    </div>
                    <!-- SAFETY TAB -->
                    <div id="tab-safety" class="ba-page">
                        <div class="ba-section-title">Anti-Detection</div>
                        <div class="ba-row">
                            <span class="ba-label">Random AFK Pauses</span>
                            <div class="ba-checkbox ${CONFIG.antiDetection.randomAFK.enabled ? 'checked' : ''}" id="toggle-random-afk"></div>
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">AFK Chance %</span>
                                <span class="ba-value" id="val-afk-chance">${Math.round(CONFIG.antiDetection.randomAFK.chance * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="1" max="20" value="${Math.round(CONFIG.antiDetection.randomAFK.chance * 100)}" id="slide-afk-chance">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Random Legal Move %</span>
                                <span class="ba-value" id="val-random-legal">${Math.round(CONFIG.antiDetection.randomLegalMoveChance * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="0" max="15" value="${Math.round(CONFIG.antiDetection.randomLegalMoveChance * 100)}" id="slide-random-legal">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Max Games / Hour</span>
                                <span class="ba-value" id="val-max-gph">${CONFIG.antiDetection.maxGamesPerHour}</span>
                            </div>
                            <input type="range" class="ba-slider" min="2" max="15" value="${CONFIG.antiDetection.maxGamesPerHour}" id="slide-max-gph">
                        </div>
                        <div class="ba-section-title">Session</div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Max Games / Session</span>
                                <span class="ba-value" id="val-max-session">${CONFIG.session.maxGamesPerSession}</span>
                            </div>
                            <input type="range" class="ba-slider" min="2" max="20" value="${CONFIG.session.maxGamesPerSession}" id="slide-max-session">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Break Duration (min)</span>
                                <span class="ba-value" id="val-break-dur">${Math.round(CONFIG.session.breakDurationMs / 60000)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="1" max="15" value="${Math.round(CONFIG.session.breakDurationMs / 60000)}" id="slide-break-dur">
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Auto-Queue</span>
                            <div class="ba-checkbox ${CONFIG.auto.autoQueue ? 'checked' : ''}" id="toggle-auto-queue"></div>
                        </div>
                        <div class="ba-section-title">Auto-Lose</div>
                        <div class="ba-row">
                            <span class="ba-label">Enabled</span>
                            <div class="ba-checkbox ${CONFIG.autoLose.enabled ? 'checked' : ''}" id="toggle-auto-lose"></div>
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Trigger Win Streak</span>
                                <span class="ba-value" id="val-lose-streak">${CONFIG.autoLose.triggerStreak}</span>
                            </div>
                            <input type="range" class="ba-slider" min="2" max="10" value="${CONFIG.autoLose.triggerStreak}" id="slide-lose-streak">
                        </div>
                        <div class="ba-section-title">Auto-Resign</div>
                        <div class="ba-row">
                            <span class="ba-label">Enabled</span>
                            <div class="ba-checkbox ${CONFIG.autoResign.enabled ? 'checked' : ''}" id="toggle-auto-resign"></div>
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Resign Eval Threshold</span>
                                <span class="ba-value" id="val-resign-eval">${CONFIG.autoResign.evalThreshold}</span>
                            </div>
                            <input type="range" class="ba-slider" min="-10" max="-2" step="0.5" value="${CONFIG.autoResign.evalThreshold}" id="slide-resign-eval">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Resign Chance %</span>
                                <span class="ba-value" id="val-resign-chance">${Math.round(CONFIG.autoResign.resignChance * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="10" max="100" value="${Math.round(CONFIG.autoResign.resignChance * 100)}" id="slide-resign-chance">
                        </div>
                        <div class="ba-section-title">Opponent Adaptation</div>
                        <div class="ba-row">
                            <span class="ba-label">Enabled</span>
                            <div class="ba-checkbox ${CONFIG.opponentAdaptation.enabled ? 'checked' : ''}" id="toggle-opp-adapt"></div>
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Rating Edge</span>
                                <span class="ba-value" id="val-rating-edge">${CONFIG.opponentAdaptation.ratingEdge}</span>
                            </div>
                            <input type="range" class="ba-slider" min="50" max="500" step="25" value="${CONFIG.opponentAdaptation.ratingEdge}" id="slide-rating-edge">
                        </div>
                        <div class="ba-section-title">Time Pressure</div>
                        <div class="ba-row">
                            <span class="ba-label">Accuracy Drop</span>
                            <div class="ba-checkbox ${CONFIG.timePressure.enabled ? 'checked' : ''}" id="toggle-time-pressure"></div>
                        </div>
                        <div class="ba-section-title">Humanization</div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Blunder Chance %</span>
                                <span class="ba-value" id="val-blunder">${(CONFIG.humanization.blunder.chance * 100).toFixed(1)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="0" max="15" step="0.5" value="${(CONFIG.humanization.blunder.chance * 100).toFixed(1)}" id="slide-blunder">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Max Win Streak</span>
                                <span class="ba-value" id="val-max-streak">${CONFIG.session.maxWinStreak}</span>
                            </div>
                            <input type="range" class="ba-slider" min="2" max="10" value="${CONFIG.session.maxWinStreak}" id="slide-max-streak">
                        </div>

                        <div class="ba-section-title">Anti-Detection Layers</div>
                        <div class="ba-row" title="Master switch for all humanization. OFF = pure engine play (= instant ban).">
                            <span class="ba-label">Humanization Master</span>
                            <div class="ba-checkbox ${CONFIG.humanization.enabled ? 'checked' : ''}" id="toggle-human-master"></div>
                        </div>
                        <div class="ba-row" title="Vary streaks of best moves and sloppy moves so we don't chain perfect play forever.">
                            <span class="ba-label">Streak Limiter</span>
                            <div class="ba-checkbox ${CONFIG.humanization.streaks.enabled ? 'checked' : ''}" id="toggle-streaks"></div>
                        </div>
                        <div class="ba-row" title="Cluster accuracy across moves so games don't all look identical.">
                            <span class="ba-label">Accuracy Clustering</span>
                            <div class="ba-checkbox ${CONFIG.humanization.accuracyClustering.enabled ? 'checked' : ''}" id="toggle-clustering"></div>
                        </div>
                        <div class="ba-row" title="Persistent per-account 'weakness' (e.g. weaker in endgames). Makes profile coherent over time.">
                            <span class="ba-label">Weakness Profile</span>
                            <div class="ba-checkbox ${CONFIG.humanization.weaknessProfile.enabled ? 'checked' : ''}" id="toggle-weakness"></div>
                        </div>
                        <div class="ba-row" title="Use Lichess explorer to pick moves real human players make in the same position.">
                            <span class="ba-label">Player Move DB</span>
                            <div class="ba-checkbox ${CONFIG.humanization.playerMoveDB.enabled ? 'checked' : ''}" id="toggle-playerdb"></div>
                        </div>
                        <div class="ba-row" title="Couple thinking time and move quality (long think -> better move).">
                            <span class="ba-label">Timing Coupling</span>
                            <div class="ba-checkbox ${CONFIG.humanization.timingAccuracyCoupling.enabled ? 'checked' : ''}" id="toggle-timing-couple"></div>
                        </div>
                        <div class="ba-row" title="Occasionally start dragging toward the wrong square then redirect.">
                            <span class="ba-label">Change-of-Mind Drag</span>
                            <div class="ba-checkbox ${CONFIG.antiDetection.changeOfMind.enabled ? 'checked' : ''}" id="toggle-cofmind"></div>
                        </div>
                        <div class="ba-row" title="Background mouse hovers, premove flicks, UI clicks to look like an active player.">
                            <span class="ba-label">Telemetry Noise</span>
                            <div class="ba-checkbox ${CONFIG.antiDetection.telemetryNoise.enabled ? 'checked' : ''}" id="toggle-telemetry"></div>
                        </div>
                        <div class="ba-slider-container" title="Hard cap on how often we may play SF#1. Lower = safer but weaker.">
                            <div class="ba-slider-header">
                                <span class="ba-label">SF#1 Move Cap %</span>
                                <span class="ba-value" id="val-topcap">${Math.round(CONFIG.humanization.antiCorrelation.maxTopMoveRate * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="35" max="80" value="${Math.round(CONFIG.humanization.antiCorrelation.maxTopMoveRate * 100)}" id="slide-topcap">
                        </div>
                        <div class="ba-slider-container" title="+/- variation on session length so we don't always quit at the same game number.">
                            <div class="ba-slider-header">
                                <span class="ba-label">Session Jitter %</span>
                                <span class="ba-value" id="val-sjitter">${Math.round(CONFIG.antiDetection.sessionLengthJitter * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="0" max="60" value="${Math.round(CONFIG.antiDetection.sessionLengthJitter * 100)}" id="slide-sjitter">
                        </div>
                        <div class="ba-slider-container" title="Minimum delay between consecutive games (lower bound, in seconds).">
                            <div class="ba-slider-header">
                                <span class="ba-label">Between-Game Min (s)</span>
                                <span class="ba-value" id="val-bgmin">${Math.round(CONFIG.antiDetection.minBreakBetweenGames.min / 1000)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="3" max="60" value="${Math.round(CONFIG.antiDetection.minBreakBetweenGames.min / 1000)}" id="slide-bgmin">
                        </div>

                        <div class="ba-section-title">Account &amp; Behavior (v15.1)</div>
                        <div class="ba-row" title="Auto-detect new account and play -350 ELO for first 20 games, ramping up.">
                            <span class="ba-label">Warmup (auto)</span>
                            <div class="ba-checkbox ${CONFIG.warmup.enabled ? 'checked' : ''}" id="toggle-warmup"></div>
                        </div>
                        <div class="ba-row" title="Force warmup ON for fresh accounts regardless of game count.">
                            <span class="ba-label">Warmup (force ON)</span>
                            <div class="ba-checkbox ${CONFIG.warmup.manualOverride ? 'checked' : ''}" id="toggle-warmup-force"></div>
                        </div>
                        <div class="ba-slider-container" title="How many games the warmup ramp lasts.">
                            <div class="ba-slider-header">
                                <span class="ba-label">Warmup Games</span>
                                <span class="ba-value" id="val-warmup-games">${CONFIG.warmup.durationGames}</span>
                            </div>
                            <input type="range" class="ba-slider" min="5" max="50" value="${CONFIG.warmup.durationGames}" id="slide-warmup-games">
                        </div>
                        <div class="ba-row" title="Scale auto-lose probability based on long-window winrate to keep account around target %.">
                            <span class="ba-label">Winrate Targeting</span>
                            <div class="ba-checkbox ${CONFIG.winrateTarget.enabled ? 'checked' : ''}" id="toggle-wrtarget"></div>
                        </div>
                        <div class="ba-slider-container" title="Target winrate % the account should converge toward.">
                            <div class="ba-slider-header">
                                <span class="ba-label">Target Winrate %</span>
                                <span class="ba-value" id="val-wrtarget">${Math.round(CONFIG.winrateTarget.target * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="40" max="65" value="${Math.round(CONFIG.winrateTarget.target * 100)}" id="slide-wrtarget">
                        </div>
                        <div class="ba-row" title="Pick 1-2 openings per color per account and stick to them (real players have a repertoire).">
                            <span class="ba-label">Hard Repertoire</span>
                            <div class="ba-checkbox ${CONFIG.repertoireHard.enabled ? 'checked' : ''}" id="toggle-repertoire"></div>
                        </div>
                        <div class="ba-row" title="After a loss, next 2-4 games have higher blunder rate + slower timing (humans tilt).">
                            <span class="ba-label">Tilt After Loss</span>
                            <div class="ba-checkbox ${CONFIG.tilt.enabled ? 'checked' : ''}" id="toggle-tilt"></div>
                        </div>
                        <div class="ba-row" title="Only premove when opponent reply is forced or obvious recapture. Unchecked = premove whenever reply is predicted.">
                            <span class="ba-label">Smart Premove Gating</span>
                            <div class="ba-checkbox ${CONFIG.premoveGating.enabled ? 'checked' : ''}" id="toggle-pregate"></div>
                        </div>
                        <div class="ba-row" title="Refuse to auto-queue games of a different time control than the first one this session.">
                            <span class="ba-label">Time Control Lock</span>
                            <div class="ba-checkbox ${CONFIG.tcLock.enabled ? 'checked' : ''}" id="toggle-tclock"></div>
                        </div>
                        <div class="ba-row" title="Per-game random engine source (API / local / shallow local) so tactical signature varies.">
                            <span class="ba-label">Engine Rotation</span>
                            <div class="ba-checkbox ${CONFIG.engineRotation.enabled ? 'checked' : ''}" id="toggle-engrot"></div>
                        </div>
                        <div class="ba-row" title="Hesitate / blunder / sometimes hold lost positions instead of clean resigning.">
                            <span class="ba-label">Messy Resignation</span>
                            <div class="ba-checkbox ${CONFIG.messyResign.enabled ? 'checked' : ''}" id="toggle-messy"></div>
                        </div>
                        <div class="ba-row" title="Persistent per-account input device persona (mouse/trackpad/tablet) affecting drag feel.">
                            <span class="ba-label">Hardware Persona</span>
                            <div class="ba-checkbox ${CONFIG.hardwarePersona.enabled ? 'checked' : ''}" id="toggle-hwp"></div>
                        </div>
                        <div class="ba-row" title="Clear saved account state (game count, repertoire, hardware, recent results). Use when switching accounts.">
                            <span class="ba-label">Reset Account State</span>
                            <div class="ba-checkbox" id="btn-reset-account" style="background: rgba(255, 80, 80, 0.2); border-color: rgba(255, 80, 80, 0.6);"></div>
                        </div>
                    </div>
                    <!-- VISUALS TAB -->
                    <div id="tab-visuals" class="ba-page">
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Arrow Opacity</span>
                                <span class="ba-value" id="val-opacity">${Math.round(CONFIG.arrowOpacity * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="10" max="100" value="${Math.round(CONFIG.arrowOpacity * 100)}" id="slide-opacity">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Drag Speed</span>
                                <span class="ba-value" id="val-drag-speed">${CONFIG.dragSpeed.toFixed(1)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="2" max="20" value="${Math.round(CONFIG.dragSpeed * 10)}" id="slide-drag-speed">
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Show Threats</span>
                            <div class="ba-checkbox ${CONFIG.showThreats ? 'checked' : ''}" id="toggle-threats"></div>
                        </div>

                    </div>
                    <!-- STATS TAB -->
                    <div id="tab-stats" class="ba-page">
                        <div class="ba-section-title">Current Game</div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Engine Correlation</span>
                            <span class="ba-stats-value" id="stat-corr">---</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Best Moves Played</span>
                            <span class="ba-stats-value" id="stat-best">0/0</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Move Count</span>
                            <span class="ba-stats-value" id="stat-moves">0</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Game Phase</span>
                            <span class="ba-stats-value" id="stat-phase">---</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Cluster Mode</span>
                            <span class="ba-stats-value" id="stat-cluster">normal</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Time Pressure</span>
                            <span class="ba-stats-value" id="stat-tp">---</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Opponent Rating</span>
                            <span class="ba-stats-value" id="stat-opp-rating">---</span>
                        </div>
                        <div class="ba-section-title">Session</div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Games Played</span>
                            <span class="ba-stats-value" id="stat-games">${CONFIG.session.gamesPlayed}</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Win Streak</span>
                            <span class="ba-stats-value" id="stat-streak">${CONFIG.session.currentWinStreak}</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">Engine Source</span>
                            <span class="ba-stats-value" id="stat-engine">---</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">API Status</span>
                            <span class="ba-stats-value" id="stat-api">checking...</span>
                        </div>
                        <div class="ba-stats-row">
                            <span class="ba-stats-label">My Clock</span>
                            <span class="ba-stats-value" id="stat-clock">---</span>
                        </div>
                    </div>
                </div>
                <div class="ba-footer">
                    <span><span class="ba-key">A</span> Auto</span>
                    <span><span class="ba-key">X</span> Stealth</span>
                    <span><span class="ba-key">R</span> Reset</span>
                </div>
            `;
            document.body.appendChild(panel);
            State.ui.panel = panel;
            UI.makeDraggable(panel);
            UI.initListeners(panel);
        },

        initListeners: (panel) => {
            // Tab switching
            panel.querySelectorAll('.ba-tab').forEach(t => {
                t.addEventListener('click', (e) => {
                    panel.querySelectorAll('.ba-tab').forEach(x => x.classList.remove('active'));
                    panel.querySelectorAll('.ba-page').forEach(x => x.classList.remove('active'));
                    e.target.classList.add('active');
                    panel.querySelector(`#tab-${e.target.dataset.tab}`).classList.add('active');
                });
            });

            // Minimize
            panel.querySelector('.ba-minimize').addEventListener('click', () => {
                panel.classList.toggle('ba-minimized');
            });

            // Checkbox toggles
            const toggle = (id, getter, setter) => {
                const el = panel.querySelector(`#${id}`);
                if (!el) return;
                el.addEventListener('click', (e) => {
                    const newVal = !getter();
                    setter(newVal);
                    e.target.classList.toggle('checked', newVal);
                    Settings.save(CONFIG);
                });
            };

            toggle('toggle-auto', () => CONFIG.auto.enabled, (v) => CONFIG.auto.enabled = v);
            toggle('toggle-book', () => CONFIG.useBook, (v) => CONFIG.useBook = v);
            toggle('toggle-tablebase', () => CONFIG.useTablebase, (v) => CONFIG.useTablebase = v);
            toggle('toggle-threats', () => CONFIG.showThreats, (v) => CONFIG.showThreats = v);
            toggle('toggle-dynamic-depth', () => CONFIG.engineDepth.dynamicDepth, (v) => CONFIG.engineDepth.dynamicDepth = v);
            toggle('toggle-clock-aware', () => CONFIG.timing.clockAware.enabled, (v) => CONFIG.timing.clockAware.enabled = v);
            toggle('toggle-premove', () => CONFIG.timing.premove.enabled, (v) => CONFIG.timing.premove.enabled = v);
            // SAFETY tab toggles
            toggle('toggle-random-afk', () => CONFIG.antiDetection.randomAFK.enabled, (v) => CONFIG.antiDetection.randomAFK.enabled = v);
            toggle('toggle-auto-queue', () => CONFIG.auto.autoQueue, (v) => CONFIG.auto.autoQueue = v);
            toggle('toggle-auto-lose', () => CONFIG.autoLose.enabled, (v) => CONFIG.autoLose.enabled = v);
            toggle('toggle-auto-resign', () => CONFIG.autoResign.enabled, (v) => CONFIG.autoResign.enabled = v);
            toggle('toggle-opp-adapt', () => CONFIG.opponentAdaptation.enabled, (v) => CONFIG.opponentAdaptation.enabled = v);
            toggle('toggle-time-pressure', () => CONFIG.timePressure.enabled, (v) => CONFIG.timePressure.enabled = v);
            // New: advanced humanization toggles
            toggle('toggle-human-master',  () => CONFIG.humanization.enabled,                       (v) => CONFIG.humanization.enabled = v);
            toggle('toggle-streaks',       () => CONFIG.humanization.streaks.enabled,               (v) => CONFIG.humanization.streaks.enabled = v);
            toggle('toggle-clustering',    () => CONFIG.humanization.accuracyClustering.enabled,    (v) => CONFIG.humanization.accuracyClustering.enabled = v);
            toggle('toggle-weakness',      () => CONFIG.humanization.weaknessProfile.enabled,       (v) => CONFIG.humanization.weaknessProfile.enabled = v);
            toggle('toggle-playerdb',      () => CONFIG.humanization.playerMoveDB.enabled,          (v) => CONFIG.humanization.playerMoveDB.enabled = v);
            toggle('toggle-timing-couple', () => CONFIG.humanization.timingAccuracyCoupling.enabled,(v) => CONFIG.humanization.timingAccuracyCoupling.enabled = v);
            toggle('toggle-cofmind',       () => CONFIG.antiDetection.changeOfMind.enabled,         (v) => CONFIG.antiDetection.changeOfMind.enabled = v);
            toggle('toggle-telemetry',     () => CONFIG.antiDetection.telemetryNoise.enabled,       (v) => CONFIG.antiDetection.telemetryNoise.enabled = v);
            // v15.1: Account & behavior toggles
            toggle('toggle-warmup',        () => CONFIG.warmup.enabled,          (v) => CONFIG.warmup.enabled = v);
            toggle('toggle-warmup-force',  () => CONFIG.warmup.manualOverride,   (v) => CONFIG.warmup.manualOverride = v);
            toggle('toggle-wrtarget',      () => CONFIG.winrateTarget.enabled,   (v) => CONFIG.winrateTarget.enabled = v);
            toggle('toggle-repertoire',    () => CONFIG.repertoireHard.enabled,  (v) => CONFIG.repertoireHard.enabled = v);
            toggle('toggle-tilt',          () => CONFIG.tilt.enabled,            (v) => CONFIG.tilt.enabled = v);
            toggle('toggle-pregate',       () => CONFIG.premoveGating.enabled,   (v) => CONFIG.premoveGating.enabled = v);
            toggle('toggle-tclock',        () => CONFIG.tcLock.enabled,          (v) => CONFIG.tcLock.enabled = v);
            toggle('toggle-engrot',        () => CONFIG.engineRotation.enabled,  (v) => CONFIG.engineRotation.enabled = v);
            toggle('toggle-messy',         () => CONFIG.messyResign.enabled,     (v) => CONFIG.messyResign.enabled = v);
            toggle('toggle-hwp',           () => CONFIG.hardwarePersona.enabled, (v) => CONFIG.hardwarePersona.enabled = v);

            // Account reset button (not a toggle — behaves as one-shot action)
            const resetBtn = panel.querySelector('#btn-reset-account');
            if (resetBtn) {
                resetBtn.addEventListener('click', () => {
                    if (!confirm('Reset account state?\n\nThis clears: total games played, chosen repertoire, hardware persona, recent results window, locked session TC.\n\nYou should do this when switching to a different Chess.com account.')) return;
                    CONFIG.account = {
                        totalGamesPlayed: 0,
                        repertoire: { white: null, black: null },
                        hardware: null,
                        recentResults: [],
                        sessionTC: null,
                        currentEngine: null,
                    };
                    Account._tiltGamesLeft = 0;
                    State.gameEngineOverride = null;
                    Settings.save(CONFIG);
                    UI.toast('Account Reset', 'Account state cleared — repertoire and hardware will be re-chosen next game', 'info', 4500);
                    Utils.log('Account state reset by user', 'warn');
                });
            }

            // Sliders
            const slider = (id, valId, setter) => {
                const el = panel.querySelector(`#${id}`);
                const display = panel.querySelector(`#${valId}`);
                if (!el || !display) return;
                el.addEventListener('input', (e) => {
                    const val = parseInt(e.target.value);
                    setter(val);
                    display.textContent = val;
                    Settings.save(CONFIG);
                });
            };

            slider('slide-rating', 'val-rating', (v) => {
                CONFIG.targetRating = v;
                RatingProfile.apply(v);
                // Update other sliders to reflect new values
                UI.refreshSliders();
            });
            slider('slide-corr', 'val-corr', (v) => { CONFIG.humanization.targetEngineCorrelation = v / 100; });
            slider('slide-min', 'val-min', (v) => { CONFIG.timing.base.min = v; });
            slider('slide-max', 'val-max', (v) => { CONFIG.timing.base.max = v; });
            slider('slide-subopt', 'val-subopt', (v) => { CONFIG.humanization.suboptimalMoveRate.middlegame = v / 100; });
            slider('slide-depth', 'val-depth', (v) => { CONFIG.engineDepth.base = v; });
            slider('slide-mpv', 'val-mpv', (v) => {
                CONFIG.multiPV = v;
                // Update local engine MultiPV
                if (State.workers.stockfish) {
                    State.workers.stockfish.postMessage(`setoption name MultiPV value ${v}`);
                }
            });
            slider('slide-opacity', 'val-opacity', (v) => { CONFIG.arrowOpacity = v / 100; });
            // Drag speed: slider 2-20 maps to 0.2-2.0
            const dragSlider = panel.querySelector('#slide-drag-speed');
            const dragVal = panel.querySelector('#val-drag-speed');
            if (dragSlider) {
                dragSlider.addEventListener('input', (e) => {
                    const v = parseInt(e.target.value) / 10;
                    CONFIG.dragSpeed = v;
                    if (dragVal) dragVal.textContent = v.toFixed(1);
                    Settings.save(CONFIG);
                });
            }

            // SAFETY tab sliders
            slider('slide-afk-chance', 'val-afk-chance', (v) => { CONFIG.antiDetection.randomAFK.chance = v / 100; });
            slider('slide-random-legal', 'val-random-legal', (v) => { CONFIG.antiDetection.randomLegalMoveChance = v / 100; });
            slider('slide-max-gph', 'val-max-gph', (v) => { CONFIG.antiDetection.maxGamesPerHour = v; });
            slider('slide-max-session', 'val-max-session', (v) => { CONFIG.session.maxGamesPerSession = v; });
            slider('slide-break-dur', 'val-break-dur', (v) => { CONFIG.session.breakDurationMs = v * 60000; });
            slider('slide-lose-streak', 'val-lose-streak', (v) => { CONFIG.autoLose.triggerStreak = v; });
            slider('slide-max-streak', 'val-max-streak', (v) => { CONFIG.session.maxWinStreak = v; });
            slider('slide-rating-edge', 'val-rating-edge', (v) => { CONFIG.opponentAdaptation.ratingEdge = v; });
            slider('slide-resign-chance', 'val-resign-chance', (v) => { CONFIG.autoResign.resignChance = v / 100; });
            // New: advanced humanization sliders
            slider('slide-topcap',  'val-topcap',  (v) => { CONFIG.humanization.antiCorrelation.maxTopMoveRate = v / 100; });
            slider('slide-sjitter', 'val-sjitter', (v) => { CONFIG.antiDetection.sessionLengthJitter = v / 100; });
            slider('slide-bgmin',   'val-bgmin',   (v) => {
                CONFIG.antiDetection.minBreakBetweenGames.min = v * 1000;
                // Keep max >= min + 5s
                if (CONFIG.antiDetection.minBreakBetweenGames.max < (v + 5) * 1000) {
                    CONFIG.antiDetection.minBreakBetweenGames.max = (v + 15) * 1000;
                }
            });
            // v15.1 account sliders
            slider('slide-warmup-games', 'val-warmup-games', (v) => { CONFIG.warmup.durationGames = v; });
            slider('slide-wrtarget',     'val-wrtarget',     (v) => { CONFIG.winrateTarget.target = v / 100; });

            // Float sliders (resign eval, blunder chance)
            const floatSlider = (id, valId, setter) => {
                const el = panel.querySelector(`#${id}`);
                const display = panel.querySelector(`#${valId}`);
                if (!el || !display) return;
                el.addEventListener('input', (e) => {
                    const val = parseFloat(e.target.value);
                    setter(val);
                    display.textContent = val;
                    Settings.save(CONFIG);
                });
            };
            floatSlider('slide-resign-eval', 'val-resign-eval', (v) => { CONFIG.autoResign.evalThreshold = v; });
            floatSlider('slide-blunder', 'val-blunder', (v) => { CONFIG.humanization.blunder.chance = v / 100; });

            // Selects
            const engineSelect = panel.querySelector('#select-engine');
            if (engineSelect) {
                engineSelect.addEventListener('change', (e) => {
                    CONFIG.engineType = e.target.value;
                    Utils.log(`Engine changed to: ${e.target.value}`);
                    Settings.save(CONFIG);
                });
            }

            const styleSelect = panel.querySelector('#select-style');
            if (styleSelect) {
                styleSelect.addEventListener('change', (e) => {
                    CONFIG.playStyle = e.target.value;
                    RatingProfile.apply(CONFIG.targetRating);
                    UI.refreshSliders();
                    UI.setAccent();
                });
            }
            UI.setAccent();

        },

        refreshSliders: () => {
            const panel = State.ui.panel;
            if (!panel) return;
            const updates = {
                'slide-corr': { val: 'val-corr', v: Math.round(CONFIG.humanization.targetEngineCorrelation * 100) },
                'slide-min': { val: 'val-min', v: CONFIG.timing.base.min },
                'slide-max': { val: 'val-max', v: CONFIG.timing.base.max },
                'slide-subopt': { val: 'val-subopt', v: Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100) },
                'slide-depth': { val: 'val-depth', v: CONFIG.engineDepth.base },
                // SAFETY tab
                'slide-afk-chance': { val: 'val-afk-chance', v: Math.round(CONFIG.antiDetection.randomAFK.chance * 100) },
                'slide-random-legal': { val: 'val-random-legal', v: Math.round(CONFIG.antiDetection.randomLegalMoveChance * 100) },
                'slide-max-gph': { val: 'val-max-gph', v: CONFIG.antiDetection.maxGamesPerHour },
                'slide-max-session': { val: 'val-max-session', v: CONFIG.session.maxGamesPerSession },
                'slide-break-dur': { val: 'val-break-dur', v: Math.round(CONFIG.session.breakDurationMs / 60000) },
                'slide-lose-streak': { val: 'val-lose-streak', v: CONFIG.autoLose.triggerStreak },
                'slide-max-streak': { val: 'val-max-streak', v: CONFIG.session.maxWinStreak },
                'slide-rating-edge': { val: 'val-rating-edge', v: CONFIG.opponentAdaptation.ratingEdge },
                'slide-resign-chance': { val: 'val-resign-chance', v: Math.round(CONFIG.autoResign.resignChance * 100) },
                'slide-resign-eval': { val: 'val-resign-eval', v: CONFIG.autoResign.evalThreshold },
                'slide-blunder': { val: 'val-blunder', v: (CONFIG.humanization.blunder.chance * 100).toFixed(1) },
                // New advanced sliders
                'slide-topcap':  { val: 'val-topcap',  v: Math.round(CONFIG.humanization.antiCorrelation.maxTopMoveRate * 100) },
                'slide-sjitter': { val: 'val-sjitter', v: Math.round(CONFIG.antiDetection.sessionLengthJitter * 100) },
                'slide-bgmin':   { val: 'val-bgmin',   v: Math.round(CONFIG.antiDetection.minBreakBetweenGames.min / 1000) },
                // v15.1
                'slide-warmup-games': { val: 'val-warmup-games', v: CONFIG.warmup.durationGames },
                'slide-wrtarget':     { val: 'val-wrtarget',     v: Math.round(CONFIG.winrateTarget.target * 100) },
            };
            for (const [slideId, info] of Object.entries(updates)) {
                const slider = panel.querySelector(`#${slideId}`);
                const display = panel.querySelector(`#${info.val}`);
                if (slider) slider.value = info.v;
                if (display) display.textContent = info.v;
            }
        },

        makeDraggable: (el) => {
            const header = el.querySelector('.ba-header');
            let isDragging = false;
            let startX, startY, initialLeft, initialTop;
            header.addEventListener('mousedown', (e) => {
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                initialLeft = el.offsetLeft;
                initialTop = el.offsetTop;
                header.style.cursor = 'grabbing';
            });
            document.addEventListener('mousemove', (e) => {
                if (!isDragging) return;
                el.style.left = `${initialLeft + (e.clientX - startX)}px`;
                el.style.top = `${initialTop + (e.clientY - startY)}px`;
            });
            document.addEventListener('mouseup', () => {
                isDragging = false;
                header.style.cursor = 'grab';
            });
        },

        toggleStealth: () => {
            CONFIG.stealthMode = !CONFIG.stealthMode;
            const p = State.ui.panel;
            if (p) p.style.display = CONFIG.stealthMode ? 'none' : 'flex';
            document.querySelectorAll('.ba-overlay').forEach(el => {
                el.classList.toggle('ba-stealth', CONFIG.stealthMode);
            });
            if (UI._toastContainer) {
                if (CONFIG.stealthMode) {
                    UI._toastContainer.innerHTML = '';
                    UI._toastContainer.style.display = 'none';
                } else {
                    UI._toastContainer.style.display = 'flex';
                }
            }
        },

        updatePanel: (evalData, bestMove) => {
            if (!State.ui.panel) return;
            const evalBox = State.ui.panel.querySelector('#eval-display');
            const moveBox = State.ui.panel.querySelector('#move-display');
            const badge = State.ui.panel.querySelector('#engine-badge');

            if (evalData) {
                evalBox.textContent = evalData.type === 'mate' ? `M${evalData.value}` : evalData.value.toFixed(2);
                evalBox.style.color = evalData.value > 0.5 ? '#4caf50' : (evalData.value < -0.5 ? '#ff5252' : '#e0e0e0');
            }
            if (bestMove) {
                moveBox.textContent = bestMove.move || '...';
            }

            // Update engine badge
            const engineNames = {
                'api': 'chess-api.com SF18',
                'stockfish_online': 'SF Online',
                'local': 'Local SF10',
                'tablebase': 'Syzygy TB',
                'book': 'Opening Book',
            };
            if (badge && State._lastEngineSource) {
                badge.textContent = engineNames[State._lastEngineSource] || State._lastEngineSource;
            }

            // Update stats tab
            UI.updateStats();

            State.ui.panel.querySelector('#toggle-auto').classList.toggle('checked', CONFIG.auto.enabled);
        },

        updateStats: () => {
            const panel = State.ui.panel;
            if (!panel) return;
            const h = State.human;
            const corr = h.totalMoveCount > 0 ? `${((h.bestMoveCount / h.totalMoveCount) * 100).toFixed(0)}%` : '---';
            const setVal = (id, val) => {
                const el = panel.querySelector(`#${id}`);
                if (el) el.textContent = val;
            };
            setVal('stat-corr', corr);
            setVal('stat-best', `${h.bestMoveCount}/${h.totalMoveCount}`);
            setVal('stat-moves', State.moveCount);
            setVal('stat-phase', HumanStrategy.getGamePhase(State.lastFen));
            setVal('stat-cluster', h.clusterMode);
            // Time pressure indicator
            const tpMult = h.timePressureMult?.suboptimal || 1;
            setVal('stat-tp', tpMult > 1 ? `ACTIVE x${tpMult.toFixed(1)}` : 'normal');
            // Opponent rating
            setVal('stat-opp-rating', h.opponentRating ? `${h.opponentRating}` : '---');
            setVal('stat-games', CONFIG.session.gamesPlayed);
            const streakText = h.autoLoseActive
                ? `${CONFIG.session.currentWinStreak} [AUTO-LOSE]`
                : `${CONFIG.session.currentWinStreak}`;
            setVal('stat-streak', streakText);
            setVal('stat-engine', State.gameEngineOverride || CONFIG.engineType);
            setVal('stat-api', State.apiAvailable ? 'OK' : `DOWN (${State.apiFailCount} fails)`);
            setVal('stat-clock', State.clock.myTime != null ? `${State.clock.myTime}s` : '---');
        },

        updateStatus: (color) => {
            if (!State.ui.panel) return;
            State.ui.panel.style.borderLeftColor = color;
            const logo = State.ui.panel.querySelector('.ba-logo');
            if (logo) logo.style.color = color;
        },

        _toastContainer: null,
        _toastIcons: {
            info: '\u{1F4A1}', warn: '\u26A0\uFE0F', error: '\u{1F6A8}', success: '\u2705', move: '\u265E',
            blunder: '\u{1F4A5}', suboptimal: '\u{1F504}', book: '\u{1F4D6}', resign: '\u{1F3F3}\uFE0F',
            afk: '\u{1F634}', fakeout: '\u{1F3AD}', streak: '\u{1F525}', engine: '\u2699\uFE0F',
            tablebase: '\u{1F4BE}', draw: '\u{1F91D}', queue: '\u{1F504}', time: '\u23F1\uFE0F',
        },
        toast: (title, msg, type = 'info', durationMs = 4000) => {
            if (CONFIG.stealthMode) return;
            if (!UI._toastContainer) {
                UI._toastContainer = document.createElement('div');
                UI._toastContainer.className = 'ba-toast-container';
                document.body.appendChild(UI._toastContainer);
            }
            const typeClass = ['info','warn','error','success','move'].includes(type) ? type : 'info';
            const icon = UI._toastIcons[type] || UI._toastIcons[typeClass] || UI._toastIcons.info;
            const el = document.createElement('div');
            el.className = `ba-toast ba-toast-${typeClass}`;
            el.innerHTML = `
                <span class="ba-toast-icon">${icon}</span>
                <div class="ba-toast-body">
                    <div class="ba-toast-title">${title}</div>
                    <div class="ba-toast-msg">${msg}</div>
                </div>
                <div class="ba-toast-progress" style="animation-duration:${durationMs}ms"></div>
            `;
            el.addEventListener('click', () => {
                el.style.animation = 'ba-toast-out 0.25s ease-in forwards';
                setTimeout(() => el.remove(), 250);
            });
            UI._toastContainer.appendChild(el);
            // Cap at 5 visible toasts
            while (UI._toastContainer.children.length > 5) {
                UI._toastContainer.firstChild.remove();
            }
            setTimeout(() => {
                if (el.parentNode) {
                    el.style.animation = 'ba-toast-out 0.25s ease-in forwards';
                    setTimeout(() => el.remove(), 250);
                }
            }, durationMs);
        },

        clearOverlay: () => {
            document.querySelectorAll('.ba-overlay').forEach(e => e.remove());
        },

        _arrowId: 0,
        drawMove: (move, color = '#4caf50', secondary = false) => {
            if (CONFIG.stealthMode || !move || move.length < 4) return;
            if (!secondary) UI.clearOverlay();
            const board = Game.getBoard();
            if (!board) return;
            const rect = board.getBoundingClientRect();
            let overlay = secondary ? document.querySelector('.ba-overlay') : null;
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'ba-overlay';
                if (CONFIG.stealthMode) overlay.classList.add('ba-stealth');
                overlay.style.width = rect.width + 'px';
                overlay.style.height = rect.height + 'px';
                overlay.style.left = rect.left + window.scrollX + 'px';
                overlay.style.top = rect.top + window.scrollY + 'px';
                document.body.appendChild(overlay);
            }
            const from = move.substring(0, 2), to = move.substring(2, 4);
            const fl = (c) => c.charCodeAt(0) - 97, rk = (c) => parseInt(c) - 1;
            const flipped = State.playerColor === 'b';
            const sz = rect.width / 8;
            const pos = (sq) => ({
                x: (flipped ? 7 - fl(sq[0]) : fl(sq[0])) * sz + sz / 2,
                y: (flipped ? rk(sq[1]) : 7 - rk(sq[1])) * sz + sz / 2
            });
            const p1 = pos(from), p2 = pos(to);
            const ns = 'http://www.w3.org/2000/svg';
            let svg = overlay.querySelector('svg');
            if (!svg) {
                svg = document.createElementNS(ns, 'svg');
                svg.setAttribute('width', '100%');
                svg.setAttribute('height', '100%');
                svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);
                overlay.appendChild(svg);
            }
            let defs = svg.querySelector('defs');
            if (!defs) { defs = document.createElementNS(ns, 'defs'); svg.prepend(defs); }

            const id = ++UI._arrowId;
            const opacity = CONFIG.arrowOpacity * (secondary ? 0.55 : 1);
            const w = sz * (secondary ? 0.065 : 0.12);
            const headLen = sz * (secondary ? 0.18 : 0.32);
            const headW = w * (secondary ? 2.8 : 3.2);

            const dx = p2.x - p1.x, dy = p2.y - p1.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const ux = dx / dist, uy = dy / dist;
            const shorten = headLen * 0.7;
            const ex = p2.x - ux * shorten, ey = p2.y - uy * shorten;

            const grp = document.createElementNS(ns, 'g');
            grp.setAttribute('class', 'ba-arrow-group');
            grp.setAttribute('opacity', opacity);

            const marker = document.createElementNS(ns, 'marker');
            marker.setAttribute('id', `ah${id}`);
            marker.setAttribute('markerWidth', headLen);
            marker.setAttribute('markerHeight', headW);
            marker.setAttribute('refX', headLen - 1);
            marker.setAttribute('refY', headW / 2);
            marker.setAttribute('orient', 'auto');
            marker.setAttribute('markerUnits', 'userSpaceOnUse');
            const arrow = document.createElementNS(ns, 'path');
            arrow.setAttribute('d', `M0,${headW * 0.15} L${headLen},${headW / 2} L0,${headW * 0.85} Z`);
            arrow.setAttribute('fill', color);
            if (!secondary) arrow.setAttribute('filter', 'url(#aglow)');
            marker.appendChild(arrow);
            defs.appendChild(marker);

            if (!defs.querySelector('#aglow')) {
                const filt = document.createElementNS(ns, 'filter');
                filt.setAttribute('id', 'aglow');
                filt.setAttribute('x', '-50%'); filt.setAttribute('y', '-50%');
                filt.setAttribute('width', '200%'); filt.setAttribute('height', '200%');
                const blur = document.createElementNS(ns, 'feGaussianBlur');
                blur.setAttribute('stdDeviation', '2.5');
                blur.setAttribute('result', 'glow');
                const merge = document.createElementNS(ns, 'feMerge');
                const mn1 = document.createElementNS(ns, 'feMergeNode'); mn1.setAttribute('in', 'glow');
                const mn2 = document.createElementNS(ns, 'feMergeNode'); mn2.setAttribute('in', 'SourceGraphic');
                merge.appendChild(mn1); merge.appendChild(mn2);
                filt.appendChild(blur); filt.appendChild(merge);
                defs.appendChild(filt);
            }

            if (!secondary) {
                const sqX = (flipped ? 7 - fl(from[0]) : fl(from[0])) * sz;
                const sqY = (flipped ? rk(from[1]) : 7 - rk(from[1])) * sz;
                const highlight = document.createElementNS(ns, 'rect');
                highlight.setAttribute('x', sqX); highlight.setAttribute('y', sqY);
                highlight.setAttribute('width', sz); highlight.setAttribute('height', sz);
                highlight.setAttribute('rx', sz * 0.06);
                highlight.setAttribute('fill', color);
                highlight.setAttribute('opacity', '0.18');
                grp.appendChild(highlight);
            }

            const line = document.createElementNS(ns, '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', w);
            line.setAttribute('stroke-linecap', 'round');
            line.setAttribute('marker-end', `url(#ah${id})`);
            if (!secondary) {
                line.setAttribute('filter', 'url(#aglow)');
            } else {
                line.setAttribute('stroke-dasharray', `${w * 1.5} ${w * 2.5}`);
                line.setAttribute('class', 'ba-dash-anim');
            }
            grp.appendChild(line);

            const ring = document.createElementNS(ns, 'circle');
            ring.setAttribute('cx', p2.x); ring.setAttribute('cy', p2.y);
            ring.setAttribute('r', sz * (secondary ? 0.15 : 0.22));
            ring.setAttribute('fill', 'none');
            ring.setAttribute('stroke', color);
            ring.setAttribute('stroke-width', w * 0.6);
            ring.setAttribute('class', 'ba-pulse');
            grp.appendChild(ring);

            if (!secondary) {
                const dot = document.createElementNS(ns, 'circle');
                dot.setAttribute('cx', p2.x); dot.setAttribute('cy', p2.y);
                dot.setAttribute('r', sz * 0.06);
                dot.setAttribute('fill', color);
                dot.setAttribute('opacity', '0.9');
                grp.appendChild(dot);
            }

            svg.appendChild(grp);
        }
    };

    // ═══════════════════════════════════════════
    //  GAME INTERFACE
    // ═══════════════════════════════════════════
    const Game = {
        getBoard: () => {
            if (State.cache.board && State.cache.board.isConnected) return State.cache.board;
            State.cache.board = Utils.query(SELECTORS.board);
            return State.cache.board;
        },

        getBoardGame: () => {
            const b = Game.getBoard();
            if (!b) return null;
            if (b.game) return b.game;
            try { const ub = unsafeWindow.document.querySelector('wc-chess-board'); if (ub && ub.game) return ub.game; } catch (e) {}
            return null;
        },

        squareToCoords: (sq) => {
            const file = sq.charCodeAt(0) - 96;
            const rank = sq[1];
            return `${file}${rank}`;
        },

        detectColor: () => {
            const g = Game.getBoardGame();
            if (g) {
                try { if (g.getOptions) { const o = g.getOptions(); if (o && typeof o.flipped === 'boolean') return o.flipped ? 'b' : 'w'; if (o && typeof o.isWhiteOnBottom === 'boolean') return o.isWhiteOnBottom ? 'w' : 'b'; } } catch (e) {}
                try { if (g.getPlayingAs) { const pa = g.getPlayingAs(); if (pa === 1) return 'w'; if (pa === 2) return 'b'; } } catch (e) {}
            }
            const b = Game.getBoard();
            if (b) {
                try { if (typeof b.isFlipped === 'function') return b.isFlipped() ? 'b' : 'w'; } catch (e) {}
            }
            Utils.log('detectColor: fallback w', 'warn');
            return 'w';
        },

        isValidFen: (fen) => {
            if (!fen || typeof fen !== 'string') return false;
            return fen.split(' ').length >= 4;
        },

        getFen: () => {
            const g = Game.getBoardGame();
            if (g) {
                try { if (g.getFEN) return g.getFEN(); } catch (e) {}
            }
            const board = Game.getBoard();
            if (!board) return null;
            const keys = Object.keys(board);
            const reactKey = keys.find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternal'));
            if (reactKey) {
                let curr = board[reactKey];
                while (curr) {
                    if (curr.memoizedProps?.game?.fen) return curr.memoizedProps.game.fen;
                    if (typeof curr.memoizedProps?.fen === 'string') return curr.memoizedProps.fen;
                    curr = curr.return;
                }
            }
            return null;
        },

        isMyTurn: (fen) => {
            if (!fen || !State.playerColor) return false;
            return fen.split(' ')[1] === State.playerColor;
        },

        isCapture: (move) => {
            const board = Game.getBoard();
            if (!board) return false;
            const to = move.substring(2, 4);
            const coords = Game.squareToCoords(to);
            return !!board.querySelector(`.piece.square-${coords}`);
        },

        // Read clock time from the DOM
        readClock: () => {
            try {
                const clocks = document.querySelectorAll('.clock-component, .clock-time-monospace');
                if (clocks.length < 2) return;
                // Determine which clock is ours based on position
                // Bottom clock is always the current player's
                const clockEls = Array.from(clocks);
                const sorted = clockEls.sort((a, b) => {
                    const ra = a.getBoundingClientRect();
                    const rb = b.getBoundingClientRect();
                    return rb.top - ra.top; // bottom first
                });
                const parseTime = (el) => {
                    const text = el.textContent.trim().replace(/[^\d:\.]/g, '');
                    const parts = text.split(':').map(Number);
                    if (parts.some(isNaN)) return null;
                    if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
                    if (parts.length === 2) return parts[0] * 60 + parts[1];
                    if (parts.length === 1) return parts[0];
                    return null;
                };
                State.clock.myTime = parseTime(sorted[0]);
                State.clock.oppTime = parseTime(sorted[1]);
            } catch (e) { /* ignore clock read errors */ }
        },

        // Read opponent's rating from the page.
        // Chess.com's DOM has shifted multiple times; this tries every selector
        // pattern seen across recent UI versions, then falls back to a generic
        // "find a 3-4 digit number near the top player area" heuristic.
        readOpponentRating: () => {
            try {
                const parseRating = (raw) => {
                    if (!raw) return null;
                    const txt = String(raw).replace(/[(),\s]/g, '');
                    const m = txt.match(/(\d{3,4})/);
                    if (!m) return null;
                    const n = parseInt(m[1], 10);
                    return (n >= 100 && n <= 4000) ? n : null;
                };

                // Targeted selectors covering live, daily, computer and bullet UIs
                const selectors = [
                    // Current "user-tagline" (live)
                    '.player-component.player-top .user-tagline-rating',
                    '.player-row-top .cc-user-rating',
                    '.player-top .cc-user-rating',
                    '.board-layout-top .user-tagline-rating',
                    '.board-player-default-top .user-tagline-rating',
                    '.player-top .rating-tagline',
                    // V5 / new live game UI
                    '[data-test-element="user-tagline-username"] ~ .user-tagline-rating',
                    '.cc-user-tagline-component-top .cc-user-rating',
                    // Generic fallback: any rating element inside any "top" container
                    '[class*="top"] [class*="rating"]',
                ];
                for (const sel of selectors) {
                    const els = document.querySelectorAll(sel);
                    for (const el of els) {
                        const r = parseRating(el.textContent);
                        if (r) return r;
                    }
                }

                // Heuristic fallback: every visible element that holds a 3-4 digit
                // rating. We pick the topmost one geometrically (board isn't flipped
                // ⇒ opponent is on top of viewport).
                const candidates = [];
                document.querySelectorAll('span, div').forEach((el) => {
                    if (el.children.length > 0) return; // leaf nodes only
                    const cls = el.className || '';
                    if (typeof cls !== 'string') return;
                    if (!/rating|tagline|user/i.test(cls)) return;
                    const rect = el.getBoundingClientRect();
                    if (rect.width === 0 || rect.height === 0) return;
                    const r = parseRating(el.textContent);
                    if (r) candidates.push({ rating: r, top: rect.top });
                });
                if (candidates.length >= 2) {
                    candidates.sort((a, b) => a.top - b.top);
                    return candidates[0].rating;
                }
                if (candidates.length === 1) return candidates[0].rating;
            } catch (e) { /* ignore */ }
            return null;
        },

        // Resign the current game
        _clickConfirmResign: async () => {
            // Wait for the confirmation dialog to appear, retrying a few times
            for (let attempt = 0; attempt < 10; attempt++) {
                await Utils.sleep(300);
                // Selector-based matches
                const selectorCandidates = [
                    'button[data-cy="confirm-resign-button"]',
                    '.resign-confirm-button',
                    '.modal-confirm-button',
                    '.ui_v5-button-primary',
                    '.modal-seo-close-button',
                ];
                for (const sel of selectorCandidates) {
                    const btn = document.querySelector(sel);
                    if (btn && btn.offsetParent !== null) {
                        btn.click();
                        Utils.log('Auto-Resign: Confirmed via selector', 'info');
                        return true;
                    }
                }
                // Text-based match: find any visible button containing "Yes" or "Resign"
                const allButtons = document.querySelectorAll('button, [role="button"]');
                for (const btn of allButtons) {
                    if (btn.offsetParent === null) continue;
                    const text = btn.textContent.trim().toLowerCase();
                    if (text === 'yes' || text === 'resign' || text === 'confirm') {
                        btn.click();
                        Utils.log(`Auto-Resign: Confirmed via text match ("${btn.textContent.trim()}")`, 'info');
                        return true;
                    }
                }
            }
            Utils.log('Auto-Resign: Confirmation dialog not found', 'warn');
            return false;
        },

        resign: async () => {
            try {
                Utils.log('Auto-Resign: Attempting to resign...', 'warn');
                const resignSelectors = [
                    'button[data-cy="resign-button"]',
                    '.resign-button-component',
                    '[data-tooltip="Resign"]',
                    '.board-controls-btn-resign',
                ];
                for (const sel of resignSelectors) {
                    const btn = document.querySelector(sel);
                    if (btn && btn.offsetParent !== null) {
                        btn.click();
                        const confirmed = await Game._clickConfirmResign();
                        if (confirmed) return true;
                        return true;
                    }
                }
                // Text-based fallback: find any button/icon whose text or tooltip says "Resign"
                const allBtns = document.querySelectorAll('button, [role="button"], .board-controls-btn');
                for (const btn of allBtns) {
                    if (btn.offsetParent === null) continue;
                    const text = (btn.textContent + ' ' + (btn.getAttribute('data-tooltip') || '') + ' ' + (btn.getAttribute('aria-label') || '')).toLowerCase();
                    if (text.includes('resign')) {
                        btn.click();
                        const confirmed = await Game._clickConfirmResign();
                        if (confirmed) return true;
                        return true;
                    }
                }
                // Fallback: try the game menu
                const menuBtn = document.querySelector('.board-controls-btn-menu, [data-cy="game-controls-menu"]');
                if (menuBtn) {
                    menuBtn.click();
                    await Utils.sleep(500);
                    const resignItem = Array.from(document.querySelectorAll('.board-controls-menu-item, [class*="menu-item"]'))
                        .find(el => el.textContent.toLowerCase().includes('resign'));
                    if (resignItem) {
                        resignItem.click();
                        const confirmed = await Game._clickConfirmResign();
                        if (confirmed) return true;
                        return true;
                    }
                }
                Utils.log('Auto-Resign: Could not find resign button', 'warn');
                return false;
            } catch (e) {
                Utils.log('Auto-Resign: Error - ' + e, 'error');
                return false;
            }
        },
    };

    // ═══════════════════════════════════════════
    //  OPENING BOOK
    // ═══════════════════════════════════════════
    const OpeningBook = {
        _repertoire: { w: {}, b: {} },  // track chosen lines for consistency

        fetchMove: (fen) => {
            if (!CONFIG.useBook) return Promise.resolve(null);
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://explorer.lichess.ovh/masters?fen=${encodeURIComponent(fen)}`,
                    timeout: 4000,
                    onload: (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.moves && data.moves.length > 0) {
                                const color = State.playerColor || 'w';
                                const posKey = fen.split(' ').slice(0, 4).join(' ');

                                // Repertoire consistency: if we've played this position before, prefer same move
                                if (CONFIG.humanization.repertoireConsistency?.enabled && OpeningBook._repertoire[color][posKey]) {
                                    const prevMove = OpeningBook._repertoire[color][posKey];
                                    const found = data.moves.find(m => m.uci === prevMove);
                                    if (found) {
                                        Utils.log(`Book: Repertoire hit - playing ${prevMove} again`, 'debug');
                                        resolve(prevMove);
                                        return;
                                    }
                                }

                                // Weighted selection from top 3
                                const topMoves = data.moves.slice(0, 3);
                                const totalGames = topMoves.reduce((sum, m) => sum + m.white + m.draw + m.black, 0);
                                let r = Math.random() * totalGames;
                                let chosen = topMoves[0].uci;
                                for (const move of topMoves) {
                                    const games = move.white + move.draw + move.black;
                                    if (r < games) {
                                        chosen = move.uci;
                                        break;
                                    }
                                    r -= games;
                                }

                                // Remember this choice for repertoire consistency
                                OpeningBook._repertoire[color][posKey] = chosen;
                                resolve(chosen);
                            } else {
                                resolve(null);
                            }
                        } catch (e) {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null),
                    ontimeout: () => resolve(null),
                });
            });
        }
    };

    // ═══════════════════════════════════════════
    //  SYZYGY TABLEBASE
    // ═══════════════════════════════════════════
    const Tablebase = {
        probe: (fen) => {
            if (!CONFIG.useTablebase) return Promise.resolve(null);
            const pieceCount = Utils.countPieces(fen);
            if (pieceCount > CONFIG.tablebase.maxPieces) return Promise.resolve(null);

            Utils.log(`Tablebase: Probing ${pieceCount}-piece position`, 'debug');
            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `${CONFIG.tablebase.url}?fen=${encodeURIComponent(fen)}`,
                    timeout: 5000,
                    onload: (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.moves && data.moves.length > 0) {
                                // Sort by DTZ: winning moves first (positive DTZ for side to move)
                                // category: "win", "draw", "loss", etc.
                                const winning = data.moves.filter(m => m.category === 'win');
                                const drawing = data.moves.filter(m => m.category === 'draw' || m.category === 'blessed-loss');
                                const best = winning.length > 0 ? winning : (drawing.length > 0 ? drawing : data.moves);

                                // Don't always pick the absolute best DTZ - humans don't know tablebase
                                // Pick from top 3 winning moves for variety
                                const candidates = best.slice(0, 3);
                                const chosen = candidates[Math.floor(Math.random() * candidates.length)];
                                Utils.log(`Tablebase: ${chosen.uci} (${chosen.category}, DTZ: ${chosen.dtz})`, 'info');
                                resolve({ move: chosen.uci, category: chosen.category, dtz: chosen.dtz });
                            } else {
                                resolve(null);
                            }
                        } catch (e) {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null),
                    ontimeout: () => resolve(null),
                });
            });
        }
    };

    // ═══════════════════════════════════════════
    //  API ENGINE (chess-api.com / stockfish.online)
    // ═══════════════════════════════════════════
    const APIEngine = {
        // chess-api.com (Stockfish 18.1)
        analyzeChessApi: (fen, depth) => {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: 'POST',
                    url: CONFIG.api.chessApi.url,
                    headers: { 'Content-Type': 'application/json' },
                    data: JSON.stringify({
                        fen: fen,
                        depth: Math.min(depth, 18),
                        maxThinkingTime: CONFIG.api.chessApi.maxThinkingTime,
                    }),
                    timeout: CONFIG.api.chessApi.timeout,
                    onload: (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.move || data.text) {
                                let move = data.move;
                                if (!move && data.text) {
                                    // Parse "bestmove e2e4 ponder d7d5"
                                    const match = data.text.match(/bestmove\s+(\S+)/);
                                    if (match) move = match[1];
                                }
                                if (!move) { reject('No move in response'); return; }

                                let evalValue = 0;
                                let evalType = 'cp';
                                if (data.mate != null && data.mate !== false) {
                                    evalType = 'mate';
                                    evalValue = data.mate;
                                } else if (data.eval != null) {
                                    evalValue = typeof data.eval === 'number' ? data.eval / 100 : parseFloat(data.eval) / 100;
                                }

                                // Flip eval for black (API usually gives from white's perspective)
                                if (State.playerColor === 'b' && evalType === 'cp') {
                                    evalValue = -evalValue;
                                }

                                resolve({
                                    move: move,
                                    eval: { type: evalType, value: evalValue },
                                    depth: data.depth || depth,
                                    ponder: data.ponder || null,
                                    continuation: data.continuationArr || (data.continuation ? data.continuation.split(' ') : []),
                                    source: 'api',
                                });
                            } else {
                                reject('Invalid API response');
                            }
                        } catch (e) {
                            reject(e);
                        }
                    },
                    onerror: (e) => reject(e),
                    ontimeout: () => reject('timeout'),
                });
            });
        },

        // stockfish.online (Stockfish 16)
        analyzeStockfishOnline: (fen, depth) => {
            return new Promise((resolve, reject) => {
                const url = `${CONFIG.api.stockfishOnline.url}?fen=${encodeURIComponent(fen)}&depth=${Math.min(depth, 15)}`;
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: url,
                    timeout: CONFIG.api.stockfishOnline.timeout,
                    onload: (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.success && data.bestmove) {
                                const moveMatch = data.bestmove.match(/bestmove\s+(\S+)/);
                                if (!moveMatch) { reject('No move parsed'); return; }

                                let evalValue = data.evaluation || 0;
                                let evalType = 'cp';
                                if (data.mate != null && data.mate !== null) {
                                    evalType = 'mate';
                                    evalValue = data.mate;
                                } else {
                                    evalValue = parseFloat(evalValue);
                                }

                                if (State.playerColor === 'b' && evalType === 'cp') {
                                    evalValue = -evalValue;
                                }

                                resolve({
                                    move: moveMatch[1],
                                    eval: { type: evalType, value: evalValue },
                                    depth: depth,
                                    ponder: null,
                                    continuation: data.continuation ? data.continuation.split(' ') : [],
                                    source: 'stockfish_online',
                                });
                            } else {
                                reject('API returned error');
                            }
                        } catch (e) {
                            reject(e);
                        }
                    },
                    onerror: (e) => reject(e),
                    ontimeout: () => reject('timeout'),
                });
            });
        },

        // Unified API call with fallback chain
        analyze: async (fen, depth) => {
            const tryApi = async (method, name) => {
                try {
                    const result = await method(fen, depth);
                    State.apiAvailable = true;
                    State.apiFailCount = 0;
                    return result;
                } catch (e) {
                    Utils.log(`${name} failed: ${e}`, 'warn');
                    return null;
                }
            };

            // Try based on active engine type (rotation-aware)
            const activeType = Account.currentEngine().type;
            if (activeType === 'api' || activeType === 'stockfish_online') {
                // Try primary
                let result = null;
                if (activeType === 'api') {
                    result = await tryApi(APIEngine.analyzeChessApi, 'chess-api.com');
                    if (!result) result = await tryApi(APIEngine.analyzeStockfishOnline, 'stockfish.online');
                } else {
                    result = await tryApi(APIEngine.analyzeStockfishOnline, 'stockfish.online');
                    if (!result) result = await tryApi(APIEngine.analyzeChessApi, 'chess-api.com');
                }

                if (result) return result;

                // Both APIs failed
                State.apiFailCount++;
                if (State.apiFailCount >= 3) {
                    State.apiAvailable = false;
                    Utils.log('APIs unreachable, falling back to local engine', 'error');
                }
            }
            return null; // will trigger local fallback
        }
    };

    // ═══════════════════════════════════════════
    //  LOCAL ENGINE (Stockfish.js 10.0.2 fallback)
    // ═══════════════════════════════════════════
    const LocalEngine = {
        init: async () => {
            if (State.workers.stockfish) return;
            Utils.log('Initializing local Stockfish fallback...');
            try {
                const scriptContent = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: 'https://unpkg.com/[email protected]/stockfish.js',
                        timeout: 15000,
                        onload: (res) => resolve(res.responseText),
                        onerror: (err) => reject(err),
                        ontimeout: () => reject('timeout'),
                    });
                });
                const blob = new Blob([scriptContent], { type: 'application/javascript' });
                State.workers.stockfish = new Worker(URL.createObjectURL(blob));
                State.workers.stockfish.onmessage = (e) => {
                    const msg = e.data;
                    if (msg === 'uciok') {
                        State.engineReady = true;
                        Utils.log('Local Stockfish ready (fallback)');
                    }
                    if (msg.startsWith('bestmove')) {
                        const move = msg.split(' ')[1];
                        if (State._localResolve) {
                            State._localResolve(move);
                            State._localResolve = null;
                        }
                    }
                    if (msg.startsWith('info') && msg.includes('score')) {
                        LocalEngine.parseScore(msg);
                    }
                };
                State.workers.stockfish.postMessage('uci');
                State.workers.stockfish.postMessage('isready');
                State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
            } catch (e) {
                Utils.log('Local Stockfish init failed: ' + e, 'error');
            }
        },

        parseScore: (msg) => {
            const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/);
            const pvMatch = msg.match(/multipv (\d+)/);
            const depthMatch = msg.match(/depth (\d+)/);
            const moveMatch = msg.match(/ pv (\w+)/);

            if (scoreMatch && pvMatch && moveMatch) {
                const type = scoreMatch[1];
                let value = parseInt(scoreMatch[2]);
                const mpv = parseInt(pvMatch[1]);
                const depth = parseInt(depthMatch?.[1] || 0);

                if (type === 'cp') value = value / 100;

                State.candidates[mpv] = {
                    move: moveMatch[1],
                    eval: { type, value },
                    depth,
                };

                if (mpv === 1) {
                    State.currentEval = { type, value, depth };
                    State.currentBestMove = moveMatch[1];
                }

                if (mpv === 1 && msg.includes(' pv ')) {
                    const pvMoves = msg.split(' pv ')[1].split(' ');
                    if (pvMoves.length > 1) {
                        State.opponentResponse = pvMoves[1];
                    }
                }
            }
        },

        // Analyze with local SF.js - returns promise that resolves with best move
        analyze: (fen, depth) => {
            if (!State.workers.stockfish || !State.engineReady) return Promise.resolve(null);

            return new Promise((resolve) => {
                // For local SF 10.0.2, cap depth to prevent hanging
                // SF10 can't handle high depths efficiently
                const safedepth = Math.min(depth, 12);

                State.candidates = {};
                State._localResolve = resolve;

                State.workers.stockfish.postMessage('stop');
                State.workers.stockfish.postMessage(`position fen ${fen}`);
                State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
                State.workers.stockfish.postMessage(`go depth ${safedepth}`);

                // Safety timeout: if SF doesn't respond in 10s, resolve null
                setTimeout(() => {
                    if (State._localResolve === resolve) {
                        State._localResolve = null;
                        Utils.log('Local SF timeout, resolving with current best', 'warn');
                        resolve(State.currentBestMove);
                    }
                }, 10000);
            });
        },

        // Quick analysis for multi-PV candidates (runs alongside API)
        analyzeForCandidates: (fen) => {
            if (!State.workers.stockfish || !State.engineReady) return;
            State.candidates = {};
            State.workers.stockfish.postMessage('stop');
            State.workers.stockfish.postMessage(`position fen ${fen}`);
            State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
            // Use low depth for quick candidates - just need alternatives, not perfect eval
            State.workers.stockfish.postMessage(`go depth ${Math.min(10, CONFIG.engineDepth.base)}`);
        },
    };

    // ═══════════════════════════════════════════
    //  UNIFIED ENGINE
    // ═══════════════════════════════════════════
    const Engine = {
        init: async () => {
            UI.updateStatus('orange');
            // Always init local engine as fallback
            await LocalEngine.init();
            // Test API availability
            if (CONFIG.engineType !== 'local') {
                Utils.log('Testing API engine...');
                try {
                    const testResult = await APIEngine.analyze('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1', 8);
                    if (testResult) {
                        Utils.log(`API engine OK: ${testResult.source} returned ${testResult.move}`, 'info');
                        State.apiAvailable = true;
                    } else {
                        Utils.log('API engines unavailable, will use local fallback', 'warn');
                        State.apiAvailable = false;
                    }
                } catch (e) {
                    State.apiAvailable = false;
                }
            }
            UI.updateStatus(UI._styleAccents[CONFIG.playStyle] || '#4caf50');
            HumanStrategy.initGamePersonality();
        },

        analyze: async (fen) => {
            if (State.isThinking) return;
            State.isThinking = true;
            State.candidates = {};
            State.apiResult = null;
            State._lastEngineSource = null;

            // --- Opening book check (with tapering — item 7) ---
            // Real players don't have a hard cutoff where they leave book.
            // They gradually forget their prep — first few moves are automatic,
            // then the probability of still being "in book" drops off.
            if (CONFIG.useBook && State.moveCount < 20) {
                let bookChance = 1.0;
                if (State.moveCount > 14) bookChance = 0.25;
                else if (State.moveCount > 10) bookChance = 0.50;
                else if (State.moveCount > 7) bookChance = 0.80;

                if (Math.random() < bookChance) {
                    const bookMove = await OpeningBook.fetchMove(fen);
                    if (bookMove) {
                        Utils.log(`Book Move: ${bookMove} (bookChance=${(bookChance*100).toFixed(0)}%)`);
                        UI.toast('Opening Book', `Playing known book move ${bookMove}`, 'book', 3000);
                        State._lastEngineSource = 'book';
                        Engine.handleResult(bookMove, fen, true);
                        return;
                    }
                }
            }

            // --- Tablebase check ---
            if (CONFIG.useTablebase) {
                const tbResult = await Tablebase.probe(fen);
                if (tbResult) {
                    Utils.log(`Tablebase Move: ${tbResult.move} (${tbResult.category})`);
                    UI.toast('Tablebase', `Perfect endgame: ${tbResult.move} (${tbResult.category})`, 'tablebase', 3500);
                    State._lastEngineSource = 'tablebase';
                    // Set eval based on tablebase result
                    State.currentEval = {
                        type: tbResult.category === 'win' ? 'mate' : 'cp',
                        value: tbResult.category === 'win' ? Math.abs(tbResult.dtz) : 0,
                    };
                    Engine.handleResult(tbResult.move, fen, false);
                    return;
                }
            }

            // --- Fetch player DB moves in parallel (non-blocking) ---
            const playerDBPromise = PlayerMoveDB.fetch(fen);

            // --- Main engine analysis (rotation-aware) ---
            const engineInfo = Account.currentEngine();
            const activeType = engineInfo.type;
            let depth = HumanStrategy.getDynamicDepth(fen);
            if (engineInfo.depthCap != null) {
                depth = Math.min(depth, engineInfo.depthCap);
            }
            Utils.log(`Analyzing depth ${depth} (engine: ${activeType}${engineInfo.depthCap ? ', shallow-rotation' : ''})`, 'debug');

            // Start local SF for multi-PV candidates in parallel
            if (activeType !== 'local') {
                LocalEngine.analyzeForCandidates(fen);
            }

            // Try API engine first (if configured)
            if (activeType !== 'local' && State.apiAvailable) {
                const apiResult = await APIEngine.analyze(fen, depth);
                if (apiResult) {
                    State.apiResult = apiResult;
                    State._lastEngineSource = apiResult.source;

                    // Use API result as PV1, overriding local SF's PV1
                    State.currentEval = apiResult.eval;
                    State.currentBestMove = apiResult.move;
                    State.candidates[1] = {
                        move: apiResult.move,
                        eval: apiResult.eval,
                        depth: apiResult.depth,
                    };

                    // Extract opponent response from continuation
                    if (apiResult.continuation && apiResult.continuation.length > 1) {
                        State.opponentResponse = apiResult.continuation[1];
                    }

                    // Wait a tiny bit for local SF to populate PV2-5 candidates
                    await Utils.sleep(500);

                    Engine.handleResult(apiResult.move, fen, false);
                    return;
                }
            }

            // Fallback: use local SF.js for everything
            State._lastEngineSource = 'local';
            const localBest = await LocalEngine.analyze(fen, depth);
            if (localBest) {
                Engine.handleResult(localBest, fen, false);
            } else {
                State.isThinking = false;
                Utils.log('All engines failed!', 'error');
                UI.updateStatus('red');
            }
        },

        handleResult: async (bestMove, fen, isBook) => {
            State.isThinking = false;
            State.moveCount++;

            // Update time pressure multipliers before move selection
            HumanStrategy.updateTimePressure();

            // --- Opponent-move surprise detection ---
            // Compare eval now vs eval before opponent moved — big swing = surprising move
            if (State.human.evalBeforeOpponentMove != null && State.currentEval && State.currentEval.type === 'cp') {
                const swing = Math.abs(State.currentEval.value - State.human.evalBeforeOpponentMove);
                State.human.opponentMoveSurprise = Math.min(1.0, Math.max(0, (swing - 0.3) / 1.7));
            } else {
                State.human.opponentMoveSurprise = 0;
            }

            const phase = HumanStrategy.getGamePhase(fen);

            // Check auto-accept draw when losing
            if (CONFIG.auto.enabled && !isBook && State.currentEval) {
                const drawOffer = document.querySelector(SELECTORS.drawOffer);
                if (drawOffer && State.currentEval.value <= -1.0) {
                    const acceptBtn = drawOffer.querySelector('button, [class*="accept"], [data-cy*="accept"]') || drawOffer;
                    if (acceptBtn) {
                        const drawDelay = Utils.humanDelay(1500, 5000);
                        Utils.log(`Auto-Accept Draw: eval ${State.currentEval.value}, accepting in ${Math.round(drawDelay)}ms`, 'warn');
                        UI.toast('Draw Accepted', `Eval ${State.currentEval.value.toFixed(1)} — accepting draw offer`, 'draw', 5000);
                        await Utils.sleep(drawDelay);
                        acceptBtn.click();
                        return;
                    }
                }
            }

            // --- Draw offer behavior (item 8) ---
            // Real humans offer draws in dead-equal, simplified positions after move 35+
            // Never offering draws across hundreds of games is a detectable pattern
            if (CONFIG.auto.enabled && !isBook && State.currentEval && State.moveCount >= 35) {
                const ev = State.currentEval.value;
                const pieces = Utils.countPieces(fen);
                const isDrawish = Math.abs(ev) < 0.25 && pieces <= 14; // equal + simplified
                const isLongGame = State.moveCount >= 50;

                if (isDrawish || (isLongGame && Math.abs(ev) < 0.5)) {
                    // 8% chance per qualifying move to offer a draw
                    if (Math.random() < 0.08) {
                        const drawBtn = document.querySelector('[data-cy="draw"], [aria-label*="draw" i], .draw-button-component button, button[class*="draw"]');
                        if (drawBtn) {
                            const drawDelay = Utils.humanDelay(2000, 6000);
                            Utils.log(`Draw Offer: eval ${ev.toFixed(2)}, ${pieces} pieces, move ${State.moveCount}`, 'info');
                            UI.toast('Draw Offered', `Position is dead equal — offering draw`, 'draw', 4000);
                            await Utils.sleep(drawDelay);
                            drawBtn.click();
                            // Don't return — still play a move in case they decline
                        }
                    }
                }
            }

            // Check auto-resign before playing (don't waste a move if we're resigning)
            if (CONFIG.auto.enabled && !isBook) {
                const resigned = await HumanStrategy.checkAutoResign();
                if (resigned) return;
            }

            // Use HumanStrategy to decide which move to actually play
            const moveResult = HumanStrategy.selectMove(fen, bestMove, isBook);
            const finalMove = moveResult.move;

            const modeTag = State.human.autoLoseActive ? ' [AUTO-LOSE]' : '';
            const tpTag = State.human.timePressureMult.suboptimal > 1 ? ` [TP x${State.human.timePressureMult.suboptimal}]` : '';
            Utils.log(`Move #${State.moveCount} [${phase}]: ${finalMove} (${moveResult.reason})${moveResult.isBest ? '' : ' [SUBOPTIMAL]'}${modeTag}${tpTag}`);

            // Toast notification based on move type
            const evalStr = State.currentEval ? (State.currentEval.type === 'mate' ? `M${State.currentEval.value}` : `${State.currentEval.value > 0 ? '+' : ''}${State.currentEval.value.toFixed(1)}`) : '?';
            if (moveResult.reason === 'blunder') {
                UI.toast('Blunder', `Intentional mistake: ${finalMove} (eval ${evalStr})`, 'blunder', 4500);
            } else if (moveResult.reason === 'suboptimal') {
                UI.toast('Suboptimal', `Playing 2nd/3rd choice: ${finalMove} instead of ${bestMove}`, 'suboptimal', 3500);
            } else if (moveResult.reason === 'random-legal') {
                UI.toast('Random Move', `Anti-correlation: ${finalMove} (not from engine PV)`, 'fakeout', 4000);
            } else if (moveResult.reason === 'missed-tactic') {
                UI.toast('Missed Tactic', `Played safe instead of small tactic: ${finalMove}`, 'suboptimal', 4000);
            } else if (moveResult.reason === 'close-alt') {
                UI.toast('Close Alternative', `${finalMove} nearly equal to ${bestMove} — diversifying`, 'move', 3000);
            } else if (moveResult.reason === 'correlation-cap') {
                UI.toast('Anti-Correlation', `Top move rate too high — playing ${finalMove}`, 'fakeout', 3500);
            } else if (moveResult.reason === 'player-db') {
                UI.toast('Human Move', `Real players at this rating play ${finalMove}`, 'book', 3500);
            } else if (moveResult.reason === 'book') {
                // already toasted above
            } else {
                UI.toast(`Move #${State.moveCount}`, `Best: ${finalMove} [${phase}] (${evalStr})`, 'move', 2500);
            }

            // Draw arrows
            UI.drawMove(bestMove, '#4caf50');
            if (finalMove !== bestMove) {
                UI.drawMove(finalMove, '#ffcc00', true);
            }
            if (CONFIG.showThreats && State.opponentResponse) {
                UI.drawMove(State.opponentResponse, '#ff5252', true);
            }

            UI.updatePanel(State.currentEval, { move: finalMove });

            // Track for correlation management
            HumanStrategy.trackMove(moveResult.isBest, moveResult);

            // Store predicted opponent reply for premove simulation
            if (State.opponentResponse) {
                State.human.predictedReply = State.opponentResponse;
                State.human.predictedReplyFen = fen;
            }

            // Auto-play
            if (CONFIG.auto.enabled && Game.isMyTurn(fen)) {
                // ANTI-DETECTION: Random AFK delay (simulates human distraction)
                const afk = CONFIG.antiDetection.randomAFK;
                if (afk.enabled && Math.random() < afk.chance) {
                    const afkDelay = Utils.humanDelay(afk.delay.min, afk.delay.max);
                    Utils.log(`AFK pause: ${Math.round(afkDelay)}ms (simulating distraction)`, 'debug');
                    UI.toast('AFK Pause', `Simulating distraction for ${(afkDelay / 1000).toFixed(1)}s`, 'afk', Math.min(afkDelay, 5000));
                    await Utils.sleep(afkDelay);
                    // Re-check position hasn't changed during AFK
                    if (Game.getFen() !== fen) {
                        Utils.log('Position changed during AFK, aborting', 'warn');
                        return;
                    }
                }

                const delay = HumanStrategy.calculateDelay(fen, moveResult, isBook);
                State.human.lastThinkTime = delay; // store for momentum tracking
                Utils.log(`Waiting ${Math.round(delay)}ms (${moveResult.reason})...`);
                await Utils.sleep(delay);

                // Verify position hasn't changed during delay
                const currentFen = Game.getFen();
                if (currentFen === fen) {
                    await Humanizer.executeMove(finalMove);
                } else {
                    Utils.log('Position changed during delay, aborting move', 'warn');
                }
            }
        }
    };

    // ═══════════════════════════════════════════
    //  HUMAN STRATEGY
    // ═══════════════════════════════════════════
    const HumanStrategy = {
        getGamePhase: (fen) => {
            if (!fen) return 'middlegame';
            const board = fen.split(' ')[0];
            const moveNum = State.moveCount;
            const minorMajor = (board.match(/[rnbqRNBQ]/g) || []).length;
            const queens = (board.match(/[qQ]/g) || []).length;
            if (moveNum <= 12) return 'opening';
            if (minorMajor <= 6 || (queens === 0 && minorMajor <= 8)) return 'endgame';
            return 'middlegame';
        },

        getPositionComplexity: (fen) => {
            if (!fen) return 0.5;
            const board = fen.split(' ')[0];
            const pieces = (board.match(/[rnbqRNBQ]/g) || []).length;
            const pawns = (board.match(/[pP]/g) || []).length;
            let complexity = (pieces + pawns * 0.5) / 24;
            if (State.currentEval && Math.abs(State.currentEval.value) < 0.5) {
                complexity += 0.2;
            }
            // More pawns in center = more tactical complexity
            const ranks = board.split('/');
            let centerPawns = 0;
            for (const rank of ranks) {
                let col = 0;
                for (const ch of rank) {
                    if (ch >= '1' && ch <= '8') col += parseInt(ch);
                    else { if ((col === 3 || col === 4) && (ch === 'p' || ch === 'P')) centerPawns++; col++; }
                }
            }
            if (centerPawns >= 2) complexity += 0.15;
            if (pieces > 10) complexity += 0.1;
            return Math.min(1, Math.max(0, complexity));
        },

        initGamePersonality: () => {
            const pv = CONFIG.humanization.personalityVariance;
            if (!pv.enabled) {
                State.human.gamePersonality = { suboptimalMult: 1, depthOffset: 0, timingMult: 1 };
                return;
            }
            State.human.gamePersonality = {
                suboptimalMult: 1 + (Math.random() * 2 - 1) * pv.suboptimalRateJitter,
                depthOffset: Math.round((Math.random() * 2 - 1) * pv.depthJitter),
                timingMult: 1 + (Math.random() * 2 - 1) * pv.timingJitter,
            };
            Utils.log(`Personality: subopt x${State.human.gamePersonality.suboptimalMult.toFixed(2)}, depth ${State.human.gamePersonality.depthOffset > 0 ? '+' : ''}${State.human.gamePersonality.depthOffset}, timing x${State.human.gamePersonality.timingMult.toFixed(2)}`, 'debug');
        },

        resetGame: () => {
            State.human.perfectStreak = 0;
            State.human.sloppyStreak = 0;
            State.human.bestMoveCount = 0;
            State.human.totalMoveCount = 0;
            State.human.lastMoveWasBest = true;
            State.human.clusterMode = 'normal';
            State.human.clusterMovesLeft = 0;
            State.human.predictedReply = null;
            State.human.consecutiveLosingEvals = 0;
            State.human.autoLoseActive = false;
            // Reset messy-resign decision cache each game
            State.human.resignDecisionMade = false;
            State.human.resignExtraMoves = null;
            State.human.resignBlunderNext = false;
            State.human.resignHolding = false;
            State.human.timePressureMult = { suboptimal: 1, blunder: 1, maxCPLoss: 1 };
            State.human.topMoveCount = 0;
            State.human._prevEval = 0;
            State.human.thinkCategory = 'normal';
            State.human.opponentMoveSurprise = 0;
            State.human.lastThinkTime = 0;
            State.human.evalBeforeOpponentMove = null;
            // NOTE: playerTempo and playerAccuracyBand are NOT reset — they persist per account
            State.lastMoveWasCapture = false;
            State.moveCount = 0;
            State.candidates = {};
            State.recentTimings = [];
            HumanStrategy.initGamePersonality();

            // ---------- Account-level setup for this game ----------
            Account.ensureHardwarePersona();
            Account.ensureRepertoire();
            Account.rollEngineForGame();
            // Apply tilt for this game if active
            const tiltCfg = Account.consumeTiltTick();
            State.human.tiltActive = !!tiltCfg;
            if (tiltCfg) {
                Utils.log(`Tilt active this game: subopt+${tiltCfg.suboptimalBoost}, blunder x${tiltCfg.blunderMult}, timing x${tiltCfg.timingMult}`, 'warn');
            }

            // Determine effective target rating for this game.
            // Priority: warmup ramp > opponent adaptation > base.
            let effectiveTarget = Account.effectiveTargetRating();
            const inWarmup = Account.isInWarmup();
            if (inWarmup) {
                Utils.log(`Warmup: game ${CONFIG.account.totalGamesPlayed + 1}/${CONFIG.warmup.durationGames}, target=${effectiveTarget} (base ${CONFIG.targetRating})`, 'info');
                UI.toast('Warmup', `Game ${CONFIG.account.totalGamesPlayed + 1}/${CONFIG.warmup.durationGames} - playing as ~${effectiveTarget}`, 'info', 3500);
            }

            // Opponent adaptation: read opponent rating and adjust target.
            // Skip when in warmup (warmup target wins).
            if (CONFIG.opponentAdaptation.enabled && !inWarmup) {
                const oppRating = Game.readOpponentRating();
                if (oppRating) {
                    State.human.opponentRating = oppRating;
                    effectiveTarget = Math.max(
                        CONFIG.opponentAdaptation.minRating,
                        Math.min(CONFIG.opponentAdaptation.maxRating, oppRating + CONFIG.opponentAdaptation.ratingEdge)
                    );
                    Utils.log(`Opponent Adaptation: Opponent is ${oppRating}, targeting ${effectiveTarget} (edge: +${CONFIG.opponentAdaptation.ratingEdge})`, 'info');
                } else {
                    Utils.log('Opponent Adaptation: Could not read opponent rating, using default', 'debug');
                }
            }
            RatingProfile.apply(effectiveTarget);

            // Auto-lose mode — adaptive instead of binary trigger.
            //
            // Old behavior: when streak >= triggerStreak, ALWAYS throw the game.
            // That produces an obvious pattern: "wins N in a row, then a clean
            // textbook resign". Real players don't do that; they slowly degrade.
            //
            // New behavior: from (triggerStreak - 2) onwards, the probability of
            // throwing this game ramps smoothly with each additional win, so
            // some streaks naturally end in losses earlier and others go a game
            // or two longer.
            if (CONFIG.autoLose.enabled) {
                const streak  = CONFIG.session.currentWinStreak;
                const trigger = CONFIG.autoLose.triggerStreak;
                let loseChance = 0;
                if (streak >= trigger)        loseChance = 0.85;
                else if (streak === trigger - 1) loseChance = 0.45;
                else if (streak === trigger - 2) loseChance = 0.18;

                // Session winrate bump (short-window, intra-session)
                const wr = CONFIG.session.gamesPlayed > 4
                    ? CONFIG.session.wins / CONFIG.session.gamesPlayed
                    : 0;
                if (wr >= 0.85) loseChance = Math.max(loseChance, 0.35);

                // Lifetime winrate-overshoot bump (sliding window across sessions)
                const overshootBoost = Account.winrateOvershootBoost();
                if (overshootBoost > 0) {
                    loseChance = Math.min(0.95, loseChance + overshootBoost);
                    const wrLong = Account.recentWinrate();
                    Utils.log(`WinrateTarget: long-window WR ${(wrLong*100).toFixed(0)}% > target ${(CONFIG.winrateTarget.target*100).toFixed(0)}% -> +${(overshootBoost*100).toFixed(0)}% lose chance`, 'warn');
                }

                if (loseChance > 0 && Math.random() < loseChance) {
                    State.human.autoLoseActive = true;
                    Utils.log(`AUTO-LOSE: streak=${streak} sessWR=${wr.toFixed(2)} boost=${overshootBoost.toFixed(2)} -> p(lose)=${loseChance.toFixed(2)} ROLLED`, 'error');
                    UI.toast('Throwing this one', `Streak ${streak} - playing to lose for cover`, 'error', 6000);
                } else if (loseChance > 0) {
                    Utils.log(`Auto-Lose: p(lose)=${loseChance.toFixed(2)} did NOT roll, playing normally`, 'warn');
                }
            }
        },

        // Update time pressure accuracy multipliers based on current clock
        updateTimePressure: () => {
            const tp = CONFIG.timePressure;
            if (!tp.enabled || State.clock.myTime == null) {
                State.human.timePressureMult = { suboptimal: 1, blunder: 1, maxCPLoss: 1 };
                return;
            }
            for (const t of tp.thresholds) {
                if (State.clock.myTime <= t.secondsBelow) {
                    State.human.timePressureMult = {
                        suboptimal: t.suboptimalMult,
                        blunder: t.blunderMult,
                        maxCPLoss: t.maxCPLossMult,
                    };
                    Utils.log(`Time Pressure: ${State.clock.myTime}s left, subopt x${t.suboptimalMult}, blunder x${t.blunderMult}`, 'debug');
                    if (t.suboptimalMult >= 2) UI.toast('Time Pressure', `${State.clock.myTime}s left — accuracy dropping`, 'time', 3000);
                    return;
                }
            }
            State.human.timePressureMult = { suboptimal: 1, blunder: 1, maxCPLoss: 1 };
        },

        // Check if auto-resign should trigger
        checkAutoResign: async () => {
            const ar = CONFIG.autoResign;
            if (!ar.enabled) return false;
            if (State.moveCount < ar.minMoveNumber) return false;

            const eval_ = State.currentEval;
            if (!eval_) return false;

            if (eval_.value <= ar.evalThreshold || (eval_.type === 'mate' && eval_.value < 0)) {
                State.human.consecutiveLosingEvals++;
                Utils.log(`Auto-Resign: Losing eval #${State.human.consecutiveLosingEvals}/${ar.consecutiveMoves} (eval: ${eval_.value})`, 'debug');
            } else {
                State.human.consecutiveLosingEvals = 0;
                // Eval recovered — clear any messy-resign scheduling
                State.human.resignExtraMoves = null;
                State.human.resignBlunderNext = false;
                State.human.resignHolding = false;
            }

            if (State.human.consecutiveLosingEvals < ar.consecutiveMoves) return false;

            const mr = CONFIG.messyResign;

            // First time we hit the threshold for this lost position — roll all
            // the behavioral dice ONCE and cache the outcome so we're consistent
            // for the rest of the game.
            if (mr.enabled && State.human.resignDecisionMade !== true) {
                State.human.resignDecisionMade = true;

                // Roll: hold the lost position (refuse to resign entirely)?
                if (Math.random() < mr.holdLostChance) {
                    State.human.resignHolding = true;
                    Utils.log(`MessyResign: Chose to HOLD lost position (no resign this game)`, 'warn');
                    UI.toast('Stubborn', `Not resigning — playing it out`, 'warn', 4000);
                    return false;
                }

                // Roll: play some extra moves before resigning (looks like contemplation)
                const extra = mr.extraMovesBeforeResign;
                State.human.resignExtraMoves = Math.round(Utils.randomRange(extra.min, extra.max));

                // Roll: blunder once more before resigning
                State.human.resignBlunderNext = Math.random() < mr.blunderBeforeChance;

                Utils.log(`MessyResign: will play ${State.human.resignExtraMoves} more move(s)${State.human.resignBlunderNext ? ' with a blunder' : ''} before resigning`, 'warn');
            }

            if (State.human.resignHolding) return false;

            // If we have remaining "extra moves before resign", count this check as
            // one, and signal to the caller that we're NOT resigning this turn.
            // The actual blunder behavior (if scheduled) is wired into selectMove.
            if (mr.enabled && State.human.resignExtraMoves > 0) {
                State.human.resignExtraMoves--;
                Utils.log(`MessyResign: playing on, ${State.human.resignExtraMoves} more moves before resign`, 'debug');
                return false;
            }

            // Fallback / disabled mode: use the old random-chance gate
            if (!mr.enabled && Math.random() >= ar.resignChance) {
                Utils.log('Auto-Resign: Skipped (random chance - playing on like a stubborn human)', 'debug');
                State.human.consecutiveLosingEvals = 0;
                return false;
            }

            const delay = Utils.humanDelay(ar.delay.min, ar.delay.max);
            Utils.log(`Auto-Resign: Triggering in ${Math.round(delay)}ms (eval: ${eval_.value}, ${State.human.consecutiveLosingEvals} consecutive)`, 'warn');
            UI.toast('Auto-Resign', `Eval ${eval_.value.toFixed(1)} for ${State.human.consecutiveLosingEvals} moves - resigning`, 'resign', 5000);
            await Utils.sleep(delay);
            return await Game.resign();
        },

        getDynamicDepth: (fen) => {
            const cfg = CONFIG.engineDepth;
            if (!cfg.dynamicDepth) return cfg.base;

            const complexity = HumanStrategy.getPositionComplexity(fen);
            const phase = HumanStrategy.getGamePhase(fen);
            const personality = State.human.gamePersonality || { depthOffset: 0 };

            let depth = cfg.base + personality.depthOffset;
            if (complexity < 0.3) depth -= 2;
            else if (complexity > 0.7) depth += 2;
            if (phase === 'endgame') depth += 1;
            depth += Math.round(Math.random() * 2 - 1);

            return Math.max(cfg.min, Math.min(cfg.max, depth));
        },

        // Accuracy clustering: simulate hot/cold streaks
        updateCluster: () => {
            const ac = CONFIG.humanization.accuracyClustering;
            if (!ac?.enabled) return;

            if (State.human.clusterMovesLeft > 0) {
                State.human.clusterMovesLeft--;
                if (State.human.clusterMovesLeft === 0) {
                    Utils.log(`Cluster: Exiting ${State.human.clusterMode} mode`, 'debug');
                    State.human.clusterMode = 'normal';
                }
                return;
            }

            // Chance to enter a streak
            const r = Math.random();
            if (r < ac.hotStreakChance) {
                State.human.clusterMode = 'hot';
                State.human.clusterMovesLeft = Math.round(Utils.randomRange(ac.streakDuration.min, ac.streakDuration.max));
                Utils.log(`Cluster: Entering HOT streak (${State.human.clusterMovesLeft} moves)`, 'debug');
                UI.toast('Hot Streak', `Playing sharp for ${State.human.clusterMovesLeft} moves`, 'streak', 3000);
            } else if (r < ac.hotStreakChance + ac.coldStreakChance) {
                State.human.clusterMode = 'cold';
                State.human.clusterMovesLeft = Math.round(Utils.randomRange(ac.streakDuration.min, ac.streakDuration.max));
                Utils.log(`Cluster: Entering COLD streak (${State.human.clusterMovesLeft} moves)`, 'debug');
                UI.toast('Cold Streak', `Playing sloppy for ${State.human.clusterMovesLeft} moves`, 'warn', 3000);
            }
        },

        shouldPlaySuboptimal: (fen, extraBoost = 0) => {
            const h = CONFIG.humanization;
            if (!h.enabled) return false;

            // AUTO-LOSE MODE: massively increase error rate
            if (State.human.autoLoseActive && State.moveCount >= CONFIG.autoLose.minMovesBeforeLosing) {
                const rate = CONFIG.autoLose.suboptimalRate;
                Utils.log(`Auto-Lose: suboptimal rate ${(rate * 100).toFixed(0)}%`, 'debug');
                return Math.random() < rate;
            }

            const phase = HumanStrategy.getGamePhase(fen);
            const personality = State.human.gamePersonality || { suboptimalMult: 1 };
            const eval_ = State.currentEval;

            let rate = h.suboptimalMoveRate[phase] || 0.25;
            rate *= personality.suboptimalMult;

            // Persistent per-account accuracy band (item 5: multi-game consistency)
            rate += State.human.playerAccuracyBand;

            // Apply extra boost from timing-accuracy coupling and weakness profile
            rate += extraBoost;

            // Time pressure accuracy drop
            rate *= State.human.timePressureMult.suboptimal;

            // Tilt: active after a loss — additive suboptimal boost
            if (State.human.tiltActive && CONFIG.tilt?.enabled) {
                rate += CONFIG.tilt.suboptimalBoost;
            }

            // Accuracy cluster modifier
            if (State.human.clusterMode === 'hot') {
                rate *= 0.3; // 70% fewer errors during hot streak
            } else if (State.human.clusterMode === 'cold') {
                rate *= 1.3; // 30% more errors during cold streak
            }

            // Winning degradation
            if (h.winningDegradation.enabled && eval_) {
                const adv = eval_.value;
                for (const tier of h.winningDegradation.tiers) {
                    if (adv >= tier.evalAbove) {
                        rate += tier.extraSuboptimalRate;
                    }
                }
            }

            // Losing sharpness
            if (h.losingSharpness.enabled && eval_ && eval_.value <= h.losingSharpness.evalBelow) {
                rate *= h.losingSharpness.suboptimalReduction;
            }

            // Streak management
            if (h.streaks.enabled) {
                if (State.human.perfectStreak >= h.streaks.perfectStreakMax) {
                    Utils.log('Streak: Forcing suboptimal after perfect run', 'debug');
                    return true;
                }
                if (State.human.sloppyStreak >= h.streaks.sloppyStreakMax) {
                    Utils.log('Streak: Forcing best after sloppy run', 'debug');
                    return false;
                }
            }

            // Engine correlation guard
            if (State.human.totalMoveCount >= 8) {
                const currentCorrelation = State.human.bestMoveCount / State.human.totalMoveCount;
                if (currentCorrelation > h.targetEngineCorrelation + 0.05) {
                    rate += 0.08;
                    Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% > target, boosting suboptimal`, 'debug');
                } else if (currentCorrelation < h.targetEngineCorrelation - 0.10) {
                    rate *= 0.3;
                    Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% < target, reducing suboptimal`, 'debug');
                }
            }

            // Cap: never go above 45% even with all multipliers stacked
            // When losing, cap even lower — don't throw away a game that's close
            const maxRate = (eval_ && eval_.value < -0.5) ? 0.10 : 0.35;
            rate = Math.max(0.03, Math.min(maxRate, rate));
            return Math.random() < rate;
        },

        pickSuboptimalMove: (fen) => {
            const phase = HumanStrategy.getGamePhase(fen);
            let maxCPLoss = State.human.autoLoseActive
                ? CONFIG.autoLose.maxCPLoss
                : (CONFIG.humanization.maxAcceptableCPLoss[phase] || 60);
            // Time pressure: allow slightly bigger mistakes, but cap the multiplier
            maxCPLoss *= Math.min(1.5, State.human.timePressureMult.maxCPLoss);

            // HARD SAFETY CAP: never allow suboptimal moves that lose a piece (300cp = minor piece+pawn)
            // unless in auto-lose mode
            if (!State.human.autoLoseActive) {
                maxCPLoss = Math.min(maxCPLoss, 200);
            }

            const candidates = State.candidates;
            const bestEval = candidates[1]?.eval;

            if (!bestEval || Object.keys(candidates).length < 2) {
                return candidates[1]?.move || null;
            }

            const alternatives = [];
            for (let i = 2; i <= CONFIG.multiPV; i++) {
                const c = candidates[i];
                if (!c || !c.eval || !c.move) continue;

                // Self-awareness: don't make suboptimal Queen/King moves early in the game
                const fromSq = c.move.substring(0, 2);
                const pieceVal = HumanStrategy.getPieceValueFromFen(fen, fromSq);
                if (pieceVal >= 800 && State.moveCount < 30) continue;

                let cpLoss;
                if (bestEval.type === 'mate' && c.eval.type === 'mate') {
                    cpLoss = Math.abs(c.eval.value - bestEval.value) * 50;
                } else if (bestEval.type === 'mate') {
                    cpLoss = 300; // losing a forced mate is always bad
                } else if (c.eval.type === 'mate' && c.eval.value > 0) {
                    cpLoss = 0;
                } else if (c.eval.type === 'mate' && c.eval.value < 0) {
                    cpLoss = 900; // getting mated = worst possible
                } else {
                    cpLoss = (bestEval.value - c.eval.value) * 100;
                }
                if (cpLoss <= maxCPLoss && cpLoss >= 0) {
                    alternatives.push({ move: c.move, cpLoss, pvIndex: i });
                }
            }

            if (alternatives.length === 0) {
                Utils.log('No acceptable suboptimal moves, playing best', 'debug');
                return null;
            }

            // Weight: use a log-normal-like distribution to match real human CP loss curves
            // Real humans have: lots of 10-35cp mistakes, fewer 40-80cp, rare 100-200cp
            // Pure exponential decay (old) creates too many 0-5cp "errors" that look engine-like
            const weights = alternatives.map(a => {
                const cp = Math.max(a.cpLoss, 1); // avoid log(0)
                // Log-normal peak around 20-35cp, gentle tail toward larger losses
                const logCp = Math.log(cp);
                const mu = 3.2; // ln(~25cp) = peak of the distribution
                const sigma = 0.9;
                return Math.exp(-Math.pow(logCp - mu, 2) / (2 * sigma * sigma)) / cp;
            });
            const totalWeight = weights.reduce((s, w) => s + w, 0);
            let r = Math.random() * totalWeight;
            for (let i = 0; i < alternatives.length; i++) {
                r -= weights[i];
                if (r <= 0) {
                    Utils.log(`Suboptimal: PV${alternatives[i].pvIndex} (${alternatives[i].move}, -${alternatives[i].cpLoss.toFixed(0)}cp)`, 'warn');
                    return alternatives[i].move;
                }
            }
            return alternatives[0].move;
        },

        shouldBlunder: (fen) => {
            const b = CONFIG.humanization.blunder;
            if (!b || b.chance <= 0) return false;
            const eval_ = State.currentEval;
            if (!eval_) return false;

            // AUTO-LOSE MODE: blunder frequently regardless of position
            if (State.human.autoLoseActive && State.moveCount >= CONFIG.autoLose.minMovesBeforeLosing) {
                const chance = CONFIG.autoLose.blunderChance;
                Utils.log(`Auto-Lose: blunder chance ${(chance * 100).toFixed(0)}%`, 'debug');
                return Math.random() < chance;
            }

            if (eval_.value >= b.disableWhenEvalBetween[0] && eval_.value <= b.disableWhenEvalBetween[1]) return false;
            if (b.onlyInComplexPositions && HumanStrategy.getPositionComplexity(fen) < 0.4) return false;
            if (eval_.value < 0) return false;

            // Cluster: never blunder during hot streak, double chance during cold
            let chance = b.chance;
            if (State.human.clusterMode === 'hot') return false;
            if (State.human.clusterMode === 'cold') chance *= 2;

            // Time pressure: much more likely to blunder under time trouble
            chance *= State.human.timePressureMult.blunder;

            // Tilt: multiply blunder chance for N games after a loss
            if (State.human.tiltActive && CONFIG.tilt?.enabled) {
                chance *= CONFIG.tilt.blunderMult;
            }

            return Math.random() < chance;
        },

        pickBlunderMove: () => {
            const fen = State.lastFen;
            const candidates = State.candidates;
            const bestEval = candidates[1]?.eval;
            if (!bestEval) return null;

            let maxLoss = State.human.autoLoseActive
                ? CONFIG.autoLose.maxCPLoss
                : CONFIG.humanization.blunder.maxCPLoss;
            // Cap time pressure blunder loss to 1.3x — don't hang queens just because clock is low
            maxLoss *= Math.min(1.3, State.human.timePressureMult.maxCPLoss);
            // HARD CAP: blunders should lose a pawn or minor piece, not the queen
            if (!State.human.autoLoseActive) maxLoss = Math.min(maxLoss, 300);

            const blunderCandidates = [];
            for (let i = 2; i <= CONFIG.multiPV; i++) {
                const c = candidates[i];
                if (!c || !c.eval || !c.move) continue;

                // Self-awareness: don't blunder the Queen/King early in the game
                if (fen) {
                    const fromSq = c.move.substring(0, 2);
                    const pieceVal = HumanStrategy.getPieceValueFromFen(fen, fromSq);
                    if (pieceVal >= 800 && State.moveCount < 30) continue;
                }

                let cpLoss;
                if (c.eval.type === 'mate' && c.eval.value < 0) {
                    cpLoss = 900; // getting mated
                } else if (bestEval.type === 'cp' && c.eval.type === 'cp') {
                    cpLoss = (bestEval.value - c.eval.value) * 100;
                } else {
                    continue;
                }
                // Only consider moves that lose between 30cp and maxLoss
                // (below 30cp is an inaccuracy, not a blunder)
                if (cpLoss >= 30 && cpLoss <= maxLoss) {
                    blunderCandidates.push({ move: c.move, cpLoss, pvIndex: i });
                }
            }
            if (blunderCandidates.length === 0) return null;

            // Pick randomly (weighted toward mid-range losses, not the absolute worst)
            const weights = blunderCandidates.map(a => Math.exp(-Math.abs(a.cpLoss - maxLoss * 0.4) / 50));
            const totalWeight = weights.reduce((s, w) => s + w, 0);
            let r = Math.random() * totalWeight;
            for (let i = 0; i < blunderCandidates.length; i++) {
                r -= weights[i];
                if (r <= 0) {
                    Utils.log(`BLUNDER: ${blunderCandidates[i].move} (-${blunderCandidates[i].cpLoss.toFixed(0)}cp)`, 'error');
                    return blunderCandidates[i].move;
                }
            }
            const pick = blunderCandidates[0];
            Utils.log(`BLUNDER: ${pick.move} (-${pick.cpLoss.toFixed(0)}cp)`, 'error');
            return pick.move;
        },

        // Pick a random legal move NOT in the engine PV list (breaks correlation fingerprint)
        pickRandomLegalMove: (fen) => {
            try {
                const g = Game.getBoardGame();
                if (!g || !g.getLegalMoves) return null;
                const legalMoves = g.getLegalMoves();
                if (!legalMoves || legalMoves.length === 0) return null;

                const uciMoves = [];
                for (const m of legalMoves) {
                    if (m.from && m.to) {
                        uciMoves.push(m.from + m.to + (m.promotion || ''));
                    } else if (typeof m === 'string') {
                        uciMoves.push(m);
                    }
                }

                const pvMoves = new Set();
                for (let i = 1; i <= CONFIG.multiPV; i++) {
                    if (State.candidates[i]?.move) pvMoves.add(State.candidates[i].move.substring(0, 4));
                }

                let nonPvMoves = uciMoves.filter(m => !pvMoves.has(m.substring(0, 4)));
                if (nonPvMoves.length === 0) return null;

                // Filter by max CP loss if we have eval data from PV candidates
                const bestEval = State.candidates[1]?.eval;
                if (bestEval && bestEval.type === 'cp') {
                    const maxLoss = CONFIG.antiDetection.randomLegalMaxCPLoss || 250;
                    // We can't know exact eval of random moves, but avoid obviously bad ones:
                    // exclude moves that hang pieces (move to a square attacked and not defended)
                    // Simple heuristic: avoid moving king, prefer pawn pushes for "random" moves
                    nonPvMoves = nonPvMoves.filter(m => {
                        // Self-awareness: NEVER move the Queen or King as a completely random legal move!
                        const fromSq = m.substring(0, 2);
                        const pieceVal = HumanStrategy.getPieceValueFromFen(fen, fromSq);
                        if (pieceVal >= 800) return false;

                        // Full board analysis: reject any move that hangs material
                        return HumanStrategy.isMoveSafe(m, fen);
                    });
                    if (nonPvMoves.length === 0) return null;
                }

                const pick = nonPvMoves[Math.floor(Math.random() * nonPvMoves.length)];
                Utils.log(`Random legal move (non-PV): ${pick} — breaks engine correlation`, 'warn');
                return pick;
            } catch (e) {
                return null;
            }
        },

        // --- PIECE PROTECTION ---
        // Piece values in centipawns
        _pieceValues: { p: 100, n: 300, b: 320, r: 500, q: 900, k: 99999 },

        // Parse FEN board into 8x8 array: board[rank][file] = char or null
        // rank 0 = rank 1 (white's back rank), file 0 = a-file
        _parseFenBoard: (fen) => {
            const rows = fen.split(' ')[0].split('/').reverse(); // rank 1 first
            const board = [];
            for (let r = 0; r < 8; r++) {
                board[r] = [];
                let f = 0;
                for (const ch of (rows[r] || '')) {
                    if (/\d/.test(ch)) { for (let i = 0; i < parseInt(ch); i++) board[r][f++] = null; }
                    else board[r][f++] = ch;
                }
                while (f < 8) board[r][f++] = null;
            }
            return board;
        },

        // Convert algebraic square to [rank, file] indices
        _sqToIdx: (sq) => [parseInt(sq[1]) - 1, sq.charCodeAt(0) - 97],

        // Get all squares that attack a given [rank, file], filtered by color
        // color: 'w' or 'b' — which side's attackers to find
        _getAttackers: (board, rank, file, color) => {
            const attackers = [];
            const isUpper = (ch) => ch && ch === ch.toUpperCase(); // white pieces
            const isColor = (ch) => color === 'w' ? isUpper(ch) : (ch && !isUpper(ch));
            const inBounds = (r, f) => r >= 0 && r < 8 && f >= 0 && f < 8;
            const pv = HumanStrategy._pieceValues;

            // Pawn attacks
            const pawnDir = color === 'w' ? -1 : 1; // pawns of color attack FROM this direction
            const pawnChar = color === 'w' ? 'P' : 'p';
            for (const df of [-1, 1]) {
                const pr = rank + pawnDir, pf = file + df;
                if (inBounds(pr, pf) && board[pr][pf] === pawnChar) {
                    attackers.push({ r: pr, f: pf, piece: 'p', value: pv.p });
                }
            }

            // Knight attacks
            const knightChar = color === 'w' ? 'N' : 'n';
            for (const [dr, df] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
                const nr = rank + dr, nf = file + df;
                if (inBounds(nr, nf) && board[nr][nf] === knightChar) {
                    attackers.push({ r: nr, f: nf, piece: 'n', value: pv.n });
                }
            }

            // Sliding pieces: bishop/queen (diagonals), rook/queen (straights)
            const bishopChar = color === 'w' ? 'B' : 'b';
            const rookChar = color === 'w' ? 'R' : 'r';
            const queenChar = color === 'w' ? 'Q' : 'q';

            // Diagonals (bishop + queen)
            for (const [dr, df] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
                for (let dist = 1; dist < 8; dist++) {
                    const sr = rank + dr * dist, sf = file + df * dist;
                    if (!inBounds(sr, sf)) break;
                    const p = board[sr][sf];
                    if (p) {
                        if (p === bishopChar || p === queenChar) {
                            attackers.push({ r: sr, f: sf, piece: p.toLowerCase(), value: pv[p.toLowerCase()] });
                        }
                        break; // blocked
                    }
                }
            }

            // Straights (rook + queen)
            for (const [dr, df] of [[-1,0],[1,0],[0,-1],[0,1]]) {
                for (let dist = 1; dist < 8; dist++) {
                    const sr = rank + dr * dist, sf = file + df * dist;
                    if (!inBounds(sr, sf)) break;
                    const p = board[sr][sf];
                    if (p) {
                        if (p === rookChar || p === queenChar) {
                            attackers.push({ r: sr, f: sf, piece: p.toLowerCase(), value: pv[p.toLowerCase()] });
                        }
                        break; // blocked
                    }
                }
            }

            // King attacks
            const kingChar = color === 'w' ? 'K' : 'k';
            for (let dr = -1; dr <= 1; dr++) {
                for (let df = -1; df <= 1; df++) {
                    if (dr === 0 && df === 0) continue;
                    const kr = rank + dr, kf = file + df;
                    if (inBounds(kr, kf) && board[kr][kf] === kingChar) {
                        attackers.push({ r: kr, f: kf, piece: 'k', value: pv.k });
                    }
                }
            }

            return attackers;
        },

        // Returns centipawn value of the piece on a given square (from FEN)
        getPieceValueFromFen: (fen, sq) => {
            if (!fen || !sq || sq.length < 2) return 0;
            const piece = WeaknessProfile._identifyPiece(fen, sq);
            const values = { pawn: 100, knight: 300, bishop: 320, rook: 500, queen: 900, king: 99999 };
            return values[piece] || 0;
        },

        // Check if a move is safe — will the moved piece be hanging on its destination?
        // Returns true if safe, false if the piece would be lost or a bad trade
        isMoveSafe: (move, fen) => {
            if (!move || move.length < 4) return true;

            // --- Method 1: If this move is in the engine PV, check eval + board safety ---
            const candidates = State.candidates;
            const bestEval = candidates[1]?.eval;
            let inPV = false;
            for (let i = 1; i <= CONFIG.multiPV; i++) {
                const c = candidates[i];
                if (!c?.move || !c?.eval) continue;
                if (c.move === move) {
                    if (bestEval?.type === 'cp' && c.eval.type === 'cp') {
                        const cpLoss = (bestEval.value - c.eval.value) * 100;
                        if (cpLoss > 250) return false;
                    }
                    if (c.eval.type === 'mate' && c.eval.value < 0) return false;
                    inPV = true;
                    break; // don't return yet — still check board for high-value pieces
                }
            }

            // --- Method 2: Board-level attack analysis ---
            // Always run for non-PV moves. For PV moves, only run if a queen or rook is moving
            // (engine might say a queen move is "only -200cp" due to compensation, but visually
            // hanging your queen looks terrible and is a dead giveaway)
            try {
                const fromSq = move.substring(0, 2);
                const movingValue = HumanStrategy.getPieceValueFromFen(fen, fromSq);

                // If it's a PV move and NOT a high-value piece, trust the engine
                if (inPV && movingValue < 500) return true;

                // For high-value PV moves (queen/rook) or any non-PV move: run full board analysis
                const board = HumanStrategy._parseFenBoard(fen);
                const sideToMove = fen.split(' ')[1] || 'w';
                const enemyColor = sideToMove === 'w' ? 'b' : 'w';

                const toSq = move.substring(2, 4);
                const [toR, toF] = HumanStrategy._sqToIdx(toSq);
                const [fromR, fromF] = HumanStrategy._sqToIdx(fromSq);

                const movingPieceChar = board[fromR]?.[fromF];
                if (!movingPieceChar) return true; // can't identify piece

                // Is there an enemy piece on the destination? (capture)
                const capturedChar = board[toR]?.[toF];
                const capturedValue = capturedChar ? (HumanStrategy._pieceValues[capturedChar.toLowerCase()] || 0) : 0;

                // Simulate the move on the board for attack detection
                const simBoard = board.map(row => [...row]);
                simBoard[fromR][fromF] = null;
                simBoard[toR][toF] = movingPieceChar;

                // Who attacks the destination AFTER the move?
                const enemyAttackers = HumanStrategy._getAttackers(simBoard, toR, toF, enemyColor);
                const friendlyDefenders = HumanStrategy._getAttackers(simBoard, toR, toF, sideToMove);

                // If no enemy attacks the square, it's safe
                if (enemyAttackers.length === 0) return true;

                // If it's a capture and we win material even if they recapture, it's fine
                // e.g. knight takes undefended rook — even if enemy recaptures we traded 300 for 500
                if (capturedValue >= movingValue) return true;

                // Enemy attacks the square — check if we have enough defenders
                if (friendlyDefenders.length === 0) {
                    // Piece is hanging with no defenders — BAD
                    // Allow it only if the piece is a pawn (losing 100cp is minor)
                    if (movingValue <= 100) return true;
                    Utils.log(`PieceProtect: ${move} hangs ${movingPieceChar} (${movingValue}cp) with no defenders`, 'debug');
                    return false;
                }

                // Both sides attack — do simple static exchange evaluation (SEE)
                // Sort attackers by value (cheapest first, like real exchanges)
                const atkSorted = [...enemyAttackers].sort((a, b) => a.value - b.value);
                const defSorted = [...friendlyDefenders].sort((a, b) => a.value - b.value);

                // Simulate exchange: enemy captures first, then we recapture, etc.
                let materialOnSquare = movingValue; // our piece is there
                let balance = 0; // net material change from our perspective
                let turn = 0; // 0 = enemy captures, 1 = we recapture

                let atkIdx = 0, defIdx = 0;
                while (true) {
                    if (turn % 2 === 0) {
                        // Enemy captures
                        if (atkIdx >= atkSorted.length) break; // enemy can't capture
                        balance -= materialOnSquare; // we lose the piece on the square
                        materialOnSquare = atkSorted[atkIdx].value; // enemy piece now sits there
                        atkIdx++;
                    } else {
                        // We recapture
                        if (defIdx >= defSorted.length) break; // we can't recapture
                        balance += materialOnSquare; // we take their piece
                        materialOnSquare = defSorted[defIdx].value; // our piece now sits there
                        defIdx++;
                    }
                    turn++;
                    // If it's the enemy's turn and balance is already positive for us, they'd stop
                    if (turn % 2 === 0 && balance > 0) break;
                    // If it's our turn and balance is very negative, we'd stop
                    if (turn % 2 === 1 && balance < -movingValue) break;
                }

                // Reject threshold depends on piece value:
                // Queen/Rook: reject if losing ANY material (> 50cp, to allow rounding)
                // Minor pieces: reject if losing more than a pawn (> 120cp)
                // Pawns: always ok (losing a pawn is minor)
                const rejectThreshold = movingValue >= 500 ? -50 : -120;
                if (balance < rejectThreshold) {
                    Utils.log(`PieceProtect: ${move} loses ~${Math.abs(balance)}cp in exchange (${movingPieceChar} worth ${movingValue}cp vs ${atkSorted.length} attackers, ${defSorted.length} defenders)`, 'debug');
                    return false;
                }

                return true;
            } catch (e) {
                // If analysis fails, fall back to conservative: don't move high-value pieces
                const fromSq = move.substring(0, 2);
                const val = HumanStrategy.getPieceValueFromFen(fen, fromSq);
                return val < 500; // only allow pawns/knights/bishops as fallback
            }
        },

        selectMove: (fen, bestMove, isBook) => {
            if (isBook || !CONFIG.humanization.enabled) {
                return { move: bestMove, reason: isBook ? 'book' : 'engine', isBest: true };
            }

            // Repertoire-hard override (only on move 1 of each color)
            try {
                const g = Game.getBoardGame();
                const legal = g && g.getLegalMoves ? g.getLegalMoves().map(m =>
                    (m.from && m.to) ? m.from + m.to + (m.promotion || '') : (typeof m === 'string' ? m : null)
                ).filter(Boolean) : [];
                if (legal.length) {
                    const rep = Account.repertoireMove(fen, legal);
                    if (rep && rep !== bestMove) {
                        Utils.log(`Repertoire: forcing ${rep} (engine preferred ${bestMove})`, 'info');
                        return { move: rep, reason: 'repertoire', isBest: false };
                    }
                }
            } catch (e) { /* non-fatal */ }

            // Messy-resign: blunder ONE move before resigning, if scheduled
            if (State.human.resignBlunderNext) {
                const bl = HumanStrategy.pickBlunderMove();
                if (bl) {
                    State.human.resignBlunderNext = false; // consume the roll
                    Utils.log('MessyResign: playing blunder move before resign', 'warn');
                    return { move: bl, reason: 'resign-blunder', isBest: false };
                }
            }

            // Update accuracy cluster
            HumanStrategy.updateCluster();

            // --- PRE-DECIDE think time category (used later by calculateDelay) ---
            // This drives timing-accuracy coupling: we decide HOW LONG we'll think FIRST,
            // then that influences move quality
            const tac = CONFIG.humanization.timingAccuracyCoupling;
            if (tac.enabled) {
                const r = Math.random();
                if (r < 0.25) State.human.thinkCategory = 'fast';
                else if (r < 0.70) State.human.thinkCategory = 'normal';
                else State.human.thinkCategory = 'slow';
            }

            // --- ANTI-CORRELATION POISONING ---
            // Skip when losing — accuracy matters more than stealth when behind
            const ac = CONFIG.humanization.antiCorrelation;
            const isLosing = State.currentEval && State.currentEval.type === 'cp' && State.currentEval.value < -1.0;
            if (ac.enabled && State.moveCount > 3 && !isLosing) {
                // Track top move rate and enforce hard cap
                const topRate = State.human.totalMoveCount > 0
                    ? State.human.topMoveCount / State.human.totalMoveCount : 0;

                // A) Miss small tactics sometimes
                if (State.currentEval && State.candidates[1]?.eval) {
                    const prevEval = State.human._prevEval || 0;
                    const evalJump = State.currentEval.value - prevEval;
                    if (evalJump > 0.3 && evalJump < ac.missSmallTacticThreshold) {
                        if (Math.random() < ac.missSmallTacticRate) {
                            // Play a "safe" alternative instead of the tactic
                            const safeMove = HumanStrategy.pickSuboptimalMove(fen);
                            if (safeMove && HumanStrategy.isMoveSafe(safeMove, fen)) {
                                Utils.log(`AntiCorr: Missed small tactic (eval jump ${evalJump.toFixed(2)}), playing safe`, 'debug');
                                return { move: safeMove, reason: 'missed-tactic', isBest: false };
                            }
                        }
                    }
                }

                // B) When SF#2/3 are close in eval, prefer them sometimes
                if (Object.keys(State.candidates).length >= 2) {
                    const best = State.candidates[1];
                    for (let i = 2; i <= Math.min(3, CONFIG.multiPV); i++) {
                        const alt = State.candidates[i];
                        if (!alt?.eval || !best?.eval) continue;
                        if (best.eval.type !== 'cp' || alt.eval.type !== 'cp') continue;
                        const diff = Math.abs(best.eval.value - alt.eval.value);
                        if (diff <= ac.closeEvalThreshold) {
                            // These moves are essentially equal — sometimes pick the alternative
                            let preferRate = ac.closeEvalPreferRate;
                            // If we're over the top move cap, strongly prefer alternatives
                            if (topRate > ac.maxTopMoveRate) preferRate += 0.25;
                            if (Math.random() < preferRate) {
                                Utils.log(`AntiCorr: SF#${i} within ${(diff*100).toFixed(0)}cp, playing ${alt.move} instead of ${best.move}`, 'debug');
                                return { move: alt.move, reason: 'close-alt', isBest: false, isCloseAlt: true };
                            }
                        }
                    }
                }

                // C) Hard cap enforcement: if top move rate is too high, force a non-best move
                if (topRate > ac.maxTopMoveRate && State.human.totalMoveCount >= 10) {
                    const subMove = HumanStrategy.pickSuboptimalMove(fen);
                    if (subMove && HumanStrategy.isMoveSafe(subMove, fen)) {
                        Utils.log(`AntiCorr: Top move rate ${(topRate*100).toFixed(0)}% > cap ${(ac.maxTopMoveRate*100).toFixed(0)}%, forcing alt`, 'debug');
                        return { move: subMove, reason: 'correlation-cap', isBest: false };
                    }
                }
            }

            // --- TIMING-ACCURACY COUPLING ---
            // Fast think = more errors, slow think = fewer errors (with noise)
            let couplingSuboptBoost = 0;
            let couplingBestBoost = 0;
            if (tac.enabled) {
                const isNoisy = Math.random() < tac.noiseRate; // occasional inversion
                if (State.human.thinkCategory === 'fast' && !isNoisy) {
                    couplingSuboptBoost = tac.fastMoveSuboptimalBoost;
                } else if (State.human.thinkCategory === 'slow' && !isNoisy) {
                    couplingBestBoost = tac.slowMoveBestBoost;
                } else if (State.human.thinkCategory === 'fast' && isNoisy) {
                    // "Fast good move" — pattern recognition hit
                    couplingBestBoost = 0.10;
                } else if (State.human.thinkCategory === 'slow' && isNoisy) {
                    // "Slow bad move" — overthinking
                    couplingSuboptBoost = 0.07;
                }
            }

            // --- WEAKNESS PROFILE ---
            const weaknessExtra = WeaknessProfile.getExtraErrorRate(fen, bestMove);

            // --- PLAYER MOVE DB DIVERSIFICATION ---
            // Skip when losing — play engine moves to fight back
            const pdb = CONFIG.humanization.playerMoveDB;
            if (pdb.enabled && State.moveCount > 4 && !isLosing) {
                const cacheKey = fen.split(' ').slice(0, 4).join(' ');
                const playerMoves = State.playerDBCache.get(cacheKey);
                if (playerMoves && playerMoves.length > 0 && Math.random() < pdb.preferRate) {
                    const dbMove = PlayerMoveDB.pickMove(playerMoves);
                    if (dbMove && dbMove !== bestMove) {
                        // Only use if it's not catastrophically bad — check if it's in PV or at least legal
                        const inPV = Object.values(State.candidates).some(c => c?.move === dbMove);
                        if (inPV && HumanStrategy.isMoveSafe(dbMove, fen)) {
                            Utils.log(`PlayerDB: Playing human move ${dbMove} (from ${playerMoves.length} options at target rating)`, 'debug');
                            return { move: dbMove, reason: 'player-db', isBest: dbMove === bestMove };
                        }
                        // Even if not in PV, if it's the most popular human move, trust it — but still check safety
                        const topDBMove = playerMoves.reduce((a, b) => a.games > b.games ? a : b);
                        if (dbMove === topDBMove.uci && topDBMove.games >= 20 && HumanStrategy.isMoveSafe(dbMove, fen)) {
                            Utils.log(`PlayerDB: Playing top human move ${dbMove} (${topDBMove.games} games)`, 'debug');
                            return { move: dbMove, reason: 'player-db', isBest: false };
                        }
                    }
                }
            }

            // --- RANDOM LEGAL MOVE (anti-detection) ---
            if (CONFIG.antiDetection.randomLegalMoveChance > 0 && State.moveCount > 5) {
                if (Math.random() < CONFIG.antiDetection.randomLegalMoveChance) {
                    const randomMove = HumanStrategy.pickRandomLegalMove(fen);
                    if (randomMove && HumanStrategy.isMoveSafe(randomMove, fen)) {
                        return { move: randomMove, reason: 'random-legal', isBest: false };
                    }
                }
            }

            // --- BLUNDER CHECK (very rare) ---
            if (HumanStrategy.shouldBlunder(fen)) {
                const blunderMove = HumanStrategy.pickBlunderMove();
                if (blunderMove && HumanStrategy.isMoveSafe(blunderMove, fen)) {
                    return { move: blunderMove, reason: 'blunder', isBest: false };
                }
            }

            // --- SUBOPTIMAL MOVE CHECK ---
            // Apply coupling and weakness boosts to the suboptimal decision
            if (HumanStrategy.shouldPlaySuboptimal(fen, couplingSuboptBoost + weaknessExtra)) {
                // If best move would be forced by coupling slow-think boost, override
                if (couplingBestBoost > 0 && Math.random() < couplingBestBoost) {
                    Utils.log('TimingCoupling: Slow think → playing best despite suboptimal trigger', 'debug');
                    return { move: bestMove, reason: 'best', isBest: true };
                }
                const subMove = HumanStrategy.pickSuboptimalMove(fen);
                if (subMove && HumanStrategy.isMoveSafe(subMove, fen)) {
                    return { move: subMove, reason: 'suboptimal', isBest: false };
                }
            }

            return { move: bestMove, reason: 'best', isBest: true };
        },

        trackMove: (isBest, moveResult) => {
            State.human.totalMoveCount++;
            if (isBest) {
                State.human.bestMoveCount++;
                State.human.topMoveCount++;
                State.human.perfectStreak++;
                State.human.sloppyStreak = 0;
            } else {
                // Close alternatives still count toward top move for correlation tracking
                // (Chess.com sees them as essentially engine moves too)
                if (moveResult?.isCloseAlt) State.human.topMoveCount++;
                State.human.perfectStreak = 0;
                State.human.sloppyStreak++;
            }
            State.human.lastMoveWasBest = isBest;
            // Store eval for next move's tactic detection
            State.human._prevEval = State.currentEval?.value || 0;
        },

        calculateDelay: (fen, moveResult, isBook) => {
            const t = CONFIG.timing;
            const personality = State.human.gamePersonality || { timingMult: 1 };
            const phase = HumanStrategy.getGamePhase(fen);
            let min, max;

            // ============================================================
            // LAYER 0: Premove simulation (instant — bypasses everything)
            //
            // Gated by CONFIG.premoveGating. Real players premove ONLY when the
            // opponent's reply is effectively forced — single legal response,
            // obvious recapture, or king-moves-out-of-check. Premoving on any
            // "predicted" reply is a bot tell because it happens too often in
            // non-forcing positions.
            // ============================================================
            if (t.premove.enabled && State.human.predictedReply && moveResult.isBest) {
                if (Math.random() < t.premove.chance && State.moveCount > 5) {
                    const gate = CONFIG.premoveGating;
                    let allowPremove = !gate.enabled; // if gating disabled, always allow

                    if (gate.enabled) {
                        // Check: is opponent reply forced (single legal move)?
                        let opponentHasSingleReply = false;
                        try {
                            const g = Game.getBoardGame();
                            // We'd need to see the position AFTER our move. Approximation:
                            // use predictedReplyFen (we stored the fen BEFORE our move).
                            // Without a move generator that can push/pop, we rely on the
                            // engine's signal: if MultiPV gave us a strongly-dominant
                            // single top reply, treat it as effectively forced.
                            const cands = State.candidates || {};
                            const scores = Object.values(cands).map(c => c.score || 0);
                            if (scores.length >= 2) {
                                scores.sort((a, b) => b - a);
                                // Dominance: top is >200cp better than 2nd (or mate)
                                if ((scores[0] - scores[1]) > 200) opponentHasSingleReply = true;
                            }
                        } catch (e) {}

                        if (gate.forcedOnly && opponentHasSingleReply) allowPremove = true;

                        // Recapture gate: our move was a capture AND the predicted
                        // reply is also a capture on the same destination square.
                        if (!allowPremove && gate.allowRecaptures) {
                            const ourMove = moveResult.move || '';
                            const replyMove = State.human.predictedReply || '';
                            if (ourMove.length >= 4 && replyMove.length >= 4) {
                                const ourTo = ourMove.substring(2, 4);
                                const replyTo = replyMove.substring(2, 4);
                                if (Game.isCapture(ourMove) && ourTo === replyTo) {
                                    allowPremove = true;
                                }
                            }
                        }
                    }

                    if (allowPremove) {
                        Utils.log(`Timing: Premove (gated: ${CONFIG.premoveGating.enabled ? 'pass' : 'disabled'})`, 'debug');
                        return Utils.humanDelay(t.premove.delay.min, t.premove.delay.max);
                    } else {
                        Utils.log('Timing: Premove suppressed by gating (position not forcing enough)', 'debug');
                    }
                }
            }

            // ============================================================
            // LAYER 0B: Recapture speed (item 6)
            // When opponent just captured and our best move is to recapture,
            // humans respond almost instantly — it's the most obvious move.
            // ============================================================
            if (State.lastMoveWasCapture && Game.isCapture(moveResult.move) && moveResult.isBest && !isBook) {
                min = 300; max = 1100;
                Utils.log('Timing: Instant recapture', 'debug');
                // Still apply clock awareness to recaptures
                if (t.clockAware.enabled && State.clock.myTime != null) {
                    for (const threshold of t.clockAware.thresholds) {
                        if (State.clock.myTime <= threshold.secondsBelow) {
                            min *= threshold.timingMult;
                            max *= threshold.timingMult;
                            break;
                        }
                    }
                }
                const recapDelay = Utils.humanDelay(min, max);
                State.recentTimings.push(recapDelay);
                if (State.recentTimings.length > 20) State.recentTimings.shift();
                return recapDelay;
            }

            // ============================================================
            // LAYER 1: Base timing by move type
            // ============================================================
            if (isBook) {
                min = t.book.min;
                max = t.book.max;
            } else if (State.moveCount <= 6) {
                min = t.earlyGame.min;
                max = t.earlyGame.max;
            } else if (Game.isCapture(moveResult.move) && State.currentEval && Math.abs(State.currentEval.value) > 2) {
                min = t.forced.min;
                max = t.forced.max;
            } else if (Math.random() < t.instantMove.chance && State.moveCount > 5) {
                min = t.instantMove.min;
                max = t.instantMove.max;
                Utils.log('Timing: Instant/pre-move');
            } else if (Math.random() < t.longThink.chance) {
                min = t.longThink.min;
                max = t.longThink.max;
                Utils.log('Timing: Long think');
            } else {
                const complexity = HumanStrategy.getPositionComplexity(fen);
                if (complexity > 0.65) {
                    min = t.complex.min;
                    max = t.complex.max;
                } else if (complexity < 0.3 || (State.currentEval && State.currentEval.value > 3)) {
                    min = t.simple.min;
                    max = t.simple.max;
                } else {
                    min = t.base.min;
                    max = t.base.max;
                }

                if (State.currentEval && Math.abs(State.currentEval.value) < 0.3) {
                    min += 500;
                    max += 1200;
                }
                if (moveResult.reason === 'suboptimal') {
                    min += 200;
                    max += 600;
                }
                if (moveResult.reason === 'blunder') {
                    if (Math.random() < 0.5) {
                        min = t.forced.min;
                        max = t.forced.max + 300;
                    } else {
                        min = t.complex.min;
                        max = t.complex.max;
                    }
                }
            }

            // ============================================================
            // LAYER 2: Time management curve (item 3)
            // Real players spend ~15% of time in opening, ~55% middlegame, ~30% endgame
            // This shapes the base timing to match that distribution.
            // ============================================================
            if (!isBook) {
                if (phase === 'opening') {
                    min *= 0.65; max *= 0.75; // play faster in opening (known territory)
                } else if (phase === 'middlegame') {
                    min *= 1.15; max *= 1.30; // think most in middlegame (critical decisions)
                } else if (phase === 'endgame') {
                    // Endgame: a bit faster than middlegame but slower than opening
                    // (technique phase — fewer choices but need precision)
                    min *= 0.85; max *= 0.95;
                }
            }

            // ============================================================
            // LAYER 3: Opponent-move surprise reaction (item 1)
            // After a surprising opponent move, think longer.
            // After an expected/obvious move, respond faster.
            // ============================================================
            if (!isBook && State.moveCount > 3) {
                const surprise = State.human.opponentMoveSurprise; // 0-1
                if (surprise > 0.5) {
                    // Surprising move — need extra time to recalculate
                    const surpriseMult = 1.0 + surprise * 0.6; // up to 1.6x
                    min *= surpriseMult;
                    max *= surpriseMult;
                    Utils.log(`Timing: Surprised by opponent move (${(surprise*100).toFixed(0)}%) → ${surpriseMult.toFixed(2)}x`, 'debug');
                } else if (surprise < 0.1 && State.human.predictedReply) {
                    // Completely expected move — respond a bit faster
                    min *= 0.80; max *= 0.85;
                }
            }

            // ============================================================
            // LAYER 4: Think momentum / inertia (item 2)
            // After a long think, the next move is often faster because
            // you already calculated the continuation during the long think.
            // After a fast move, the next might be slower (didn't plan ahead).
            // ============================================================
            if (!isBook && State.human.lastThinkTime > 0) {
                const lastThink = State.human.lastThinkTime;
                if (lastThink > 8000) {
                    // Last move was a long think — this one should be faster (calculated ahead)
                    min *= 0.55; max *= 0.70;
                    Utils.log('Timing: Momentum — fast follow-up after long think', 'debug');
                } else if (lastThink > 5000) {
                    min *= 0.75; max *= 0.85;
                } else if (lastThink < 1200 && State.moveCount > 8) {
                    // Last move was very fast — might need to slow down and actually think now
                    min *= 1.10; max *= 1.25;
                }
            }

            // ============================================================
            // LAYER 5: Fatigue
            // ============================================================
            if (t.fatigue.enabled && State.moveCount > t.fatigue.startMove) {
                const extra = Math.min(t.fatigue.cap, (State.moveCount - t.fatigue.startMove) * t.fatigue.msPerMove);
                min += extra;
                max += extra;
            }

            // ============================================================
            // LAYER 6: Clock awareness
            // ============================================================
            if (t.clockAware.enabled && State.clock.myTime != null) {
                for (const threshold of t.clockAware.thresholds) {
                    if (State.clock.myTime <= threshold.secondsBelow) {
                        min *= threshold.timingMult;
                        max *= threshold.timingMult;
                        break;
                    }
                }
            }

            // ============================================================
            // LAYER 7: Per-game personality jitter + persistent player tempo (item 5 partial)
            // ============================================================
            min *= personality.timingMult;
            max *= personality.timingMult;
            // Persistent player tempo: some accounts are naturally fast/slow players
            min *= State.human.playerTempo;
            max *= State.human.playerTempo;
            // Tilt: thinking slower/frustrated overthinking after a loss
            if (State.human.tiltActive && CONFIG.tilt?.enabled) {
                min *= CONFIG.tilt.timingMult;
                max *= CONFIG.tilt.timingMult;
            }

            // ============================================================
            // LAYER 8: Timing-accuracy coupling (existing)
            // ============================================================
            if (CONFIG.humanization.timingAccuracyCoupling.enabled && !isBook) {
                const cat = State.human.thinkCategory;
                if (cat === 'fast') {
                    min *= 0.45; max *= 0.55;
                    Utils.log('TimingCoupling: Fast think', 'debug');
                } else if (cat === 'slow') {
                    min *= 1.4; max *= 1.8;
                    Utils.log('TimingCoupling: Slow think', 'debug');
                }
            }

            // ============================================================
            // LAYER 9: Eval-aware timing (existing)
            // ============================================================
            if (State.currentEval && !isBook) {
                const ev = State.currentEval.value;
                if (Math.abs(ev) < 0.5) {
                    min *= 1.15; max *= 1.25;
                } else if (ev > 3.0) {
                    min *= 0.7; max *= 0.8;
                } else if (ev < -2.0) {
                    if (Math.random() < 0.4) {
                        min *= 0.5; max *= 0.65;
                    } else {
                        min *= 1.2; max *= 1.5;
                    }
                }
            }

            // ============================================================
            // CUMULATIVE MULTIPLIER DAMPENING
            // ============================================================
            // Layers 2-9 multiply min/max cumulatively. In worst case the total
            // multiplier reaches ~5-8x which then hits the hard ceiling every
            // time, creating a detectable "many moves at exactly max" cluster.
            // Dampen the cumulative multiplier using sqrt-compression when it
            // goes beyond 2.0x (or below 0.5x), so extreme stacks pull toward
            // the middle while still preserving per-layer intent.
            const baseMean = (t.base.min + t.base.max) / 2;
            const currMean = (min + max) / 2;
            const cumMult = baseMean > 0 ? currMean / baseMean : 1;
            if (cumMult > 2.0) {
                // sqrt-compress: 4x becomes 2x, 9x becomes 3x, 16x becomes 4x
                const damped = Math.sqrt(cumMult * 2.0);
                const scale = damped / cumMult;
                min *= scale; max *= scale;
                Utils.log(`Timing: Dampened cumulative ${cumMult.toFixed(2)}x -> ${damped.toFixed(2)}x`, 'debug');
            } else if (cumMult < 0.5) {
                // Same compression for sub-0.5x stacks (prevents absurdly fast plays)
                const damped = 0.5 / Math.sqrt(0.5 / cumMult);
                const scale = damped / cumMult;
                min *= scale; max *= scale;
            }

            // ============================================================
            // FINAL: Sequence variation + clamp
            // ============================================================
            let delay = Utils.humanDelay(min, max);
            if (t.sequenceVariation.enabled && State.recentTimings.length >= t.sequenceVariation.windowSize) {
                const recent = State.recentTimings.slice(-t.sequenceVariation.windowSize);
                const mean = recent.reduce((a, b) => a + b, 0) / recent.length;
                const variance = recent.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / recent.length;
                const stdDev = Math.sqrt(variance);
                const cv = mean > 0 ? stdDev / mean : 0;

                if (cv < t.sequenceVariation.similarityThreshold) {
                    if (Math.random() < 0.5) {
                        delay = Utils.humanDelay(min * 0.3, min * 0.6);
                        Utils.log('Timing: Forced fast outlier (sequence variation)', 'debug');
                    } else {
                        delay = Utils.humanDelay(max * 1.15, max * 1.8);
                        Utils.log('Timing: Forced slow outlier (sequence variation)', 'debug');
                    }
                }
            }

            // Hard clamp: respect user-configured base max.
            // Use a softer, noisier ceiling so we don't produce a spike at exactly
            // 1.5x base.max across every long-think move.
            const userMax = t.base.max;
            const noisyCeiling = userMax * (1.35 + Math.random() * 0.30); // 1.35-1.65x
            if (delay > noisyCeiling) {
                Utils.log(`Timing: Clamped ${Math.round(delay)}ms -> ${Math.round(noisyCeiling)}ms`, 'debug');
                delay = noisyCeiling;
            }
            // Minimum floor: 250ms unless it's an explicit premove/instant/recapture case.
            // 150ms was inhumanly fast — nobody makes non-recapture decisions that quick.
            delay = Math.max(delay, 250);

            // Time-scramble override: if < 5 seconds left, NEVER think more than 600ms
            if (State.clock.myTime != null && State.clock.myTime < 5) {
                delay = Math.min(delay, 400 + Math.random() * 200);
            }

            State.recentTimings.push(delay);
            if (State.recentTimings.length > 20) State.recentTimings.shift();

            return delay;
        },
    };

    // ═══════════════════════════════════════════
    //  HUMANIZER (Mouse simulation & move execution)
    // ═══════════════════════════════════════════
    const Humanizer = {
        createEvent: (type, x, y, options = {}) => {
            const defaults = {
                bubbles: true, cancelable: true, view: window, detail: 1,
                screenX: x, screenY: y, clientX: x, clientY: y,
                pointerId: 1, pointerType: 'mouse', isPrimary: true,
                button: 0, buttons: 1, which: 1, composed: true
            };
            return new PointerEvent(type, { ...defaults, ...options });
        },

        showClick: (x, y, color = 'red') => {
            const dot = document.createElement('div');
            dot.style.cssText = `
                position: absolute; left: ${x}px; top: ${y}px;
                width: 12px; height: 12px; background: ${color}; border-radius: 50%;
                z-index: 100000; pointer-events: none; transform: translate(-50%, -50%);
                box-shadow: 0 0 4px white;
            `;
            document.body.appendChild(dot);
            setTimeout(() => dot.remove(), 800);
        },

        dragDrop: async (fromSq, toSq) => {
            const board = Game.getBoard();
            if (!board) return;
            const startPos = Humanizer.getCoords(fromSq);
            const endPos = Humanizer.getCoords(toSq);
            if (!startPos || !endPos) return;

            // Hardware persona shapes the drag character:
            //   trackpad -> slower, noisier, longer click-hold
            //   mouse    -> baseline
            //   tablet   -> medium noise, slow click-hold
            const persona = Account.currentPersona() || { jitterScale: 1, clickHoldMs: { min: 50, max: 110 }, speedScale: 1 };
            const jScale = persona.jitterScale;

            Humanizer.showClick(startPos.x, startPos.y, '#00ff00');
            const fromCoords = Game.squareToCoords(fromSq);
            const pieceEl = board.querySelector(`.piece.square-${fromCoords}`) ||
                document.elementFromPoint(startPos.x, startPos.y);
            const targetSource = pieceEl || board;
            const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };

            const pickupNoise = () => Utils.gaussianRandom(0, 2 * jScale);
            const sx = startPos.x + pickupNoise();
            const sy = startPos.y + pickupNoise();

            // pointerType advertises what device the "user" is on. Trackpads still
            // register as 'mouse' in browser API but some sites sniff this; we keep
            // it as 'mouse' for all personas (trackpad is a mouse device to the DOM).
            const realisticPointerProps = (x, y, prevX, prevY) => ({
                width: 1,
                height: 1,
                pressure: 0.5 + Math.random() * 0.25,
                tangentialPressure: 0,
                tiltX: Math.round(Utils.gaussianRandom(0, 3 * jScale)),
                tiltY: Math.round(Utils.gaussianRandom(0, 3 * jScale)),
                twist: 0,
                pointerType: 'mouse',
                movementX: prevX != null ? Math.round(x - prevX) : 0,
                movementY: prevY != null ? Math.round(y - prevY) : 0,
            });

            targetSource.dispatchEvent(new PointerEvent('pointerover', { ...opts, ...realisticPointerProps(sx, sy), clientX: sx, clientY: sy }));
            targetSource.dispatchEvent(new PointerEvent('pointerdown', { ...opts, ...realisticPointerProps(sx, sy), clientX: sx, clientY: sy }));
            targetSource.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: sx, clientY: sy }));

            // Click-hold time is persona-specific (trackpad/tablet hold longer).
            const spd = (CONFIG.dragSpeed || 1.0) / persona.speedScale;
            const clickHold = Utils.randomRange(persona.clickHoldMs.min, persona.clickHoldMs.max);
            await Utils.sleep(clickHold);

            // Helper: run a noisy human-like drag path between two points.
            // IMPORTANT: we dispatch pointermove to the SAME element that received
            // pointerdown (`targetSource`) whenever possible. This preserves the
            // implicit pointer-capture contract Chess.com's drag handler expects.
            // Dispatching to `document` breaks that contract and leaves a detectable
            // gap in the pointer event target chain.
            const bezierPath = async (from, to, stepCount, speedMult = 1) => {
                const pdx = to.x - from.x, pdy = to.y - from.y;
                const pDist = Math.sqrt(pdx * pdx + pdy * pdy);
                if (pDist < 1) return;

                // Multiple random control points for a wobbly spline, not a clean curve
                const perpX = -pdy / pDist, perpY = pdx / pDist;
                const cp1t = 0.25 + Math.random() * 0.15;
                const cp2t = 0.55 + Math.random() * 0.15;
                const wobble1 = Utils.gaussianRandom(0, pDist * 0.18 * jScale);
                const wobble2 = Utils.gaussianRandom(0, pDist * 0.14 * jScale);
                const cp1 = { x: from.x + pdx * cp1t + perpX * wobble1, y: from.y + pdy * cp1t + perpY * wobble1 };
                const cp2 = { x: from.x + pdx * cp2t + perpX * wobble2, y: from.y + pdy * cp2t + perpY * wobble2 };

                // Cubic bezier eval
                const cubicBez = (a, b, c, d, t) => {
                    const omt = 1 - t;
                    return omt*omt*omt*a + 3*omt*omt*t*b + 3*omt*t*t*c + t*t*t*d;
                };

                // Wobble state that drifts smoothly (fake Perlin)
                let wobX = 0, wobY = 0;
                const wobDrift = () => {
                    wobX += Utils.gaussianRandom(0, 1.2 * jScale);
                    wobY += Utils.gaussianRandom(0, 1.2 * jScale);
                    wobX *= 0.7; wobY *= 0.7; // dampen so it doesn't run away
                };

                const totalSteps = Math.max(stepCount, Math.round(pDist / 6));
                let lastPauseAt = 0;

                for (let i = 1; i <= totalSteps; i++) {
                    const t = i / totalSteps;

                    // Base position from cubic bezier
                    let cx_ = cubicBez(from.x, cp1.x, cp2.x, to.x, t);
                    let cy_ = cubicBez(from.y, cp1.y, cp2.y, to.y, t);

                    // Perpendicular wobble — stronger in the middle, fades at endpoints
                    wobDrift();
                    const wobbleEnvelope = Math.sin(t * Math.PI) * 1.5;
                    cx_ += wobX * wobbleEnvelope;
                    cy_ += wobY * wobbleEnvelope;

                    // Random high-freq noise (hand tremor)
                    const tremor = Math.max(0.3, (1 - t) * 2.5 + Math.sin(t * 12) * 0.5) * jScale;
                    cx_ += Utils.gaussianRandom(0, tremor);
                    cy_ += Utils.gaussianRandom(0, tremor);

                    const rpp = realisticPointerProps(cx_, cy_, prevMoveX, prevMoveY);
                    targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp, clientX: cx_, clientY: cy_ }));
                    targetSource.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: cx_, clientY: cy_, movementX: rpp.movementX, movementY: rpp.movementY }));
                    prevMoveX = cx_;
                    prevMoveY = cy_;

                    // Speed: slow start, fast middle, slow end (bell curve)
                    const bell = Math.sin(t * Math.PI);
                    const baseDelay = Utils.randomRange(6, 18) * (1.4 - bell * 0.9);
                    const delay = Math.max(3, Math.round(baseDelay * speedMult * spd));

                    // Most steps get a delay, but vary the chance
                    if (Math.random() < 0.70) await Utils.sleep(delay);

                    // Occasional micro-pause (human recalculating / hand jitter)
                    if (t > 0.15 && t < 0.85 && (t - lastPauseAt) > 0.2 && Math.random() < 0.08) {
                        await Utils.sleep(Utils.randomRange(30, 80) * spd);
                        lastPauseAt = t;
                    }
                }
            };

            const dx = endPos.x - startPos.x;
            const dy = endPos.y - startPos.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const sqSize = board.getBoundingClientRect().width / 8;
            let prevMoveX = sx, prevMoveY = sy;

            // --- CHANGE-OF-MIND FAKE-OUT ---
            const com = CONFIG.antiDetection.changeOfMind;
            const doFakeout = com.enabled && Math.random() < com.chance && dist > sqSize * 1.2;

            if (doFakeout) {
                // Pick a fake target: a square adjacent to the real target but NOT the real target
                const offsets = [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[1,1],[-1,1],[1,-1]];
                const realFile = endPos.x, realRank = endPos.y;
                const pick = offsets[Math.floor(Math.random() * offsets.length)];
                const fakeX = endPos.x + pick[0] * sqSize;
                const fakeY = endPos.y + pick[1] * sqSize;
                // Clamp to board bounds
                const bRect = board.getBoundingClientRect();
                const clampX = Math.max(bRect.left + sqSize * 0.5, Math.min(bRect.right - sqSize * 0.5, fakeX));
                const clampY = Math.max(bRect.top + sqSize * 0.5, Math.min(bRect.bottom - sqSize * 0.5, fakeY));
                const fakePos = { x: clampX, y: clampY };

                // Phase 1: drag toward the fake square (go ~75-90% of the way)
                const fakeSteps = Math.max(6, Math.min(14, Math.round(dist / 10)));
                const approach = 0.75 + Math.random() * 0.15;
                const nearFake = {
                    x: startPos.x + (fakePos.x - startPos.x) * approach,
                    y: startPos.y + (fakePos.y - startPos.y) * approach
                };
                await bezierPath(startPos, nearFake, fakeSteps, 1.0);

                // Phase 2: slow down near the fake square (decelerating micro-movements)
                const slowSteps = Math.round(Utils.randomRange(2, 5));
                for (let i = 0; i < slowSteps; i++) {
                    const driftX = prevMoveX + Utils.gaussianRandom(0, 3);
                    const driftY = prevMoveY + Utils.gaussianRandom(0, 3);
                    const rpp = realisticPointerProps(driftX, driftY, prevMoveX, prevMoveY);
                    targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp, clientX: driftX, clientY: driftY }));
                    targetSource.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: driftX, clientY: driftY, movementX: rpp.movementX, movementY: rpp.movementY }));
                    prevMoveX = driftX;
                    prevMoveY = driftY;
                    await Utils.sleep(Utils.randomRange(25, 60));
                }

                // Phase 3: hesitate — hold still
                const hesitate = Utils.humanDelay(com.hesitateMs.min, com.hesitateMs.max);
                Utils.log(`Change-of-mind: faked toward (${pick[0]},${pick[1]}), hesitating ${Math.round(hesitate)}ms`, 'debug');
                UI.toast('Fake-Out', `Changed mind mid-drag — redirecting to real target`, 'fakeout', 2500);
                await Utils.sleep(hesitate);

                // Phase 4: redirect to real target (slightly faster, more decisive)
                const redirectSteps = Math.max(6, Math.min(12, Math.round(dist / 12)));
                await bezierPath({ x: prevMoveX, y: prevMoveY }, endPos, redirectSteps, 0.7);
            } else {
                // Normal drag path
                const steps = Math.max(8, Math.min(18, Math.round(dist / 8) + Math.round(Math.random() * 4)));
                await bezierPath(startPos, endPos, steps, 1.0);
            }

            // Overshoot + settle — common in real mouse movement
            if (Math.random() < 0.35) {
                const ovMag = Utils.randomRange(3, 10);
                const ovAngle = Math.random() * Math.PI * 2;
                const ovX = endPos.x + Math.cos(ovAngle) * ovMag;
                const ovY = endPos.y + Math.sin(ovAngle) * ovMag;
                const rpp1 = realisticPointerProps(ovX, ovY, prevMoveX, prevMoveY);
                targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp1, clientX: ovX, clientY: ovY }));
                prevMoveX = ovX; prevMoveY = ovY;
                await Utils.sleep(Utils.randomRange(10, 30) * spd);
                // Correct back with a small wobble
                const settleX = endPos.x + Utils.gaussianRandom(0, 1.5);
                const settleY = endPos.y + Utils.gaussianRandom(0, 1.5);
                const rpp2 = realisticPointerProps(settleX, settleY, prevMoveX, prevMoveY);
                targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp2, clientX: settleX, clientY: settleY }));
                prevMoveX = settleX; prevMoveY = settleY;
                await Utils.sleep(Utils.randomRange(8, 20) * spd);
                // Final settle on target
                const rpp3 = realisticPointerProps(endPos.x, endPos.y, prevMoveX, prevMoveY);
                targetSource.dispatchEvent(new PointerEvent('pointermove', { ...opts, ...rpp3, clientX: endPos.x, clientY: endPos.y }));
                await Utils.sleep(Utils.randomRange(5, 15) * spd);
            }

            const toCoords = Game.squareToCoords(toSq);
            const targetEl = board.querySelector(`.square-${toCoords}`) ||
                document.elementFromPoint(endPos.x, endPos.y);
            const dropTarget = targetEl || board;

            const dropX = endPos.x + Utils.gaussianRandom(0, 1.5);
            const dropY = endPos.y + Utils.gaussianRandom(0, 1.5);
            Humanizer.showClick(endPos.x, endPos.y, 'red');

            dropTarget.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: dropX, clientY: dropY }));
            dropTarget.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: dropX, clientY: dropY }));
            dropTarget.dispatchEvent(new PointerEvent('click', { ...opts, clientX: dropX, clientY: dropY }));
        },

        executeMove: async (move) => {
            const currentFen = Game.getFen();
            if (currentFen !== State.lastFen && State.moveCount > 1) return;

            const from = move.substring(0, 2);
            const to = move.substring(2, 4);
            const promo = move.length > 4 ? move[4] : 'q';
            const isPromotion = (to[1] === '8' || to[1] === '1') && Humanizer.isPawnMove(from);

            Utils.log(`Auto-playing: ${from} -> ${to}${isPromotion ? '=' + promo : ''}`);

            // Drag-only execution. We intentionally DO NOT fall back to the internal
            // game.move() API: that bypasses the DOM, leaves no pointer telemetry, and
            // is the single strongest signal Chess.com uses to flag scripted play.
            // If the drag fails, the game simply doesn't move this turn — we'll re-try
            // on the next gameLoop tick. Better a lost game than a banned account.
            await Humanizer.dragDrop(from, to);
            await Utils.sleep(180);
            const afterFen = Game.getFen();
            if (afterFen === currentFen) {
                Utils.log(`Drag did not register for ${from}->${to}. NOT using game.move() fallback (too detectable). Will retry next tick.`, 'warn');
                UI.toast('Drag Failed', `${from}\u2192${to} didn't register. Retrying safely.`, 'warn', 3500);
                return;
            }

            if (isPromotion) {
                await Humanizer.handlePromotion(promo);
            }
        },

        isPawnMove: (fromSq) => {
            const board = Game.getBoard();
            if (!board) return false;
            const coords = Game.squareToCoords(fromSq);
            const piece = board.querySelector(`.piece.square-${coords}`);
            if (piece) {
                const classes = piece.className;
                return classes.includes('wp') || classes.includes('bp');
            }
            return fromSq[1] === '2' || fromSq[1] === '7';
        },

        handlePromotion: async (promo = 'q') => {
            const pieceMap = { q: 'queen', r: 'rook', b: 'bishop', n: 'knight' };
            const pieceName = pieceMap[promo] || 'queen';
            Utils.log(`Promotion: selecting ${pieceName}`);

            let promoEl = null;
            for (let i = 0; i < 20; i++) {
                await Utils.sleep(100);
                const selectors = [
                    `.promotion-piece[data-piece="${promo}"]`,
                    `.promotion-piece.w${promo}, .promotion-piece.b${promo}`,
                    `.promotion-window .promotion-piece:first-child`,
                    `[class*="promotion"] [class*="${pieceName}"]`,
                    `[class*="promotion"] [class*="${promo}"]`,
                    `.promotion-area .promotion-piece:first-child`,
                ];
                for (const sel of selectors) {
                    promoEl = document.querySelector(sel);
                    if (promoEl) break;
                }
                if (!promoEl) {
                    const promoContainer = document.querySelector('.promotion-window, .promotion-area, [class*="promotion-"]');
                    if (promoContainer) {
                        const pieces = promoContainer.querySelectorAll('.promotion-piece, [class*="piece"]');
                        if (pieces.length > 0) {
                            promoEl = promo === 'q' ? pieces[0] : pieces[{ r: 1, b: 2, n: 3 }[promo] || 0];
                        }
                    }
                }
                if (promoEl) break;
            }

            if (promoEl) {
                // Pick a slightly off-center hit point so chess.com's input stream
                // sees a non-perfect tap (real fingers/mice never hit dead-center).
                const rect = promoEl.getBoundingClientRect();
                const jitter = (mag) => (Math.random() * 2 - 1) * mag;
                const x = rect.left + rect.width / 2 + jitter(rect.width * 0.15);
                const y = rect.top + rect.height / 2 + jitter(rect.height * 0.15);
                const opts = {
                    bubbles: true, cancelable: true, composed: true,
                    buttons: 1, button: 0, pointerId: 2, pointerType: 'mouse', isPrimary: true,
                    pressure: 0.5, view: window
                };
                // Full natural sequence: pointerover -> pointerenter -> pointerdown
                // -> mousedown -> (small hold) -> pointerup -> mouseup -> click.
                // No raw .click() — it produces an untrusted synthetic event with no
                // associated pointerdown/up history, which Chess.com's input audit
                // can flag as scripted.
                promoEl.dispatchEvent(new PointerEvent('pointerover',  { ...opts, clientX: x, clientY: y, buttons: 0 }));
                promoEl.dispatchEvent(new PointerEvent('pointerenter', { ...opts, clientX: x, clientY: y, buttons: 0 }));
                promoEl.dispatchEvent(new MouseEvent('mouseover',      { ...opts, clientX: x, clientY: y, buttons: 0 }));
                promoEl.dispatchEvent(new MouseEvent('mouseenter',     { ...opts, clientX: x, clientY: y, buttons: 0 }));
                await Utils.sleep(Utils.randomRange(20, 60));
                promoEl.dispatchEvent(new PointerEvent('pointerdown',  { ...opts, clientX: x, clientY: y }));
                promoEl.dispatchEvent(new MouseEvent('mousedown',      { ...opts, clientX: x, clientY: y }));
                await Utils.sleep(Utils.randomRange(40, 110));
                promoEl.dispatchEvent(new PointerEvent('pointerup',    { ...opts, clientX: x, clientY: y, buttons: 0 }));
                promoEl.dispatchEvent(new MouseEvent('mouseup',        { ...opts, clientX: x, clientY: y, buttons: 0 }));
                promoEl.dispatchEvent(new MouseEvent('click',          { ...opts, clientX: x, clientY: y, buttons: 0 }));
                Utils.log(`Promotion: clicked ${pieceName}`);
            } else {
                Utils.log('Promotion dialog not found - default queen will be used', 'warn');
            }
        },

        getCoords: (sq) => {
            const board = Game.getBoard();
            if (!board) return null;
            const rect = board.getBoundingClientRect();
            const sqSize = rect.width / 8;
            const isFlipped = State.playerColor === 'b';
            const f = sq.charCodeAt(0) - 97;
            const r = parseInt(sq[1]) - 1;
            const x = rect.left + (isFlipped ? 7 - f : f) * sqSize + sqSize / 2;
            const y = rect.top + (isFlipped ? r : 7 - r) * sqSize + sqSize / 2;
            return { x, y };
        }
    };

    // ═══════════════════════════════════════════
    //  TELEMETRY NOISE GENERATOR
    // ═══════════════════════════════════════════
    const TelemetryNoise = {
        _lastTick: 0,

        tick: async () => {
            const config = CONFIG.antiDetection.telemetryNoise;
            if (!config || !config.enabled) return;

            const now = Date.now();
            // Only consider doing noise roughly once every 4 seconds to avoid looking frantic
            if (now - TelemetryNoise._lastTick < 4000) return;
            TelemetryNoise._lastTick = now;

            const fen = Game.getFen();
            if (!fen || Game.isMyTurn(fen) || State.isThinking) return;

            const r = Math.random();

            if (r < config.hoverChance) {
                TelemetryNoise.spoofPondering();
            } else if (r < config.hoverChance + config.premoveCancelChance) {
                TelemetryNoise.spoofPremove();
            } else if (r < config.hoverChance + config.premoveCancelChance + config.uiClickChance) {
                TelemetryNoise.spoofUIClick();
            }
        },

        spoofPondering: async () => {
            const board = Game.getBoard();
            if (!board) return;
            const pieces = Array.from(board.querySelectorAll('.piece'));
            if (pieces.length === 0) return;

            const target = pieces[Math.floor(Math.random() * pieces.length)];
            const rect = target.getBoundingClientRect();
            if (rect.width === 0) return;

            const x = rect.left + rect.width / 2 + Utils.gaussianRandom(0, 8);
            const y = rect.top + rect.height / 2 + Utils.gaussianRandom(0, 8);

            Utils.log('TelemetryNoise: Spoofing ponder hover', 'debug');
            const opts = { bubbles: true, composed: true, pointerId: 1 };
            board.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: x, clientY: y }));

            if (Math.random() < 0.3) {
                await Utils.sleep(200);
                board.dispatchEvent(new MouseEvent('contextmenu', { ...opts, clientX: x, clientY: y, button: 2, buttons: 2 }));
            }
        },

        spoofPremove: async () => {
             const board = Game.getBoard();
             if (!board) return;
             const myColor = State.playerColor || 'w';
             const myPieces = Array.from(board.querySelectorAll(`.piece.${myColor}p, .piece.${myColor}n`));
             if (myPieces.length === 0) return;

             const startPiece = myPieces[Math.floor(Math.random() * myPieces.length)];
             const sRect = startPiece.getBoundingClientRect();
             if (sRect.width === 0) return;

             const sx = sRect.left + sRect.width / 2;
             const sy = sRect.top + sRect.height / 2;

             Utils.log('TelemetryNoise: Spoofing canceled premove', 'debug');

             const bRect = board.getBoundingClientRect();
             const tx = sx + Utils.gaussianRandom(0, bRect.width / 4);
             const ty = sy + (myColor === 'w' ? -bRect.width/4 : bRect.width/4);

             // Borrow the bezier drag logic from Humanizer to make this look natural
             const startPos = { x: sx, y: sy };
             const endPos = { x: tx, y: ty };

             // Manually dispatch the start of the drag
             const opts = { bubbles: true, composed: true, pointerId: 1 };
             startPiece.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: sx, clientY: sy, buttons: 1 }));

             // Smooth bezier curve over ~10-15 steps
             const dist = Math.sqrt((tx - sx) ** 2 + (ty - sy) ** 2);
             const steps = Math.max(8, Math.round(dist / 10));

             let prevMoveX = sx, prevMoveY = sy;
             for (let i = 1; i <= steps; i++) {
                 const t = i / steps;
                 // Simple linear interpolation + slight arc for noise
                 const arc = Math.sin(t * Math.PI) * 15;
                 let cx = sx + (tx - sx) * t + arc;
                 let cy = sy + (ty - sy) * t + Utils.gaussianRandom(0, 2);

                 board.dispatchEvent(new PointerEvent('pointermove', {
                     ...opts, clientX: cx, clientY: cy, buttons: 1,
                     movementX: Math.round(cx - prevMoveX),
                     movementY: Math.round(cy - prevMoveY),
                 }));
                 prevMoveX = cx; prevMoveY = cy;
                 await Utils.sleep(Utils.randomRange(10, 25));
             }

             await Utils.sleep(Utils.randomRange(150, 400));
             // Cancel via right click
             board.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: prevMoveX, clientY: prevMoveY, button: 2, buttons: 2 }));
             board.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: prevMoveX, clientY: prevMoveY, buttons: 0 }));
        },

        spoofUIClick: async () => {
            const safeSelectors = [
                '.chat-scroll-area-component',
                '.evaluation-bar-component',
                '.clock-time-monospace',
                '.player-avatar-component'
            ];

            for (const sel of safeSelectors) {
                const els = document.querySelectorAll(sel);
                if (els.length > 0) {
                    const el = els[Math.floor(Math.random() * els.length)];
                    const rect = el.getBoundingClientRect();
                    if (rect.width > 0 && rect.height > 0) {
                        const x = rect.left + rect.width / 2 + Utils.gaussianRandom(0, 3);
                        const y = rect.top + rect.height / 2 + Utils.gaussianRandom(0, 3);
                        Utils.log(`TelemetryNoise: Spoofing UI click on ${sel.split('-')[0]}`, 'debug');
                        const opts = { bubbles: true, composed: true };
                        el.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: x, clientY: y }));
                        el.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: x, clientY: y }));
                        el.dispatchEvent(new MouseEvent('click', { ...opts, clientX: x, clientY: y }));
                        break;
                    }
                }
            }
        }
    };

    // ═══════════════════════════════════════════
    //  MAIN LOOP
    // ═══════════════════════════════════════════
    const Main = {
        _queueing: false,
        _gameInstanceId: 0,           // monotonically increases on every detected new game
        _lastGameResultTracked: -1,   // last instance id we credited to session stats

        showWelcome: () => {
            // Bumped key so the v15.1 first-run modal shows once even on accounts that
            // already dismissed earlier versions.
            const welcomeKey = 'ba_welcome_shown_v15_1';
            if (GM_getValue(welcomeKey, false)) return;
            GM_setValue(welcomeKey, true);

            const overlay = document.createElement('div');
            overlay.style.cssText = `
                position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100000;
                background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center;
                backdrop-filter: blur(4px); animation: fadeIn 0.3s;
            `;
            overlay.innerHTML = `
                <div style="
                    background: #1a1a2e; border: 1px solid #333; border-radius: 12px;
                    padding: 28px 32px; max-width: 560px; width: 90%; color: #e0e0e0;
                    font-family: 'Inter', 'Segoe UI', sans-serif; box-shadow: 0 8px 40px rgba(0,0,0,0.6);
                    max-height: 90vh; overflow-y: auto;
                ">
                    <div style="font-size: 20px; font-weight: 700; color: #4caf50; margin-bottom: 4px;">
                        REXXX.MENU v15.1
                    </div>
                    <div style="font-size: 12px; color: #888; margin-bottom: 16px;">Chess.com Cheat Engine - v15.1 account-level hardening</div>

                    <div style="
                        background: rgba(255,60,60,0.12); border: 1px solid #ff4444; border-radius: 8px;
                        padding: 12px 14px; margin-bottom: 14px; font-size: 12px; line-height: 1.55;
                    ">
                        <div style="font-weight: 700; color: #ff6666; margin-bottom: 4px;">READ THIS BEFORE STARTING</div>
                        <div style="color: #ffcccc;">
                            <strong>No script is undetectable.</strong> Chess.com's fair-play system runs offline analysis after games and
                            looks at engine correlation, timing distribution, click telemetry, and rating curves.
                            With aggressive use this script gets accounts flagged in days; with conservative use, weeks.
                        </div>
                        <div style="color: #ff6666; font-weight: 700; margin-top: 8px;">
                            ALWAYS use an alt. Never run this on an account you care about.
                        </div>
                    </div>

                    <div style="font-size: 13px; line-height: 1.6; margin-bottom: 12px;">
                        <strong>New in v15.1 (account-level hardening):</strong>
                        <ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
                            <li><strong>Warmup:</strong> new accounts play ~350 ELO below target for ~20 games, ramping up.</li>
                            <li><strong>Winrate targeting:</strong> script pushes your lifetime winrate toward a target % (default 52%) across sessions.</li>
                            <li><strong>Hard repertoire:</strong> each account picks 1-2 openings per color and sticks to them.</li>
                            <li><strong>Tilt after loss:</strong> worse play + slower timing for 2-4 games after a defeat.</li>
                            <li><strong>Smart premove gating:</strong> only premoves when reply is forced or an obvious recapture.</li>
                            <li><strong>Time-control lock:</strong> refuses to auto-queue a different TC than the one you started the session in.</li>
                            <li><strong>Engine rotation:</strong> per-game random engine source (varies tactical signature).</li>
                            <li><strong>Messy resignation:</strong> hesitates, sometimes blunders first, sometimes holds lost positions.</li>
                            <li><strong>Hardware persona:</strong> per-account mouse/trackpad/tablet personality affects drag feel.</li>
                        </ul>
                    </div>

                    <div style="font-size: 13px; line-height: 1.6; margin-bottom: 12px;">
                        <strong>Recommended setup for longer survival:</strong>
                        <ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
                            <li>Target rating &le; 1800 (high ratings invite review faster).</li>
                            <li>Max 4 games/hr, max 5 games/session.</li>
                            <li>Leave all v15.1 Account &amp; Behavior toggles ON.</li>
                            <li>When switching Chess.com accounts, press "Reset Account State" in the SAFETY tab.</li>
                            <li>Vary play across days. Don't grind 50 games in one sitting.</li>
                        </ul>
                    </div>

                    <div style="font-size: 12px; color: #888; margin-bottom: 16px; line-height: 1.5;">
                        Press <span style="background:#333;padding:2px 6px;border-radius:3px;font-family:monospace;">A</span> to toggle auto-play
                        once a game starts. Defaults are tuned for safety — be cautious before raising any caps.
                    </div>

                    <div style="text-align: center;">
                        <button id="ba-welcome-dismiss" style="
                            background: #4caf50; color: white; border: none; border-radius: 6px;
                            padding: 10px 32px; font-size: 14px; font-weight: 600; cursor: pointer;
                        ">I Understand — Let's Go</button>
                    </div>
                </div>
            `;
            document.body.appendChild(overlay);
            overlay.querySelector('#ba-welcome-dismiss').addEventListener('click', () => {
                overlay.style.opacity = '0';
                overlay.style.transition = 'opacity 0.3s';
                setTimeout(() => overlay.remove(), 300);
            });
        },

        init: async () => {
            UI.injectStyles();
            UI.createInterface();

            // Load saved settings
            Settings.init(CONFIG);

            // Fresh page load = fresh session: clear per-session locks/overrides.
            // Account-level persistent state (totalGamesPlayed, repertoire, hardware,
            // recentResults) stays; only per-session things like sessionTC reset.
            if (CONFIG.account) {
                CONFIG.account.sessionTC = null;
                CONFIG.account.currentEngine = null;
            }

            // Show first-time welcome modal
            Main.showWelcome();

            // Apply rating profile
            RatingProfile.apply(CONFIG.targetRating);

            // Initialize persistent weakness profile
            WeaknessProfile.init();

            let board = null;
            while (!board) {
                board = Game.getBoard();
                if (!board) await Utils.sleep(500);
            }
            Utils.log('Board detected. Starting Engine...');
            await Engine.init();

            Main.setupObservers();
            Main.gameLoop();

            // Keyboard shortcuts
            document.addEventListener('keydown', (e) => {
                if (e.target.matches('input, textarea, [contenteditable]')) return;
                if (e.key === 'a' && !e.ctrlKey && !e.shiftKey) {
                    CONFIG.auto.enabled = !CONFIG.auto.enabled;
                    Utils.log(`Auto-Play: ${CONFIG.auto.enabled}`);
                    UI.updatePanel(State.currentEval, {});
                    Settings.save(CONFIG);
                }
                if (e.key === 'x') {
                    UI.toggleStealth();
                }
                if (e.key === 'r' && !e.ctrlKey) {
                    Settings.reset(CONFIG);
                    RatingProfile.apply(CONFIG.targetRating);
                    UI.refreshSliders();
                    Utils.log('Settings reset to defaults');
                }
            });

            // Clock polling
            setInterval(() => Game.readClock(), 2000);
        },

        detectGameResult: () => {
            // Check for win
            const winHeader = document.querySelector('.game-over-modal-header-userWon');
            if (winHeader) return 'win';
            // Check title text as fallback
            const titleEl = document.querySelector('[data-cy="header-title-component"]');
            if (titleEl) {
                const text = titleEl.textContent.trim().toLowerCase();
                if (text.includes('you won')) return 'win';
                if (text.includes('you lost') || text.includes('checkmate') || text.includes('time')) return 'loss';
                if (text.includes('draw') || text.includes('stalemate')) return 'draw';
            }
            // Check loss
            const lossHeader = document.querySelector('.game-over-modal-header-userLost');
            if (lossHeader) return 'loss';
            // Check draw
            const drawHeader = document.querySelector('.game-over-modal-header-draw');
            if (drawHeader) return 'draw';
            return 'unknown';
        },

        updateSessionStats: (result) => {
            CONFIG.session.gamesPlayed++;
            if (result === 'win') {
                CONFIG.session.wins++;
                CONFIG.session.currentWinStreak++;
            } else {
                CONFIG.session.currentWinStreak = 0;
            }
            // Account-level: persistent winrate window + tilt trigger
            const code = result === 'win' ? 'W' : (result === 'loss' ? 'L' : (result === 'draw' ? 'D' : null));
            if (code) {
                Account.recordResult(code);
                Account.maybeStartTilt(code);
            }
            Utils.log(`Session: Game #${CONFIG.session.gamesPlayed}, result=${result}, winStreak=${CONFIG.session.currentWinStreak}, account totalGames=${CONFIG.account.totalGamesPlayed}`, 'info');
            Settings.save(CONFIG);
            UI.updateStats();
        },

        showStreakWarning: () => {
            // Remove existing warning if any
            const existing = document.querySelector('.ba-streak-warning');
            if (existing) existing.remove();

            const streak = CONFIG.session.currentWinStreak;
            const gamesPlayed = CONFIG.session.gamesPlayed;
            const winRate = gamesPlayed > 0 ? (CONFIG.session.wins / gamesPlayed) : 0;

            let warningLevel = null;
            let message = '';

            if (streak >= CONFIG.session.maxWinStreak) {
                warningLevel = 'critical';
                message = CONFIG.autoLose.enabled
                    ? `WIN STREAK: ${streak} in a row! Auto-Lose mode will activate next game to avoid detection.`
                    : `WIN STREAK: ${streak} in a row! You should LOSE the next game to avoid detection. Streak this high will trigger anti-cheat review.`;
            } else if (streak >= CONFIG.session.maxWinStreak - 2) {
                warningLevel = 'high';
                message = `Win streak: ${streak}. Getting suspicious - consider losing soon.`;
            } else if (gamesPlayed >= 5 && winRate > 0.85) {
                warningLevel = 'medium';
                message = `Win rate ${(winRate * 100).toFixed(0)}% over ${gamesPlayed} games. Consider losing a game to look natural.`;
            }

            if (!warningLevel) return false;

            const colors = {
                critical: { bg: 'rgba(255,0,0,0.15)', border: '#ff4444', text: '#ff6666', icon: '\u26A0' },
                high: { bg: 'rgba(255,165,0,0.12)', border: '#ff9800', text: '#ffb74d', icon: '\u26A0' },
                medium: { bg: 'rgba(255,255,0,0.08)', border: '#fdd835', text: '#fff176', icon: '\u24D8' },
            };
            const c = colors[warningLevel];

            const warning = document.createElement('div');
            warning.className = 'ba-streak-warning';
            warning.style.cssText = `
                position: fixed; top: 10px; left: 50%; transform: translateX(-50%); z-index: 10002;
                background: ${c.bg}; border: 1px solid ${c.border}; border-radius: 8px;
                padding: 12px 20px; max-width: 500px; text-align: center;
                font-family: 'Inter', sans-serif; font-size: 13px; color: ${c.text};
                box-shadow: 0 4px 20px rgba(0,0,0,0.5); backdrop-filter: blur(8px);
                animation: fadeIn 0.3s;
            `;
            warning.innerHTML = `
                <div style="font-weight:700; margin-bottom:4px;">${c.icon} ANTI-CHEAT WARNING</div>
                <div>${message}</div>
                <div style="margin-top:8px; font-size:11px; opacity:0.7; cursor:pointer;" onclick="this.parentElement.remove()">[click to dismiss]</div>
            `;
            document.body.appendChild(warning);

            // Auto-dismiss after 15s for non-critical
            if (warningLevel !== 'critical') {
                setTimeout(() => warning.remove(), 15000);
            }

            // If auto-lose is enabled, don't pause - let auto-lose handle it next game
            return warningLevel === 'critical' && !CONFIG.autoLose.enabled;
        },

        checkAutoQueue: async () => {
            if (!CONFIG.auto.autoQueue || !CONFIG.auto.enabled) return;
            if (Main._queueing) return;

            // Verify game-over modal is actually visible
            const modal = Utils.query(SELECTORS.gameOver);
            if (!modal) return;

            Main._queueing = true;

            // Detect game result and update session stats (only once per game).
            // Use a monotonic gameInstanceId so two games ending on coincidentally
            // similar FENs (e.g. repetition draws, stalemates with same final position)
            // can't share the same tracking key.
            const result = Main.detectGameResult();
            if (Main._lastGameResultTracked !== Main._gameInstanceId) {
                Main._lastGameResultTracked = Main._gameInstanceId;
                Main.updateSessionStats(result);
            }

            // Show warning if streak is suspicious
            const shouldPause = Main.showStreakWarning();
            if (shouldPause) {
                Utils.log('Auto-Queue: PAUSED - Win streak too high, user should lose a game', 'error');
                Main._queueing = false;
                return;
            }

            // TC LOCK: refuse to queue games of a different time control than
            // the one we started this session with. Real players stick to one TC.
            if (!Account.tcLockAllowsQueue()) {
                UI.toast('TC Lock', `Different time control detected - skipping auto-queue`, 'warn', 5000);
                Main._queueing = false;
                return;
            }

            // ANTI-DETECTION: Game rate limiter (max games per hour).
            // We compute the limit BEFORE pushing so we don't accidentally hit
            // (cap+1) when the just-finished game is what would push us over.
            const now = Date.now();
            if (!CONFIG.session.gameTimestamps) CONFIG.session.gameTimestamps = [];
            CONFIG.session.gameTimestamps = CONFIG.session.gameTimestamps.filter(t => now - t < 3600000);
            if (CONFIG.session.gameTimestamps.length >= CONFIG.antiDetection.maxGamesPerHour) {
                const oldest = CONFIG.session.gameTimestamps[0];
                const waitMs = 3600000 - (now - oldest) + Utils.randomRange(60000, 180000);
                Utils.log(`Rate limiter: ${CONFIG.session.gameTimestamps.length}/h reached, waiting ${Math.round(waitMs / 1000)}s`, 'warn');
                UI.toast('Rate limited', `Hour cap hit — pausing ${Math.round(waitMs / 60000)}m`, 'warn', 6000);
                await Utils.sleep(waitMs);
                // Re-filter after waking up; older entries fall off naturally.
                const after = Date.now();
                CONFIG.session.gameTimestamps = CONFIG.session.gameTimestamps.filter(t => after - t < 3600000);
            }
            CONFIG.session.gameTimestamps.push(Date.now());

            // Session break check (with jitter)
            const jitter = CONFIG.antiDetection.sessionLengthJitter;
            const jitteredMax = Math.round(CONFIG.session.maxGamesPerSession * (1 + (Math.random() * 2 - 1) * jitter));
            if (CONFIG.session.gamesPlayed >= jitteredMax) {
                const breakMs = CONFIG.session.breakDurationMs + Utils.randomRange(30000, 180000);
                Utils.log(`Session break: ${Math.round(breakMs / 1000)}s after ${CONFIG.session.gamesPlayed} games (jittered limit: ${jitteredMax})`, 'warn');
                await Utils.sleep(breakMs);
                CONFIG.session.gamesPlayed = 0;
                CONFIG.session.wins = 0;
                CONFIG.session.currentWinStreak = 0;
                // Reset TC lock so a new session can pick a fresh time control
                Account.clearSessionTC();
            }

            // ANTI-DETECTION: Minimum break between games
            const betweenGames = CONFIG.antiDetection.minBreakBetweenGames;
            const breakDelay = Utils.humanDelay(betweenGames.min, betweenGames.max);
            Utils.log(`Between-game delay: ${Math.round(breakDelay / 1000)}s`, 'debug');
            await Utils.sleep(breakDelay);

            // Try to click the new game button
            const newGameSelectors = [
                'button[data-cy="game-over-modal-new-game-button"]',
                '.game-over-secondary-actions-row-component button[data-cy="game-over-modal-new-game-button"]',
                'button[data-cy="game-over-modal-rematch-button"]',
                'button[data-cy="new-game-index-main"]',
                '.game-over-modal-shell-buttons button.cc-button-secondary',
                '.game-over-buttons-button',
                '.game-over-button-component.primary',
                '.ui_v5-button-component.ui_v5-button-primary',
            ];

            for (let attempt = 0; attempt < 10; attempt++) {
                for (const sel of newGameSelectors) {
                    const btn = document.querySelector(sel);
                    if (btn && btn.offsetParent !== null) {
                        // Prefer "New Game" over "Rematch" if both exist
                        if (sel.includes('rematch')) {
                            const newGameBtn = document.querySelector(newGameSelectors[0]);
                            if (newGameBtn && newGameBtn.offsetParent !== null) continue;
                        }
                        const delay = Utils.humanDelay(2000, 5000);
                        Utils.log(`Auto-Queue: Clicking "${btn.textContent.trim()}" in ${Math.round(delay)}ms (game #${CONFIG.session.gamesPlayed}, streak: ${CONFIG.session.currentWinStreak})...`);
                        await Utils.sleep(delay);
                        btn.click();
                        // Wait and verify the modal closed
                        await Utils.sleep(1500);
                        const stillOpen = Utils.query(SELECTORS.gameOver);
                        if (stillOpen) {
                            Utils.log('Auto-Queue: Modal still open, trying close button...', 'warn');
                            const closeBtn = document.querySelector('.game-over-modal-header-close, [data-cy="close-board-modal"], .cc-close-button-component');
                            if (closeBtn) {
                                closeBtn.click();
                                await Utils.sleep(1000);
                            }
                        }
                        Main._queueing = false;
                        return;
                    }
                }
                await Utils.sleep(1500);
            }

            // Last resort: try closing the modal and looking for new game button elsewhere
            Utils.log('Auto-Queue: Standard buttons not found, trying close + page buttons...', 'warn');
            const closeBtn = document.querySelector('.game-over-modal-header-close, [data-cy="close-board-modal"], .cc-close-button-component');
            if (closeBtn) {
                closeBtn.click();
                await Utils.sleep(2000);
                // Look for any new game button on the page
                const fallbackBtn = document.querySelector('button[data-cy="new-game-index-main"], .new-game-button, [class*="new-game"]');
                if (fallbackBtn) {
                    fallbackBtn.click();
                }
            }

            Utils.log('Auto-Queue: Could not find new game button', 'warn');
            Main._queueing = false;
        },

        _loopScheduled: false,
        _scheduleLoop: () => {
            if (Main._loopScheduled) return;
            Main._loopScheduled = true;
            setTimeout(() => {
                Main._loopScheduled = false;
                Main.gameLoop();
            }, 50);
        },

        setupObservers: () => {
            const movesList = Utils.query(SELECTORS.moves) || document.body;
            const observer = new MutationObserver(() => {
                Main._scheduleLoop();
            });
            observer.observe(movesList, { childList: true, subtree: true, characterData: true });

            let gameOverCheckScheduled = false;
            const gameOverObserver = new MutationObserver(() => {
                if (gameOverCheckScheduled) return;
                gameOverCheckScheduled = true;
                setTimeout(() => {
                    gameOverCheckScheduled = false;
                    if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
                        Main.checkAutoQueue();
                    }
                }, 500);
            });
            gameOverObserver.observe(document.body, { childList: true, subtree: true });

            setInterval(Main.gameLoop, CONFIG.pollInterval);
            setInterval(() => {
                if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
                    Main.checkAutoQueue();
                }
            }, 5000);
        },

        _autoQueueScheduled: false,
        _lastLoopRun: 0,

        gameLoop: () => {
            // Debounce: skip if called again within 100ms (prevents tab-resume storm)
            const now = Date.now();
            if (now - Main._lastLoopRun < 100) return;
            Main._lastLoopRun = now;

            // Run background telemetry noise generator
            TelemetryNoise.tick();

            const fen = Game.getFen();
            if (!fen || fen === State.lastFen) return;

            State.playerColor = Game.detectColor();
            const fenParts = fen.split(' ');
            const isStartPos = fenParts[0] === 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR';
            const fenMoveNum = parseInt(fenParts[5] || '1');

            if (isStartPos || (fenMoveNum <= 1 && State.moveCount > 3)) {
                Main._gameInstanceId++;
                Utils.log(`New game detected (#${Main._gameInstanceId}) - resetting humanization`, 'warn');
                UI.toast('New Game', `Personality reset — GL HF`, 'success', 3000);
                HumanStrategy.resetGame();
                Main._queueing = false;
            }

            State.lastFen = fen;
            State.currentEval = null;
            State.candidates = {};
            UI.clearOverlay();
            UI.updatePanel(null, null);

            // Read clock
            Game.readClock();

            if (Utils.query(SELECTORS.gameOver)) {
                if (CONFIG.auto.autoQueue && !Main._autoQueueScheduled) {
                    Main._autoQueueScheduled = true;
                    setTimeout(() => {
                        Main._autoQueueScheduled = false;
                        Main.checkAutoQueue();
                    }, 2000);
                }
                return;
            }

            if (Game.isMyTurn(fen)) {
                // --- Recapture detection ---
                // Did the opponent just capture? Compare piece count in old vs new FEN
                if (State.lastFen) {
                    const oldPieces = (State.lastFen.split(' ')[0].match(/[a-zA-Z]/g) || []).length;
                    const newPieces = (fen.split(' ')[0].match(/[a-zA-Z]/g) || []).length;
                    State.lastMoveWasCapture = newPieces < oldPieces;
                } else {
                    State.lastMoveWasCapture = false;
                }

                UI.updateStatus(UI._styleAccents[CONFIG.playStyle] || '#4caf50');
                Engine.analyze(fen);
            } else {
                // It's opponent's turn — save our current eval for surprise detection
                if (State.currentEval) {
                    State.human.evalBeforeOpponentMove = State.currentEval.value;
                }
                UI.updateStatus('#888');
            }
        }
    };

    Main.init();
})();