Chess.com Cheat Engine

Chess.com cheat engine — v16.4 (smart analysis arrows + coach mode + auto-queue fixes)

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

Advertisement:

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

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

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

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

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

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

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

Advertisement:

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      16.4
// @description  Chess.com cheat engine — v16.4 (smart analysis arrows + coach mode + auto-queue fixes)
// @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,
                    // v16 additions
                    blunderBias: config.blunderBias,
                    shallowDepth: config.shallowDepth,
                    bookExitPause: config.bookExitPause,
                    motifBlindness: config.motifBlindness,
                    endgameTechnique: config.endgameTechnique,
                    timeBankCurve: config.timeBankCurve,
                    idleMouse: config.idleMouse,
                    annotations: config.annotations,
                    stealth: config.stealth,
                    engineCache: config.engineCache,
                    forcedMove: config.forcedMove,
                    kingSafety: config.kingSafety,
                    postGame: config.postGame,
                    // v16.1 additions
                    acplGovernor: config.acplGovernor,
                    // v16.3 additions
                    coach: config.coach,
                };
                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,

        // --- COACH MODE (v16.3) ---
        // Provides GM-level analysis without playing for the user.
        // When enabled, auto-play is force-disabled and a floating overlay
        // displays best move, alternatives, threats, and natural-language
        // commentary. The user makes their own moves but gets coaching.
        coach: {
            enabled: false,
            // Display granularity
            showBestArrow: true,         // green arrow on best move
            showAlternatives: true,       // up to 2 close alternatives
            showThreats: true,            // opponent's best reply (red arrow)
            showAnnotations: true,        // natural-language commentary
            showHangingPieces: true,      // highlight hanging pieces
            showLastMoveReview: true,     // grade opponent's last move
            // Behavior
            onlyOnHotkey: false,          // if true, overlay hidden until hotkey
            disableAutoOnEnable: true,    // force auto-play off when coach turns on
            // Tuning
            altEvalWindow: 0.5,           // alternative shown if within this many pawns
            blunderThreshold: 1.5,        // |eval swing| ≥ this = blunder annotation
            mistakeThreshold: 0.6,        // smaller eval swing = mistake annotation
            // UI position (saved between sessions)
            panelX: 24,
            panelY: 120,
        },

        // --- 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: [],
            // Recent opponent ratings (newest last). Used to estimate our own
            // rating, since chess.com pairs tightly (~50-150 ELO). Average of
            // recent opponents ≈ our current rating. Critical for the climb
            // logic that fixes the "drift to ~1300" problem.
            recentOpponentRatings: [],
            // 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: 12,     // games until full target rating (was 20)
            startEloOffset: -150,  // begin this far below user target (was -350)
            shape: 'sigmoid',      // 'linear' | 'sigmoid' (sigmoid is more human-like)
        },

        // --- WINRATE TARGETING ---
        winrateTarget: {
            enabled: true,
            // 58% target: previous 52% combined with warmup-suppressed rating meant
            // we were actively sandbagging in most games. 58% is realistic for a player
            // settling into their true bracket and leaves room for the ACPL governor +
            // humanization to naturally produce losses without forced ones.
            target: 0.58,
            sampleSize: 12,        // window of recent games to compute rolling winrate
            overshootBoost: 0.40,  // each +10% above target adds this much to lose-chance (was 0.50)
        },

        // --- ACPL GOVERNOR (v16.1) ---
        // The single most important fix for "rating drifts way below target".
        // Tracks our rolling average centipawn loss over the last N moves. If we're
        // already at or above the empirical ACPL for the target rating, suppresses
        // further error injection (suboptimal/blunder) for the next moves.
        //
        // This prevents the stacking problem: previously every v15.1 + v16 layer
        // could fire independently (critical-bias × motif-blindness × endgame-slips
        // × tilt × warmup), pushing actual ACPL to 80-90 on a 1800 target.
        //
        // targetACPL is overridden per-rating by RatingProfile.apply().
        acplGovernor: {
            enabled: true,
            targetACPL: 45,        // set per-rating by RatingProfile (45 ≈ 1800)
            windowSize: 20,        // rolling window of last N of our moves
            // When current ACPL exceeds target × hardCapMult, ALL further error
            // injection is suppressed until ACPL drifts back below target.
            hardCapMult: 1.15,
            // Below target × softFloorMult we allow extra error injection (cold streak).
            // This makes us *more* error-prone when we've been playing too well lately —
            // critical for not chaining 8 perfect moves in a row.
            softFloorMult: 0.55,
            // Only start governing after this many moves have been tracked
            // (early-game samples are too noisy).
            minMoves: 6,
        },

        // --- 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 ---
        // Toned down in v16.1: previous 0.18 additive boost over 2-4 games was active
        // in ~70% of games (since the 52% winrate target lost half + tilt persistence).
        // Real player tilt is shorter and more subtle than that.
        tilt: {
            enabled: true,
            triggerOn: ['L'],             // event types that cause tilt ('L' loss, optionally 'D')
            duration: { min: 1, max: 2 }, // games of degraded play after trigger (was 2-4)
            suboptimalBoost: 0.08,        // additive to suboptimal rate (was 0.18)
            blunderMult: 1.35,            // multiplier on blunder chance (was 1.6)
            timingMult: 1.20,             // 20% slower (was 25%)
        },

        // --- 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
        },

        // --- CRITICAL-POSITION BLUNDER BIAS (T1) ---
        // Real human errors cluster in tactical/critical positions, not random
        // quiet moves. Bots blunder uniformly, which is detectable. We bias the
        // suboptimal/blunder rate by position complexity: simple positions =
        // play perfectly, complex positions = sometimes fail like a human.
        blunderBias: {
            enabled: true,
            // For positions with complexity <= simpleCutoff, REDUCE error rate
            simpleCutoff: 0.30,
            simpleErrorMult: 0.35,        // simple = 35% of normal error rate
            // For positions with complexity >= criticalCutoff, INCREASE error rate
            criticalCutoff: 0.65,
            criticalErrorMult: 1.55,      // critical = 155% of normal error rate
        },

        // --- CALCULATION DEPTH LIMITING (T2) ---
        // 1800 humans calculate ~3-5 plies, sometimes 6-7 in critical positions.
        // The bot uses depth 18-22 and finds resources humans never see. We
        // simulate "human depth" by occasionally picking from candidates that
        // would be findable at depth 8-12 instead of full depth.
        shallowDepth: {
            enabled: true,
            chance: 0.10,                 // 10% of moves use human-depth selection
            preferLongPVChance: 0.45,     // when active, this is the chance to demote a long-PV move
            // Long PV = >8 plies of forcing sequence; humans rarely see that deep
            longPVThreshold: 8,
        },

        // --- OUT-OF-BOOK CONFUSION PAUSE (A6) ---
        // The first move after leaving prep should take noticeably longer.
        bookExitPause: {
            enabled: true,
            multiplier: 2.4,              // 2.4x normal think time on first non-book move
            // Apply only if we played at least N book moves this game
            minBookMovesBefore: 3,
        },

        // --- TACTICAL MOTIF BLINDNESS (A7) ---
        // Specific patterns humans miss disproportionately often.
        motifBlindness: {
            enabled: true,
            // Long forcing sequences with a quiet (non-capture) move in the
            // middle — classic zwischenzug pattern.
            zwischenzugMissChance: 0.18,
            // Long bishop diagonals attacking distant targets
            longDiagonalMissChance: 0.12,
            // Backwards knight moves (toward our own ranks) — counterintuitive
            backwardsKnightMissChance: 0.15,
            // Deflection sacrifices: a piece sac that wins greater material
            // through a different threat
            deflectionMissChance: 0.20,
        },

        // --- ENDGAME TECHNIQUE REDUCTION (A8) ---
        // 1800 players lose technically winning endgames at ~15-20% rate.
        // Especially K+P, R+P, same-color bishop endgames.
        endgameTechnique: {
            enabled: true,
            // Detected when only kings, pawns, and at most 2 minor/major pieces total
            kpMistakeMult: 1.45,          // K+P endgames: 45% more errors
            rpMistakeMult: 1.30,          // R+P endgames: 30% more errors
            simpleMistakeMult: 1.20,      // other simple endgames
        },

        // --- TIME BANK CURVE (A9) ---
        // Real human time spending follows a bell curve peaking around moves
        // 18-25 (critical middlegame), with less spent in opening and endgame.
        timeBankCurve: {
            enabled: true,
            peakMove: 22,                 // peak think time at this move
            peakMultiplier: 1.35,         // up to 35% extra at peak
            falloffMoves: 12,             // how wide the bell is
        },

        // --- IDLE MOUSE BEHAVIOR (B10) ---
        // While "thinking," mouse should not be perfectly still. Real users
        // make tiny drifts, hovers, scroll the page slightly.
        idleMouse: {
            enabled: true,
            // Trigger if we've been thinking longer than this without moving
            triggerAfterMs: 3500,
            // Probability per check tick to do an idle action
            actionChance: 0.35,
            // Microdrift magnitude in pixels
            driftMagnitude: { min: 1, max: 6 },
        },

        // --- RIGHT-CLICK ANNOTATIONS (B11, expanded v16.4) ---
        // Real players draw red circles / arrows on the board during thinks.
        // Pros especially do this constantly — visualizing candidate moves before
        // committing to one. We bias arrows toward actual engine candidates so
        // the pattern looks like genuine analysis, not random scribbling.
        annotations: {
            enabled: true,
            chancePerLongThink: 0.40,     // probability to fire when the cooldown gate opens
            arrowChance: 0.65,            // arrows vs single-square circles (arrows more common)
            minThinkMs: 3000,             // don't annotate during quick moves
            maxPerThink: 3,               // up to N annotations during one long think
            minTimeBetweenMs: 1500,       // cooldown between successive annotations in same think
            // Where to draw:
            candidateBias: 0.55,          // % of arrows that visualize an actual engine candidate move
            pieceSquareBias: 0.85,        // % of "random" squares that are real piece squares (not empty)
        },

        // --- STEALTH MODE (B12) ---
        // Suppresses console logs and reduces DOM signature.
        stealth: {
            enabled: false,               // off by default — users can enable from UI
            silentLogs: true,             // when stealth on, suppress all Utils.log
            hideToasts: false,            // toasts stay (UX), can be turned off
        },

        // --- ENGINE QUERY CAMOUFLAGE (B13) ---
        // Cache evals and reduce API call frequency to avoid network signature.
        engineCache: {
            enabled: true,
            maxEntries: 256,              // LRU-style cap
            ttlMs: 600000,                // 10 minutes
            // If a position is similar (same FEN ignoring move counters), reuse
            normalizeMoveCounters: true,
        },

        // --- FORCED MOVE RECOGNITION (B14) ---
        // When there's exactly one good move, humans play it nearly instantly.
        forcedMove: {
            enabled: true,
            // If only N moves are within this CP gap of best, treat as forced
            gapCp: 80,
            maxAlternatives: 1,
            // When detected, override timing to this range
            instantMs: { min: 220, max: 750 },
        },

        // --- ASYMMETRIC KING SAFETY (B16) ---
        // Defending our own king triggers a longer think than attacking theirs.
        kingSafety: {
            enabled: true,
            // If our eval dropped by this much vs the previous move, we're under attack
            evalDropTrigger: 0.5,
            defenseTimingMult: 1.45,
        },

        // --- POST-GAME BEHAVIOR (B15) ---
        // Variable behavior after a game ends — not just immediate re-queue.
        postGame: {
            enabled: true,
            // Chance to "review" the game (linger on the board) before re-queue
            reviewChance: 0.18,
            reviewDurationMs: { min: 4000, max: 14000 },
            // Chance to click on opponent's profile after the game (curiosity)
            profileClickChance: 0.10,
        },

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

        // ---------- ESTIMATED RATING (v16.2) ----------
        // chess.com pairs tightly (~50-150 ELO); a rolling average of recent
        // opponents is a robust proxy for our own rating. Used by the climb
        // logic that prevents target-rating drift.
        recordOpponentRating: (rating) => {
            if (!rating || rating < 100 || rating > 4000) return;
            const arr = CONFIG.account.recentOpponentRatings || [];
            arr.push(rating);
            // Keep last 20 — enough to smooth noise, fast enough to track climbs.
            CONFIG.account.recentOpponentRatings = arr.slice(-20);
            Settings.save(CONFIG);
        },

        estimatedRating: () => {
            const arr = CONFIG.account.recentOpponentRatings || [];
            if (arr.length < 3) return null;
            // Average of last 10 (or all if fewer) — recent enough to track climbs.
            const recent = arr.slice(-10);
            const sum = recent.reduce((a, b) => a + b, 0);
            return Math.round(sum / recent.length);
        },

        // Positive = we're below user's target rating (need to climb).
        // Negative = we're above (need to decline).
        // null = not enough samples.
        ratingGap: () => {
            const est = Account.estimatedRating();
            if (est == null) return null;
            return CONFIG.targetRating - est;
        },

        // Returns 'climb' | 'maintain' | 'decline' | 'unknown'.
        climbMode: () => {
            const gap = Account.ratingGap();
            if (gap == null) return 'unknown';
            if (gap > 100) return 'climb';
            if (gap < -100) return 'decline';
            return 'maintain';
        },

        // ---------- 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 — slightly raised floor (was 8→16, now 10→16).
            // Depth 12-13 at mid ratings is enough to RANK candidates without picking
            // misjudged sub-moves, which was a contributor to drift.
            CONFIG.engineDepth.base = Math.round(lerp(10, 16, t));
            CONFIG.engineDepth.min = Math.max(8, CONFIG.engineDepth.base - 3);
            CONFIG.engineDepth.max = Math.min(20, CONFIG.engineDepth.base + 3);

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

            // Suboptimal rates — kept as-is, the issue was magnitude not frequency.
            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 — TIGHTENED (was 80→25 / 110→35 / 75→20).
            // Empirical 1800 ACPL is ~42-48. Old ceilings allowed -72cp per sub-move
            // in middlegame which is true 1300 territory. New ceilings at t=0.5:
            //   opening 36cp, middlegame 46cp, endgame 32cp → ACPL ~40-50.
            CONFIG.humanization.maxAcceptableCPLoss.opening    = Math.round(lerp(55, 18, t));
            CONFIG.humanization.maxAcceptableCPLoss.middlegame = Math.round(lerp(70, 22, t));
            CONFIG.humanization.maxAcceptableCPLoss.endgame    = Math.round(lerp(50, 14, t));

            // Blunder chance: 3% at 800 → 0.3% at 2800.
            CONFIG.humanization.blunder.chance = lerp(0.03, 0.003, t);
            // Blunder magnitude tightened (was 180→80, now 140→60). 1800s blunder
            // 1-2 times per game and the typical magnitude is a piece or two pawns,
            // not a queen.
            CONFIG.humanization.blunder.maxCPLoss = Math.round(lerp(140, 60, t));

            // ACPL governor target — empirically calibrated to chess.com rapid data.
            // At t=0.5 (1800) → 49 ACPL, matching real-world 42-48 ACPL for that level.
            //   800  → 85   1300 → 67   1800 → 49
            //   2200 → 35   2400 → 27   2800 → 12
            if (CONFIG.acplGovernor) {
                CONFIG.acplGovernor.targetACPL = Math.round(lerp(85, 12, t));
            }

            // Streaks: cap perfect run lower so we don't chain 12 best moves.
            CONFIG.humanization.streaks.perfectStreakMax = Math.round(lerp(3, 6, 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)}% | target ACPL ${CONFIG.acplGovernor?.targetACPL || '?'}`, '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
            // --- v16 fields ---
            // Book-exit pause (A6)
            bookMovesPlayed: 0,
            justLeftBook: false,
            // King safety asymmetry (B16): track previous our-eval for delta detection
            prevOurEval: null,
            // Calc depth limiting (T2): how many shallow-depth picks this game (telemetry only)
            shallowPickCount: 0,
            // Tracking last mouse position for idle drift (B10)
            lastMouseX: null,
            lastMouseY: null,
            // --- v16.1 ACPL governor state ---
            // Rolling window of our recent move CP losses. Sum + ring buffer.
            acplWindow: [],        // last N cp-loss values
            acplSum: 0,            // running sum of values in window
            acplSuppressedCount: 0,// telemetry: how many times we suppressed an error this game
        },
        // Player move DB cache (FEN -> moves array)
        playerDBCache: new Map(),
        // Track if last move was a capture (for recapture detection)
        lastMoveWasCapture: false,
        // Engine eval cache (B13): FEN -> { result, ts }
        engineCache: new Map(),
    };

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

        log: (msg, type = 'info') => {
            // Stealth mode (B12): suppress all logs except errors
            if (CONFIG.stealth?.enabled && CONFIG.stealth.silentLogs && type !== 'error') return;
            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: 360px;
                    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;
                    gap: 10px;
                }
                .ba-header-left { display: flex; align-items: center; gap: 10px; min-width: 0; }
                .ba-logo { font-weight: 800; font-size: 14px; letter-spacing: 1px; color: var(--ba-accent); white-space: nowrap; }
                .ba-logo span { color: #fff; opacity: 0.7; font-weight: 400; }
                .ba-feature-count {
                    font-family: 'JetBrains Mono', monospace; font-size: 10px;
                    color: var(--ba-accent); opacity: 0.85;
                    background: color-mix(in srgb, var(--ba-accent) 12%, transparent);
                    border: 1px solid color-mix(in srgb, var(--ba-accent) 30%, transparent);
                    padding: 2px 7px; border-radius: 10px; white-space: nowrap;
                }
                .ba-minimize { cursor: pointer; opacity: 0.5; transition: 0.2s; font-size: 16px; flex-shrink: 0; }
                .ba-minimize:hover { opacity: 1; color: #fff; }
                .ba-tabs { display: flex; background: rgba(0,0,0,0.2); flex-wrap: nowrap; }
                .ba-tab {
                    flex: 1; text-align: center; padding: 9px 0;
                    font-size: 10.5px; font-weight: 600; color: #666;
                    cursor: pointer; transition: 0.2s;
                    border-bottom: 2px solid transparent;
                    min-width: 0;
                    letter-spacing: 0.6px;
                }
                .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: 460px; overflow-y: auto; }
                .ba-content::-webkit-scrollbar { width: 6px; }
                .ba-content::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 3px; }
                .ba-content::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
                .ba-page { display: none; }
                .ba-page.active { display: block; animation: fadeIn 0.2s; }

                /* --- Rows: clickable when they contain a checkbox --- */
                .ba-row {
                    display: flex; justify-content: space-between; align-items: center;
                    margin-bottom: 8px; padding: 6px 8px;
                    border-radius: 5px;
                    transition: background 0.15s;
                }
                .ba-row.ba-toggleable { cursor: pointer; }
                .ba-row.ba-toggleable:hover { background: rgba(255,255,255,0.04); }
                .ba-label { font-size: 12px; color: #b8b8b8; }
                .ba-value { font-family: 'JetBrains Mono', monospace; font-size: 11.5px; color: var(--ba-accent); }

                /* --- Sliders --- */
                .ba-slider-container { margin-bottom: 14px; padding: 0 8px; }
                .ba-slider-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
                .ba-slider {
                    -webkit-appearance: none; width: 100%; height: 4px;
                    background: #2a2a2a; border-radius: 2px; outline: none;
                    cursor: pointer;
                }
                .ba-slider::-webkit-slider-thumb {
                    -webkit-appearance: none; width: 14px; height: 14px;
                    background: var(--ba-accent); border-radius: 50%; cursor: pointer;
                    box-shadow: 0 0 10px color-mix(in srgb, var(--ba-accent) 40%, transparent);
                    transition: transform 0.15s;
                }
                .ba-slider::-webkit-slider-thumb:hover { transform: scale(1.2); }

                /* --- Pill-style toggle (formerly checkbox) --- */
                .ba-checkbox {
                    position: relative;
                    width: 30px; height: 17px;
                    border: 1px solid #3a3a3a;
                    background: #1a1a1a;
                    border-radius: 10px;
                    cursor: pointer;
                    transition: background 0.18s, border-color 0.18s;
                    flex-shrink: 0;
                    display: block;
                }
                .ba-checkbox::before {
                    content: '';
                    position: absolute;
                    top: 2px; left: 2px;
                    width: 11px; height: 11px;
                    background: #888;
                    border-radius: 50%;
                    transition: left 0.18s, background 0.18s;
                    box-shadow: 0 1px 2px rgba(0,0,0,0.3);
                }
                .ba-checkbox.checked {
                    background: color-mix(in srgb, var(--ba-accent) 70%, #1a1a1a);
                    border-color: var(--ba-accent);
                }
                .ba-checkbox.checked::before {
                    left: 14px;
                    background: #fff;
                    box-shadow: 0 0 8px color-mix(in srgb, var(--ba-accent) 50%, transparent);
                }
                /* Override: button-style checkbox (e.g. account reset) keeps no thumb */
                .ba-checkbox.ba-action-btn {
                    width: auto; height: auto; padding: 6px 10px; border-radius: 5px;
                    font-size: 11px; font-weight: 600;
                    display: inline-flex; align-items: center; justify-content: center;
                }
                .ba-checkbox.ba-action-btn::before { display: none; }

                /* --- Status box (Main tab) --- */
                .ba-status-box {
                    background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
                    border-radius: 8px; padding: 14px; margin-bottom: 14px; text-align: center;
                }
                .ba-eval-large { font-family: 'JetBrains Mono'; font-size: 26px; font-weight: 700; color: #fff; margin-bottom: 6px; display: block; letter-spacing: 0.5px; }
                .ba-eval-bar {
                    position: relative; height: 6px; width: 100%;
                    background: linear-gradient(to right, #1a1a1a 0%, #555 50%, #fafafa 100%);
                    border-radius: 3px; margin: 8px 0 10px 0;
                    overflow: visible;
                    border: 1px solid rgba(255,255,255,0.06);
                }
                .ba-eval-bar-marker {
                    position: absolute; top: -3px; left: 50%;
                    width: 3px; height: 12px;
                    background: var(--ba-accent);
                    border-radius: 1px;
                    transform: translateX(-50%);
                    transition: left 0.4s ease-out;
                    box-shadow: 0 0 6px color-mix(in srgb, var(--ba-accent) 60%, transparent);
                }
                .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 10px; border-radius: 4px; display: inline-block; }
                .ba-engine-badge {
                    font-size: 9.5px; padding: 2px 7px; border-radius: 3px; margin-top: 8px; display: inline-block;
                    background: color-mix(in srgb, var(--ba-accent) 15%, transparent); color: var(--ba-accent); font-family: 'JetBrains Mono';
                }

                /* --- Stats rows --- */
                .ba-stats-row { display: flex; justify-content: space-between; margin-bottom: 6px; padding: 5px 10px; background: rgba(255,255,255,0.02); border-radius: 4px; }
                .ba-stats-label { font-size: 11px; color: #999; }
                .ba-stats-value { font-size: 11px; color: #ddd; font-family: 'JetBrains Mono'; }

                /* --- Selects --- */
                .ba-select {
                    background: #1a1a1a; color: #e0e0e0; border: 1px solid #3a3a3a;
                    border-radius: 4px; padding: 5px 9px; font-size: 11.5px;
                    font-family: 'Inter', sans-serif; cursor: pointer; outline: none;
                    transition: border-color 0.15s;
                }
                .ba-select:hover { border-color: #555; }
                .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; }

                /* --- Section titles + collapsible groups --- */
                .ba-section-title {
                    font-size: 10.5px; font-weight: 700; color: #888;
                    text-transform: uppercase; letter-spacing: 1.2px;
                    margin: 18px 0 8px 0; padding: 8px 6px 4px 6px;
                    border-top: 1px solid rgba(255,255,255,0.06);
                    display: flex; align-items: center; justify-content: space-between;
                }
                .ba-section-title:first-child { margin-top: 0; border-top: none; padding-top: 0; }
                .ba-section-title.ba-collapsible { cursor: pointer; user-select: none; }
                .ba-section-title.ba-collapsible:hover { color: #aaa; }
                .ba-section-title.ba-collapsible::after {
                    content: '▾';
                    font-size: 11px; opacity: 0.6;
                    transition: transform 0.18s;
                    transform: rotate(0deg);
                    margin-left: 8px;
                }
                .ba-section-group.ba-collapsed > .ba-section-title.ba-collapsible::after {
                    transform: rotate(-90deg);
                }
                .ba-section-group.ba-collapsed > *:not(.ba-section-title) { display: none; }
                .ba-section-count {
                    font-size: 9px; font-weight: 600; opacity: 0.7;
                    background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 8px;
                    margin-left: 6px; letter-spacing: 0.5px;
                }

                /* --- Footer --- */
                .ba-footer {
                    padding: 8px 14px; font-size: 10px; color: #555;
                    border-top: 1px solid rgba(255,255,255,0.05);
                    display: flex; gap: 10px; flex-wrap: wrap;
                    background: rgba(0,0,0,0.18);
                }
                .ba-key { color: #aaa; background: #1f1f1f; padding: 1px 5px; border-radius: 3px; font-family: 'JetBrains Mono', monospace; border: 1px solid #333; font-size: 9.5px; }
                .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-header-left">
                        <div class="ba-logo">REXXX<span>.MENU v16</span></div>
                        <div class="ba-feature-count" id="feature-count" title="Active humanization features">--/--</div>
                    </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>
                            <div class="ba-eval-bar" title="Position evaluation">
                                <div class="ba-eval-bar-marker" id="eval-bar-marker"></div>
                            </div>
                            <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" title="Coach Mode: GM-style analysis overlay. You make your own moves; coach guides you. Auto-play is suppressed.">
                            <span class="ba-label" style="color: #81c784;">♛ Coach Mode</span>
                            <div class="ba-checkbox ${CONFIG.coach.enabled ? 'checked' : ''}" id="toggle-coach"></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-group" data-group="adlayers">
                        <div class="ba-section-title ba-collapsible">Anti-Detection Layers <span class="ba-section-count">11</span></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><!-- /adlayers -->

                        <div class="ba-section-group ba-collapsed" data-group="account">
                        <div class="ba-section-title ba-collapsible">Account &amp; Behavior (v15.1) <span class="ba-section-count">10</span></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><!-- /account -->

                        <div class="ba-section-group" data-group="v16">
                        <div class="ba-section-title ba-collapsible">v16 — Behavioral Realism <span class="ba-section-count">13</span></div>
                        <div class="ba-row" title="Errors cluster in tactical/critical positions, not random quiet ones. Mimics human error patterns.">
                            <span class="ba-label">Critical-Position Bias</span>
                            <div class="ba-checkbox ${CONFIG.blunderBias.enabled ? 'checked' : ''}" id="toggle-blunderbias"></div>
                        </div>
                        <div class="ba-row" title="Sometimes pick a shorter-PV alternative when best move requires 8+ ply forcing calculation that humans wouldn't see.">
                            <span class="ba-label">Shallow Depth Picks</span>
                            <div class="ba-checkbox ${CONFIG.shallowDepth.enabled ? 'checked' : ''}" id="toggle-shallow"></div>
                        </div>
                        <div class="ba-row" title="Miss specific tactical patterns (zwischenzug, deflection, backwards knights) more often.">
                            <span class="ba-label">Motif Blindness</span>
                            <div class="ba-checkbox ${CONFIG.motifBlindness.enabled ? 'checked' : ''}" id="toggle-motif"></div>
                        </div>
                        <div class="ba-row" title="Increase error rate in technical endgames (K+P, R+P) where 1800 players often slip.">
                            <span class="ba-label">Endgame Slips</span>
                            <div class="ba-checkbox ${CONFIG.endgameTechnique.enabled ? 'checked' : ''}" id="toggle-endgame"></div>
                        </div>
                        <div class="ba-row" title="Pause significantly on the first move after leaving the opening book.">
                            <span class="ba-label">Book-Exit Pause</span>
                            <div class="ba-checkbox ${CONFIG.bookExitPause.enabled ? 'checked' : ''}" id="toggle-bookexit"></div>
                        </div>
                        <div class="ba-row" title="Time-bank curve: spend most time in critical middlegame (move 18-25).">
                            <span class="ba-label">Time-Bank Curve</span>
                            <div class="ba-checkbox ${CONFIG.timeBankCurve.enabled ? 'checked' : ''}" id="toggle-tbcurve"></div>
                        </div>
                        <div class="ba-row" title="Play forced moves (only good option) nearly instantly like a real player.">
                            <span class="ba-label">Forced-Move Speed</span>
                            <div class="ba-checkbox ${CONFIG.forcedMove.enabled ? 'checked' : ''}" id="toggle-forced"></div>
                        </div>
                        <div class="ba-row" title="Think longer when defending against attacks on our own king (defense > offense bias).">
                            <span class="ba-label">King-Safety Asymmetry</span>
                            <div class="ba-checkbox ${CONFIG.kingSafety.enabled ? 'checked' : ''}" id="toggle-ksafe"></div>
                        </div>
                        <div class="ba-row" title="Tiny mouse drifts and hovers during long thinks (no perfectly-still cursor).">
                            <span class="ba-label">Idle Mouse Behavior</span>
                            <div class="ba-checkbox ${CONFIG.idleMouse.enabled ? 'checked' : ''}" id="toggle-idlemouse"></div>
                        </div>
                        <div class="ba-row" title="Occasionally draw red circles or arrows on the board during long thinks.">
                            <span class="ba-label">Right-Click Annotations</span>
                            <div class="ba-checkbox ${CONFIG.annotations.enabled ? 'checked' : ''}" id="toggle-annot"></div>
                        </div>
                        <div class="ba-row" title="Variable post-game behavior: review the position, peek at opponent's profile, then re-queue.">
                            <span class="ba-label">Post-Game Behavior</span>
                            <div class="ba-checkbox ${CONFIG.postGame.enabled ? 'checked' : ''}" id="toggle-postgame"></div>
                        </div>
                        <div class="ba-row" title="Cache engine evaluations to reduce repeat API calls (avoids network fingerprint).">
                            <span class="ba-label">Engine Eval Cache</span>
                            <div class="ba-checkbox ${CONFIG.engineCache.enabled ? 'checked' : ''}" id="toggle-engcache"></div>
                        </div>
                        <div class="ba-row" title="Stealth mode: suppress all console logs (only errors shown). Use when concerned about console monitoring.">
                            <span class="ba-label">Stealth Mode (Silent Logs)</span>
                            <div class="ba-checkbox ${CONFIG.stealth.enabled ? 'checked' : ''}" id="toggle-stealth"></div>
                        </div>
                        </div><!-- /v16 -->

                        <div class="ba-section-title">Account Maintenance</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 ba-action-btn" id="btn-reset-account" style="background: rgba(255, 80, 80, 0.18); border-color: rgba(255, 80, 80, 0.55); color: #ff8080;">RESET</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" title="Rolling average centipawn loss vs target. Governor suppresses errors when we exceed cap.">
                            <span class="ba-stats-label">ACPL (rolling)</span>
                            <span class="ba-stats-value" id="stat-acpl">--- / ---</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-stats-row" title="Estimated current rating from rolling avg of recent opponents (chess.com pairs tightly).">
                            <span class="ba-stats-label">Est. My Rating</span>
                            <span class="ba-stats-value" id="stat-est-rating">---</span>
                        </div>
                        <div class="ba-stats-row" title="Climb / Maintain / Decline based on gap between estimated rating and target.">
                            <span class="ba-stats-label">Climb Mode</span>
                            <span class="ba-stats-value" id="stat-climb">---</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> Defaults</span>
                    <span style="margin-left:auto;opacity:0.55;">drag header to move</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');
            });

            // Collapsible section groups: clicking the title toggles .ba-collapsed on the group
            panel.querySelectorAll('.ba-section-title.ba-collapsible').forEach(title => {
                title.addEventListener('click', () => {
                    const group = title.closest('.ba-section-group');
                    if (group) group.classList.toggle('ba-collapsed');
                });
            });

            // Toggle helper. Wires click on the entire .ba-row (or the checkbox itself
            // if there's no row wrapper) to flip the boolean. The whole row is the
            // hit-target so users don't have to aim at the small pill.
            const toggle = (id, getter, setter) => {
                const el = panel.querySelector(`#${id}`);
                if (!el) return;
                const row = el.closest('.ba-row');
                if (row) row.classList.add('ba-toggleable');

                const handler = (e) => {
                    // Avoid double-fire when the click bubbles from the pill up to the row
                    if (handler._busy) return;
                    handler._busy = true;
                    const newVal = !getter();
                    setter(newVal);
                    el.classList.toggle('checked', newVal);
                    Settings.save(CONFIG);
                    UI.updateFeatureCount();
                    setTimeout(() => { handler._busy = false; }, 0);
                };
                if (row) row.addEventListener('click', handler);
                else el.addEventListener('click', handler);
            };

            toggle('toggle-auto', () => CONFIG.auto.enabled, (v) => CONFIG.auto.enabled = v);
            toggle('toggle-coach', () => CONFIG.coach.enabled, (v) => Coach.setEnabled(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);
            // v16 toggles
            toggle('toggle-blunderbias',   () => CONFIG.blunderBias.enabled,     (v) => CONFIG.blunderBias.enabled = v);
            toggle('toggle-shallow',       () => CONFIG.shallowDepth.enabled,    (v) => CONFIG.shallowDepth.enabled = v);
            toggle('toggle-motif',         () => CONFIG.motifBlindness.enabled,  (v) => CONFIG.motifBlindness.enabled = v);
            toggle('toggle-endgame',       () => CONFIG.endgameTechnique.enabled,(v) => CONFIG.endgameTechnique.enabled = v);
            toggle('toggle-bookexit',      () => CONFIG.bookExitPause.enabled,   (v) => CONFIG.bookExitPause.enabled = v);
            toggle('toggle-tbcurve',       () => CONFIG.timeBankCurve.enabled,   (v) => CONFIG.timeBankCurve.enabled = v);
            toggle('toggle-forced',        () => CONFIG.forcedMove.enabled,      (v) => CONFIG.forcedMove.enabled = v);
            toggle('toggle-ksafe',         () => CONFIG.kingSafety.enabled,      (v) => CONFIG.kingSafety.enabled = v);
            toggle('toggle-idlemouse',     () => CONFIG.idleMouse.enabled,       (v) => CONFIG.idleMouse.enabled = v);
            toggle('toggle-annot',         () => CONFIG.annotations.enabled,     (v) => CONFIG.annotations.enabled = v);
            toggle('toggle-postgame',      () => CONFIG.postGame.enabled,        (v) => CONFIG.postGame.enabled = v);
            toggle('toggle-engcache',      () => CONFIG.engineCache.enabled,     (v) => CONFIG.engineCache.enabled = v);
            toggle('toggle-stealth',       () => CONFIG.stealth.enabled,         (v) => CONFIG.stealth.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: [],
                        recentOpponentRatings: [],
                        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 || '...';
            }

            // Eval bar marker position
            UI.updateEvalBar(evalData);

            // 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);
            const coachToggle = State.ui.panel.querySelector('#toggle-coach');
            if (coachToggle) coachToggle.classList.toggle('checked', CONFIG.coach.enabled);
            UI.refreshCoachPanel();

            // Keep feature counter fresh
            UI.updateFeatureCount();
        },

        // --- Eval bar: map an eval to a 0-100% position on the gradient bar ---
        // Negative (black advantage) → left side, positive (white advantage) → right.
        // We clamp at ±5 pawns; mate scores pin to the relevant extreme.
        updateEvalBar: (evalData) => {
            const marker = State.ui.panel?.querySelector('#eval-bar-marker');
            if (!marker) return;
            if (!evalData) {
                marker.style.left = '50%';
                return;
            }
            let pct;
            if (evalData.type === 'mate') {
                pct = evalData.value > 0 ? 98 : 2;
            } else {
                const clamped = Math.max(-5, Math.min(5, evalData.value));
                pct = 50 + (clamped / 5) * 48; // ±48% from center
            }
            marker.style.left = `${pct}%`;
        },

        // --- Feature counter in header ---
        // Counts how many of the known humanization / anti-detection switches
        // are currently enabled. Gives the user an at-a-glance "how stealthy am I" indicator.
        _featureKeys: [
            'humanization.enabled',
            'humanization.streaks.enabled',
            'humanization.accuracyClustering.enabled',
            'humanization.weaknessProfile.enabled',
            'humanization.playerMoveDB.enabled',
            'humanization.timingAccuracyCoupling.enabled',
            'humanization.antiCorrelation.enabled',
            'antiDetection.changeOfMind.enabled',
            'antiDetection.telemetryNoise.enabled',
            'antiDetection.randomAFK.enabled',
            'timePressure.enabled',
            'opponentAdaptation.enabled',
            // v15.1
            'warmup.enabled', 'winrateTarget.enabled', 'repertoireHard.enabled',
            'tilt.enabled', 'premoveGating.enabled', 'tcLock.enabled',
            'engineRotation.enabled', 'messyResign.enabled', 'hardwarePersona.enabled',
            // v16
            'blunderBias.enabled', 'shallowDepth.enabled', 'bookExitPause.enabled',
            'motifBlindness.enabled', 'endgameTechnique.enabled', 'timeBankCurve.enabled',
            'idleMouse.enabled', 'annotations.enabled', 'engineCache.enabled',
            'forcedMove.enabled', 'kingSafety.enabled', 'postGame.enabled',
            'stealth.enabled',
        ],

        updateFeatureCount: () => {
            const el = State.ui.panel?.querySelector('#feature-count');
            if (!el) return;
            const keys = UI._featureKeys;
            let active = 0;
            for (const path of keys) {
                let v = CONFIG;
                for (const part of path.split('.')) {
                    if (v == null) break;
                    v = v[part];
                }
                if (v === true) active++;
            }
            el.textContent = `${active}/${keys.length}`;
            // Color tint based on coverage
            const ratio = active / keys.length;
            if (ratio < 0.5) {
                el.style.color = '#ff8a65';
                el.style.borderColor = 'rgba(255,138,101,0.5)';
                el.style.background = 'rgba(255,138,101,0.12)';
            } else if (ratio < 0.8) {
                el.style.color = '#ffd54f';
                el.style.borderColor = 'rgba(255,213,79,0.5)';
                el.style.background = 'rgba(255,213,79,0.10)';
            } else {
                el.style.color = '';   // back to accent default
                el.style.borderColor = '';
                el.style.background = '';
            }
        },

        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);
            // ACPL governor status
            const acplEl = panel.querySelector('#stat-acpl');
            if (acplEl) {
                const win = h.acplWindow || [];
                const target = CONFIG.acplGovernor?.targetACPL || 0;
                if (win.length < (CONFIG.acplGovernor?.minMoves || 6)) {
                    acplEl.textContent = `--- / ${target}`;
                    acplEl.style.color = '';
                } else {
                    const cur = HumanStrategy.currentACPL();
                    const suppressTag = h.acplSuppressedCount > 0 ? ` (${h.acplSuppressedCount} suppr.)` : '';
                    acplEl.textContent = `${cur.toFixed(0)} / ${target}${suppressTag}`;
                    const budget = HumanStrategy.acplBudgetState();
                    if (budget === 'over')   acplEl.style.color = '#ff8a65';
                    else if (budget === 'under') acplEl.style.color = '#81d4fa';
                    else acplEl.style.color = '#4caf50';
                }
            }
            // 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}` : '---');
            // Estimated rating + climb mode (v16.2)
            const est = Account.estimatedRating();
            const gap = Account.ratingGap();
            const mode = Account.climbMode();
            const estEl = panel.querySelector('#stat-est-rating');
            if (estEl) {
                if (est == null) {
                    estEl.textContent = '--- (need 3+ games)';
                    estEl.style.color = '';
                } else {
                    const sign = gap > 0 ? '+' : '';
                    estEl.textContent = `${est} (gap ${sign}${gap})`;
                    estEl.style.color = '';
                }
            }
            const climbEl = panel.querySelector('#stat-climb');
            if (climbEl) {
                climbEl.textContent = mode;
                if (mode === 'climb')        climbEl.style.color = '#81d4fa';
                else if (mode === 'decline') climbEl.style.color = '#ff8a65';
                else if (mode === 'maintain') climbEl.style.color = '#4caf50';
                else climbEl.style.color = '';
            }
            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());
        },

        // ═════════════════════════════════════════
        //  COACH PANEL (v16.3) — floating overlay with GM analysis
        // ═════════════════════════════════════════
        _coachPanel: null,
        _coachPanelHiddenByHotkey: false,

        createCoachPanel: () => {
            if (UI._coachPanel) return UI._coachPanel;
            const panel = document.createElement('div');
            panel.className = 'ba-coach-panel';
            panel.style.cssText = `
                position: fixed;
                left: ${CONFIG.coach.panelX || 24}px;
                top: ${CONFIG.coach.panelY || 120}px;
                width: 320px;
                max-height: 70vh;
                overflow-y: auto;
                background: rgba(20, 22, 28, 0.96);
                color: #ddd;
                border: 1px solid rgba(76, 175, 80, 0.45);
                border-left: 3px solid #4caf50;
                border-radius: 8px;
                box-shadow: 0 8px 24px rgba(0,0,0,0.5);
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                font-size: 12px;
                z-index: 999998;
                user-select: none;
            `;
            panel.innerHTML = `
                <div class="ba-coach-header" style="
                    padding: 8px 12px;
                    background: linear-gradient(180deg, rgba(76,175,80,0.15) 0%, rgba(76,175,80,0.05) 100%);
                    border-bottom: 1px solid rgba(76,175,80,0.25);
                    cursor: move;
                    display: flex; justify-content: space-between; align-items: center;
                    border-radius: 8px 8px 0 0;
                ">
                    <span style="font-weight: 600; color: #81c784; letter-spacing: 0.5px; font-size: 11px;">
                        ♛ COACH MODE
                    </span>
                    <span style="font-size: 10px; color: #888;">drag · Ctrl+Shift+C to toggle</span>
                </div>
                <div id="ba-coach-body" style="padding: 12px;">
                    <div style="color: #888; font-style: italic;">Waiting for next analysis…</div>
                </div>
            `;
            document.body.appendChild(panel);
            UI._coachPanel = panel;

            // Make draggable by header
            const header = panel.querySelector('.ba-coach-header');
            let dragging = false, startX = 0, startY = 0, origX = 0, origY = 0;
            header.addEventListener('mousedown', (e) => {
                dragging = true;
                startX = e.clientX; startY = e.clientY;
                const rect = panel.getBoundingClientRect();
                origX = rect.left; origY = rect.top;
                e.preventDefault();
            });
            document.addEventListener('mousemove', (e) => {
                if (!dragging) return;
                const x = origX + (e.clientX - startX);
                const y = origY + (e.clientY - startY);
                panel.style.left = Math.max(0, Math.min(window.innerWidth - 100, x)) + 'px';
                panel.style.top  = Math.max(0, Math.min(window.innerHeight - 50, y)) + 'px';
            });
            document.addEventListener('mouseup', () => {
                if (!dragging) return;
                dragging = false;
                CONFIG.coach.panelX = parseInt(panel.style.left, 10);
                CONFIG.coach.panelY = parseInt(panel.style.top, 10);
                Settings.save(CONFIG);
            });
            return panel;
        },

        refreshCoachPanel: () => {
            const c = CONFIG.coach;
            if (!c.enabled) {
                if (UI._coachPanel) UI._coachPanel.style.display = 'none';
                return;
            }
            UI.createCoachPanel();
            if (c.onlyOnHotkey && UI._coachPanelHiddenByHotkey) {
                UI._coachPanel.style.display = 'none';
            } else {
                UI._coachPanel.style.display = 'block';
            }
        },

        updateCoachPanel: (analysis) => {
            if (!CONFIG.coach.enabled) return;
            UI.createCoachPanel();
            const body = UI._coachPanel.querySelector('#ba-coach-body');
            if (!body) return;
            if (!analysis || !analysis.bestMove) {
                body.innerHTML = `<div style="color: #888; font-style: italic;">Engine analyzing…</div>`;
                return;
            }
            const evalText = (e) => {
                if (!e) return '';
                if (e.type === 'mate') return `M${Math.abs(e.value)}${e.value > 0 ? '' : '−'}`;
                return `${e.value >= 0 ? '+' : ''}${e.value.toFixed(2)}`;
            };

            let html = '';

            // Opp last move grade banner
            if (analysis.oppLastGrade && analysis.oppLastGrade.grade !== 'best') {
                const g = analysis.oppLastGrade;
                html += `
                    <div style="
                        padding: 6px 10px;
                        background: ${g.color}22;
                        border-left: 3px solid ${g.color};
                        border-radius: 4px;
                        margin-bottom: 10px;
                        font-size: 11px;
                    ">
                        <span style="color: ${g.color}; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px;">
                            Their last: ${g.label}
                        </span>
                        <span style="color: #aaa;"> (swing ${g.evalSwing >= 0 ? '+' : ''}${g.evalSwing.toFixed(2)})</span>
                    </div>
                `;
            }

            // Best move section
            html += `
                <div style="margin-bottom: 10px;">
                    <div style="font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Best move</div>
                    <div style="font-size: 16px; font-weight: 600; color: #4caf50; font-family: 'JetBrains Mono', monospace;">
                        ${analysis.bestMove}
                        <span style="font-size: 12px; color: #888; margin-left: 6px;">${evalText(analysis.bestEval)}</span>
                    </div>
                </div>
            `;

            // Alternatives
            if (analysis.alternatives && analysis.alternatives.length > 0) {
                html += `<div style="margin-bottom: 10px;">
                    <div style="font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Close alternatives</div>`;
                for (const alt of analysis.alternatives) {
                    html += `<div style="font-size: 12px; color: #ccc; font-family: 'JetBrains Mono', monospace; padding: 2px 0;">
                        ${alt.move} <span style="color: #888;">${evalText(alt.eval)} (${alt.gap >= 0 ? '−' : '+'}${Math.abs(alt.gap).toFixed(2)})</span>
                    </div>`;
                }
                html += `</div>`;
            }

            // Threat
            if (analysis.threat) {
                html += `
                    <div style="margin-bottom: 10px;">
                        <div style="font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">If you do nothing, opp plays</div>
                        <div style="font-size: 13px; font-weight: 500; color: #ff8a65; font-family: 'JetBrains Mono', monospace;">
                            ${analysis.threat}
                        </div>
                    </div>
                `;
            }

            // Hanging pieces
            const hangingHtml = [];
            if (analysis.hanging?.ours?.length) {
                const list = analysis.hanging.ours.map(h => `${h.piece} on ${h.sq}`).join(', ');
                hangingHtml.push(`<span style="color: #ff8a65;">⚠ Yours: ${list}</span>`);
            }
            if (analysis.hanging?.theirs?.length) {
                const list = analysis.hanging.theirs.map(h => `${h.piece} on ${h.sq}`).join(', ');
                hangingHtml.push(`<span style="color: #81c784;">★ Theirs: ${list}</span>`);
            }
            if (hangingHtml.length) {
                html += `<div style="margin-bottom: 10px;">
                    <div style="font-size: 10px; color: #888; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px;">Loose pieces</div>
                    <div style="font-size: 11px; line-height: 1.5;">
                        ${hangingHtml.join('<br>')}
                    </div>
                </div>`;
            }

            // Annotations (natural-language commentary)
            if (analysis.annotations && analysis.annotations.length > 0) {
                html += `<div style="
                    border-top: 1px solid rgba(255,255,255,0.08);
                    padding-top: 10px;
                    font-size: 12px;
                    line-height: 1.5;
                    color: #cfd8dc;
                ">`;
                for (const note of analysis.annotations) {
                    html += `<div style="margin-bottom: 6px;">${note}</div>`;
                }
                html += `</div>`;
            }

            body.innerHTML = html;
        },

        _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) => {
            // (B13) Engine cache: skip network call if we've seen this position recently
            if (CONFIG.engineCache?.enabled && fen) {
                const ec = CONFIG.engineCache;
                const cacheKey = ec.normalizeMoveCounters
                    ? fen.split(' ').slice(0, 4).join(' ')
                    : fen;
                const cached = State.engineCache.get(cacheKey);
                if (cached && (Date.now() - cached.ts) < ec.ttlMs) {
                    Utils.log(`EngineCache: hit for FEN (${State.engineCache.size} entries)`, 'debug');
                    return cached.result;
                }
            }

            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;
                }
            };

            const storeResult = (result) => {
                if (!CONFIG.engineCache?.enabled || !result || !fen) return result;
                const ec = CONFIG.engineCache;
                const cacheKey = ec.normalizeMoveCounters
                    ? fen.split(' ').slice(0, 4).join(' ')
                    : fen;
                // LRU eviction: if over cap, delete oldest entry
                if (State.engineCache.size >= ec.maxEntries) {
                    const firstKey = State.engineCache.keys().next().value;
                    State.engineCache.delete(firstKey);
                }
                State.engineCache.set(cacheKey, { result, ts: Date.now() });
                return result;
            };

            // 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 storeResult(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;

                // Capture full PV (principal variation) for shallow-depth detection (T2)
                let pvArr = [];
                if (msg.includes(' pv ')) {
                    pvArr = msg.split(' pv ')[1].split(' ').filter(m => /^[a-h][1-8][a-h][1-8][qrbn]?$/.test(m));
                }

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

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

                if (mpv === 1 && pvArr.length > 1) {
                    State.opponentResponse = pvArr[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();

            // --- Track book-exit transitions (A6) ---
            if (isBook) {
                State.human.bookMovesPlayed++;
            } else if (State.human.bookMovesPlayed > 0 && !State.human.justLeftBook) {
                // Just left the book — flag for next calculateDelay call
                State.human.justLeftBook = true;
            }

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

            // v16.3 Coach Mode: produce GM-level commentary and update the coach overlay
            if (CONFIG.coach.enabled) {
                try {
                    const analysis = Coach.analyze(fen, State.candidates, State.currentEval);
                    UI.updateCoachPanel(analysis);
                    // Snapshot eval so next analysis can grade opponent's response
                    Coach.snapshotForOppGrading(fen);
                } catch (e) {
                    Utils.log(`Coach: analyze failed: ${e.message}`, 'warn');
                }
            }

            // 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 (suppressed when Coach Mode is active)
            if (CONFIG.auto.enabled && !CONFIG.coach.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));
        },

        // (A8) Detects technical endgames where humans lose technique.
        // Returns a multiplier on error rate (1.0 = no change, >1.0 = more errors).
        getEndgameTechniqueMult: (fen) => {
            if (!fen) return 1.0;
            const cfg = CONFIG.endgameTechnique;
            if (!cfg?.enabled) return 1.0;
            const phase = HumanStrategy.getGamePhase(fen);
            if (phase !== 'endgame') return 1.0;

            const board = fen.split(' ')[0];
            const knights = (board.match(/[nN]/g) || []).length;
            const bishops = (board.match(/[bB]/g) || []).length;
            const rooks = (board.match(/[rR]/g) || []).length;
            const queens = (board.match(/[qQ]/g) || []).length;
            const pawns = (board.match(/[pP]/g) || []).length;
            const pieces = knights + bishops + rooks + queens;

            // K+P only (no minor/major pieces left)
            if (pieces === 0 && pawns > 0) return cfg.kpMistakeMult;
            // R+P (only rooks and pawns)
            if (queens === 0 && knights === 0 && bishops === 0 && rooks > 0 && rooks <= 2) return cfg.rpMistakeMult;
            // Simple endgames: 1-2 minor pieces + pawns, no queens
            if (queens === 0 && pieces <= 2) return cfg.simpleMistakeMult;
            return 1.0;
        },

        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
            // v16 resets:
            State.human.bookMovesPlayed = 0;
            State.human.justLeftBook = false;
            State.human.prevOurEval = null;
            State.human.shallowPickCount = 0;
            State.human.lastMouseX = null;
            State.human.lastMouseY = null;
            // v16.1 ACPL governor reset
            State.human.acplWindow = [];
            State.human.acplSum = 0;
            State.human.acplSuppressedCount = 0;
            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);
            }

            // Always read + record opponent rating for the estimated-rating tracker.
            // (This is critical for the climb logic — works even when
            // opponentAdaptation override is disabled.)
            const oppRating = Game.readOpponentRating();
            if (oppRating) {
                State.human.opponentRating = oppRating;
                Account.recordOpponentRating(oppRating);
            }

            // v16.2 FIX: opponentAdaptation no longer overrides the user's
            // targetRating. Previously it set effectiveTarget = oppRating + 400
            // unconditionally, completely ignoring the user's chosen rating
            // and trapping the account at chess.com's pairing bracket.
            //
            // New behavior: use the user's target as the floor. Only adapt UP
            // if the opponent is meaningfully STRONGER than the user's target,
            // to avoid getting crushed by an unexpectedly strong pair.
            if (CONFIG.opponentAdaptation.enabled && !inWarmup && oppRating) {
                const userTarget = CONFIG.targetRating;
                // Adapt only when opp is materially stronger than us.
                if (oppRating > userTarget + 100) {
                    // Play stronger to compete, but only +100 edge (was +400) so we
                    // don't suddenly play 400 ELO above ourselves and look like a smurf.
                    const adapted = Math.min(
                        CONFIG.opponentAdaptation.maxRating,
                        oppRating + 100
                    );
                    if (adapted > effectiveTarget) {
                        effectiveTarget = adapted;
                        Utils.log(`Opponent Adaptation: Strong opp ${oppRating} > target ${userTarget}, bumping play to ${effectiveTarget}`, 'info');
                    }
                } else {
                    // Opponent is weaker or near our target — play at user's chosen
                    // rating. This is what produces the climb when we're below target.
                    Utils.log(`Opponent Adaptation: Opp ${oppRating} ≤ target ${userTarget}, playing at user target`, '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;
                const climbMode = Account.climbMode();
                const ratingGap = Account.ratingGap();

                let loseChance = 0;

                // v16.2 climb-aware logic: when we're well below the user's target
                // rating, suppress auto-lose so we can actually climb to target.
                // Only kick in for absurd win streaks that would still look botty.
                if (climbMode === 'climb') {
                    // Below target — let us win freely until we approach target
                    if (streak >= trigger + 4) loseChance = 0.50; // very long streak
                    else if (streak >= trigger + 2) loseChance = 0.20;
                    // No winrate-overshoot suppression in climb mode
                    Utils.log(`ClimbMode: ratingGap=${ratingGap} (need to climb), auto-lose suppressed`, 'info');
                } else if (climbMode === 'decline') {
                    // Above target — pull us back, more aggressive auto-lose
                    if (streak >= trigger - 1)    loseChance = 0.85;
                    else if (streak === trigger - 2) loseChance = 0.55;
                    else if (streak === trigger - 3) loseChance = 0.25;
                    Utils.log(`ClimbMode: ratingGap=${ratingGap} (above target), auto-lose enhanced`, 'info');
                } else {
                    // 'maintain' or 'unknown' — standard ramp
                    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) — skip in climb mode
                const wr = CONFIG.session.gamesPlayed > 4
                    ? CONFIG.session.wins / CONFIG.session.gamesPlayed
                    : 0;
                if (wr >= 0.85 && climbMode !== 'climb') {
                    loseChance = Math.max(loseChance, 0.35);
                }

                // Lifetime winrate-overshoot bump — only active when at/above target.
                // This is the OTHER part of the climb fix: previously this forced us
                // to lose to maintain 58% WR even when rating was 300 below target.
                if (climbMode !== 'climb') {
                    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} mode=${climbMode} sessWR=${wr.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: mode=${climbMode} 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);
            }
        },

        // ============================================================
        // ACPL GOVERNOR (v16.1)
        //
        // Records every move's centipawn loss vs the engine's top choice, in a
        // rolling window. Returns a state that callers (shouldPlaySuboptimal,
        // shouldBlunder) use to either suppress further error injection or
        // (when we've been playing too perfectly) allow it more freely.
        //
        // This is what prevents the 7+ humanization layers from compounding past
        // the empirical ACPL for the target rating.
        // ============================================================
        recordMoveCPLoss: (cpLoss) => {
            const g = CONFIG.acplGovernor;
            if (!g?.enabled) return;
            // Sanity-clamp: ignore wildly negative or absurd values (mate-score noise).
            if (!Number.isFinite(cpLoss) || cpLoss < 0) cpLoss = 0;
            if (cpLoss > 500) cpLoss = 500; // single move's contribution is capped
            const win = State.human.acplWindow;
            win.push(cpLoss);
            State.human.acplSum += cpLoss;
            // Trim window to configured size
            while (win.length > g.windowSize) {
                State.human.acplSum -= win.shift();
            }
        },

        currentACPL: () => {
            const win = State.human.acplWindow;
            if (!win || win.length === 0) return 0;
            return State.human.acplSum / win.length;
        },

        // Returns: 'over' (suppress further errors), 'under' (allow extra error
        // injection — we've been too clean), or 'normal' (let layers decide).
        // 'over' is the critical case: it's what stops rating drift.
        acplBudgetState: () => {
            const g = CONFIG.acplGovernor;
            if (!g?.enabled) return 'normal';
            const win = State.human.acplWindow;
            if (!win || win.length < g.minMoves) return 'normal';
            const acpl = HumanStrategy.currentACPL();
            const target = g.targetACPL;
            if (acpl >= target * g.hardCapMult) return 'over';
            if (acpl <= target * g.softFloorMult) return 'under';
            return 'normal';
        },

        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;
            }

            // Critical-position blunder bias (T1): humans err in complex positions,
            // not random quiet ones. Simple = reduce, critical = boost.
            if (CONFIG.blunderBias?.enabled) {
                const bb = CONFIG.blunderBias;
                const complexity = HumanStrategy.getPositionComplexity(fen);
                if (complexity <= bb.simpleCutoff) {
                    rate *= bb.simpleErrorMult;
                } else if (complexity >= bb.criticalCutoff) {
                    rate *= bb.criticalErrorMult;
                } else {
                    // Linear interpolation in the middle band
                    const span = bb.criticalCutoff - bb.simpleCutoff;
                    const t = (complexity - bb.simpleCutoff) / span;
                    const mult = bb.simpleErrorMult + t * (bb.criticalErrorMult - bb.simpleErrorMult);
                    rate *= mult;
                }
            }

            // Endgame technique reduction (A8): inflate errors in technical endgames
            if (CONFIG.endgameTechnique?.enabled) {
                const techMult = HumanStrategy.getEndgameTechniqueMult(fen);
                if (techMult > 1) rate *= techMult;
            }

            // 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
            }

            // v16.1 stacking cap: multiplicative layers (critical-bias × endgame ×
            // cluster) could previously compound to 4-5× the base rate in a worst
            // case (tactical position in K+P endgame during a cold streak). Cap
            // the combined multiplicative effect at 2.2× the phase's base rate.
            const phaseBase = h.suboptimalMoveRate[phase] || 0.25;
            const multCap = phaseBase * 2.2;
            if (rate > multCap) rate = multCap;

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

            // --- ACPL governor (v16.1) ---
            // Hardest gate: if we've already exceeded the target ACPL for our rating,
            // suppress this error attempt entirely. This is the fix that stops the
            // rating from drifting hundreds of Elo below target.
            const budget = HumanStrategy.acplBudgetState();
            if (budget === 'over') {
                State.human.acplSuppressedCount++;
                Utils.log(`ACPL governor: rolling ${HumanStrategy.currentACPL().toFixed(0)} ≥ cap ${(CONFIG.acplGovernor.targetACPL * CONFIG.acplGovernor.hardCapMult).toFixed(0)} — suppressing suboptimal`, 'debug');
                return false;
            }
            if (budget === 'under') {
                // v16.2: don't boost errors in climb mode. We need clean play to
                // gain ELO when below target rating. Other layers (cluster cold
                // streak, anti-correlation, motif blindness) still provide variety.
                if (Account.climbMode() !== 'climb') {
                    // Normal/decline: prod the rate up a touch so v15/v16 layers fire.
                    rate += 0.10;
                }
            }

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

        // (T2) Pick a move at human-like calculation depth.
        // Returns an alternative if the best move requires deep forcing calculation
        // (long PV) that a 1800-rated human likely wouldn't see. Otherwise null.
        pickShallowDepthMove: (fen) => {
            const cfg = CONFIG.shallowDepth;
            if (!cfg?.enabled) return null;
            const candidates = State.candidates;
            const best = candidates[1];
            if (!best?.pv || best.pv.length < cfg.longPVThreshold) return null;
            // Best move requires deep calculation. Check if there's an alternative
            // with a SHORTER PV that's not too much worse.
            const bestEval = best.eval;
            if (!bestEval || bestEval.type !== 'cp') return null;
            for (let i = 2; i <= CONFIG.multiPV; i++) {
                const alt = candidates[i];
                if (!alt?.pv || !alt?.eval || alt.eval.type !== 'cp') continue;
                if (alt.pv.length >= cfg.longPVThreshold) continue;
                const cpLoss = (bestEval.value - alt.eval.value) * 100;
                if (cpLoss < 0 || cpLoss > 100) continue; // alt should be reasonable
                // Found a shorter-PV alternative within 100cp — humans would prefer this
                if (Math.random() < cfg.preferLongPVChance) {
                    State.human.shallowPickCount++;
                    Utils.log(`ShallowDepth: best PV is ${best.pv.length}-ply (too deep), playing ${alt.move} (PV ${alt.pv.length}-ply, -${cpLoss.toFixed(0)}cp)`, 'debug');
                    return alt.move;
                }
            }
            return null;
        },

        // (A7) Detects tactical motifs in a move that humans miss disproportionately
        // often. Returns a "miss chance" 0-1 where higher = more likely to miss.
        detectMotifBlindness: (move, fen) => {
            const cfg = CONFIG.motifBlindness;
            if (!cfg?.enabled || !move || move.length < 4 || !fen) return 0;
            try {
                const board = HumanStrategy._parseFenBoard(fen);
                const fromSq = move.substring(0, 2);
                const toSq = move.substring(2, 4);
                const [fromR, fromF] = HumanStrategy._sqToIdx(fromSq);
                const [toR, toF] = HumanStrategy._sqToIdx(toSq);
                const piece = board[fromR]?.[fromF];
                if (!piece) return 0;
                const pieceLower = piece.toLowerCase();
                const isWhite = piece === piece.toUpperCase();

                let missChance = 0;

                // Backwards knight move: knight moves toward our own back rank
                if (pieceLower === 'n') {
                    const backwards = isWhite ? (toR > fromR) : (toR < fromR);
                    if (backwards) missChance = Math.max(missChance, cfg.backwardsKnightMissChance);
                }

                // Long diagonal bishop move: 5+ squares
                if (pieceLower === 'b') {
                    const distance = Math.max(Math.abs(toR - fromR), Math.abs(toF - fromF));
                    if (distance >= 5) missChance = Math.max(missChance, cfg.longDiagonalMissChance);
                }

                // Zwischenzug detection: best PV has a quiet (non-capture) move at index 2+
                // wedged inside a forcing sequence (captures at index 1 and 3+)
                const pv = State.candidates[1]?.pv;
                if (pv && pv.length >= 4) {
                    const isCapture = (mv) => {
                        if (!mv || mv.length < 4) return false;
                        const [tR, tF] = HumanStrategy._sqToIdx(mv.substring(2, 4));
                        return board[tR]?.[tF] != null;
                    };
                    // If our best move is forcing but PV[2] is quiet between captures
                    if (isCapture(pv[0]) && pv[2] && !isCapture(pv[2]) && isCapture(pv[3] || '')) {
                        missChance = Math.max(missChance, cfg.zwischenzugMissChance);
                    }
                }

                // Deflection: piece sac (we move a higher-value piece to a square
                // attacked by a lower-value piece, but it's still best)
                const captured = board[toR]?.[toF];
                if (captured) {
                    const ourValue = HumanStrategy._pieceValues[pieceLower] || 0;
                    const theirValue = HumanStrategy._pieceValues[captured.toLowerCase()] || 0;
                    if (ourValue > theirValue + 200) {
                        // We're sacrificing — likely a deflection
                        missChance = Math.max(missChance, cfg.deflectionMissChance);
                    }
                }

                return missChance;
            } catch (e) {
                return 0;
            }
        },

        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;
            }

            // (T1) Critical-position blunder bias — concentrate blunders here
            if (CONFIG.blunderBias?.enabled) {
                const complexity = HumanStrategy.getPositionComplexity(fen);
                if (complexity >= CONFIG.blunderBias.criticalCutoff) {
                    chance *= CONFIG.blunderBias.criticalErrorMult;
                } else if (complexity <= CONFIG.blunderBias.simpleCutoff) {
                    chance *= CONFIG.blunderBias.simpleErrorMult;
                }
            }

            // --- ACPL governor (v16.1) ---
            // Blunders contribute heavily to ACPL. If we're already over budget,
            // never inject a fresh one.
            const budget = HumanStrategy.acplBudgetState();
            if (budget === 'over') {
                State.human.acplSuppressedCount++;
                Utils.log(`ACPL governor: rolling ${HumanStrategy.currentACPL().toFixed(0)} over cap — suppressing blunder`, 'debug');
                return false;
            }

            // v16.1 stacking cap: prevent runaway multipliers when many v16
            // multiplier-based features compound. A single blunder roll should
            // never exceed 4× the base chance no matter how many layers fire.
            const baseChance = b.chance;
            chance = Math.min(chance, baseChance * 4);

            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);

            // --- (T2) CALCULATION DEPTH LIMITING ---
            // Sometimes the best move requires deep forcing calculation that humans
            // wouldn't see at 1800. Pick a shorter-PV alternative.
            if (CONFIG.shallowDepth?.enabled && !isLosing && State.moveCount > 4) {
                if (Math.random() < CONFIG.shallowDepth.chance) {
                    const shallowMove = HumanStrategy.pickShallowDepthMove(fen);
                    if (shallowMove && HumanStrategy.isMoveSafe(shallowMove, fen)) {
                        return { move: shallowMove, reason: 'shallow-depth', isBest: false };
                    }
                }
            }

            // --- (A7) TACTICAL MOTIF BLINDNESS ---
            // Specific patterns humans miss often (zwischenzug, deflection, backwards knight)
            if (CONFIG.motifBlindness?.enabled && !isLosing && State.moveCount > 5) {
                const missChance = HumanStrategy.detectMotifBlindness(bestMove, fen);
                if (missChance > 0 && Math.random() < missChance) {
                    const altMove = HumanStrategy.pickSuboptimalMove(fen);
                    if (altMove && HumanStrategy.isMoveSafe(altMove, fen)) {
                        Utils.log(`MotifBlind: missed pattern (${(missChance*100).toFixed(0)}% miss), playing ${altMove}`, 'debug');
                        return { move: altMove, reason: 'motif-blind', isBest: false };
                    }
                }
            }

            // --- 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;
            // Store our eval for king-safety detection (B16)
            State.human.prevOurEval = State.currentEval?.value ?? null;

            // --- ACPL governor (v16.1): record this move's cp-loss ---
            // Best move → 0 cp loss. Otherwise look up the chosen move in
            // State.candidates and diff against candidates[1] (the engine's pick).
            try {
                let cpLoss = 0;
                if (!isBest && moveResult?.move) {
                    const bestEval = State.candidates?.[1]?.eval;
                    if (bestEval) {
                        // Find chosen move in candidates
                        let chosen = null;
                        for (let i = 1; i <= CONFIG.multiPV; i++) {
                            const c = State.candidates?.[i];
                            if (c?.move === moveResult.move) { chosen = c; break; }
                        }
                        if (chosen?.eval) {
                            if (bestEval.type === 'cp' && chosen.eval.type === 'cp') {
                                cpLoss = (bestEval.value - chosen.eval.value) * 100;
                            } else if (bestEval.type === 'mate' && chosen.eval.type === 'cp') {
                                cpLoss = 300; // missed a forced mate
                            } else if (bestEval.type === 'mate' && chosen.eval.type === 'mate') {
                                cpLoss = Math.max(0, (Math.abs(bestEval.value) - Math.abs(chosen.eval.value)) * 50);
                            } else if (chosen.eval.type === 'mate' && chosen.eval.value < 0) {
                                cpLoss = 500; // walked into being mated
                            }
                        } else {
                            // Chosen move wasn't in candidates (random-legal / motif-blind / book deviation).
                            // Use a conservative estimate: assume it's a typical sub-move for our rating.
                            cpLoss = CONFIG.humanization?.maxAcceptableCPLoss?.middlegame || 40;
                        }
                    }
                }
                HumanStrategy.recordMoveCPLoss(cpLoss);
            } catch (e) {
                // Never let ACPL bookkeeping break move tracking.
            }
        },

        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 0A: Forced-move recognition (B14)
            // When there's effectively only one good move (huge gap to next-best),
            // humans play it nearly instantly. Bots don't, which is detectable.
            // ============================================================
            if (CONFIG.forcedMove?.enabled && !isBook && moveResult.isBest && State.moveCount > 3) {
                const fm = CONFIG.forcedMove;
                const cands = State.candidates;
                const best = cands[1];
                if (best?.eval && best.eval.type === 'cp') {
                    let goodAlternatives = 0;
                    for (let i = 2; i <= CONFIG.multiPV; i++) {
                        const c = cands[i];
                        if (!c?.eval || c.eval.type !== 'cp') continue;
                        const cpGap = (best.eval.value - c.eval.value) * 100;
                        if (cpGap < fm.gapCp) goodAlternatives++;
                    }
                    if (goodAlternatives <= fm.maxAlternatives) {
                        const fastDelay = Utils.humanDelay(fm.instantMs.min, fm.instantMs.max);
                        Utils.log(`Timing: Forced move (only ${goodAlternatives + 1} good move${goodAlternatives ? 's' : ''} within ${fm.gapCp}cp) → ${Math.round(fastDelay)}ms`, 'debug');
                        State.recentTimings.push(fastDelay);
                        if (State.recentTimings.length > 20) State.recentTimings.shift();
                        return fastDelay;
                    }
                }
            }

            // ============================================================
            // 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 4A: Out-of-Book Confusion Pause (A6)
            // First move after leaving prep — humans pause significantly because
            // they're transitioning from memory to actual calculation.
            // ============================================================
            if (!isBook && CONFIG.bookExitPause?.enabled && State.human.justLeftBook) {
                if (State.human.bookMovesPlayed >= CONFIG.bookExitPause.minBookMovesBefore) {
                    const mult = CONFIG.bookExitPause.multiplier;
                    min *= mult;
                    max *= mult;
                    Utils.log(`Timing: Out-of-book confusion pause (${mult}x)`, 'debug');
                }
                State.human.justLeftBook = false; // consume the flag
            }

            // ============================================================
            // LAYER 4B: Time Bank Curve (A9)
            // Real time spending is a bell curve peaking at the critical
            // middlegame, lower in opening and endgame.
            // ============================================================
            if (!isBook && CONFIG.timeBankCurve?.enabled && State.moveCount > 3) {
                const tbc = CONFIG.timeBankCurve;
                const dist = Math.abs(State.moveCount - tbc.peakMove);
                // Bell shape: 1 at peak, falls to 1.0 at falloffMoves away
                const falloff = Math.max(0, 1 - dist / tbc.falloffMoves);
                const curveMult = 1.0 + (tbc.peakMultiplier - 1.0) * falloff;
                if (curveMult > 1.02) {
                    min *= curveMult;
                    max *= curveMult;
                }
            }

            // ============================================================
            // LAYER 4C: Asymmetric King Safety (B16)
            // Defending against an attack on our own king triggers longer
            // thinks than attacking the opponent's king.
            // ============================================================
            if (!isBook && CONFIG.kingSafety?.enabled && State.currentEval && State.human.prevOurEval != null) {
                const ks = CONFIG.kingSafety;
                const evalDelta = State.human.prevOurEval - State.currentEval.value;
                if (evalDelta >= ks.evalDropTrigger) {
                    // Eval got worse for us — possibly under attack
                    min *= ks.defenseTimingMult;
                    max *= ks.defenseTimingMult;
                    Utils.log(`Timing: King-safety defense (eval drop ${evalDelta.toFixed(2)}) → ${ks.defenseTimingMult}x`, 'debug');
                }
            }

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

    // ═══════════════════════════════════════════
    //  COACH MODE (v16.3) — In-game GM advice without auto-play
    // ═══════════════════════════════════════════
    const Coach = {
        // Tracks the prior position eval to grade the opponent's last move
        _prevEvalBeforeOpp: null,
        _prevPlayerToMove: null,
        _lastOppGrade: null,
        _lastOppMove: null,

        // ---------- Helpers ----------
        _sqFromUCI: (uci, idx) => uci.substring(idx, idx + 2),
        _fileRank: (sq) => ({ file: sq.charCodeAt(0) - 97, rank: parseInt(sq[1], 10) - 1 }),
        _pieceAt: (board, sq) => {
            const { file, rank } = Coach._fileRank(sq);
            // FEN board[0] = rank 8, board[7] = rank 1
            const row = 7 - rank;
            return board[row]?.[file] || null;
        },
        _parseFENBoard: (fen) => {
            if (!fen) return null;
            const rows = fen.split(' ')[0].split('/');
            return rows.map(row => {
                const arr = [];
                for (const ch of row) {
                    if (/\d/.test(ch)) for (let i = 0; i < parseInt(ch, 10); i++) arr.push(null);
                    else arr.push(ch);
                }
                return arr;
            });
        },
        _pieceName: (ch) => {
            const map = { p: 'pawn', n: 'knight', b: 'bishop', r: 'rook', q: 'queen', k: 'king' };
            return map[ch.toLowerCase()] || 'piece';
        },
        _pieceValue: (ch) => {
            const map = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 100 };
            return map[ch?.toLowerCase()] || 0;
        },

        // ---------- Move categorization ----------
        // Returns a short verb phrase describing what the move does.
        classifyMove: (fen, uci, candidatePV) => {
            if (!fen || !uci || uci.length < 4) return '';
            const board = Coach._parseFENBoard(fen);
            if (!board) return '';
            const from = uci.substring(0, 2);
            const to = uci.substring(2, 4);
            const promo = uci.length >= 5 ? uci[4] : null;
            const piece = Coach._pieceAt(board, from);
            if (!piece) return '';
            const pieceLower = piece.toLowerCase();
            const target = Coach._pieceAt(board, to);
            const isCapture = !!target;
            const tags = [];

            // Castles
            if (pieceLower === 'k' && Math.abs(from.charCodeAt(0) - to.charCodeAt(0)) === 2) {
                return to[0] === 'g' ? 'castles kingside' : 'castles queenside';
            }
            // Promotion
            if (promo) tags.push(`promotes to ${Coach._pieceName(promo)}`);
            // Capture
            if (isCapture) {
                const targetName = Coach._pieceName(target);
                tags.push(`captures ${targetName}`);
            }
            // Check (heuristic: if the PV continues with a king move, it was probably a check)
            if (candidatePV && candidatePV.length >= 2) {
                const reply = candidatePV[1];
                if (reply && reply.length >= 4) {
                    const replyFrom = reply.substring(0, 2);
                    const replyPiece = Coach._pieceAt(board, replyFrom);
                    // Crude: if opponent moves the king or interposes/captures attacker
                    if (replyPiece && replyPiece.toLowerCase() === 'k') tags.push('gives check');
                }
            }
            // Centralization
            if (['e4','d4','e5','d5','c4','c5','f4','f5'].includes(to)) {
                if (!isCapture) tags.push('controls the center');
            }
            // Development from back rank
            if (['n','b'].includes(pieceLower)) {
                const fromRank = parseInt(from[1], 10);
                if (fromRank === 1 || fromRank === 8) {
                    if (!isCapture) tags.unshift(`develops the ${Coach._pieceName(piece)}`);
                }
            }
            // Pawn pushes (non-capture, non-promotion)
            if (pieceLower === 'p' && !isCapture && !promo) {
                const toRank = parseInt(to[1], 10);
                if (toRank >= 6 || toRank <= 3) tags.push('advances a pawn');
            }
            // Quiet rook lift
            if (pieceLower === 'r' && !isCapture) {
                const toRank = parseInt(to[1], 10);
                if (toRank === 3 || toRank === 6) tags.push('lifts the rook');
            }
            // Quiet queen development
            if (pieceLower === 'q' && !isCapture) {
                tags.push('repositions the queen');
            }

            if (tags.length === 0) {
                tags.push(`moves the ${Coach._pieceName(piece)} to ${to}`);
            }
            return tags.join(', ');
        },

        // ---------- Hanging pieces (own + opponent) ----------
        detectHangingPieces: (fen) => {
            if (!fen) return { ours: [], theirs: [] };
            const board = Coach._parseFENBoard(fen);
            if (!board) return { ours: [], theirs: [] };
            const sideToMove = fen.split(' ')[1]; // 'w' or 'b'
            const ours = [];
            const theirs = [];
            // For each piece, check if it's attacked by an opponent piece and not defended.
            // Simple heuristic: count attackers vs defenders by walking knight/sliding/pawn rays.
            // Implementation kept compact for performance.
            for (let r = 0; r < 8; r++) {
                for (let f = 0; f < 8; f++) {
                    const p = board[r][f];
                    if (!p) continue;
                    const isWhite = p === p.toUpperCase();
                    const sq = String.fromCharCode(97 + f) + (8 - r);
                    const att = Coach._countAttackers(board, r, f, !isWhite);
                    const def = Coach._countAttackers(board, r, f, isWhite);
                    // "Hanging" = attacked, undefended, and worth >0
                    if (att > def && Coach._pieceValue(p) > 0) {
                        const owner = (isWhite && sideToMove === 'w') || (!isWhite && sideToMove === 'b') ? 'ours' : 'theirs';
                        const entry = { sq, piece: Coach._pieceName(p), value: Coach._pieceValue(p) };
                        if (owner === 'ours') ours.push(entry); else theirs.push(entry);
                    }
                }
            }
            return { ours, theirs };
        },

        _countAttackers: (board, tr, tf, byWhite) => {
            let count = 0;
            // Pawns (attack diagonally)
            const pdr = byWhite ? 1 : -1;
            for (const df of [-1, 1]) {
                const r = tr + pdr, f = tf + df;
                if (r >= 0 && r < 8 && f >= 0 && f < 8) {
                    const p = board[r][f];
                    if (p && (byWhite ? p === 'P' : p === 'p')) count++;
                }
            }
            // Knights
            const knightOffsets = [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
            for (const [dr, df] of knightOffsets) {
                const r = tr + dr, f = tf + df;
                if (r < 0 || r >= 8 || f < 0 || f >= 8) continue;
                const p = board[r][f];
                if (p && (byWhite ? p === 'N' : p === 'n')) count++;
            }
            // Sliding (bishop/queen diagonal, rook/queen straight)
            const slideDirs = [
                { dr: 1, df: 0,  pieces: ['r','q'] },
                { dr:-1, df: 0,  pieces: ['r','q'] },
                { dr: 0, df: 1,  pieces: ['r','q'] },
                { dr: 0, df:-1,  pieces: ['r','q'] },
                { dr: 1, df: 1,  pieces: ['b','q'] },
                { dr: 1, df:-1, pieces: ['b','q'] },
                { dr:-1, df: 1, pieces: ['b','q'] },
                { dr:-1, df:-1, pieces: ['b','q'] },
            ];
            for (const { dr, df, pieces } of slideDirs) {
                let r = tr + dr, f = tf + df;
                while (r >= 0 && r < 8 && f >= 0 && f < 8) {
                    const p = board[r][f];
                    if (p) {
                        const want = pieces.map(x => byWhite ? x.toUpperCase() : x);
                        if (want.includes(p)) count++;
                        break;
                    }
                    r += dr; f += df;
                }
            }
            // King (adjacent)
            for (let dr = -1; dr <= 1; dr++) {
                for (let df = -1; df <= 1; df++) {
                    if (dr === 0 && df === 0) continue;
                    const r = tr + dr, f = tf + df;
                    if (r < 0 || r >= 8 || f < 0 || f >= 8) continue;
                    const p = board[r][f];
                    if (p && (byWhite ? p === 'K' : p === 'k')) count++;
                }
            }
            return count;
        },

        // ---------- Eval helpers ----------
        // Convert engine eval to a pawn-units number from White's perspective.
        // Mate scores → ±999.
        _evalToPawns: (e) => {
            if (!e) return 0;
            if (e.type === 'mate') return e.value > 0 ? 999 : -999;
            return e.value;
        },

        // Convert to OUR perspective (positive = good for us)
        _evalForUs: (e, sideToMove) => {
            const w = Coach._evalToPawns(e);
            return sideToMove === 'w' ? w : -w;
        },

        // ---------- Opponent's last move grading ----------
        // Returns { grade, label, color, evalSwing } where grade is one of
        // 'best' | 'good' | 'inaccuracy' | 'mistake' | 'blunder'.
        gradeOpponentLastMove: () => {
            if (Coach._prevEvalBeforeOpp == null || !State.currentEval) return null;
            // _prevEvalBeforeOpp was OUR perspective before opponent moved.
            // After opponent moved, it's now our turn — State.currentEval is the
            // new eval from White's perspective. Convert to our perspective.
            const sideToMove = State.lastFen ? State.lastFen.split(' ')[1] : 'w';
            const evalNow = Coach._evalForUs(State.currentEval, sideToMove);
            // If we were +0.4 before opp moved (our perspective) and now we're +1.2,
            // opp made things worse for themselves by 0.8 pawns (a mistake on their part).
            const swing = evalNow - Coach._prevEvalBeforeOpp;
            const c = CONFIG.coach;
            let grade = 'best', label = 'best move', color = '#4caf50';
            if (swing > c.blunderThreshold)        { grade = 'blunder';     label = 'blunder';        color = '#d32f2f'; }
            else if (swing > c.mistakeThreshold)   { grade = 'mistake';     label = 'mistake';        color = '#ff7043'; }
            else if (swing > 0.25)                  { grade = 'inaccuracy';  label = 'inaccuracy';     color = '#ffb74d'; }
            else if (swing < -0.25)                 { grade = 'good';        label = 'strong move';    color = '#4caf50'; }
            return { grade, label, color, evalSwing: swing };
        },

        // ---------- Main commentary builder ----------
        // Produces a structured object the UI can render.
        analyze: (fen, candidates, eval_) => {
            const c = CONFIG.coach;
            const out = {
                bestMove: null,
                bestEval: null,
                alternatives: [],
                threat: null,
                annotations: [],
                hanging: { ours: [], theirs: [] },
                oppLastGrade: null,
            };
            if (!candidates || !candidates[1]) return out;
            const best = candidates[1];
            out.bestMove = best.move;
            out.bestEval = best.eval;

            // Alternatives within altEvalWindow pawns of the best
            if (c.showAlternatives && best.eval?.type === 'cp') {
                for (let i = 2; i <= CONFIG.multiPV; i++) {
                    const alt = candidates[i];
                    if (!alt?.move || !alt?.eval) continue;
                    if (alt.eval.type !== 'cp') continue;
                    const diff = (best.eval.value - alt.eval.value);
                    if (Math.abs(diff) <= c.altEvalWindow) {
                        out.alternatives.push({ move: alt.move, eval: alt.eval, gap: diff });
                        if (out.alternatives.length >= 2) break;
                    }
                }
            }

            // Threat: opponent's best reply (PV[1] of our best line)
            if (c.showThreats && best.pv && best.pv.length >= 2) {
                out.threat = best.pv[1];
            }

            // Hanging pieces detection
            if (c.showHangingPieces) {
                out.hanging = Coach.detectHangingPieces(fen);
            }

            // Last opponent move grade
            if (c.showLastMoveReview) {
                out.oppLastGrade = Coach.gradeOpponentLastMove();
            }

            // Natural-language annotations
            if (c.showAnnotations) {
                const ann = [];
                // Best move classification
                const cls = Coach.classifyMove(fen, best.move, best.pv);
                if (cls) {
                    const evalStr = best.eval?.type === 'mate'
                        ? `forced mate in ${Math.abs(best.eval.value)}`
                        : (best.eval ? `eval ${best.eval.value >= 0 ? '+' : ''}${best.eval.value.toFixed(2)}` : '');
                    ann.push(`Best: ${best.move} ${cls}${evalStr ? ` (${evalStr})` : ''}.`);
                }
                // Hanging pieces commentary
                if (out.hanging.ours.length > 0) {
                    const list = out.hanging.ours.map(h => `${h.piece} on ${h.sq}`).join(', ');
                    ann.push(`⚠ Your ${list} ${out.hanging.ours.length === 1 ? 'is' : 'are'} hanging — defend or trade.`);
                }
                if (out.hanging.theirs.length > 0) {
                    const top = out.hanging.theirs.sort((a,b) => b.value - a.value)[0];
                    ann.push(`Their ${top.piece} on ${top.sq} is undefended — look for a way to win it.`);
                }
                // Threat commentary
                if (out.threat) {
                    ann.push(`If you play passively, opponent's main idea is ${out.threat}.`);
                }
                // Eval-based strategic hint
                if (best.eval?.type === 'cp') {
                    const v = Coach._evalForUs(best.eval, fen.split(' ')[1]);
                    if (v >= 3) ann.push('You are winning — convert with simple, safe moves.');
                    else if (v >= 1) ann.push('You stand better — keep up the pressure.');
                    else if (v <= -3) ann.push('Position is lost — set practical problems and look for swindles.');
                    else if (v <= -1) ann.push('You are worse — defend actively, avoid trades that simplify into a losing endgame.');
                    else ann.push('Position is balanced — focus on improving your worst-placed piece.');
                }
                out.annotations = ann;
            }
            return out;
        },

        // Stash the eval before opponent moves so we can grade their move next time.
        // Called after we generate analysis (which is OUR turn — meaning opponent
        // just moved). So the "previous eval" we want is the one from BEFORE
        // opponent's move, which is what we last saved.
        snapshotForOppGrading: (fen) => {
            if (!State.currentEval) return;
            const sideToMove = fen.split(' ')[1];
            // Save eval from our perspective for the grader to compare against next turn.
            // Only update if it's our turn (we just got new analysis after opp move).
            if (sideToMove === State.playerColor) {
                // This will be compared NEXT time after we move and opponent replies.
                Coach._prevEvalBeforeOpp = Coach._evalForUs(State.currentEval, sideToMove);
            }
        },

        // Toggle coach mode on/off; applies side-effects (disables auto-play).
        setEnabled: (on) => {
            CONFIG.coach.enabled = on;
            if (on && CONFIG.coach.disableAutoOnEnable) {
                CONFIG.auto.enabled = false;
                Utils.log('Coach Mode: ON — auto-play disabled', 'info');
                UI.toast('Coach Mode', 'Auto-play disabled. Make your own moves; coach will guide you.', 'info', 4500);
            } else if (on) {
                Utils.log('Coach Mode: ON', 'info');
                UI.toast('Coach Mode', 'Coach overlay active.', 'info', 3000);
            } else {
                Utils.log('Coach Mode: OFF', 'info');
                UI.toast('Coach Mode', 'Coach disabled.', 'info', 2500);
            }
            UI.refreshCoachPanel();
            Settings.save(CONFIG);
        },
    };

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

            // (T3 / B10) Save real mouse position so IdleBehavior can drift from here
            State.human.lastMouseX = dropX;
            State.human.lastMouseY = 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;
                    }
                }
            }
        }
    };

    // ═══════════════════════════════════════════
    //  IDLE BEHAVIOR (B10 + B11)
    // ═══════════════════════════════════════════
    // Produces realistic small mouse movements during our long thinks,
    // and occasionally draws right-click annotations like real humans do.
    const IdleBehavior = {
        _thinkStart: 0,
        _lastIdleAction: 0,
        _annotationsThisThink: 0,
        _lastAnnotationAt: 0,

        // Called every game-loop tick. We fire idle actions only when we've
        // been thinking long enough.
        tick: async () => {
            const cfg = CONFIG.idleMouse;
            if (!cfg?.enabled) return;

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

            // Reset state when it's not our turn
            if (!isMyTurn && !isThinking) {
                IdleBehavior._thinkStart = 0;
                IdleBehavior._annotationsThisThink = 0;
                IdleBehavior._lastAnnotationAt = 0;
                return;
            }

            // Start the think timer if we just got the turn
            if (!IdleBehavior._thinkStart) IdleBehavior._thinkStart = Date.now();

            const thinkElapsed = Date.now() - IdleBehavior._thinkStart;
            if (thinkElapsed < cfg.triggerAfterMs) return;

            // Throttle: don't fire idle actions more than once every 1.2s
            const now = Date.now();
            if (now - IdleBehavior._lastIdleAction < 1200) return;

            // Random gate
            if (Math.random() > cfg.actionChance) return;
            IdleBehavior._lastIdleAction = now;

            // Decide if we should try an annotation first (pros draw arrows often
            // during long thinks). Otherwise fall back to drift/hover.
            const ann = CONFIG.annotations;
            const annotationEligible = ann?.enabled
                && thinkElapsed >= (ann.minThinkMs ?? 3000)
                && IdleBehavior._annotationsThisThink < (ann.maxPerThink ?? 3)
                && (now - IdleBehavior._lastAnnotationAt) >= (ann.minTimeBetweenMs ?? 1500);

            if (annotationEligible && Math.random() < ann.chancePerLongThink) {
                IdleBehavior.drawAnnotation();
                IdleBehavior._annotationsThisThink++;
                IdleBehavior._lastAnnotationAt = now;
                return;
            }

            // Pick a non-annotation idle action
            const r = Math.random();
            if (r < 0.65) {
                IdleBehavior.microDrift();
            } else {
                IdleBehavior.hoverSquare();
            }
        },

        microDrift: () => {
            const board = Game.getBoard();
            if (!board) return;
            const rect = board.getBoundingClientRect();
            if (!rect.width) return;

            // Use last known mouse pos or default to center of board
            let baseX = State.human.lastMouseX;
            let baseY = State.human.lastMouseY;
            if (baseX == null || baseY == null) {
                baseX = rect.left + rect.width / 2 + Utils.gaussianRandom(0, rect.width / 4);
                baseY = rect.top + rect.height / 2 + Utils.gaussianRandom(0, rect.height / 4);
            }

            const cfg = CONFIG.idleMouse.driftMagnitude;
            const dx = Utils.gaussianRandom(0, Utils.randomRange(cfg.min, cfg.max));
            const dy = Utils.gaussianRandom(0, Utils.randomRange(cfg.min, cfg.max));
            const nx = baseX + dx;
            const ny = baseY + dy;

            const opts = { bubbles: true, composed: true, pointerId: 1 };
            board.dispatchEvent(new PointerEvent('pointermove', {
                ...opts, clientX: nx, clientY: ny,
                movementX: Math.round(dx), movementY: Math.round(dy),
            }));
            board.dispatchEvent(new MouseEvent('mousemove', {
                ...opts, clientX: nx, clientY: ny,
                movementX: Math.round(dx), movementY: Math.round(dy),
            }));
            State.human.lastMouseX = nx;
            State.human.lastMouseY = ny;
        },

        hoverSquare: () => {
            const board = Game.getBoard();
            if (!board) return;
            const pieces = Array.from(board.querySelectorAll('.piece'));
            if (pieces.length === 0) return;
            const piece = pieces[Math.floor(Math.random() * pieces.length)];
            const rect = piece.getBoundingClientRect();
            if (!rect.width) return;
            const x = rect.left + rect.width / 2 + Utils.gaussianRandom(0, 4);
            const y = rect.top + rect.height / 2 + Utils.gaussianRandom(0, 4);
            const opts = { bubbles: true, composed: true, pointerId: 1 };
            board.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: x, clientY: y }));
            State.human.lastMouseX = x;
            State.human.lastMouseY = y;
        },

        // (B11 / v16.4) Draw a right-click annotation: red circle on a square or
        // an arrow between two squares. Uses real piece squares + engine candidate
        // moves to mimic actual analysis-style arrowing that pros do.
        drawAnnotation: async () => {
            const board = Game.getBoard();
            if (!board) return;
            const rect = board.getBoundingClientRect();
            if (!rect.width) return;
            const sq = rect.width / 8;

            const ann = CONFIG.annotations;
            const opts = { bubbles: true, composed: true };

            // ─── Square coordinate helpers ───
            // FEN-square ('e4') → screen pixel coords, accounting for board flip.
            const flipped = State.playerColor === 'b';
            const sqToPx = (sqName) => {
                if (!sqName || sqName.length < 2) return null;
                const file = sqName.charCodeAt(0) - 97; // a=0..h=7
                const rank = parseInt(sqName[1], 10) - 1; // 1=0..8=7
                if (file < 0 || file > 7 || rank < 0 || rank > 7) return null;
                const col = flipped ? (7 - file) : file;
                const row = flipped ? rank : (7 - rank); // top-row = rank 8 normally
                return {
                    x: rect.left + col * sq + sq / 2 + Utils.gaussianRandom(0, sq * 0.12),
                    y: rect.top + row * sq + sq / 2 + Utils.gaussianRandom(0, sq * 0.12),
                };
            };

            // Build list of squares with pieces from the current FEN.
            const piecesSquares = [];
            const ourSquares = [];
            const theirSquares = [];
            const fen = Game.getFen();
            if (fen) {
                const rows = fen.split(' ')[0].split('/');
                const sideToMove = fen.split(' ')[1];
                for (let r = 0; r < 8; r++) {
                    let f = 0;
                    for (const ch of rows[r]) {
                        if (/\d/.test(ch)) { f += parseInt(ch, 10); continue; }
                        const sqName = String.fromCharCode(97 + f) + (8 - r);
                        piecesSquares.push(sqName);
                        const isWhite = ch === ch.toUpperCase();
                        if ((isWhite && State.playerColor === 'w') || (!isWhite && State.playerColor === 'b')) {
                            ourSquares.push(sqName);
                        } else {
                            theirSquares.push(sqName);
                        }
                        f++;
                    }
                }
            }

            // Pick a "natural" target square — biased toward piece squares
            // (with extra weight on opponent pieces, since we're often considering
            // attacks against them).
            const pickSquare = () => {
                if (piecesSquares.length && Math.random() < (ann.pieceSquareBias ?? 0.85)) {
                    // 60% opponent pieces, 30% our pieces, 10% any
                    const r = Math.random();
                    if (r < 0.60 && theirSquares.length) return theirSquares[Math.floor(Math.random() * theirSquares.length)];
                    if (r < 0.90 && ourSquares.length)   return ourSquares[Math.floor(Math.random() * ourSquares.length)];
                    return piecesSquares[Math.floor(Math.random() * piecesSquares.length)];
                }
                // Fully random fallback
                const file = String.fromCharCode(97 + Math.floor(Math.random() * 8));
                const rank = (Math.floor(Math.random() * 8) + 1);
                return file + rank;
            };

            // Pick from engine candidates (best + alternates) — the moves we'd
            // most plausibly be analyzing right now.
            const pickCandidateMove = () => {
                const cands = [];
                for (let i = 1; i <= CONFIG.multiPV; i++) {
                    const c = State.candidates?.[i];
                    if (c?.move && c.move.length >= 4) cands.push(c.move);
                }
                if (!cands.length) return null;
                // Weight: 50% best, 30% #2, 20% #3+
                const r = Math.random();
                if (r < 0.50) return cands[0];
                if (r < 0.80 && cands[1]) return cands[1];
                return cands[Math.floor(Math.random() * cands.length)];
            };

            const isArrow = Math.random() < (ann.arrowChance ?? 0.65);

            if (isArrow) {
                // Decide: real candidate move, or random plausible squares?
                let fromSq = null, toSq = null, source = 'random';
                if (Math.random() < (ann.candidateBias ?? 0.55)) {
                    const cand = pickCandidateMove();
                    if (cand) {
                        fromSq = cand.substring(0, 2);
                        toSq = cand.substring(2, 4);
                        source = 'candidate';
                    }
                }
                if (!fromSq) {
                    fromSq = pickSquare();
                    toSq = pickSquare();
                    // Avoid degenerate same-square arrows
                    let tries = 0;
                    while (toSq === fromSq && tries++ < 5) toSq = pickSquare();
                }

                const from = sqToPx(fromSq);
                const to = sqToPx(toSq);
                if (!from || !to) return;

                Utils.log(`IdleBehavior: arrow ${fromSq}->${toSq} (${source})`, 'debug');
                board.dispatchEvent(new MouseEvent('contextmenu', { ...opts, clientX: from.x, clientY: from.y, button: 2, buttons: 2, preventDefault: () => {} }));
                board.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: from.x, clientY: from.y, button: 2, buttons: 2 }));
                await Utils.sleep(Utils.randomRange(40, 110));

                // Smooth drag with slight curvature (humans don't drag in straight lines)
                const dist = Math.hypot(to.x - from.x, to.y - from.y);
                const steps = Math.max(6, Math.min(14, Math.round(dist / 25)));
                const curveAmt = Utils.gaussianRandom(0, dist * 0.04);
                const perpX = -(to.y - from.y) / Math.max(dist, 1);
                const perpY = (to.x - from.x) / Math.max(dist, 1);
                for (let i = 1; i <= steps; i++) {
                    const t = i / steps;
                    const easeT = 0.5 - 0.5 * Math.cos(Math.PI * t); // ease in-out
                    const arc = Math.sin(Math.PI * t) * curveAmt;
                    const cx = from.x + (to.x - from.x) * easeT + perpX * arc;
                    const cy = from.y + (to.y - from.y) * easeT + perpY * arc;
                    board.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: cx, clientY: cy, buttons: 2 }));
                    await Utils.sleep(Utils.randomRange(10, 22));
                }
                board.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: to.x, clientY: to.y, button: 2, buttons: 0 }));
            } else {
                // Single-square highlight: bias toward a piece square (often a
                // weak/hanging piece or a key square in your plan).
                const sqName = pickSquare();
                const at = sqToPx(sqName);
                if (!at) return;
                Utils.log(`IdleBehavior: circle on ${sqName}`, 'debug');
                board.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: at.x, clientY: at.y, button: 2, buttons: 2 }));
                await Utils.sleep(Utils.randomRange(50, 130));
                board.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: at.x, clientY: at.y, button: 2, buttons: 0 }));
                board.dispatchEvent(new MouseEvent('contextmenu', { ...opts, clientX: at.x, clientY: at.y, button: 2, buttons: 0, preventDefault: () => {} }));
            }
        },
    };

    // ═══════════════════════════════════════════
    //  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 v16 first-run modal shows once even on accounts that
            // already dismissed earlier versions.
            const welcomeKey = 'ba_welcome_shown_v16';
            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: 580px; 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: 22px; font-weight: 700; color: #4caf50; margin-bottom: 4px;">
                        REXXX.MENU v16
                    </div>
                    <div style="font-size: 12px; color: #888; margin-bottom: 16px;">Chess.com Cheat Engine — v16 behavioral realism overhaul</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 v16 (behavioral realism):</strong>
                        <ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
                            <li><strong>Critical-position bias:</strong> errors cluster in tactical positions, not random quiet ones.</li>
                            <li><strong>Shallow-depth picks:</strong> sometimes prefer shorter-PV moves humans would actually find.</li>
                            <li><strong>Motif blindness:</strong> miss zwischenzug / deflection / backwards-knight patterns.</li>
                            <li><strong>Endgame slips:</strong> elevated error rate in K+P, R+P, and other technical endgames.</li>
                            <li><strong>Book-exit pause:</strong> long think on the first move after leaving prep.</li>
                            <li><strong>Time-bank curve:</strong> bell-shaped think-time peaking at move ~22.</li>
                            <li><strong>Forced-move recognition:</strong> single-good-move positions played near-instantly.</li>
                            <li><strong>King-safety asymmetry:</strong> longer thinks when defending our own king.</li>
                            <li><strong>Idle mouse:</strong> tiny drifts &amp; hovers during long thinks (no still cursor).</li>
                            <li><strong>Right-click annotations:</strong> occasional circles / arrows on the board.</li>
                            <li><strong>Post-game review behavior:</strong> sometimes linger on the board before re-queue.</li>
                            <li><strong>Engine eval cache:</strong> reduces API call rate (network-fingerprint reduction).</li>
                            <li><strong>Stealth mode:</strong> opt-in console-log silencing.</li>
                        </ul>
                    </div>

                    <div style="font-size: 13px; line-height: 1.6; margin-bottom: 12px;">
                        <strong>UI updates:</strong>
                        <ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
                            <li>Pill toggles + clickable rows (entire row is the hit-target).</li>
                            <li>Collapsible sections in SAFETY — Account &amp; v16 fold up to keep things tidy.</li>
                            <li>Active-features badge in the header (e.g. <code style="background:#222;padding:1px 5px;border-radius:3px">28/33</code>) — at-a-glance stealth coverage.</li>
                            <li>Eval bar in MAIN status box.</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 default toggles ON (v15.1 + v16 sections).</li>
                            <li>When switching Chess.com accounts, click "RESET" 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,
                        <span style="background:#333;padding:2px 6px;border-radius:3px;font-family:monospace;">X</span> for stealth,
                        <span style="background:#333;padding:2px 6px;border-radius:3px;font-family:monospace;">R</span> to reset to defaults,
                        <span style="background:#333;padding:2px 6px;border-radius:3px;font-family:monospace;">Ctrl+Shift+C</span> for coach mode.
                    </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);

            // Initial header badge + eval bar
            UI.updateFeatureCount();
            UI.updateEvalBar(null);

            // 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');
                }
                // v16.3 Coach Mode hotkeys
                // Ctrl+Shift+C → toggle coach mode entirely
                if ((e.key === 'C' || e.key === 'c') && e.ctrlKey && e.shiftKey) {
                    e.preventDefault();
                    Coach.setEnabled(!CONFIG.coach.enabled);
                    UI.updatePanel(State.currentEval, {});
                }
                // Plain c (no modifiers) → peek-toggle coach panel visibility (only when coach is on)
                else if (e.key === 'c' && !e.ctrlKey && !e.shiftKey && !e.altKey && CONFIG.coach.enabled) {
                    UI._coachPanelHiddenByHotkey = !UI._coachPanelHiddenByHotkey;
                    UI.refreshCoachPanel();
                }
            });

            // 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;
        },

        // v16.3: lock-timeout watchdog. If the lock has been held for more than
        // 10 minutes (e.g. a rate-limit wait was interrupted by a page navigation
        // or thrown exception), force-reset it on next attempt.
        _queueLockSetAt: 0,
        _LOCK_MAX_HOLD_MS: 10 * 60 * 1000,

        // Dismiss any blocking popups that may sit on top of the game-over modal
        // (chess.com loves to throw premium upsells, "rate this game", daily-puzzle
        // tease, etc.). These eat clicks meant for the New Game button.
        _dismissBlockers: async () => {
            const blockerSelectors = [
                '.modal-close-button-component',
                '.cc-modal-close-component',
                'button[aria-label="Close"]',
                'button[aria-label="close"]',
                '.upgrade-modal-close',
                '.diamond-membership-close',
                '[data-cy="modal-close"]',
                '.cc-icon-close',
                '.close-button',
                '.fade-modal-close',
            ];
            let dismissed = 0;
            for (const sel of blockerSelectors) {
                document.querySelectorAll(sel).forEach((el) => {
                    if (!el.offsetParent) return;
                    // Don't close the game-over modal itself
                    if (el.closest('[data-cy="game-over-modal-content"], .game-over-modal-shell-content, .game-over-modal-container')) return;
                    try { el.click(); dismissed++; } catch (e) {}
                });
            }
            if (dismissed > 0) {
                Utils.log(`Auto-Queue: dismissed ${dismissed} blocking popup(s)`, 'debug');
                await Utils.sleep(400);
            }
        },

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

            // v16.3: lock timeout watchdog
            if (Main._queueing) {
                const heldFor = Date.now() - (Main._queueLockSetAt || 0);
                if (heldFor > Main._LOCK_MAX_HOLD_MS) {
                    Utils.log(`Auto-Queue: stale lock held for ${Math.round(heldFor/1000)}s — force-releasing`, 'warn');
                    Main._queueing = false;
                } else {
                    return;
                }
            }

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

            Main._queueing = true;
            Main._queueLockSetAt = Date.now();
            try {
                await Main._checkAutoQueueImpl();
            } catch (err) {
                Utils.log(`Auto-Queue: exception during queue: ${err && err.message || err}`, 'error');
            } finally {
                Main._queueing = false;
                Main._queueLockSetAt = 0;
            }
        },

        _checkAutoQueueImpl: async () => {

            // 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');
                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);
                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);

            // (B15) Post-game review behavior: sometimes linger on the board
            // before re-queuing. Real humans look at the final position, scroll
            // through moves, check the eval graph, etc.
            const pg = CONFIG.postGame;
            if (pg?.enabled && Math.random() < pg.reviewChance) {
                const reviewMs = Utils.randomRange(pg.reviewDurationMs.min, pg.reviewDurationMs.max);
                Utils.log(`PostGame: reviewing game for ${Math.round(reviewMs / 1000)}s`, 'debug');
                UI.toast('Post-game', 'Reviewing the position...', 'info', 2500);
                // During the review, do some idle mouse activity over the board
                const reviewStart = Date.now();
                while (Date.now() - reviewStart < reviewMs) {
                    if (Math.random() < 0.4) IdleBehavior.microDrift();
                    else if (Math.random() < 0.5) IdleBehavior.hoverSquare();
                    await Utils.sleep(Utils.randomRange(600, 1800));
                }
            }

            // (B15) Profile click: occasionally click on opponent's profile
            if (pg?.enabled && Math.random() < pg.profileClickChance) {
                try {
                    const profileEl = document.querySelector(
                        '.user-username-component, [data-cy="opponent-username"], .player-row-component .user-username-component'
                    );
                    if (profileEl) {
                        const rect = profileEl.getBoundingClientRect();
                        if (rect.width > 0) {
                            const x = rect.left + rect.width / 2 + Utils.gaussianRandom(0, 2);
                            const y = rect.top + rect.height / 2 + Utils.gaussianRandom(0, 2);
                            const opts = { bubbles: true, composed: true };
                            Utils.log('PostGame: peeking at opponent profile', 'debug');
                            profileEl.dispatchEvent(new MouseEvent('mouseover', { ...opts, clientX: x, clientY: y }));
                            await Utils.sleep(Utils.randomRange(800, 2200));
                        }
                    }
                } catch (e) { /* non-fatal */ }
            }

            // v16.3: dismiss any blocking popups (premium upsell, rate-the-game,
            // daily puzzle teaser, etc.) before trying to click new-game.
            await Main._dismissBlockers();

            // Try to click the new game button.
            // v16.3: expanded selector list covering newer chess.com UI revisions
            // plus broader fallbacks (any button text containing "New"/"Play").
            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"]',
                'button[data-cy="play-button"]',
                'button[data-cy="game-over-new-game-button"]',
                'button[data-cy="new-game-button"]',
                '.game-over-modal-shell-buttons button.cc-button-secondary',
                '.game-over-modal-shell-buttons button.cc-button-primary',
                '.game-over-buttons-button',
                '.game-over-button-component.primary',
                '.game-over-modal-buttons-button-primary',
                '.ui_v5-button-component.ui_v5-button-primary',
                '.cc-button-component.cc-button-primary',
            ];

            // v16.3: helper that finds a New Game button via selectors OR text match.
            const findNewGameButton = () => {
                // Strict selectors first
                for (const sel of newGameSelectors) {
                    const els = document.querySelectorAll(sel);
                    for (const btn of els) {
                        if (!btn.offsetParent) continue;
                        // Reject rematch when a non-rematch alternative exists
                        if (sel.includes('rematch')) {
                            const newGameBtn = document.querySelector(newGameSelectors[0]);
                            if (newGameBtn && newGameBtn.offsetParent) continue;
                        }
                        return { btn, sel };
                    }
                }
                // Text-based fallback: any visible button inside the game-over modal whose
                // text matches new-game variants. Avoids false-matches like "Review Game".
                const modal = Utils.query(SELECTORS.gameOver);
                if (modal) {
                    const buttons = modal.querySelectorAll('button, [role="button"]');
                    for (const btn of buttons) {
                        if (!btn.offsetParent) continue;
                        const txt = (btn.textContent || '').trim().toLowerCase();
                        if (!txt) continue;
                        // Match "new ... game", "play again", standalone "new game"
                        if (/^(new game|play again|new \d+ ?\| ?\d+|play \d+ ?\| ?\d+)/.test(txt) ||
                            txt === 'new' || txt === 'play') {
                            // Skip review/analysis buttons
                            if (/review|analysis|share/.test(txt)) continue;
                            return { btn, sel: `text:"${txt}"` };
                        }
                    }
                }
                return null;
            };

            for (let attempt = 0; attempt < 12; attempt++) {
                // Re-dismiss popups each attempt (chess.com sometimes throws them late)
                if (attempt > 0 && attempt % 3 === 0) await Main._dismissBlockers();

                const found = findNewGameButton();
                if (found) {
                    const { btn, sel } = found;
                    const delay = Utils.humanDelay(2000, 5000);
                    Utils.log(`Auto-Queue: Found via [${sel}] -> "${btn.textContent.trim()}" — clicking in ${Math.round(delay)}ms (game #${CONFIG.session.gamesPlayed}, streak: ${CONFIG.session.currentWinStreak})`);
                    await Utils.sleep(delay);
                    try { btn.click(); } catch (e) { /* fallthrough */ }
                    // Some chess.com buttons need a real synthesized click event
                    try {
                        const r = btn.getBoundingClientRect();
                        const x = r.left + r.width / 2, y = r.top + r.height / 2;
                        ['mousedown', 'mouseup', 'click'].forEach(type => {
                            btn.dispatchEvent(new MouseEvent(type, { bubbles: true, composed: true, clientX: x, clientY: y, button: 0 }));
                        });
                    } catch (e) { /* non-fatal */ }

                    // Wait and verify the modal closed
                    await Utils.sleep(1800);
                    const stillOpen = Utils.query(SELECTORS.gameOver);
                    if (!stillOpen) return; // success

                    Utils.log('Auto-Queue: Modal still open after click, trying close button + ESC', 'warn');
                    const closeBtn = document.querySelector('.game-over-modal-header-close, [data-cy="close-board-modal"], .cc-close-button-component, [aria-label="Close"]');
                    if (closeBtn && closeBtn.offsetParent) {
                        try { closeBtn.click(); } catch (e) {}
                    }
                    // ESC keypress as last-ditch dismissal
                    document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, bubbles: true }));
                    await Utils.sleep(1200);
                    // Loop continues to retry from the top
                }
                await Utils.sleep(1500);
            }

            // Last resort: close modal and look for new-game button on the play page.
            Utils.log('Auto-Queue: Standard buttons not found, trying close + page-level buttons...', 'warn');
            const closeBtn = document.querySelector('.game-over-modal-header-close, [data-cy="close-board-modal"], .cc-close-button-component');
            if (closeBtn) {
                try { closeBtn.click(); } catch (e) {}
                await Utils.sleep(2000);
                const fallbackBtn = document.querySelector(
                    'button[data-cy="new-game-index-main"], button[data-cy="play-button"], .new-game-button, .play-button-component, [class*="new-game"]'
                );
                if (fallbackBtn && fallbackBtn.offsetParent) {
                    Utils.log(`Auto-Queue: Clicking page-level fallback "${fallbackBtn.textContent.trim()}"`, 'warn');
                    fallbackBtn.click();
                    return;
                }
            }

            Utils.log('Auto-Queue: Could not find new game button after all attempts', 'error');
            UI.toast('Auto-Queue', 'Could not find New Game button — chess.com UI may have changed', 'warn', 6000);
        },

        _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();
            // (B10 + B11) Run idle mouse / annotation behavior during our long thinks
            IdleBehavior.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();
})();