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)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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