Chess.com Cheat Engine

Chess.com chess assistant — v17.3 (calibrated rating model, time-control budgets, history context, verified engine assets)

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      17.3.0
// @description  Chess.com chess assistant — v17.3 (calibrated rating model, time-control budgets, history context, verified engine assets)
// @author       rexxx
// @license      MIT
// @match        https://www.chess.com/*
// @noframes
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      unpkg.com
// @connect      lichess.org
// @connect      classicchess.com
// @connect      stockfish.online
// @connect      chess-api.com
// @connect      tablebase.lichess.ovh
// @run-at       document-idle
// @grant        unsafeWindow
// @grant        window.onurlchange
// ==/UserScript==

/* global GM_getValue, GM_setValue, GM_xmlhttpRequest, GM_addStyle, unsafeWindow */
(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');
                }
            }
            // Keep reloads within one play session, but do not let a stale
            // persisted "session" survive for days and trigger phantom limits.
            const now = Date.now();
            const session = defaults.session;
            session.gameTimestamps = Array.isArray(session.gameTimestamps)
                ? session.gameTimestamps.filter(ts => Number.isFinite(ts) && now - ts < 3600000)
                : [];
            if (!Number.isFinite(session.lastGameAt) || now - session.lastGameAt > 2 * 3600000) {
                session.gamesPlayed = 0;
                session.wins = 0;
                session.currentWinStreak = 0;
                session.lastGameAt = null;
            }
            // v17.2 migration: older builds persisted two attempts and shorter
            // watchdogs. Three alternating attempts need a full recovery window.
            if (defaults.inputExecution) {
                defaults.inputExecution.maxAttempts = Math.max(3, Number(defaults.inputExecution.maxAttempts) || 0);
                defaults.inputExecution.confirmationTimeoutMs = Math.max(750,
                    Number(defaults.inputExecution.confirmationTimeoutMs) || 1200);
                defaults.inputExecution.watchdogMs = Math.max(12000,
                    Number(defaults.inputExecution.watchdogMs) || 12000);
                defaults.inputExecution.pollMs = Math.max(25,
                    Number(defaults.inputExecution.pollMs) || 50);
            }
            if (defaults.auto) defaults.auto.afkGuard = defaults.auto.afkGuard !== false;
            // Rating Fidelity and intentional-loss mode are deliberately
            // mutually exclusive. Resolve legacy saved configs toward fidelity.
            if (defaults.humanization?.ratingFidelity?.enabled && defaults.autoLose) {
                defaults.autoLose.enabled = false;
            }
        },

        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,
                    inputExecution: config.inputExecution,
                    forcedMove: config.forcedMove,
                    kingSafety: config.kingSafety,
                    postGame: config.postGame,
                    // v16.1 additions
                    acplGovernor: config.acplGovernor,
                    // v16.3 additions
                    coach: config.coach,
                    // v17.3 additions
                    humanMoveModel: config.humanMoveModel,
                };
                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 (key === '__proto__' || key === 'prototype' || key === 'constructor') continue;
                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' (cloud MultiPV + SF18), 'stockfish_online', 'local' (SF18 lite WASM)
        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,
        localEngine: {
            version: '18-lite-single',
            jsUrl: 'https://unpkg.com/[email protected]/src/stockfish-18-lite-single.js',
            wasmUrl: 'https://unpkg.com/[email protected]/src/stockfish-18-lite-single.wasm',
            // Pinned hashes prevent a CDN or dependency update from silently
            // changing executable engine code inside the userscript sandbox.
            jsSha256: '2278005057f381491f1c9bb3e44c9f5920b3a00bef9759e33cc6582769a1f1fe',
            wasmSha256: 'a8fbc05ec6920b56d7485826dcb02c5ffd2826bcbf751cf973046f237a9096f1',
            timeout: 20000,
        },
        // API engine config
        api: {
            chessApi: {
                url: 'https://chess-api.com/v1',
                maxThinkingTime: 100, // ms server-side limit
                timeout: 5000,
            },
            lichessCloud: {
                enabled: true,
                url: 'https://lichess.org/api/cloud-eval',
                timeout: 2200,
                minDepth: 14,
            },
            stockfishOnline: {
                url: 'https://stockfish.online/api/s/v2.php',
                timeout: 8000,
            },
        },
        // Human input is executed as a single-flight transaction. Confirmation
        // combines exact move history, a validated FEN transition and game-over UI.
        inputExecution: {
            clickMoveChance: 0.30,
            maxAttempts: 3,
            confirmationTimeoutMs: 1200,
            watchdogMs: 12000,
            pollMs: 50,
            stableReads: 2,
        },
        // 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,
            ratingFidelity: {
                enabled: true,
                // Dynamic opponent adjustments remain inside the selected skill
                // band. Intentional-loss logic is suppressed so "1500" keeps
                // playing like a 1500 instead of periodically collapsing.
                maxProfileShift: 75,
                suppressIntentionalLoss: true,
            },
            // RatingProfile tunes how often archived human opening choices are
            // preferred over fresh engine calculation.
            bookUseRate: 0.82,
            tablebaseUseRate: 0.32,
            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,
            },
            // --- HUMAN MOVE DATABASE ---
            playerMoveDB: {
                enabled: true,
                // Classic Chess exposes an unauthenticated ~1M-game master DB.
                // (Lichess Opening Explorer became authentication-only in 2026.)
                minGames: 5,
                preferRate: 0.15,
                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
            },
        },
        // v17.3: one calibrated probability distribution replaces the old
        // stack of independent error coin-flips. Every non-book choice is now
        // scored once from strength, CP loss, human popularity, time control,
        // position complexity and recent context.
        humanMoveModel: {
            enabled: true,
            candidateLimit: 12,
            historyPlies: 12,
            explanationLimit: 5,
            minProbability: 0.000001,
            // Chess.com classifies controls by base + 40 increments: under
            // three minutes bullet, under ten blitz, otherwise rapid.
            timeControls: {
                bullet:    { qualityMult: 1.34, timingMult: 0.42, maxSpendFraction: 0.040, minReserveSeconds: 3,  incrementCredit: 0.72 },
                blitz:     { qualityMult: 1.14, timingMult: 0.72, maxSpendFraction: 0.060, minReserveSeconds: 8,  incrementCredit: 0.68 },
                rapid:     { qualityMult: 1.00, timingMult: 1.00, maxSpendFraction: 0.085, minReserveSeconds: 25, incrementCredit: 0.62 },
                classical: { qualityMult: 0.88, timingMult: 1.28, maxSpendFraction: 0.110, minReserveSeconds: 90, incrementCredit: 0.58 },
            },
        },
        // --- 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,
            lastGameAt: null,
            // Tracking for rate limiter
            gameTimestamps: [],
        },
        // --- AUTO-LOSE MODE ---
        autoLose: {
            enabled: false,
            // 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,
            // Maximum temporary boost above the selected profile when Rating
            // Fidelity is disabled. Fidelity applies its tighter 75-point cap.
            ratingEdge: 100,
            // 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,
            // Background heartbeat, stall recovery and non-blocking queue waits.
            afkGuard: 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
            // Ignore only the fullmove number. The halfmove clock is retained so
            // cached analysis cannot cross the fifty-move-rule boundary.
            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;
            const configuredRemote = CONFIG.engineType === 'stockfish_online' ? 'stockfish_online' : 'api';
            if (r < w.api) pick = configuredRemote;
            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 },
        },

        // Anchored skill bands avoid the artificial behavior produced by one
        // straight line from beginner to super-GM. Each band is a coherent
        // package: move quality, error shape, opening familiarity, calculation,
        // time use, tactical blindness and endgame conversion all move together.
        anchors: [
            { rating: 800,  depth: 10, correlation: 0.43, suboptimal: [0.46, 0.52, 0.44], cpLoss: [65, 90, 70], blunder: 0.040, blunderLoss: 240, acpl: 95, topCap: 0.44, bookRate: 0.35, tablebaseRate: 0.05, dbRate: 0.05, shallow: 0.28, motif: 1.55, endgame: [1.85, 1.65, 1.40], timing: [1700, 5600, 3000, 9000], longThink: 0.09, repertoireDeviation: 0.18, randomMove: 0.015, fastError: 0.16, slowBest: 0.12, hot: 0.09, cold: 0.13 },
            { rating: 1200, depth: 11, correlation: 0.48, suboptimal: [0.36, 0.43, 0.35], cpLoss: [52, 72, 56], blunder: 0.028, blunderLoss: 210, acpl: 75, topCap: 0.49, bookRate: 0.52, tablebaseRate: 0.10, dbRate: 0.08, shallow: 0.22, motif: 1.30, endgame: [1.70, 1.52, 1.32], timing: [1500, 5200, 3300, 9400], longThink: 0.10, repertoireDeviation: 0.14, randomMove: 0.012, fastError: 0.14, slowBest: 0.16, hot: 0.11, cold: 0.11 },
            { rating: 1500, depth: 12, correlation: 0.52, suboptimal: [0.29, 0.35, 0.28], cpLoss: [43, 58, 45], blunder: 0.018, blunderLoss: 180, acpl: 58, topCap: 0.54, bookRate: 0.68, tablebaseRate: 0.18, dbRate: 0.11, shallow: 0.17, motif: 1.10, endgame: [1.55, 1.42, 1.25], timing: [1300, 4700, 3500, 9800], longThink: 0.12, repertoireDeviation: 0.11, randomMove: 0.008, fastError: 0.12, slowBest: 0.20, hot: 0.13, cold: 0.09 },
            { rating: 1800, depth: 14, correlation: 0.57, suboptimal: [0.22, 0.27, 0.21], cpLoss: [34, 46, 34], blunder: 0.010, blunderLoss: 150, acpl: 42, topCap: 0.59, bookRate: 0.82, tablebaseRate: 0.32, dbRate: 0.15, shallow: 0.11, motif: 0.90, endgame: [1.40, 1.30, 1.18], timing: [1100, 4300, 3600, 9600], longThink: 0.13, repertoireDeviation: 0.08, randomMove: 0.005, fastError: 0.10, slowBest: 0.24, hot: 0.15, cold: 0.07 },
            { rating: 2100, depth: 15, correlation: 0.61, suboptimal: [0.17, 0.21, 0.16], cpLoss: [27, 36, 26], blunder: 0.006, blunderLoss: 125, acpl: 30, topCap: 0.63, bookRate: 0.90, tablebaseRate: 0.50, dbRate: 0.18, shallow: 0.075, motif: 0.70, endgame: [1.28, 1.22, 1.13], timing: [950, 4000, 3700, 9900], longThink: 0.14, repertoireDeviation: 0.065, randomMove: 0.003, fastError: 0.085, slowBest: 0.27, hot: 0.17, cold: 0.055 },
            { rating: 2400, depth: 16, correlation: 0.65, suboptimal: [0.13, 0.16, 0.12], cpLoss: [21, 28, 20], blunder: 0.0035, blunderLoss: 105, acpl: 21, topCap: 0.67, bookRate: 0.95, tablebaseRate: 0.68, dbRate: 0.21, shallow: 0.045, motif: 0.50, endgame: [1.18, 1.15, 1.09], timing: [850, 3800, 3900, 10500], longThink: 0.16, repertoireDeviation: 0.05, randomMove: 0.0015, fastError: 0.07, slowBest: 0.30, hot: 0.19, cold: 0.04 },
            { rating: 2600, depth: 17, correlation: 0.68, suboptimal: [0.10, 0.135, 0.09], cpLoss: [18, 23, 16], blunder: 0.0023, blunderLoss: 90, acpl: 16, topCap: 0.70, bookRate: 0.98, tablebaseRate: 0.80, dbRate: 0.23, shallow: 0.030, motif: 0.36, endgame: [1.12, 1.10, 1.06], timing: [780, 3600, 4000, 11200], longThink: 0.17, repertoireDeviation: 0.04, randomMove: 0.0008, fastError: 0.06, slowBest: 0.32, hot: 0.20, cold: 0.03 },
            { rating: 2800, depth: 18, correlation: 0.71, suboptimal: [0.08, 0.11, 0.07], cpLoss: [15, 19, 12], blunder: 0.0015, blunderLoss: 80, acpl: 12, topCap: 0.72, bookRate: 1.00, tablebaseRate: 0.90, dbRate: 0.25, shallow: 0.018, motif: 0.25, endgame: [1.07, 1.06, 1.04], timing: [700, 3400, 4200, 12000], longThink: 0.18, repertoireDeviation: 0.03, randomMove: 0.0003, fastError: 0.05, slowBest: 0.34, hot: 0.21, cold: 0.025 },
        ],

        _blend: (left, right, amount) => {
            if (typeof left === 'number' && typeof right === 'number') return left + (right - left) * amount;
            if (Array.isArray(left) && Array.isArray(right)) {
                return left.map((value, index) => RatingProfile._blend(value, right[index], amount));
            }
            const output = {};
            for (const key of Object.keys(left)) output[key] = key === 'rating'
                ? left.rating + (right.rating - left.rating) * amount
                : RatingProfile._blend(left[key], right[key], amount);
            return output;
        },

        profileFor: (targetRating) => {
            const rating = Math.max(800, Math.min(2800, Number(targetRating) || 1800));
            const anchors = RatingProfile.anchors;
            if (rating <= anchors[0].rating) return { ...anchors[0] };
            if (rating >= anchors[anchors.length - 1].rating) return { ...anchors[anchors.length - 1] };
            for (let index = 1; index < anchors.length; index++) {
                if (rating > anchors[index].rating) continue;
                const left = anchors[index - 1];
                const right = anchors[index];
                return RatingProfile._blend(left, right, (rating - left.rating) / (right.rating - left.rating));
            }
            return { ...anchors[anchors.length - 1] };
        },

        // Map target rating to all humanization parameters.
        //
        // These are behavioral targets rather than promises of an exact platform
        // rating. A lower-depth engine is not, by itself, a human model; the
        // anchored bundles jointly tune move quality, error type and time use.
        apply(targetRating) {
            const r = Math.max(800, Math.min(2800, Number(targetRating) || 1800));
            const profile = RatingProfile.profileFor(r);
            const style = RatingProfile.styles[CONFIG.playStyle] || RatingProfile.styles.universal;

            CONFIG.engineDepth.base = Math.round(profile.depth);
            CONFIG.engineDepth.min = Math.max(8, CONFIG.engineDepth.base - 3);
            CONFIG.engineDepth.max = Math.min(22, CONFIG.engineDepth.base + 3);

            const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
            const h = CONFIG.humanization;
            h.targetEngineCorrelation = profile.correlation;
            h.suboptimalMoveRate.opening = clamp(profile.suboptimal[0] * style.opening, 0.03, 0.55);
            h.suboptimalMoveRate.middlegame = clamp(profile.suboptimal[1] * style.middlegame, 0.03, 0.55);
            h.suboptimalMoveRate.endgame = clamp(profile.suboptimal[2] * style.endgame, 0.025, 0.50);
            h.maxAcceptableCPLoss.opening = Math.round(profile.cpLoss[0]);
            h.maxAcceptableCPLoss.middlegame = Math.round(profile.cpLoss[1]);
            h.maxAcceptableCPLoss.endgame = Math.round(profile.cpLoss[2]);
            h.blunder.chance = profile.blunder;
            h.blunder.maxCPLoss = Math.round(profile.blunderLoss);
            h.bookUseRate = profile.bookRate;
            h.tablebaseUseRate = profile.tablebaseRate;
            h.playerMoveDB.preferRate = profile.dbRate;
            h.losingSharpness.suboptimalReduction = clamp(0.70 - (r - 800) / 4000, 0.20, 0.70);

            CONFIG.acplGovernor.targetACPL = Math.round(profile.acpl);
            h.streaks.perfectStreakMax = Math.round(3 + (r - 800) / 500);
            h.streaks.sloppyStreakMax = r >= 2300 ? 1 : (r >= 1500 ? 2 : 3);
            h.antiCorrelation.maxTopMoveRate = profile.topCap;
            h.antiCorrelation.closeEvalPreferRate = clamp(0.34 - (r - 800) / 10000, 0.14, 0.34);

            CONFIG.timing.base.min = Math.round(profile.timing[0]);
            CONFIG.timing.base.max = Math.round(profile.timing[1]);
            CONFIG.timing.complex.min = Math.round(profile.timing[2]);
            CONFIG.timing.complex.max = Math.round(profile.timing[3]);
            CONFIG.timing.longThink.chance = profile.longThink;
            h.timingAccuracyCoupling.fastMoveSuboptimalBoost = profile.fastError;
            h.timingAccuracyCoupling.slowMoveBestBoost = profile.slowBest;
            h.accuracyClustering.hotStreakChance = profile.hot;
            h.accuracyClustering.coldStreakChance = profile.cold;

            CONFIG.shallowDepth.chance = profile.shallow;
            CONFIG.shallowDepth.preferLongPVChance = clamp(0.62 - (r - 800) / 5000, 0.22, 0.62);
            CONFIG.shallowDepth.longPVThreshold = Math.round(clamp(6 + (r - 800) / 400, 6, 11));
            CONFIG.motifBlindness.zwischenzugMissChance = clamp(0.18 * profile.motif, 0.02, 0.35);
            CONFIG.motifBlindness.longDiagonalMissChance = clamp(0.12 * profile.motif, 0.015, 0.25);
            CONFIG.motifBlindness.backwardsKnightMissChance = clamp(0.15 * profile.motif, 0.02, 0.30);
            CONFIG.motifBlindness.deflectionMissChance = clamp(0.20 * profile.motif, 0.025, 0.38);
            CONFIG.endgameTechnique.kpMistakeMult = profile.endgame[0];
            CONFIG.endgameTechnique.rpMistakeMult = profile.endgame[1];
            CONFIG.endgameTechnique.simpleMistakeMult = profile.endgame[2];
            CONFIG.repertoireHard.deviationRate = profile.repertoireDeviation;
            CONFIG.antiDetection.randomLegalMoveChance = profile.randomMove;
            CONFIG.blunderBias.simpleErrorMult = clamp(0.68 - (r - 800) / 5000, 0.28, 0.68);
            CONFIG.blunderBias.criticalErrorMult = clamp(1.25 + (r - 800) / 3000, 1.25, 1.90);
            CONFIG.forcedMove.instantMs.min = Math.round(clamp(520 - (r - 800) * 0.15, 200, 520));
            CONFIG.forcedMove.instantMs.max = Math.round(clamp(1250 - (r - 800) * 0.30, 650, 1250));
            CONFIG.engineRotation.shallowDepth = Math.max(9, Math.round(profile.depth - 2));
            State.human.ratingProfile = { rating: r, label: r >= 2600 ? 'grandmaster' : r >= 2300 ? 'master' : r >= 1900 ? 'expert' : r >= 1500 ? 'club' : 'developing' };
            State.human.activeProfile = { ...profile, rating: r };

            Utils.log(`Rating profile applied: ${r} ELO ${State.human.ratingProfile.label} (${CONFIG.playStyle}) | target corr ${(h.targetEngineCorrelation*100).toFixed(0)}% | top cap ${(h.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',
        liveControls: {
            draw: ['button[aria-label="Draw"]', '.draw-button-component', 'button[data-cy="draw-button"]', '.board-controls-btn-draw'],
            resign: ['button[aria-label="Resign"]', '.resign-button-component', 'button[data-cy="resign-button"]', '.board-controls-btn-resign'],
        },
        replayControls: [
            'button[aria-label="First Move"]',
            'button[aria-label="Previous Move"]',
            'button[aria-label="Play / Pause"]',
            'button[aria-label="Next Move"]',
            'button[aria-label="Last Move"]',
        ],
        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,
        apiRetryAt: 0,
        // Per-game engine override (set by Account.rollEngineForGame).
        // 'api' | 'stockfish_online' | 'local_full' | 'local_shallow' | null
        gameEngineOverride: null,
        isThinking: false,
        analysisId: 0,
        lastFen: null,
        lastHandledFen: 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,
            maxMyTime: null,
            maxOppTime: null,
        },
        // Recent move timings for sequence variation
        recentTimings: [],
        diagnostics: {
            providers: {},
            cacheHits: 0,
            lastCacheHit: false,
            staleAborts: 0,
            inputRetries: 0,
            inputFailures: 0,
            inputSuperseded: 0,
            inputWatchdogs: 0,
            inputMethods: {
                click: { attempts: 0, confirmed: 0, dispatchFailures: 0, timeouts: 0, confirmMs: 0 },
                drag: { attempts: 0, confirmed: 0, dispatchFailures: 0, timeouts: 0, confirmMs: 0 },
            },
            confirmationSources: {},
            lastInput: null,
            context: null,
            positionData: { requests: 0, deduped: 0, cacheHits: 0, misses: 0, failures: 0, rateLimits: 0 },
            afk: { heartbeats: 0, recoveries: 0, queueRetries: 0, lastBeatAt: 0, lastRecovery: null, realtime: 'off' },
            humanModel: { choices: 0, probabilitySum: 0, cpLossSum: 0, last: null },
        },
        input: {
            sequence: 0,
            active: null,
            history: [],
        },
        // Humanization tracking
        human: {
            ratingProfile: null,
            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,
            predictedReplyMatched: null,
            predictedReplyForced: false,
            // 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
            // v17.3 calibrated-model context
            activeProfile: null,
            timeControl: null,
            moveHistory: [],
            lastChoiceExplanation: null,
        },
        // Player move DB cache (FEN -> moves array)
        playerDBCache: new Map(),
        // Shared Classic Chess response cache and in-flight requests.
        positionDataCache: new Map(),
        positionDataInflight: new Map(),
        // Track if last move was a capture (for recapture detection)
        lastMoveWasCapture: false,
        lastCaptureSquare: null,
        // 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,

        isVisible: (el) => {
            if (!el || el.isConnected === false || el.hidden) return false;
            const rect = el.getBoundingClientRect?.();
            if (rect && (rect.width <= 0 || rect.height <= 0)) return false;
            try {
                if (typeof getComputedStyle === 'function') {
                    const style = getComputedStyle(el);
                    if (style.display === 'none' || style.visibility === 'hidden' || Number(style.opacity) === 0) return false;
                }
            } catch (e) { /* geometry is enough when style access is unavailable */ }
            return el.offsetParent !== null || !rect || (rect.width > 0 && rect.height > 0);
        },

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

    // ═══════════════════════════════════════════
    //  HUMAN MOVE DATABASE — public Classic Chess master archive
    // ═══════════════════════════════════════════
    const PositionDataStore = {
        cooldownUntil: 0,
        maxEntries: 512,
        positiveTtlMs: 10 * 60 * 1000,
        negativeTtlMs: 60 * 1000,

        key: (fen) => String(fen || '').split(' ').slice(0, 4).join(' '),

        _cacheSet: (key, data, ttlMs) => {
            if (State.positionDataCache.has(key)) State.positionDataCache.delete(key);
            while (State.positionDataCache.size >= PositionDataStore.maxEntries) {
                State.positionDataCache.delete(State.positionDataCache.keys().next().value);
            }
            State.positionDataCache.set(key, { data, expiresAt: Date.now() + ttlMs });
        },

        _retryAfterMs: (response) => {
            const seconds = String(response?.responseHeaders || '').match(/^retry-after:\s*(\d+)/im)?.[1];
            // Classic Chess documents a rolling 15-minute public budget. If a
            // proxy strips Retry-After, wait out the full window.
            return seconds ? Number(seconds) * 1000 : 15 * 60 * 1000;
        },

        fetch: (fen, timeout = 4000) => {
            const key = PositionDataStore.key(fen);
            if (!key || Date.now() < PositionDataStore.cooldownUntil) return Promise.resolve(null);

            const cached = State.positionDataCache.get(key);
            if (cached && cached.expiresAt > Date.now()) {
                State.positionDataCache.delete(key);
                State.positionDataCache.set(key, cached);
                State.diagnostics.positionData.cacheHits++;
                return Promise.resolve(cached.data);
            }
            if (cached) State.positionDataCache.delete(key);

            const inflight = State.positionDataInflight.get(key);
            if (inflight) {
                State.diagnostics.positionData.deduped++;
                return inflight;
            }

            State.diagnostics.positionData.requests++;
            const url = `https://classicchess.com/api/v1/opening-explorer/?fen=${encodeURIComponent(fen)}&moves=12&topGames=0`;
            const promise = (async () => {
                try {
                    const response = await APIEngine._requestJSON({
                        method: 'GET',
                        url,
                        timeout,
                        headers: { 'User-Agent': 'REXXX-MENU/17.3 opening-explorer' },
                    });
                    if (response.status === 429) {
                        const cooldownMs = PositionDataStore._retryAfterMs(response);
                        PositionDataStore.cooldownUntil = Date.now() + cooldownMs;
                        State.diagnostics.positionData.rateLimits++;
                        Utils.log(`Position data rate limited; cooling down ${Math.ceil(cooldownMs / 1000)}s`, 'warn');
                        return null;
                    }
                    if (response.status === 404) {
                        State.diagnostics.positionData.misses++;
                        PositionDataStore._cacheSet(key, null, PositionDataStore.negativeTtlMs);
                        return null;
                    }
                    if (response.status < 200 || response.status >= 300) throw new Error(`Classic Chess HTTP ${response.status}`);
                    const data = APIEngine._data(response);
                    if (!data?.ok || !Array.isArray(data.moves)) throw new Error('Classic Chess returned an invalid schema');
                    data.moves = data.moves.filter(move => /^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(move?.uci || ''));
                    if (!data.moves.length) {
                        State.diagnostics.positionData.misses++;
                        PositionDataStore._cacheSet(key, null, PositionDataStore.negativeTtlMs);
                        return null;
                    }
                    PositionDataStore._cacheSet(key, data, PositionDataStore.positiveTtlMs);
                    return data;
                } catch (error) {
                    if (!/aborted/i.test(error?.message || '')) {
                        State.diagnostics.positionData.failures++;
                        Utils.log(`Position data unavailable: ${error?.message || error}`, 'debug');
                    }
                    return null;
                } finally {
                    State.positionDataInflight.delete(key);
                }
            })();
            State.positionDataInflight.set(key, promise);
            return promise;
        },
    };

    const PlayerMoveDB = {
        cacheKey: (fen) => {
            return PositionDataStore.key(fen);
        },

        derivedCacheKey: (fen) => `${PlayerMoveDB.cacheKey(fen)}|${State.playerColor || 'w'}`,

        getCached: (fen) => State.playerDBCache.get(PlayerMoveDB.derivedCacheKey(fen)) || null,

        _cacheSet: (key, value) => {
            if (State.playerDBCache.has(key)) State.playerDBCache.delete(key);
            while (State.playerDBCache.size >= 512) {
                State.playerDBCache.delete(State.playerDBCache.keys().next().value);
            }
            State.playerDBCache.set(key, value);
        },

        fetch: async (fen) => {
            const cfg = CONFIG.humanization.playerMoveDB;
            if (!cfg.enabled) return null;

            const cacheKey = PlayerMoveDB.derivedCacheKey(fen);
            if (State.playerDBCache.has(cacheKey)) {
                const cached = State.playerDBCache.get(cacheKey);
                State.playerDBCache.delete(cacheKey);
                State.playerDBCache.set(cacheKey, cached);
                return cached;
            }
            const data = await PositionDataStore.fetch(fen, cfg.timeout);
            if (!data?.moves?.length) {
                PlayerMoveDB._cacheSet(cacheKey, null);
                return null;
            }
            const validMoves = data.moves
                .map(m => {
                    const games = Number(m.games ?? (Number(m.white || 0) + Number(m.draws || 0) + Number(m.black || 0)));
                    return {
                        uci: m.uci,
                        games,
                        winRate: (Number(State.playerColor === 'b' ? m.black : m.white) + Number(m.draws || 0) * 0.5) /
                            Math.max(1, games),
                    };
                })
                .filter(m => m.games >= cfg.minGames);
            PlayerMoveDB._cacheSet(cacheKey, validMoves.length ? validMoves : null);
            if (!validMoves.length) return null;
            Utils.log(`PlayerMoveDB: ${validMoves.length} human master moves (${data.position?.totalGames || 'many'} games)`, 'debug');
            return validMoves;
        },

        // 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 v17.3.0</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">ENGINE INITIALIZING</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" title="Keep opponent adaptation inside the selected skill band and suppress intentional-loss collapses.">
                            <span class="ba-label">Rating Fidelity</span>
                            <div class="ba-checkbox ${CONFIG.humanization.ratingFidelity.enabled ? 'checked' : ''}" id="toggle-rating-fidelity"></div>
                        </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' : ''}>Cloud MultiPV + SF18</option>
                                <option value="stockfish_online" ${CONFIG.engineType === 'stockfish_online' ? 'selected' : ''}>stockfish.online (SF17.1)</option>
                                <option value="local" ${CONFIG.engineType === 'local' ? 'selected' : ''}>Local SF18 Lite WASM</option>
                            </select>
                        </div>
                        <div class="ba-row" title="Live provider latency, cache use, and recovery status.">
                            <span class="ba-label">Engine Health</span>
                            <span class="ba-value" id="engine-health">Initializing…</span>
                        </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-row" title="Worker heartbeat, timer-gap recovery, stale-lock cleanup, and non-blocking session breaks for background play.">
                            <span class="ba-label">AFK Session Guard</span>
                            <div class="ba-checkbox ${CONFIG.auto.afkGuard ? 'checked' : ''}" id="toggle-afk-guard"></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">Max Rating Boost</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 a public master-game archive to diversify among moves humans played in the same position.">
                            <span class="ba-label">Human 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" title="The effective rating bundle active for this game.">
                            <span class="ba-stats-label">Active Profile</span>
                            <span class="ba-stats-value" id="stat-profile">---</span>
                        </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" title="Parsed pool plus the maximum safe spend for this position.">
                            <span class="ba-stats-label">Time Model</span>
                            <span class="ba-stats-value" id="stat-time-model">---</span>
                        </div>
                        <div class="ba-stats-row" title="Last sampled move probability and estimated centipawn loss.">
                            <span class="ba-stats-label">Human Model</span>
                            <span class="ba-stats-value" id="stat-human-model">---</span>
                        </div>
                        <div class="ba-stats-row" title="Background keepalive state and recovery count.">
                            <span class="ba-stats-label">AFK Guard</span>
                            <span class="ba-stats-value" id="stat-afk">---</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-rating-fidelity', () => CONFIG.humanization.ratingFidelity.enabled, (v) => {
                CONFIG.humanization.ratingFidelity.enabled = v;
                if (v) {
                    CONFIG.autoLose.enabled = false;
                    panel.querySelector('#toggle-auto-lose')?.classList.remove('checked');
                }
            });
            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-afk-guard', () => CONFIG.auto.afkGuard, (v) => {
                CONFIG.auto.afkGuard = v;
                if (v) AFKSupervisor.start();
                else AFKSupervisor.stop();
            });
            toggle('toggle-auto-lose', () => CONFIG.autoLose.enabled, (v) => {
                CONFIG.autoLose.enabled = v;
                if (v) {
                    CONFIG.humanization.ratingFidelity.enabled = false;
                    panel.querySelector('#toggle-rating-fidelity')?.classList.remove('checked');
                }
            });
            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');
            const health = State.ui.panel.querySelector('#engine-health');

            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',
                'chess-api': 'chess-api.com SF18',
                'lichess-cloud': 'Lichess Cloud MultiPV',
                'stockfish_online': 'SF Online',
                'stockfish-online': 'SF Online 17.1',
                'local': 'Local SF18 Lite',
                'tablebase': 'Syzygy TB',
                'book': 'Opening Book',
            };
            if (badge && State._lastEngineSource) {
                badge.textContent = engineNames[State._lastEngineSource] || State._lastEngineSource;
            }
            if (health) {
                const source = State._lastEngineSource;
                const provider = source ? State.diagnostics.providers[source] : null;
                const activeInput = State.input.active;
                const lastInput = State.diagnostics.lastInput;
                const clickStats = State.diagnostics.inputMethods.click;
                const dragStats = State.diagnostics.inputMethods.drag;
                const inputDetails = `click ${clickStats.confirmed}/${clickStats.attempts}, drag ${dragStats.confirmed}/${dragStats.attempts}; ` +
                    `${State.diagnostics.inputRetries} retries, ${State.diagnostics.inputFailures} failures, ${State.diagnostics.inputWatchdogs} watchdog recoveries`;
                const afkDetails = `AFK ${State.diagnostics.afk.realtime}, ${State.diagnostics.afk.recoveries} recoveries; ` +
                    `position data ${State.diagnostics.positionData.cacheHits} cache hits/${State.diagnostics.positionData.deduped} deduped`;
                if (activeInput && !activeInput.terminal) {
                    health.textContent = `Input ${activeInput.attempt || 1}/3 · ${activeInput.state}`;
                    health.style.color = activeInput.state === 'retrying' ? '#ffca6b' : '#81c784';
                    health.title = `${activeInput.move}: ${activeInput.method || 'preparing'}; ${inputDetails}; ${afkDetails}`;
                } else if (State.diagnostics.lastCacheHit && source) {
                    health.textContent = `Cache · ${State.diagnostics.cacheHits} hits`;
                    health.style.color = '#81c784';
                    health.title = `${engineNames[source] || source}: reused a fresh cached analysis; ${inputDetails}; ${afkDetails}`;
                } else if (provider?.ok) {
                    health.textContent = `${Math.round(provider.latencyMs)} ms · ${provider.successes} ok`;
                    health.style.color = provider.latencyMs < 1500 ? '#81c784' : '#ffca6b';
                    health.title = `${engineNames[source] || source}: ${provider.successes} successful, ${provider.failures} failed; ${State.diagnostics.cacheHits} cache hits; ${inputDetails}; ${afkDetails}${lastInput?.reason ? `; last input: ${lastInput.reason}` : ''}`;
                } else if (source === 'book' || source === 'tablebase') {
                    health.textContent = source === 'book' ? 'Instant · cached theory' : 'Exact · solved position';
                    health.style.color = '#81c784';
                    health.title = `${State.diagnostics.cacheHits} engine cache hits this session`;
                } else if (!State.apiAvailable && State.apiRetryAt > Date.now()) {
                    health.textContent = `Recovering · retry ${Math.ceil((State.apiRetryAt - Date.now()) / 1000)}s`;
                    health.style.color = '#ffca6b';
                } else {
                    health.textContent = source ? 'Ready' : 'Initializing…';
                    health.style.color = '#aaa';
                    health.title = `${State.diagnostics.cacheHits} cache hits · ${State.diagnostics.staleAborts} stale searches cancelled · ${inputDetails}${lastInput?.reason ? ` · last input: ${lastInput.reason}` : ''}`;
                }
            }

            // 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.ratingFidelity.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',
            'auto.afkGuard',
            '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-profile', h.ratingProfile
                ? `${Math.round(h.ratingProfile.rating)} · ${h.ratingProfile.label}${CONFIG.humanization.ratingFidelity.enabled ? ' · fidelity' : ''}`
                : '---');
            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');
            const tc = h.timeControl;
            setVal('stat-time-model', tc
                ? `${TimeControlContext.describe()} · ${Math.round(TimeControlContext.maxSpendMs(State.lastFen))}ms cap`
                : 'detecting');
            const modelLast = h.lastChoiceExplanation;
            setVal('stat-human-model', modelLast
                ? `${modelLast.reason} · ${(modelLast.probability * 100).toFixed(0)}% · ${Math.round(modelLast.cpLoss)}cp`
                : 'waiting');
            setVal('stat-afk', CONFIG.auto.afkGuard
                ? `${State.diagnostics.afk.realtime} · ${State.diagnostics.afk.recoveries} rec.`
                : 'off');
            // 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 = {
        isSupportedContext: () => {
            let path = '';
            try { path = location.pathname || new URL(location.href).pathname; } catch (e) {}
            // Chess.com reuses the same board component in analysis, puzzles,
            // lessons and drills. Never treat those boards as a live game.
            return !/(^|\/)(analysis|puzzles?|lessons?|vision|drills?|practice|library)(\/|$)/i.test(path);
        },

        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) {}
                const orientation = String(b.getAttribute?.('orientation') || b.dataset?.orientation || '').toLowerCase();
                if (orientation === 'black' || orientation === 'b') return 'b';
                if (orientation === 'white' || orientation === 'w') return 'w';
                if (String(b.className || '').toLowerCase().includes('flipped')) return 'b';
            }
            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;
        },

        _fenPieces: (fen) => {
            const pieces = new Map();
            if (!fen) return pieces;
            const rows = fen.split(' ')[0].split('/');
            for (let row = 0; row < 8; row++) {
                let file = 0;
                for (const token of rows[row] || '') {
                    if (/\d/.test(token)) {
                        file += Number(token);
                    } else {
                        pieces.set(`${String.fromCharCode(97 + file)}${8 - row}`, token);
                        file++;
                    }
                }
            }
            return pieces;
        },

        transitionMatchesMove: (startingFen, observedFen, move) => {
            if (!Game.isValidFen(startingFen) || !Game.isValidFen(observedFen) ||
                !/^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(move || '')) return false;
            const startParts = startingFen.split(' ');
            const observedParts = observedFen.split(' ');
            if (startParts[1] === observedParts[1]) return false;

            const from = move.substring(0, 2).toLowerCase();
            const to = move.substring(2, 4).toLowerCase();
            const promotion = move[4]?.toLowerCase() || null;
            const before = Game._fenPieces(startingFen);
            const after = Game._fenPieces(observedFen);
            const movingPiece = before.get(from);
            const arrivedPiece = after.get(to);
            if (!movingPiece || !arrivedPiece) return false;
            if ((movingPiece === movingPiece.toUpperCase()) !== (arrivedPiece === arrivedPiece.toUpperCase())) return false;
            if (after.get(from) === movingPiece) return false;

            const expectedType = promotion || movingPiece.toLowerCase();
            return arrivedPiece.toLowerCase() === expectedType;
        },

        moveRecordSignature: (record) => {
            if (!record) return '';
            return [record.from || '', record.to || '', record.promotion || '', record.notation || ''].join('|').toLowerCase();
        },

        recordMatchesMove: (record, move) => {
            if (!record || !/^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(move || '')) return false;
            const expectedFrom = move.substring(0, 2).toLowerCase();
            const expectedTo = move.substring(2, 4).toLowerCase();
            if (String(record.from || '').toLowerCase() !== expectedFrom ||
                String(record.to || '').toLowerCase() !== expectedTo) return false;
            const expectedPromotion = move[4]?.toLowerCase();
            return !expectedPromotion || !record.promotion || String(record.promotion).toLowerCase() === expectedPromotion;
        },

        isCapture: (move, fen = Game.getFen()) => {
            if (!move || move.length < 4) return false;
            const from = move.substring(0, 2);
            const to = move.substring(2, 4);
            const pieces = Game._fenPieces(fen);
            if (pieces.has(to)) return true;

            // En passant: a pawn moves diagonally to the FEN en-passant square.
            const moving = pieces.get(from)?.toLowerCase();
            const epSquare = fen?.split(' ')[3];
            if (moving === 'p' && from[0] !== to[0] && epSquare === to) return true;

            // Last-resort DOM lookup for incomplete/non-standard FEN providers.
            const board = Game.getBoard();
            if (!board) return false;
            return !!board.querySelector(`.piece.square-${Game.squareToCoords(to)}`);
        },

        _moveRecord: (entry) => {
            if (!entry) return null;
            if (typeof entry === 'string') {
                const uci = entry.match(/^([a-h][1-8])([a-h][1-8])[qrbn]?$/i);
                return {
                    notation: entry,
                    from: uci?.[1] || null,
                    to: uci?.[2] || null,
                    promotion: uci && entry.length > 4 ? entry[4].toLowerCase() : null,
                    isCapture: entry.includes('x'),
                };
            }
            const notation = entry.san || entry.notation || entry.algebraic || entry.move?.san || '';
            const from = entry.from || entry.move?.from || null;
            const to = entry.to || entry.move?.to || null;
            const promotion = entry.promotion || entry.move?.promotion || String(notation).match(/=([QRBN])/i)?.[1] || null;
            const flags = String(entry.flags || entry.move?.flags || '');
            const captured = entry.captured ?? entry.move?.captured;
            return {
                notation: String(notation),
                from: typeof from === 'string' ? from : null,
                to: typeof to === 'string' ? to : null,
                promotion: promotion ? String(promotion).toLowerCase() : null,
                isCapture: Boolean(captured) || flags.includes('c') || flags.includes('e') || String(notation).includes('x'),
            };
        },

        // Read the final SAN move from Chess.com's game model first and the
        // rendered move list second. The FEN delta below remains a resilient
        // fallback for UI/model revisions.
        getLastMoveRecord: () => {
            const game = Game.getBoardGame();
            if (game) {
                for (const method of ['getHistory', 'getMoveHistory', 'getCurrentMove']) {
                    try {
                        if (typeof game[method] !== 'function') continue;
                        const value = game[method]();
                        const entry = Array.isArray(value) ? value[value.length - 1] : value;
                        const record = Game._moveRecord(entry);
                        if (record?.notation || record?.to) return record;
                    } catch (e) { /* try the next source */ }
                }
            }

            const moveList = Utils.query(SELECTORS.moves);
            if (!moveList) return null;
            const selectors = [
                '[data-san]', '[data-uci]', '.move-text-component', '.move-text',
                '.node-highlight-content', '[class*="move-text"]', '[class*="node-san"]',
            ];
            for (const selector of selectors) {
                const nodes = moveList.querySelectorAll(selector);
                if (!nodes.length) continue;
                const node = nodes[nodes.length - 1];
                const notation = node.dataset?.san || node.dataset?.uci || node.textContent?.trim();
                if (notation) return Game._moveRecord(notation);
            }

            // Text-only fallback for web-component variants.
            const tokens = (moveList.textContent || '').match(/(?:O-O-O|O-O|[KQRBN]?[a-h]?[1-8]?x?[a-h][1-8](?:=[QRBN])?[+#]?)/g);
            return tokens?.length ? Game._moveRecord(tokens[tokens.length - 1]) : null;
        },

        getLastCaptureInfo: (previousFen, currentFen) => {
            const record = Game.getLastMoveRecord();
            if (record?.isCapture) {
                const sanTarget = record.notation.match(/x([a-h][1-8])/i)?.[1];
                const uciTarget = record.notation.match(/^[a-h][1-8]([a-h][1-8])/i)?.[1];
                const square = record.to || sanTarget || uciTarget || null;
                if (square) return { isCapture: true, square, source: 'move-list' };
            }

            if (!previousFen || !currentFen) return { isCapture: false, square: null, source: 'none' };
            const before = Game._fenPieces(previousFen);
            const after = Game._fenPieces(currentFen);
            if (after.size >= before.size) return { isCapture: false, square: null, source: 'fen' };

            const sideToMove = currentFen.split(' ')[1];
            const moverIsWhite = sideToMove === 'b';
            const isMover = piece => piece && (piece === piece.toUpperCase()) === moverIsWhite;
            let emptyDestination = null;
            for (const [square, piece] of after) {
                if (!isMover(piece) || before.get(square) === piece) continue;
                const previousPiece = before.get(square);
                if (previousPiece && !isMover(previousPiece)) {
                    return { isCapture: true, square, source: 'fen' };
                }
                if (!previousPiece) emptyDestination = square; // en passant fallback
            }
            return { isCapture: true, square: emptyDestination, source: 'fen' };
        },

        // Read clock time from the DOM
        readClock: () => {
            try {
                // Do not use a selector union here: modern Chess.com nests the
                // monospace time inside the clock component, which otherwise
                // yields two copies of each clock and reads our time twice.
                let clockEls = [];
                for (const selector of ['[data-cy="clock-time"]', '.clock-time-monospace', '.clock-component']) {
                    const visible = Array.from(document.querySelectorAll(selector))
                        .filter(el => el.getBoundingClientRect().height > 0);
                    if (visible.length >= 2) {
                        clockEls = visible;
                        break;
                    }
                }
                if (clockEls.length < 2) return;
                const sorted = clockEls.sort((a, b) => {
                    const ra = a.getBoundingClientRect();
                    const rb = b.getBoundingClientRect();
                    return rb.top - ra.top; // bottom first
                });
                const uniqueRows = sorted.filter((el, index, all) => {
                    const top = el.getBoundingClientRect().top;
                    return all.findIndex(other => Math.abs(other.getBoundingClientRect().top - top) < 4) === index;
                });
                if (uniqueRows.length < 2) return;
                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(uniqueRows[0]);
                State.clock.oppTime = parseTime(uniqueRows[1]);
                if (Number.isFinite(State.clock.myTime)) {
                    State.clock.maxMyTime = Math.max(State.clock.maxMyTime || 0, State.clock.myTime);
                }
                if (Number.isFinite(State.clock.oppTime)) {
                    State.clock.maxOppTime = Math.max(State.clock.maxOppTime || 0, State.clock.oppTime);
                }
                TimeControlContext.refresh();
            } 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;
            }
        },
    };

    // Positive, explainable context detection. URL exclusions remain the first
    // gate, while live controls and the board model prove that this is a game in
    // which the signed-in player can actually move.
    const GameContext = {
        _visibleAny: (selectors) => {
            for (const selector of selectors || []) {
                const nodes = document.querySelectorAll(selector);
                for (const node of nodes) if (Utils.isVisible(node)) return true;
            }
            return false;
        },

        snapshot: () => {
            const routeSupported = Game.isSupportedContext();
            const board = Game.getBoard();
            const boardReady = !!board && Utils.isVisible(board);
            const gameOver = GameContext._visibleAny(SELECTORS.gameOver);
            const hasDraw = GameContext._visibleAny(SELECTORS.liveControls.draw);
            const hasResign = GameContext._visibleAny(SELECTORS.liveControls.resign);
            const liveControls = hasDraw && hasResign;
            const replayControls = (SELECTORS.replayControls || []).reduce((count, selector) =>
                count + (GameContext._visibleAny([selector]) ? 1 : 0), 0);

            let modelPlayer = false;
            const game = Game.getBoardGame();
            if (game) {
                try {
                    const playingAs = typeof game.getPlayingAs === 'function' ? game.getPlayingAs() : null;
                    modelPlayer = playingAs === 1 || playingAs === 2 || playingAs === 'w' || playingAs === 'b' ||
                        playingAs === 'white' || playingAs === 'black';
                } catch (e) { /* live controls remain the primary signal */ }
            }

            const active = routeSupported && boardReady && !gameOver && (liveControls || modelPlayer);
            const review = routeSupported && boardReady && !active && replayControls >= 3;
            const spectator = routeSupported && boardReady && !active && !review && !gameOver;
            let reason = 'active-player-game';
            if (!routeSupported) reason = 'unsupported-route';
            else if (!boardReady) reason = 'board-not-ready';
            else if (gameOver) reason = 'game-over';
            else if (review) reason = 'review-navigation';
            else if (spectator) reason = 'no-player-controls';

            return {
                active, review, spectator, gameOver, boardReady, liveControls,
                modelPlayer, replayControls, reason,
            };
        },
    };

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

        fetchMove: async (fen) => {
            if (!CONFIG.useBook) return null;
            const data = await PositionDataStore.fetch(fen, 4000);
            if (!data?.moves?.length) return null;

            const color = State.playerColor || 'w';
            const posKey = PositionDataStore.key(fen);

            // 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];
                if (data.moves.some(move => move.uci === prevMove)) {
                    Utils.log(`Book: Repertoire hit - playing ${prevMove} again`, 'debug');
                    return prevMove;
                }
            }

            const topMoves = data.moves.slice(0, 3);
            const gameCount = move => Number(move.games ??
                (Number(move.white || 0) + Number(move.draws ?? move.draw ?? 0) + Number(move.black || 0)));
            const totalGames = topMoves.reduce((sum, move) => sum + Math.max(0, gameCount(move)), 0);
            if (totalGames <= 0) return topMoves[0]?.uci || null;
            let roll = Math.random() * totalGames;
            let chosen = topMoves[0].uci;
            for (const move of topMoves) {
                const games = Math.max(0, gameCount(move));
                if (roll < games) {
                    chosen = move.uci;
                    break;
                }
                roll -= games;
            }
            OpeningBook._repertoire[color][posKey] = chosen;
            return chosen;
        }
    };

    // ═══════════════════════════════════════════
    //  SYZYGY TABLEBASE
    // ═══════════════════════════════════════════
    const Tablebase = {
        cache: new Map(),
        maxEntries: 128,
        ttlMs: 60 * 60 * 1000,
        cooldownUntil: 0,

        _cacheKey: fen => String(fen || '').split(' ').slice(0, 5).join(' '),

        _cacheSet: (key, result) => {
            if (Tablebase.cache.has(key)) Tablebase.cache.delete(key);
            while (Tablebase.cache.size >= Tablebase.maxEntries) {
                Tablebase.cache.delete(Tablebase.cache.keys().next().value);
            }
            Tablebase.cache.set(key, { result, expiresAt: Date.now() + Tablebase.ttlMs });
        },

        _desiredChildCategory: category => ({
            win: 'loss',
            'cursed-win': 'blessed-loss',
            draw: 'draw',
            'blessed-loss': 'cursed-win',
            loss: 'win',
        })[category],

        probe: async (fen) => {
            if (!CONFIG.useTablebase) return null;
            const pieceCount = Utils.countPieces(fen);
            if (pieceCount > CONFIG.tablebase.maxPieces) return null;
            if (Date.now() < Tablebase.cooldownUntil) return null;

            const cacheKey = Tablebase._cacheKey(fen);
            const cached = Tablebase.cache.get(cacheKey);
            if (cached && cached.expiresAt > Date.now()) {
                Tablebase.cache.delete(cacheKey);
                Tablebase.cache.set(cacheKey, cached);
                return cached.result;
            }
            if (cached) Tablebase.cache.delete(cacheKey);

            Utils.log(`Tablebase: Probing ${pieceCount}-piece position`, 'debug');
            try {
                const response = await APIEngine._requestJSON({
                    method: 'GET',
                    url: `${CONFIG.tablebase.url}?fen=${encodeURIComponent(fen)}`,
                    timeout: 5000,
                });
                if (response.status === 404) return null;
                APIEngine._assertSuccess(response, 'Lichess tablebase');
                const data = APIEngine._data(response);
                if (!Array.isArray(data?.moves) || !data.moves.length) return null;

                // Move categories describe the resulting position from the
                // opponent's perspective. Preserve the root WDL by inversion.
                const desiredChild = Tablebase._desiredChildCategory(data.category);
                const validMoves = data.moves.filter(move =>
                    /^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(move?.uci || '') && typeof move.category === 'string');
                const optimal = validMoves.filter(move => move.category === desiredChild);
                const best = optimal.length ? optimal : validMoves;
                if (!best.length) return null;
                const candidates = best.slice(0, 3);
                const chosen = candidates[Math.floor(Math.random() * candidates.length)];
                Utils.log(`Tablebase: ${chosen.uci} (root ${data.category}, child ${chosen.category}, DTZ: ${data.dtz})`, 'info');
                const result = {
                    move: chosen.uci,
                    category: data.category,
                    childCategory: chosen.category,
                    dtz: data.dtz,
                    dtm: data.dtm,
                };
                Tablebase._cacheSet(cacheKey, result);
                return result;
            } catch (error) {
                if (error?.retryAfterMs) Tablebase.cooldownUntil = Date.now() + error.retryAfterMs;
                if (!/aborted/i.test(error?.message || '')) Utils.log(`Tablebase unavailable: ${error?.message || error}`, 'debug');
                return null;
            }
        }
    };

    // ═══════════════════════════════════════════
    //  API ENGINE (chess-api.com / stockfish.online)
    // ═══════════════════════════════════════════
    const APIEngine = {
        _requests: new Set(),

        // Tampermonkey 5.5 keeps GM_xmlhttpRequest abortable under MV3. Wrap it
        // once so stale position requests can be cancelled and every provider
        // gets consistent JSON/status/error handling.
        _requestJSON: (details) => {
            let handle = null;
            let settled = false;
            const requestDetails = { ...details };
            if (requestDetails.responseType === 'text') delete requestDetails.responseType;
            else requestDetails.responseType = requestDetails.responseType || 'json';
            const promise = new Promise((resolve, reject) => {
                const finish = (fn, value) => {
                    if (settled) return;
                    settled = true;
                    fn(value);
                };
                handle = GM_xmlhttpRequest({
                    ...requestDetails,
                    onload: response => finish(resolve, response),
                    onerror: error => finish(reject, new Error(error?.error || error?.statusText || 'network error')),
                    ontimeout: () => finish(reject, new Error('timeout')),
                    onabort: () => finish(reject, new Error('aborted')),
                });
            });
            promise.abort = () => {
                try { handle?.abort(); } catch (e) { /* already complete */ }
            };
            APIEngine._requests.add(promise);
            const cleanup = () => APIEngine._requests.delete(promise);
            promise.then(cleanup, cleanup);
            return promise;
        },

        abortAll: () => {
            for (const request of APIEngine._requests) request.abort?.();
            APIEngine._requests.clear();
        },

        _data: (response) => {
            if (response?.response && typeof response.response === 'object') return response.response;
            if (typeof response?.responseText === 'string' && response.responseText) return JSON.parse(response.responseText);
            return null;
        },

        _assertSuccess: (response, provider) => {
            if (response.status < 200 || response.status >= 300) {
                const error = new Error(`${provider} HTTP ${response.status}`);
                error.status = response.status;
                const retryAfter = String(response.responseHeaders || '').match(/^retry-after:\s*(\d+)/im)?.[1];
                if (retryAfter) error.retryAfterMs = Number(retryAfter) * 1000;
                else if (response.status === 429) error.retryAfterMs = 60000;
                throw error;
            }
        },

        // All internal scores are in pawns and from the user's point of view.
        // Public engine APIs report from White's point of view, including mate.
        _evalFromWhite: ({ fen, mate = null, centipawns = null, pawns = null }) => {
            const perspective = (State.playerColor || fen.split(' ')[1]) === 'b' ? -1 : 1;
            if (mate !== null && mate !== false && Number.isFinite(Number(mate))) {
                return { type: 'mate', value: Number(mate) * perspective };
            }
            const raw = centipawns !== null ? Number(centipawns) / 100 : Number(pawns);
            if (!Number.isFinite(raw)) throw new Error('invalid engine evaluation');
            return { type: 'cp', value: raw * perspective };
        },

        // Provider adapters expose continuation moves *after* the primary move.
        _continuationAfter: (primaryMove, moves) => {
            const pv = (Array.isArray(moves) ? moves : String(moves || '').split(/\s+/))
                .filter(move => /^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move));
            return pv[0] === primaryMove ? pv.slice(1) : pv;
        },

        // Lichess has up to five deeply cached PVs. A 404 is an ordinary cache
        // miss, not a provider failure.
        analyzeLichessCloud: async (fen) => {
            const cfg = CONFIG.api.lichessCloud;
            if (!cfg?.enabled) return null;
            const url = `${cfg.url}?fen=${encodeURIComponent(fen)}&multiPv=${Math.min(CONFIG.multiPV, 5)}&variant=standard`;
            const response = await APIEngine._requestJSON({ method: 'GET', url, timeout: cfg.timeout });
            if (response.status === 404) return null;
            APIEngine._assertSuccess(response, 'Lichess cloud');
            const data = APIEngine._data(response);
            if (!data || !Array.isArray(data.pvs) || !data.pvs.length) return null;
            if (Number(data.depth || 0) < cfg.minDepth) return null;

            const variants = data.pvs.slice(0, CONFIG.multiPV).map(pv => {
                const line = String(pv.moves || '').trim().split(/\s+/).filter(Boolean);
                const move = line[0];
                if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move || '')) return null;
                return {
                    move,
                    eval: APIEngine._evalFromWhite({ fen, mate: pv.mate, centipawns: pv.cp }),
                    depth: Number(data.depth || 0),
                    pv: line,
                };
            }).filter(Boolean);
            if (!variants.length) return null;

            const best = variants[0];
            return {
                move: best.move,
                eval: best.eval,
                depth: best.depth,
                nodes: Number(data.knodes || 0) * 1000,
                ponder: best.pv[1] || null,
                continuation: best.pv.slice(1),
                variants,
                source: 'lichess-cloud',
            };
        },

        // chess-api.com (Stockfish 18). Its HTTP endpoint currently returns one
        // best move even when `variants` is requested, so MultiPV is optional.
        analyzeChessApi: async (fen, depth) => {
            const cfg = CONFIG.api.chessApi;
            const response = await APIEngine._requestJSON({
                method: 'POST',
                url: cfg.url,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({
                    fen,
                    depth: Math.min(depth, 18),
                    maxThinkingTime: cfg.maxThinkingTime,
                    variants: Math.min(CONFIG.multiPV, 5),
                }),
                timeout: cfg.timeout,
            });
            APIEngine._assertSuccess(response, 'chess-api.com');
            const data = APIEngine._data(response);
            if (!data) throw new Error('empty chess-api.com response');

            const bestmoveText = String(data.bestmove || data.text || '');
            const move = data.move || data.lan || bestmoveText.match(/bestmove\s+(\S+)/)?.[1];
            if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move || '')) throw new Error('invalid chess-api.com move');
            const evaluation = APIEngine._evalFromWhite({
                fen,
                mate: data.mate,
                centipawns: data.centipawns,
                pawns: data.eval,
            });

            const variants = Array.isArray(data.variants) ? data.variants.map(variant => {
                const variantMove = variant.move || variant.lan;
                if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(variantMove || '')) return null;
                const continuation = APIEngine._continuationAfter(variantMove, variant.continuationArr || variant.continuation);
                return {
                    move: variantMove,
                    eval: APIEngine._evalFromWhite({
                        fen,
                        mate: variant.mate,
                        centipawns: variant.centipawns,
                        pawns: variant.eval,
                    }),
                    depth: Number(variant.depth || data.depth || depth),
                    pv: [variantMove, ...continuation],
                };
            }).filter(Boolean) : null;

            const continuation = APIEngine._continuationAfter(move, data.continuationArr || data.continuation);
            return {
                move,
                eval: evaluation,
                depth: Number(data.depth || depth),
                ponder: data.ponder || continuation[0] || bestmoveText.match(/ponder\s+(\S+)/)?.[1] || null,
                continuation,
                variants: variants?.length ? variants : null,
                source: 'chess-api',
            };
        },

        // stockfish.online (Stockfish 17.1 as of 2026)
        analyzeStockfishOnline: async (fen, depth) => {
            const cfg = CONFIG.api.stockfishOnline;
            const url = `${cfg.url}?fen=${encodeURIComponent(fen)}&depth=${Math.min(depth, 15)}`;
            const response = await APIEngine._requestJSON({ method: 'GET', url, timeout: cfg.timeout });
            APIEngine._assertSuccess(response, 'stockfish.online');
            const data = APIEngine._data(response);
            if (!data?.success || !data.bestmove) throw new Error(data?.data || 'stockfish.online rejected request');
            const move = String(data.bestmove).match(/bestmove\s+(\S+)/)?.[1];
            if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move || '')) throw new Error('invalid stockfish.online move');
            const continuation = APIEngine._continuationAfter(move, data.continuation);
            return {
                move,
                eval: APIEngine._evalFromWhite({ fen, mate: data.mate, pawns: data.evaluation }),
                depth: Math.min(depth, 15),
                ponder: continuation[0] || String(data.bestmove).match(/ponder\s+(\S+)/)?.[1] || null,
                continuation,
                variants: null,
                source: 'stockfish-online',
            };
        },

        shouldAttempt: () => State.apiAvailable || Date.now() >= State.apiRetryAt,

        // Unified provider chain with an actual LRU cache and cooldown recovery.
        analyze: async (fen, depth) => {
            const ec = CONFIG.engineCache;
            State.diagnostics.lastCacheHit = false;
            const cacheKey = ec?.normalizeMoveCounters ? fen.split(' ').slice(0, 5).join(' ') : fen;
            if (ec?.enabled && fen) {
                const cached = State.engineCache.get(cacheKey);
                if (cached && Date.now() - cached.ts < ec.ttlMs) {
                    State.engineCache.delete(cacheKey);
                    State.engineCache.set(cacheKey, cached); // refresh LRU order
                    State.diagnostics.cacheHits++;
                    State.diagnostics.lastCacheHit = true;
                    Utils.log(`EngineCache: hit (${cached.result.source}, ${State.engineCache.size} entries)`, 'debug');
                    return cached.result;
                }
                if (cached) State.engineCache.delete(cacheKey);
            }

            if (!APIEngine.shouldAttempt()) return null;
            const tryProvider = async (method, key, label) => {
                const startedAt = performance.now();
                const record = State.diagnostics.providers[key] || { successes: 0, failures: 0 };
                if (record.retryAt > Date.now()) return null;
                try {
                    const result = await method(fen, depth);
                    // `null` can be an ordinary cache miss (notably Lichess cloud
                    // 404 or insufficient cached depth), so fall through quietly.
                    if (!result) return null;
                    State.diagnostics.providers[key] = {
                        ...record,
                        ok: true,
                        latencyMs: performance.now() - startedAt,
                        successes: record.successes + 1,
                        consecutiveFailures: 0,
                        retryAt: 0,
                        lastAt: Date.now(),
                    };
                    State.apiAvailable = true;
                    State.apiFailCount = 0;
                    State.apiRetryAt = 0;
                    return result;
                } catch (error) {
                    // Aborting one request means its entire position is obsolete.
                    // Propagate instead of launching fallback providers for stale FENs.
                    if (String(error?.message || error) === 'aborted') throw error;
                    const consecutiveFailures = (record.consecutiveFailures || 0) + 1;
                    const cooldownMs = Number(error?.retryAfterMs) || (consecutiveFailures >= 3
                        ? Math.min(300000, 30000 * 2 ** (consecutiveFailures - 3))
                        : 0);
                    const retryAt = cooldownMs ? Date.now() + cooldownMs : 0;
                    State.diagnostics.providers[key] = {
                        ...record,
                        ok: false,
                        latencyMs: performance.now() - startedAt,
                        failures: record.failures + 1,
                        consecutiveFailures,
                        retryAt,
                        lastAt: Date.now(),
                    };
                    Utils.log(`${label} failed: ${error?.message || error}${retryAt ? ' (temporarily bypassed)' : ''}`, 'warn');
                    return null;
                }
            };

            const activeType = Account.currentEngine().type;
            if (activeType !== 'api' && activeType !== 'stockfish_online') return null;
            const providers = activeType === 'stockfish_online'
                ? [[APIEngine.analyzeStockfishOnline, 'stockfish-online', 'stockfish.online'], [APIEngine.analyzeLichessCloud, 'lichess-cloud', 'Lichess cloud'], [APIEngine.analyzeChessApi, 'chess-api', 'chess-api.com']]
                : [[APIEngine.analyzeLichessCloud, 'lichess-cloud', 'Lichess cloud'], [APIEngine.analyzeChessApi, 'chess-api', 'chess-api.com'], [APIEngine.analyzeStockfishOnline, 'stockfish-online', 'stockfish.online']];

            for (const [method, key, label] of providers) {
                const result = await tryProvider(method, key, label);
                if (!result) continue;
                if (ec?.enabled) {
                    while (State.engineCache.size >= ec.maxEntries) {
                        State.engineCache.delete(State.engineCache.keys().next().value);
                    }
                    State.engineCache.set(cacheKey, { result, ts: Date.now() });
                }
                return result;
            }

            State.apiFailCount++;
            if (State.apiFailCount >= 3) {
                State.apiAvailable = false;
                const cooldown = Math.min(300000, 30000 * (State.apiFailCount - 2));
                State.apiRetryAt = Date.now() + cooldown;
                Utils.log(`Remote engines cooling down for ${Math.round(cooldown / 1000)}s; using local fallback`, 'error');
            }
            return null;
        },
    };

    // ═══════════════════════════════════════════
    //  LOCAL ENGINE (Stockfish 18 lite-single WASM fallback)
    // ═══════════════════════════════════════════
    const LocalEngine = {
        _initPromise: null,
        _readyResolve: null,
        _activeSearch: false,
        _resolve: null,
        _timeoutId: null,
        _drainPromise: null,
        _drainResolve: null,

        _sha256: async data => {
            const subtle = globalThis.crypto?.subtle;
            if (!subtle) throw new Error('Web Crypto unavailable; refusing unverified engine asset');
            const bytes = typeof data === 'string'
                ? new TextEncoder().encode(data)
                : new Uint8Array(data);
            const digest = await subtle.digest('SHA-256', bytes);
            return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
        },

        _verifyAsset: async (label, data, expected) => {
            if (!expected || !/^[a-f0-9]{64}$/i.test(expected)) throw new Error(`${label} integrity hash missing`);
            const actual = await LocalEngine._sha256(data);
            if (actual.toLowerCase() !== expected.toLowerCase()) {
                throw new Error(`${label} integrity mismatch (${actual.slice(0, 12)} != ${expected.slice(0, 12)})`);
            }
            return true;
        },

        init: async () => {
            if (State.workers.stockfish && State.engineReady) return true;
            if (LocalEngine._initPromise) return LocalEngine._initPromise;
            LocalEngine._initPromise = (async () => {
                Utils.log('Initializing local Stockfish fallback...');
                let scriptBlobUrl = null;
                let wasmBlobUrl = null;
                try {
                    const cfg = CONFIG.localEngine;
                    const [jsResponse, wasmResponse] = await Promise.all([
                        APIEngine._requestJSON({ method: 'GET', url: cfg.jsUrl, timeout: cfg.timeout, responseType: 'text', anonymous: true }),
                        APIEngine._requestJSON({ method: 'GET', url: cfg.wasmUrl, timeout: cfg.timeout, responseType: 'arraybuffer', anonymous: true }),
                    ]);
                    APIEngine._assertSuccess(jsResponse, 'Stockfish.js CDN');
                    APIEngine._assertSuccess(wasmResponse, 'Stockfish WASM CDN');
                    const scriptContent = jsResponse.responseText;
                    const wasmBinary = wasmResponse.response;
                    if (!scriptContent || scriptContent.length < 1000) throw new Error('empty worker script');
                    // Avoid cross-realm `instanceof` checks: extension sandboxes can
                    // return a perfectly valid ArrayBuffer from a different realm.
                    if (!wasmBinary || typeof wasmBinary.byteLength !== 'number' || wasmBinary.byteLength < 1000000) {
                        throw new Error('empty WASM binary');
                    }
                    await Promise.all([
                        LocalEngine._verifyAsset('Stockfish.js', scriptContent, cfg.jsSha256),
                        LocalEngine._verifyAsset('Stockfish WASM', wasmBinary, cfg.wasmSha256),
                    ]);
                    scriptBlobUrl = URL.createObjectURL(new Blob([scriptContent], { type: 'application/javascript' }));
                    wasmBlobUrl = URL.createObjectURL(new Blob([wasmBinary], { type: 'application/wasm' }));
                    // Stockfish.js reads the encoded WASM URL from the worker hash.
                    const worker = new Worker(`${scriptBlobUrl}#${encodeURIComponent(wasmBlobUrl)}`);
                    State.workers.stockfish = worker;
                    State.engineReady = false;
                    worker.onmessage = LocalEngine._onMessage;
                    worker.onerror = error => {
                        Utils.log(`Local Stockfish worker error: ${error.message || 'unknown error'}`, 'error');
                        LocalEngine._destroyWorker();
                    };
                    worker.onmessageerror = () => {
                        Utils.log('Local Stockfish worker emitted an unreadable message; restarting on next search', 'error');
                        LocalEngine._destroyWorker();
                    };

                    const ready = new Promise(resolve => { LocalEngine._readyResolve = resolve; });
                    worker.postMessage('uci');
                    const ok = await Promise.race([ready, Utils.sleep(6000).then(() => false)]);
                    if (!ok) throw new Error('UCI initialization timeout');
                    Utils.log('Local Stockfish ready (fallback)');
                    return true;
                } catch (error) {
                    Utils.log(`Local Stockfish init failed: ${error?.message || error}`, 'error');
                    LocalEngine._destroyWorker();
                    return false;
                } finally {
                    if (scriptBlobUrl) URL.revokeObjectURL(scriptBlobUrl);
                    if (wasmBlobUrl) URL.revokeObjectURL(wasmBlobUrl);
                    LocalEngine._readyResolve = null;
                }
            })();
            try {
                return await LocalEngine._initPromise;
            } finally {
                LocalEngine._initPromise = null;
            }
        },

        _onMessage: event => {
            const msg = String(event.data || '');
            if (msg === 'uciok') {
                const worker = State.workers.stockfish;
                worker?.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
                worker?.postMessage('isready');
                return;
            }
            if (msg === 'readyok') {
                State.engineReady = true;
                LocalEngine._readyResolve?.(true);
                return;
            }
            if (msg.startsWith('info') && msg.includes('score') && LocalEngine._activeSearch) {
                LocalEngine.parseScore(msg);
                return;
            }
            if (!msg.startsWith('bestmove')) return;

            if (LocalEngine._drainResolve) {
                const drained = LocalEngine._drainResolve;
                LocalEngine._drainResolve = null;
                LocalEngine._drainPromise = null;
                drained(true);
                return;
            }

            const move = msg.split(/\s+/)[1];
            const validMove = /^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move || '') ? move : null;
            LocalEngine._activeSearch = false;
            clearTimeout(LocalEngine._timeoutId);
            LocalEngine._timeoutId = null;
            const resolve = LocalEngine._resolve;
            LocalEngine._resolve = null;
            resolve?.(validMove);
        },

        _destroyWorker: () => {
            try { State.workers.stockfish?.terminate(); } catch (e) {}
            State.workers.stockfish = null;
            State.engineReady = false;
            LocalEngine._activeSearch = false;
            clearTimeout(LocalEngine._timeoutId);
            LocalEngine._timeoutId = null;
            LocalEngine._resolve?.(null);
            LocalEngine._resolve = null;
            LocalEngine._drainResolve?.(false);
            LocalEngine._drainResolve = null;
            LocalEngine._drainPromise = null;
        },

        cancel: () => {
            if (!LocalEngine._activeSearch || !State.workers.stockfish) return;
            LocalEngine._activeSearch = false;
            clearTimeout(LocalEngine._timeoutId);
            LocalEngine._timeoutId = null;
            LocalEngine._resolve?.(null);
            LocalEngine._resolve = null;
            LocalEngine._drainPromise = new Promise(resolve => { LocalEngine._drainResolve = resolve; });
            State.workers.stockfish.postMessage('stop');
        },

        _awaitDrain: async () => {
            if (!LocalEngine._drainPromise) return true;
            const drained = await Promise.race([
                LocalEngine._drainPromise,
                Utils.sleep(1500).then(() => false),
            ]);
            if (drained) return true;
            Utils.log('Local engine failed to drain stale search; restarting worker', 'warn');
            LocalEngine._destroyWorker();
            return LocalEngine.init();
        },

        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 ([a-h][1-8][a-h][1-8][qrbn]?)/);
            if (!scoreMatch || !pvMatch || !moveMatch) return;

            const type = scoreMatch[1];
            let value = Number(scoreMatch[2]);
            const mpv = Number(pvMatch[1]);
            const depth = Number(depthMatch?.[1] || 0);
            if (!Number.isFinite(value) || mpv < 1 || mpv > CONFIG.multiPV) return;
            if (type === 'cp') value /= 100;
            const pv = msg.split(' pv ')[1]?.split(' ').filter(move => /^[a-h][1-8][a-h][1-8][qrbn]?$/.test(move)) || [];
            State.candidates[mpv] = { move: moveMatch[1], eval: { type, value }, depth, pv };
            if (mpv === 1) {
                State.currentEval = { type, value, depth };
                State.currentBestMove = moveMatch[1];
                State.opponentResponse = pv[1] || null;
            }
        },

        analyze: async (fen, depth) => {
            if (!await LocalEngine._awaitDrain()) return null;
            if (!State.workers.stockfish || !State.engineReady) {
                if (!await LocalEngine.init()) return null;
            }
            const safeDepth = Math.min(depth, 16);
            State.candidates = {};
            State.currentBestMove = null;
            State.currentEval = null;
            return new Promise(resolve => {
                LocalEngine._resolve = resolve;
                LocalEngine._activeSearch = true;
                const worker = State.workers.stockfish;
                worker.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
                worker.postMessage(`position fen ${fen}`);
                worker.postMessage(`go depth ${safeDepth}`);
                LocalEngine._timeoutId = setTimeout(() => {
                    if (LocalEngine._resolve !== resolve) return;
                    const fallback = State.currentBestMove;
                    Utils.log('Local Stockfish timed out; using latest completed PV', 'warn');
                    LocalEngine._activeSearch = false;
                    LocalEngine._resolve = null;
                    LocalEngine._timeoutId = null;
                    LocalEngine._drainPromise = new Promise(drain => { LocalEngine._drainResolve = drain; });
                    State.workers.stockfish?.postMessage('stop');
                    resolve(fallback);
                }, 10000);
            });
        },
    };

    // ═══════════════════════════════════════════
    //  UNIFIED ENGINE
    // ═══════════════════════════════════════════
    const Engine = {
        init: async () => {
            UI.updateStatus('orange');
            // The modern local WASM bundle is ~7 MB; load it eagerly only when
            // selected. Remote modes initialize it lazily if their chain fails.
            if (CONFIG.engineType === 'local') 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 || !fen) return;
            const analysisId = ++State.analysisId;
            const isCurrent = () => State.analysisId === analysisId && Game.getFen() === fen;
            State.isThinking = true;
            State.candidates = {};
            State.apiResult = null;
            State.currentBestMove = null;
            State.opponentResponse = null;
            State._lastEngineSource = null;
            State.diagnostics.lastCacheHit = false;

            try {
                // --- Opening book check (with gradual tapering) ---
                if (CONFIG.useBook && State.moveCount < 20) {
                    let bookChance = CONFIG.humanization.bookUseRate ?? 0.82;
                    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 (!isCurrent()) return;
                        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';
                            await Engine.handleResult(bookMove, fen, true);
                            return;
                        }
                    }
                }

                // --- Tablebase check ---
                const tablebaseRate = CONFIG.humanization.enabled
                    ? (CONFIG.humanization.tablebaseUseRate ?? 0.32)
                    : 1;
                if (CONFIG.useTablebase && (CONFIG.coach.enabled || Math.random() < tablebaseRate)) {
                    const tbResult = await Tablebase.probe(fen);
                    if (!isCurrent()) return;
                    if (tbResult) {
                        Utils.log(`Tablebase Move: ${tbResult.move} (${tbResult.category})`);
                        UI.toast('Tablebase', `Perfect endgame: ${tbResult.move} (${tbResult.category})`, 'tablebase', 3500);
                        State._lastEngineSource = 'tablebase';
                        const tablebaseEval = {
                            win: 100,
                            'cursed-win': 0.5,
                            draw: 0,
                            'blessed-loss': -0.5,
                            loss: -100,
                        }[tbResult.category] ?? 0;
                        State.currentEval = { type: 'cp', value: tablebaseEval };
                        await Engine.handleResult(tbResult.move, fen, false);
                        return;
                    }
                }

                // Start the archived human-master database concurrently with analysis.
                const playerDBPromise = PlayerMoveDB.fetch(fen);
                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');

                if (activeType !== 'local' && APIEngine.shouldAttempt()) {
                    const apiResult = await APIEngine.analyze(fen, depth);
                    if (!isCurrent()) return;
                    if (apiResult) {
                        State.apiResult = apiResult;
                        State._lastEngineSource = apiResult.source;
                        State.currentEval = apiResult.eval;
                        State.currentBestMove = apiResult.move;
                        State.candidates = {};

                        // Prefer provider-native MultiPV. Always force the actual
                        // primary result into slot 1, then append unique alternatives.
                        const variants = Array.isArray(apiResult.variants) ? apiResult.variants : [];
                        State.candidates[1] = {
                            move: apiResult.move,
                            eval: apiResult.eval,
                            depth: apiResult.depth,
                            pv: [apiResult.move, ...(apiResult.continuation || [])],
                        };
                        let slot = 2;
                        for (const candidate of variants) {
                            if (!candidate?.move || candidate.move === apiResult.move) continue;
                            if (slot > CONFIG.multiPV) break;
                            State.candidates[slot++] = candidate;
                        }

                        // Every adapter defines continuation[0] as the opponent reply.
                        State.opponentResponse = apiResult.continuation?.[0] || apiResult.ponder || null;

                        // The DB request has already overlapped engine latency. Give it
                        // a bounded final window so the human-move layer actually fires.
                        const dbWait = State.clock.myTime != null && State.clock.myTime < 15 ? 150 : 850;
                        await Promise.race([playerDBPromise, Utils.sleep(dbWait)]);
                        if (!isCurrent()) return;
                        await Engine.handleResult(apiResult.move, fen, false);
                        return;
                    }
                }

                // Fallback: local Stockfish. No concurrent worker search is started,
                // avoiding stale `bestmove` messages resolving the wrong position.
                State._lastEngineSource = 'local';
                const localStartedAt = performance.now();
                const localBest = await LocalEngine.analyze(fen, depth);
                const localRecord = State.diagnostics.providers.local || { successes: 0, failures: 0 };
                State.diagnostics.providers.local = {
                    ...localRecord,
                    ok: Boolean(localBest),
                    latencyMs: performance.now() - localStartedAt,
                    successes: localRecord.successes + (localBest ? 1 : 0),
                    failures: localRecord.failures + (localBest ? 0 : 1),
                    lastAt: Date.now(),
                };
                if (!isCurrent()) return;
                if (localBest) {
                    await Promise.race([playerDBPromise, Utils.sleep(300)]);
                    await Engine.handleResult(localBest, fen, false);
                } else {
                    Utils.log('All engines failed!', 'error');
                    UI.updateStatus('red');
                }
            } catch (error) {
                if (isCurrent()) {
                    Utils.log(`Analysis failed: ${error?.message || error}`, 'error');
                    UI.updateStatus('red');
                }
            } finally {
                if (State.analysisId === analysisId) State.isThinking = false;
            }
        },

        handleResult: async (bestMove, fen, isBook) => {
            if (!bestMove || Game.getFen() !== fen) return;
            const isRetry = State.lastHandledFen === fen;
            if (!isRetry) {
                State.lastHandledFen = fen;
                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 === 'error-tail') {
                UI.toast('Human Error', `${finalMove} · ${Math.round(moveResult.cpLoss || 0)}cp loss`, 'blunder', 4000);
            } 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', `Archived master games favor ${finalMove}`, 'book', 3500);
            } else if (moveResult.reason === 'human-popular') {
                UI.toast('Human Move', `Archived play and the active profile favor ${finalMove}`, 'book', 3500);
            } else if (moveResult.reason === 'calculation-limit') {
                UI.toast('Calculation Limit', `Profile chose the shallower practical line ${finalMove}`, 'suboptimal', 3500);
            } else if (moveResult.reason === 'motif-miss' || moveResult.reason === 'profile-alt') {
                UI.toast('Profile Move', `${finalMove} · ${Math.round(moveResult.cpLoss || 0)}cp from best`, 'suboptimal', 3200);
            } 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
            if (!isRetry) 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 = Math.min(
                        Utils.humanDelay(afk.delay.min, afk.delay.max),
                        TimeControlContext.maxSpendMs(fen) * 0.55,
                    );
                    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) {
                    const moved = await Humanizer.executeMove(finalMove);
                    if (!moved) {
                        // Always arm the recovery loop after an input failure. The
                        // board model can be temporarily unavailable while Chess.com
                        // rerenders, so a raw FEN equality check here could suppress
                        // the only retry. The loop itself safely re-reads the current
                        // position and turn before doing any more work.
                        State.lastFen = null;
                        setTimeout(Main._scheduleLoop, 350);
                    }
                } else {
                    Utils.log('Position changed during delay, aborting move', 'warn');
                }
            }
        }
    };

    // ═══════════════════════════════════════════
    //  TIME-CONTROL CONTEXT (v17.3)
    // ═══════════════════════════════════════════
    const TimeControlContext = {
        parse: label => {
            const raw = String(label || '').trim();
            const text = raw.toLowerCase().replace(/[–—+]/g, '|').replace(/\s+/g, ' ');
            let baseSeconds = null;
            let incrementSeconds = 0;
            let match = text.match(/(\d+(?:\.\d+)?)\s*\|\s*(\d+(?:\.\d+)?)/);
            if (match) {
                baseSeconds = Number(match[1]) * 60;
                incrementSeconds = Number(match[2]);
            } else if ((match = text.match(/(\d+(?:\.\d+)?)\s*(?:min(?:ute)?s?|m)\b/))) {
                baseSeconds = Number(match[1]) * 60;
            } else if ((match = text.match(/(\d+(?:\.\d+)?)\s*(?:sec(?:ond)?s?|s)\b/))) {
                baseSeconds = Number(match[1]);
            }
            let bucket = null;
            if (/bullet|ultrabullet|hyperbullet/.test(text)) bucket = 'bullet';
            else if (/blitz/.test(text)) bucket = 'blitz';
            else if (/rapid/.test(text)) bucket = 'rapid';
            else if (/classical|standard/.test(text)) bucket = 'classical';
            if (!bucket && Number.isFinite(baseSeconds)) {
                const fortyMoveSeconds = baseSeconds + incrementSeconds * 40;
                bucket = fortyMoveSeconds < 180 ? 'bullet'
                    : fortyMoveSeconds < 600 ? 'blitz'
                        : fortyMoveSeconds < 3600 ? 'rapid' : 'classical';
            }
            return {
                label: raw || null,
                baseSeconds: Number.isFinite(baseSeconds) ? baseSeconds : null,
                incrementSeconds: Number.isFinite(incrementSeconds) ? incrementSeconds : 0,
                bucket: bucket || 'rapid',
            };
        },

        refresh: () => {
            const label = State.human.timeControl?.label || Account.readTimeControl() || CONFIG.account.sessionTC;
            const parsed = TimeControlContext.parse(label);
            const observedMax = Math.max(State.clock.maxMyTime || 0, State.clock.maxOppTime || 0);
            if (parsed.baseSeconds == null && observedMax > 0) parsed.baseSeconds = observedMax;
            if (!label && observedMax > 0) {
                const inferred = TimeControlContext.parse(`${Math.max(0.25, observedMax / 60).toFixed(2)} min`);
                parsed.bucket = inferred.bucket;
                parsed.label = `inferred ${Math.round(observedMax)}s`;
            }
            State.human.timeControl = parsed;
            return parsed;
        },

        current: () => State.human.timeControl || TimeControlContext.refresh(),
        profile: context => {
            const tc = context || TimeControlContext.current();
            return CONFIG.humanMoveModel.timeControls[tc?.bucket]
                || CONFIG.humanMoveModel.timeControls.rapid;
        },

        maxSpendMs: (fen, clockSeconds = State.clock.myTime) => {
            const tc = TimeControlContext.current();
            const cfg = TimeControlContext.profile(tc);
            if (!Number.isFinite(clockSeconds) || clockSeconds <= 0) {
                return Math.max(1200, CONFIG.timing.base.max * cfg.timingMult * 1.35);
            }
            const phase = HumanStrategy.getGamePhase(fen);
            const movesRemaining = phase === 'opening' ? 28 : phase === 'middlegame' ? 18 : 12;
            const reserve = Math.min(clockSeconds * 0.45, cfg.minReserveSeconds);
            const usable = Math.max(0.35, clockSeconds - reserve);
            const sustainable = usable / movesRemaining + (tc.incrementSeconds || 0) * cfg.incrementCredit;
            const fractionalCap = Math.max(0.35, clockSeconds * cfg.maxSpendFraction);
            const flagSafeCap = Math.max(0.20, clockSeconds - 0.75);
            const budgetMs = Math.min(sustainable, fractionalCap, flagSafeCap) * 1000;
            // Sub-second emergency: preserve a fraction for dispatch instead of
            // letting the ordinary 180ms floor exceed the remaining clock.
            if (clockSeconds < 0.75) return Math.max(50, Math.min(budgetMs, clockSeconds * 800));
            return Math.max(180, budgetMs);
        },

        clampDelay: (delay, fen, minimum = 250) => {
            const budget = TimeControlContext.maxSpendMs(fen);
            return Math.max(Math.min(minimum, budget), Math.min(Number(delay) || minimum, budget));
        },

        describe: () => {
            const tc = TimeControlContext.current();
            const inc = tc.incrementSeconds ? `+${tc.incrementSeconds}` : '';
            return `${tc.bucket}${tc.baseSeconds ? ` ${Math.round(tc.baseSeconds / 6) / 10}${inc}` : ''}`;
        },
    };

    // ═══════════════════════════════════════════
    //  CALIBRATED HUMAN MOVE MODEL (v17.3)
    // ═══════════════════════════════════════════
    const HumanMoveModel = {
        _rngOverride: null,

        hashString: value => {
            let hash = 2166136261 >>> 0;
            for (const ch of String(value)) {
                hash ^= ch.charCodeAt(0);
                hash = Math.imul(hash, 16777619);
            }
            return hash >>> 0;
        },

        seededRng: seed => {
            let state = typeof seed === 'number' ? seed >>> 0 : HumanMoveModel.hashString(seed);
            if (!state) state = 0x6d2b79f5;
            return () => {
                state += 0x6d2b79f5;
                let value = state;
                value = Math.imul(value ^ value >>> 15, value | 1);
                value ^= value + Math.imul(value ^ value >>> 7, value | 61);
                return ((value ^ value >>> 14) >>> 0) / 4294967296;
            };
        },

        setRng: rng => { HumanMoveModel._rngOverride = typeof rng === 'function' ? rng : null; },
        _positionRng: fen => {
            if (HumanMoveModel._rngOverride) return HumanMoveModel._rngOverride;
            const personaSeed = CONFIG.humanization.weaknessProfile.seed || 'rexxx-persona';
            const gameSeed = Main?._gameInstanceId || 0;
            return HumanMoveModel.seededRng(`${personaSeed}|${gameSeed}|${fen}`);
        },

        _evalLossCp: (best, candidate) => {
            if (!best || !candidate) return 0;
            if (best.type === 'cp' && candidate.type === 'cp') return Math.max(0, (best.value - candidate.value) * 100);
            if (best.type === 'mate' && candidate.type === 'mate') {
                if (best.value > 0 && candidate.value > 0) return Math.max(0, (candidate.value - best.value) * 50);
                if (best.value < 0 && candidate.value < 0) return Math.max(0, (Math.abs(best.value) - Math.abs(candidate.value)) * 50);
                return best.value > 0 && candidate.value < 0 ? 900 : 0;
            }
            if (best.type === 'mate' && best.value > 0) return 700;
            if (candidate.type === 'mate' && candidate.value < 0) return 700;
            return 0;
        },

        _legalMoves: () => {
            try {
                const game = Game.getBoardGame();
                if (!game?.getLegalMoves) return [];
                return game.getLegalMoves().map(move => (move?.from && move?.to)
                    ? `${move.from}${move.to}${move.promotion || ''}`
                    : (typeof move === 'string' ? move : null)).filter(Boolean);
            } catch (e) { return []; }
        },

        _activeProfile: () => State.human.activeProfile || RatingProfile.profileFor(CONFIG.targetRating),
        selectThinkCategory: (tc, rng, rating) => {
            const bucket = tc?.bucket || 'rapid';
            const base = {
                bullet: [0.62, 0.34],
                blitz: [0.38, 0.52],
                rapid: [0.24, 0.55],
                classical: [0.16, 0.54],
            }[bucket];
            const skill = Math.max(0, Math.min(1, ((rating || 1800) - 800) / 2000));
            const fast = Math.max(0.10, base[0] - skill * 0.05);
            const normal = Math.min(0.72, base[1] + skill * 0.04);
            const draw = rng();
            return draw < fast ? 'fast' : draw < fast + normal ? 'normal' : 'slow';
        },

        _buildCandidates: (fen, bestMove, profile) => {
            const output = new Map();
            const bestEval = State.candidates?.[1]?.eval || State.currentEval;
            const add = (move, fields = {}) => {
                if (!move || !/^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(move)) return;
                const current = output.get(move) || { move, sources: new Set() };
                const sourceSet = current.sources instanceof Set ? current.sources : new Set(current.sources || []);
                const { sources = [], ...values } = fields;
                Object.assign(current, values);
                current.sources = sourceSet;
                for (const source of sources) current.sources.add(source);
                output.set(move, current);
            };
            add(bestMove, { rank: 1, eval: bestEval, depth: State.candidates?.[1]?.depth || 0, pv: State.candidates?.[1]?.pv || [bestMove], sources: ['engine'] });
            for (const key of Object.keys(State.candidates || {}).sort((a, b) => Number(a) - Number(b))) {
                const candidate = State.candidates[key];
                if (!candidate?.move) continue;
                add(candidate.move, { rank: Number(key) || output.size + 1, eval: candidate.eval, depth: candidate.depth || 0, pv: candidate.pv || [candidate.move], sources: ['engine'] });
            }
            const legalMoves = HumanMoveModel._legalMoves();
            const legalSet = new Set(legalMoves);
            const dbMoves = PlayerMoveDB.getCached(fen) || [];
            for (const dbMove of dbMoves) {
                if (output.has(dbMove.uci)) add(dbMove.uci, { games: dbMove.games || 0, winRate: dbMove.winRate, sources: ['human-db'] });
            }
            if (legalMoves.length) {
                const repertoire = Account.repertoireMove(fen, legalMoves);
                if (repertoire) add(repertoire, { rank: output.get(repertoire)?.rank || 99, estimatedLossCp: Math.min(18, profile.cpLoss?.[0] || 30), sources: ['repertoire'] });
            }
            const phase = HumanStrategy.getGamePhase(fen);
            const phaseIndex = phase === 'opening' ? 0 : phase === 'middlegame' ? 1 : 2;
            const phaseLoss = profile.cpLoss?.[phaseIndex] || CONFIG.humanization.maxAcceptableCPLoss[phase];
            const tcProfile = TimeControlContext.profile();
            const pressure = State.human.timePressureMult?.maxCPLoss || 1;
            const hardCap = Math.max(20, Math.min(profile.blunderLoss || 200, phaseLoss * 1.85 * tcProfile.qualityMult * pressure));
            const maxGames = Math.max(1, ...dbMoves.map(move => move.games || 0));
            return Array.from(output.values())
                .filter(candidate => !legalSet.size || legalSet.has(candidate.move))
                .map(candidate => {
                    const cpLoss = candidate.move === bestMove ? 0 : candidate.eval && bestEval
                        ? HumanMoveModel._evalLossCp(bestEval, candidate.eval)
                        : candidate.estimatedLossCp ?? hardCap;
                    const safe = candidate.move === bestMove || HumanStrategy.isMoveSafe(candidate.move, fen);
                    return { ...candidate, sources: Array.from(candidate.sources), cpLoss: Math.max(0, cpLoss), safe, tail: cpLoss > hardCap, popularity: (candidate.games || 0) / maxGames, phase, phaseLoss, hardCap };
                })
                .filter(candidate => candidate.safe && (!candidate.tail || candidate.cpLoss <= (profile.blunderLoss || 200)))
                .sort((a, b) => (a.rank || 99) - (b.rank || 99))
                .slice(0, Math.max(2, CONFIG.humanMoveModel.candidateLimit));
        },

        score: (fen, bestMove, options = {}) => {
            const profile = options.profile || HumanMoveModel._activeProfile();
            const tc = options.timeControl || TimeControlContext.current();
            const tcProfile = CONFIG.humanMoveModel.timeControls[tc.bucket] || CONFIG.humanMoveModel.timeControls.rapid;
            const rng = options.rng || HumanMoveModel._positionRng(fen);
            const thinkCategory = options.thinkCategory || HumanMoveModel.selectThinkCategory(tc, rng, profile.rating);
            const candidates = options.candidates || HumanMoveModel._buildCandidates(fen, bestMove, profile);
            const best = candidates.find(candidate => candidate.move === bestMove) || candidates[0];
            if (!best) return { distribution: [], thinkCategory, profile, factors: ['no-candidates'] };
            if (candidates.length === 1) return { distribution: [{ ...best, probability: 1, score: 0 }], thinkCategory, profile, factors: ['only-legal-candidate'], rng };

            const skill = Math.max(0, Math.min(1, (profile.rating - 800) / 2000));
            const complexity = HumanStrategy.getPositionComplexity(fen);
            const history = options.history || State.human.moveHistory || [];
            const lastHistory = history[history.length - 1];
            const bestPvLength = best.pv?.length || 1;
            const minAltLoss = Math.min(...candidates.filter(candidate => candidate.move !== bestMove).map(candidate => candidate.cpLoss));
            const forced = minAltLoss >= (CONFIG.forcedMove?.gapCp || 80);
            const predictedContinuation = State.human.predictedReplyMatched === true;
            const motifRisk = State.moveCount > 5 ? HumanStrategy.detectMotifBlindness(bestMove, fen) : 0;
            const budgetState = options.budgetState || HumanStrategy.acplBudgetState();
            const isLosing = State.currentEval?.type === 'cp' && State.currentEval.value < -0.8;
            const factors = [];

            let targetBest = profile.correlation;
            targetBest -= (tcProfile.qualityMult - 1) * (0.13 - skill * 0.05);
            // Correlation anchors describe an average position. Complexity
            // shifts around that baseline instead of always taxing it.
            targetBest -= (complexity - 0.50) * (1 - skill) * 0.07;
            if (thinkCategory === 'fast') targetBest -= (profile.fastError || 0.10) * 0.55;
            if (thinkCategory === 'slow') targetBest += (profile.slowBest || 0.20) * 0.32;
            if (State.human.clusterMode === 'hot') targetBest += 0.07;
            if (State.human.clusterMode === 'cold') targetBest -= 0.07;
            if (budgetState === 'over') targetBest += 0.12;
            if (budgetState === 'under') targetBest -= 0.045;
            if (forced) { targetBest += 0.21; factors.push('forced-gap'); }
            if (predictedContinuation) { targetBest += 0.045; factors.push('planned-continuation'); }
            if (isLosing) { targetBest += 0.055; factors.push('defensive-focus'); }
            if (motifRisk > 0) { targetBest -= motifRisk * (0.45 - skill * 0.15); factors.push('motif-load'); }
            if (bestPvLength > (CONFIG.shallowDepth?.longPVThreshold || 8)) { targetBest -= (profile.shallow || 0.1) * (0.16 + (1 - skill) * 0.08); factors.push('deep-calculation'); }
            if (State.human.totalMoveCount >= 6) {
                const observed = State.human.topMoveCount / Math.max(1, State.human.totalMoveCount);
                targetBest += Math.max(-0.10, Math.min(0.10, (profile.correlation - observed) * 0.22));
            }
            targetBest = Math.max(0.12, Math.min(0.96, targetBest));

            const phaseScale = best.phaseLoss * tcProfile.qualityMult
                * (thinkCategory === 'fast' ? 1.22 : thinkCategory === 'slow' ? 0.82 : 1)
                * (State.human.clusterMode === 'cold' ? 1.18 : State.human.clusterMode === 'hot' ? 0.84 : 1);
            const temperature = (1.24 - skill * 0.40) * (0.92 + (tcProfile.qualityMult - 0.88) * 0.45);
            const alternatives = candidates.filter(candidate => candidate.move !== bestMove).map(candidate => {
                // Developing players' errors have a broader loss tail; expert
                // alternatives cluster tightly around the engine choice.
                const lossDiscipline = 0.38 + skill * 1.02;
                let score = -Math.pow(candidate.cpLoss / Math.max(8, phaseScale), 1.10) * lossDiscipline;
                score -= Math.max(0, (candidate.rank || 4) - 2) * (0.12 + skill * 0.22);
                if (candidate.cpLoss <= CONFIG.humanization.antiCorrelation.closeEvalThreshold * 100) score += 0.32;
                if (candidate.sources.includes('human-db')) score += 0.72 * Math.sqrt(candidate.popularity) * Math.min(1, (profile.dbRate || 0.05) / 0.25);
                if (candidate.sources.includes('repertoire')) score += 1.15;
                if (bestPvLength > (CONFIG.shallowDepth?.longPVThreshold || 8) && (candidate.pv?.length || 1) + 2 < bestPvLength) score += (1 - skill) * 0.38;
                if (lastHistory?.move && candidate.move.substring(0, 2) === lastHistory.move.substring(2, 4)) score += 0.10;
                if (candidate.tail) score += Math.log(Math.max(0.0001, profile.blunder || 0.01)) - 0.8;
                return { ...candidate, score: score / Math.max(0.45, temperature) };
            });
            const maxScore = Math.max(...alternatives.map(candidate => candidate.score));
            let altWeightTotal = 0;
            for (const candidate of alternatives) {
                candidate.weight = Math.exp(candidate.score - maxScore);
                altWeightTotal += candidate.weight;
            }
            const distribution = [{ ...best, score: 0, probability: targetBest }];
            for (const candidate of alternatives) distribution.push({ ...candidate, probability: (1 - targetBest) * candidate.weight / Math.max(Number.EPSILON, altWeightTotal) });
            const probabilityFloor = Math.max(0, Number(CONFIG.humanMoveModel.minProbability) || 0);
            let probabilityTotal = 0;
            for (const candidate of distribution) {
                candidate.probability = Math.max(probabilityFloor, candidate.probability);
                probabilityTotal += candidate.probability;
            }
            for (const candidate of distribution) candidate.probability /= probabilityTotal;
            distribution.sort((a, b) => b.probability - a.probability);
            return { distribution, thinkCategory, profile, targetBest, complexity, factors, rng };
        },

        choose: (fen, bestMove, options = {}) => {
            const scored = HumanMoveModel.score(fen, bestMove, options);
            if (!scored.distribution.length) return { move: bestMove, reason: 'engine-fallback', isBest: true };
            const rng = options.rng || scored.rng || HumanMoveModel._positionRng(fen);
            let draw = rng();
            let selected = scored.distribution[scored.distribution.length - 1];
            for (const candidate of scored.distribution) {
                draw -= candidate.probability;
                if (draw <= 0) { selected = candidate; break; }
            }
            State.human.thinkCategory = scored.thinkCategory;
            const isBest = selected.move === bestMove;
            const close = selected.cpLoss <= CONFIG.humanization.antiCorrelation.closeEvalThreshold * 100;
            let reason = 'profile-alt';
            if (isBest) reason = scored.factors.includes('forced-gap') ? 'forced-best' : scored.factors.includes('planned-continuation') ? 'planned-best' : 'best';
            else if (selected.sources?.includes('repertoire')) reason = 'repertoire';
            else if (selected.sources?.includes('human-db') && selected.popularity >= 0.35) reason = 'human-popular';
            else if (close) reason = 'close-alt';
            else if (selected.tail) reason = 'error-tail';
            else if (scored.factors.includes('deep-calculation') && (selected.pv?.length || 1) < 6) reason = 'calculation-limit';
            else if (scored.factors.includes('motif-load')) reason = 'motif-miss';

            const explanation = {
                selected: selected.move, probability: selected.probability, cpLoss: selected.cpLoss, reason,
                targetBest: scored.targetBest ?? 1, thinkCategory: scored.thinkCategory,
                timeControl: TimeControlContext.describe(), complexity: scored.complexity ?? 0, factors: scored.factors,
                distribution: scored.distribution.slice(0, CONFIG.humanMoveModel.explanationLimit).map(candidate => ({ move: candidate.move, probability: candidate.probability, cpLoss: candidate.cpLoss, sources: candidate.sources })),
            };
            State.human.lastChoiceExplanation = explanation;
            const diagnostics = State.diagnostics.humanModel;
            diagnostics.choices++;
            diagnostics.probabilitySum += selected.probability;
            diagnostics.cpLossSum += selected.cpLoss;
            diagnostics.last = explanation;
            Utils.log(`HumanModel: ${selected.move} ${(selected.probability * 100).toFixed(1)}% · ${Math.round(selected.cpLoss)}cp · ${reason} · ${TimeControlContext.describe()}`, 'debug');
            return { move: selected.move, reason, isBest, isCloseAlt: !isBest && close, cpLoss: selected.cpLoss, probability: selected.probability, explanation, fen };
        },

        recordHistory: entry => {
            if (!entry?.move) return;
            const history = State.human.moveHistory;
            const signature = `${entry.role || 'unknown'}:${entry.move}:${entry.fen || ''}`;
            if (history[history.length - 1]?.signature === signature) return;
            history.push({ ...entry, signature, at: Date.now() });
            const limit = Math.max(4, CONFIG.humanMoveModel.historyPlies);
            if (history.length > limit) history.splice(0, history.length - limit);
        },
    };

    // ═══════════════════════════════════════════
    //  HUMAN STRATEGY
    // ═══════════════════════════════════════════
    const HumanStrategy = {
        // Convert any pair of normalized evaluations into loss from the user's
        // perspective. Mate distances are not ordinary numbers: winning sooner
        // is better, while delaying an unavoidable loss is better.
        evalLossCp: (best, candidate) => {
            if (!best || !candidate) return 0;
            if (best.type === 'cp' && candidate.type === 'cp') {
                return Math.max(0, (best.value - candidate.value) * 100);
            }
            if (best.type === 'mate' && candidate.type === 'cp') {
                return best.value > 0 ? 300 : 0;
            }
            if (best.type === 'cp' && candidate.type === 'mate') {
                return candidate.value > 0 ? 0 : 900;
            }
            if (best.type === 'mate' && candidate.type === 'mate') {
                if (best.value > 0 && candidate.value > 0) {
                    return Math.max(0, (Math.abs(candidate.value) - Math.abs(best.value)) * 50);
                }
                if (best.value < 0 && candidate.value < 0) {
                    return Math.max(0, (Math.abs(best.value) - Math.abs(candidate.value)) * 50);
                }
                return candidate.value > 0 ? 0 : 900;
            }
            return 0;
        },

        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.predictedReplyFen = null;
            State.human.predictedReplyMatched = null;
            State.human.predictedReplyForced = false;
            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.human.moveHistory = [];
            State.human.lastChoiceExplanation = null;
            State.human.timeControl = null;
            State.clock.myTime = null;
            State.clock.oppTime = null;
            State.clock.maxMyTime = null;
            State.clock.maxOppTime = null;
            State.lastMoveWasCapture = false;
            State.lastCaptureSquare = null;
            State.lastHandledFen = null;
            State.moveCount = 0;
            State.candidates = {};
            State.recentTimings = [];
            State.diagnostics.humanModel = { choices: 0, probabilitySum: 0, cpLossSum: 0, last: null };
            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.
            const fidelity = CONFIG.humanization.ratingFidelity || { enabled: false };
            let effectiveTarget = Account.effectiveTargetRating();
            let inWarmup = Account.isInWarmup();
            // Automatic new-account ramping should not silently override an
            // explicitly selected skill profile. It remains available through
            // Warmup's manual override when the user wants that behavior.
            if (fidelity.enabled && inWarmup && !CONFIG.warmup.manualOverride) {
                effectiveTarget = CONFIG.targetRating;
                inWarmup = false;
            }
            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 maximumShift = fidelity.enabled
                        ? Math.max(0, Number(fidelity.maxProfileShift) || 75)
                        : Math.max(0, Number(CONFIG.opponentAdaptation.ratingEdge) || 100);
                    const adapted = Math.min(
                        CONFIG.opponentAdaptation.maxRating,
                        CONFIG.targetRating + maximumShift
                    );
                    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 && !(fidelity.enabled && fidelity.suppressIntentionalLoss)) {
                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;

                const cpLoss = HumanStrategy.evalLossCp(bestEval, c.eval);
                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];
                let capturedValue = capturedChar ? (HumanStrategy._pieceValues[capturedChar.toLowerCase()] || 0) : 0;
                const promotion = move[4]?.toLowerCase();
                const placedPieceChar = promotion
                    ? (sideToMove === 'w' ? promotion.toUpperCase() : promotion)
                    : movingPieceChar;
                const resultingValue = promotion
                    ? (HumanStrategy._pieceValues[promotion] || movingValue)
                    : movingValue;

                // Simulate the move on the board for attack detection
                const simBoard = board.map(row => [...row]);
                simBoard[fromR][fromF] = null;
                simBoard[toR][toF] = placedPieceChar;
                // En passant removes a pawn beside the destination, not on it.
                const epSquare = fen.split(' ')[3];
                if (movingPieceChar.toLowerCase() === 'p' && fromF !== toF && !capturedChar && epSquare === toSq) {
                    simBoard[fromR][toF] = null;
                    capturedValue = HumanStrategy._pieceValues.p;
                }

                // 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 >= resultingValue) 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 (resultingValue <= 100) return true;
                    Utils.log(`PieceProtect: ${move} hangs ${placedPieceChar} (${resultingValue}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 = resultingValue; // 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 (atkIdx < atkSorted.length || defIdx < defSorted.length) {
                    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 < -resultingValue) 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 = resultingValue >= 500 ? -50 : -120;
                if (balance < rejectThreshold) {
                    Utils.log(`PieceProtect: ${move} loses ~${Math.abs(balance)}cp in exchange (${placedPieceChar} worth ${resultingValue}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 };
            }

            // v17.3 scores repertoire familiarity inside the same distribution.
            // Keep the old hard override only as a compatibility path when the
            // calibrated model is explicitly disabled.
            if (!CONFIG.humanMoveModel.enabled) {
                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();

            // The v17.3 path makes exactly one stochastic choice. All accuracy,
            // time-control, history, database and tactical signals contribute to
            // one calibrated candidate distribution instead of stacking rolls.
            if (CONFIG.humanMoveModel.enabled) {
                return HumanMoveModel.choose(fen, bestMove);
            }

            // --- 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 playerMoves = PlayerMoveDB.getCached(fen);
                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 archived human master move ${dbMove} (from ${playerMoves.length} options)`, '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 = Number.isFinite(moveResult?.cpLoss) ? moveResult.cpLoss : 0;
                if (!isBest && moveResult?.move && !Number.isFinite(moveResult?.cpLoss)) {
                    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) {
                            cpLoss = HumanStrategy.evalLossCp(bestEval, chosen.eval);
                        } 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);
                HumanMoveModel.recordHistory({
                    role: 'self',
                    move: moveResult?.move,
                    fen: moveResult?.fen || State.lastHandledFen,
                    cpLoss,
                    reason: moveResult?.reason,
                    thinkCategory: State.human.thinkCategory,
                    eval: State.currentEval?.value ?? null,
                });
            } 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) {
                        // Never claim a premove if the opponent deviated from the
                        // stored PV. `predictedReplyForced` is only set when a future
                        // provider can prove a single legal response.
                        if (State.human.predictedReplyMatched && State.human.predictedReplyForced) {
                            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 || '';
                            if (State.human.predictedReplyMatched && State.lastMoveWasCapture &&
                                State.lastCaptureSquare && ourMove.substring(2, 4) === State.lastCaptureSquare &&
                                Game.isCapture(ourMove, fen)) {
                                allowPremove = true;
                            }
                        }
                    }

                    if (allowPremove) {
                        Utils.log(`Timing: Premove (gated: ${CONFIG.premoveGating.enabled ? 'pass' : 'disabled'})`, 'debug');
                        return TimeControlContext.clampDelay(
                            Utils.humanDelay(t.premove.delay.min, t.premove.delay.max), fen, 50);
                    } 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;
                    let availableAlternatives = 0;
                    for (let i = 2; i <= CONFIG.multiPV; i++) {
                        const c = cands[i];
                        if (!c?.eval || c.eval.type !== 'cp') continue;
                        availableAlternatives++;
                        const cpGap = (best.eval.value - c.eval.value) * 100;
                        if (cpGap < fm.gapCp) goodAlternatives++;
                    }
                    // We need at least two searched alternatives (unless MultiPV
                    // itself is only 2) before claiming the position is forced.
                    // One returned alternative is insufficient evidence that the
                    // provider did not simply have a shallow/partial cache entry.
                    const minimumEvidence = Math.min(2, Math.max(1, CONFIG.multiPV - 1));
                    if (availableAlternatives >= minimumEvidence && 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');
                        const boundedFastDelay = TimeControlContext.clampDelay(fastDelay, fen, 180);
                        State.recentTimings.push(boundedFastDelay);
                        if (State.recentTimings.length > 20) State.recentTimings.shift();
                        return boundedFastDelay;
                    }
                }
            }

            // ============================================================
            // 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.
            // ============================================================
            const isTrueRecapture = State.lastMoveWasCapture && State.lastCaptureSquare &&
                moveResult.move?.substring(2, 4) === State.lastCaptureSquare &&
                Game.isCapture(moveResult.move, fen);
            if (isTrueRecapture && 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);
                const boundedRecapDelay = TimeControlContext.clampDelay(recapDelay, fen, 180);
                State.recentTimings.push(boundedRecapDelay);
                if (State.recentTimings.length > 20) State.recentTimings.shift();
                return boundedRecapDelay;
            }

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

            // v17.3 time-control conditioning: faster pools compress the whole
            // timing shape while long controls preserve deliberate thinks. The
            // final spend budget below remains the authoritative clock guard.
            const tcTiming = TimeControlContext.profile().timingMult;
            min *= tcTiming;
            max *= tcTiming;

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

            const unclampedByBudget = delay;
            delay = TimeControlContext.clampDelay(delay, fen, 250);
            if (delay < unclampedByBudget - 1) {
                Utils.log(`Timing: Clock budget ${Math.round(unclampedByBudget)}ms -> ${Math.round(delay)}ms (${TimeControlContext.describe()})`, 'debug');
            }

            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 false;
            const startPos = Humanizer.getCoords(fromSq);
            const endPos = Humanizer.getCoords(toSq);
            if (!startPos || !endPos) return false;

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

        clickSquare: async (square, pointerId = 1) => {
            const board = Game.getBoard();
            const pos = Humanizer.getCoords(square);
            if (!board || !pos) return false;
            const coords = Game.squareToCoords(square);
            const target = board.querySelector(`.piece.square-${coords}`) ||
                board.querySelector(`.square-${coords}`) ||
                document.elementFromPoint(pos.x, pos.y) || board;
            const x = pos.x + Utils.gaussianRandom(0, 2.2);
            const y = pos.y + Utils.gaussianRandom(0, 2.2);
            const base = {
                bubbles: true, cancelable: true, composed: true, view: window,
                clientX: x, clientY: y, screenX: x, screenY: y,
                button: 0, pointerId, pointerType: 'mouse', isPrimary: true,
            };
            target.dispatchEvent(new PointerEvent('pointerover', { ...base, buttons: 0, pressure: 0 }));
            target.dispatchEvent(new MouseEvent('mouseover', { ...base, buttons: 0 }));
            await Utils.sleep(Utils.randomRange(18, 55));
            target.dispatchEvent(new PointerEvent('pointerdown', { ...base, buttons: 1, pressure: 0.5 }));
            target.dispatchEvent(new MouseEvent('mousedown', { ...base, buttons: 1 }));
            await Utils.sleep(Utils.randomRange(45, 125));
            target.dispatchEvent(new PointerEvent('pointerup', { ...base, buttons: 0, pressure: 0 }));
            target.dispatchEvent(new MouseEvent('mouseup', { ...base, buttons: 0 }));
            target.dispatchEvent(new MouseEvent('click', { ...base, buttons: 0, detail: 1 }));
            Humanizer.showClick(x, y, '#4caf50');
            State.human.lastMouseX = x;
            State.human.lastMouseY = y;
            return true;
        },

        clickMove: async (fromSq, toSq) => {
            if (!await Humanizer.clickSquare(fromSq, 3)) return false;
            await Utils.sleep(Utils.randomRange(90, 260));
            return Humanizer.clickSquare(toSq, 4);
        },

        _setTransactionState: (transaction, state, details = {}) => {
            transaction.state = state;
            transaction.updatedAt = Date.now();
            if (details.method) transaction.method = details.method;
            if (details.attempt) transaction.attempt = details.attempt;
            if (details.source) transaction.confirmationSource = details.source;
            if (details.reason) transaction.reason = details.reason;
            State.diagnostics.lastInput = {
                id: transaction.id,
                move: transaction.move,
                state,
                method: transaction.method || null,
                attempt: transaction.attempt || 0,
                source: transaction.confirmationSource || null,
                reason: transaction.reason || null,
                elapsedMs: Date.now() - transaction.startedAt,
            };
        },

        _finishTransaction: (transaction, state, details = {}) => {
            if (transaction.terminal) return;
            transaction.terminal = true;
            Humanizer._setTransactionState(transaction, state, details);
            transaction.endedAt = Date.now();
            clearTimeout(transaction.watchdog);

            if (state === 'confirmed') {
                const source = transaction.confirmationSource || 'unknown';
                State.diagnostics.confirmationSources[source] = (State.diagnostics.confirmationSources[source] || 0) + 1;
                const metrics = State.diagnostics.inputMethods[transaction.method];
                if (metrics) {
                    metrics.confirmed++;
                    metrics.confirmMs += Math.max(0, transaction.endedAt - (transaction.attemptStartedAt || transaction.startedAt));
                }
            } else if (state === 'superseded') {
                State.diagnostics.inputSuperseded++;
            } else if (state === 'failed') {
                State.diagnostics.inputFailures++;
            }

            const historyEntry = { ...State.diagnostics.lastInput };
            State.input.history.push(historyEntry);
            if (State.input.history.length > 20) State.input.history.splice(0, State.input.history.length - 20);
            if (State.input.active?.id === transaction.id) State.input.active = null;
        },

        confirmMove: async (transaction, timeoutMs = 1200) => {
            const input = CONFIG.inputExecution || {};
            const deadline = Date.now() + Math.max(250, timeoutMs);
            const requiredStableReads = Math.max(1, Number(input.stableReads) || 2);
            const pollMs = Math.max(25, Number(input.pollMs) || 50);
            let changedFen = null;
            let stableReads = 0;

            while (Date.now() < deadline) {
                if (transaction.cancelled) return { status: 'cancelled', reason: 'watchdog' };

                const record = Game.getLastMoveRecord();
                const recordSignature = Game.moveRecordSignature(record);
                if (transaction.move && recordSignature && recordSignature !== transaction.initialMoveSignature && record?.from && record?.to) {
                    if (Game.recordMatchesMove(record, transaction.move)) {
                        return { status: 'confirmed', source: 'move-history' };
                    }
                    return { status: 'superseded', reason: 'unexpected-move-history' };
                }

                const observedFen = Game.getFen();
                // A transient null is unknown state, never proof of success.
                if (Game.isValidFen(observedFen) && observedFen !== transaction.startingFen) {
                    if (observedFen === changedFen) stableReads++;
                    else {
                        changedFen = observedFen;
                        stableReads = 1;
                    }
                    if (stableReads >= requiredStableReads) {
                        if (!transaction.move || Game.transitionMatchesMove(transaction.startingFen, observedFen, transaction.move)) {
                            return { status: 'confirmed', source: 'fen-transition', fen: observedFen };
                        }
                        return { status: 'superseded', reason: 'unexpected-fen-transition', fen: observedFen };
                    }
                } else if (Game.isValidFen(observedFen)) {
                    changedFen = null;
                    stableReads = 0;
                }

                if (GameContext._visibleAny(SELECTORS.gameOver)) {
                    return { status: 'confirmed', source: 'game-over' };
                }
                await Utils.sleep(pollMs);
            }
            return { status: 'timeout', reason: 'confirmation-timeout' };
        },

        // Compatibility wrapper retained for tests and external diagnostics.
        waitForMoveConfirmation: async (startingFen, timeoutMs = 1200, move = null) => {
            const transaction = {
                id: 0,
                move,
                startingFen,
                initialMoveSignature: Game.moveRecordSignature(Game.getLastMoveRecord()),
                startedAt: Date.now(),
                cancelled: false,
            };
            const result = await Humanizer.confirmMove(transaction, timeoutMs);
            return result.status === 'confirmed';
        },

        resetInputState: async (fromSq) => {
            const board = Game.getBoard();
            const pos = Humanizer.getCoords(fromSq);
            try {
                if (board && pos) {
                    const target = document.elementFromPoint(pos.x, pos.y) || board;
                    const base = {
                        bubbles: true, cancelable: true, composed: true,
                        clientX: pos.x, clientY: pos.y, pointerId: 1,
                        pointerType: 'mouse', isPrimary: true, buttons: 0,
                    };
                    target.dispatchEvent(new PointerEvent('pointercancel', base));
                    target.dispatchEvent(new MouseEvent('mouseup', { ...base, button: 0 }));
                }
                document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true }));
                document.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape', code: 'Escape', bubbles: true }));
            } catch (e) { /* cleanup is best-effort */ }
            await Utils.sleep(90);
        },

        _methodOrder: () => {
            const metrics = State.diagnostics.inputMethods;
            const clickSamples = metrics.click.attempts;
            const dragSamples = metrics.drag.attempts;
            let clickFirst = Math.random() < (CONFIG.inputExecution?.clickMoveChance ?? 0.3);
            if (clickSamples >= 3 && dragSamples >= 3) {
                const clickRate = metrics.click.confirmed / Math.max(1, clickSamples);
                const dragRate = metrics.drag.confirmed / Math.max(1, dragSamples);
                if (Math.abs(clickRate - dragRate) >= 0.15) clickFirst = clickRate > dragRate;
            }
            return clickFirst ? ['click', 'drag', 'click'] : ['drag', 'click', 'drag'];
        },

        _runInputTransaction: async (transaction) => {
            const input = CONFIG.inputExecution || { maxAttempts: 3, confirmationTimeoutMs: 1200, watchdogMs: 12000 };
            const methods = Humanizer._methodOrder();
            const attempts = Math.max(1, Math.min(Number(input.maxAttempts) || 3, methods.length));
            const from = transaction.move.substring(0, 2);
            const to = transaction.move.substring(2, 4);
            const promo = transaction.move.length > 4 ? transaction.move[4] : 'q';
            const isPromotion = (to[1] === '8' || to[1] === '1') && Humanizer.isPawnMove(from);

            transaction.watchdog = setTimeout(() => {
                if (transaction.terminal) return;
                transaction.cancelled = true;
                State.diagnostics.inputWatchdogs++;
                Humanizer._setTransactionState(transaction, 'recovering', { reason: 'input-watchdog' });
                Humanizer.resetInputState(from).catch(() => {});
                State.lastFen = null;
                Main._scheduleLoop();
            }, Math.max(12000, Number(input.watchdogMs) || 12000));

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

            try {
                for (let attempt = 0; attempt < attempts; attempt++) {
                    if (transaction.cancelled) break;
                    const beforeAttempt = Game.getFen();
                    if (Game.isValidFen(beforeAttempt) && beforeAttempt !== transaction.startingFen) {
                        const matched = Game.transitionMatchesMove(transaction.startingFen, beforeAttempt, transaction.move);
                        Humanizer._finishTransaction(transaction, matched ? 'confirmed' : 'superseded', {
                            source: matched ? 'pre-attempt-fen' : null,
                            reason: matched ? null : 'position-changed-before-retry',
                        });
                        Main._scheduleLoop();
                        return true;
                    }

                    if (attempt > 0) {
                        State.diagnostics.inputRetries++;
                        UI.toast('Retrying Move', `${from}\u2192${to} · attempt ${attempt + 1}/${attempts}`, 'warn', 1800);
                        await Humanizer.resetInputState(from);
                        State.cache.board = null;
                    }

                    const method = methods[attempt];
                    const metrics = State.diagnostics.inputMethods[method];
                    metrics.attempts++;
                    transaction.attemptStartedAt = Date.now();
                    Humanizer._setTransactionState(transaction, 'dispatched', { method, attempt: attempt + 1 });
                    Utils.log(`Input transaction #${transaction.id}, attempt ${attempt + 1}/${attempts}: ${method}-move`, 'debug');
                    const dispatched = method === 'click'
                        ? await Humanizer.clickMove(from, to)
                        : await Humanizer.dragDrop(from, to);
                    if (dispatched === false) metrics.dispatchFailures++;

                    await Utils.sleep(120);
                    const promotionFen = Game.getFen();
                    if (isPromotion && (!Game.isValidFen(promotionFen) || promotionFen === transaction.startingFen)) {
                        await Humanizer.handlePromotion(promo);
                    }

                    Humanizer._setTransactionState(transaction, 'confirming', { method, attempt: attempt + 1 });
                    const outcome = await Humanizer.confirmMove(transaction, Number(input.confirmationTimeoutMs) || 1200);
                    if (outcome.status === 'confirmed') {
                        Humanizer._finishTransaction(transaction, 'confirmed', { source: outcome.source });
                        return true;
                    }
                    if (outcome.status === 'superseded') {
                        Humanizer._finishTransaction(transaction, 'superseded', { reason: outcome.reason });
                        Main._scheduleLoop();
                        return true;
                    }
                    if (transaction.cancelled) break;

                    metrics.timeouts++;
                    Humanizer._setTransactionState(transaction, 'retrying', {
                        method, attempt: attempt + 1,
                        reason: dispatched === false ? 'dispatch-failed' : outcome.reason,
                    });
                    Utils.log(`${method}-move ${dispatched === false ? 'could not dispatch' : 'did not register'} for ${from}->${to}`, 'warn');
                    if (attempt + 1 < attempts) await Utils.sleep(Utils.randomRange(140, 300));
                }

                if (!transaction.terminal) {
                    Humanizer._finishTransaction(transaction, 'failed', {
                        reason: transaction.cancelled ? 'watchdog-timeout' : 'attempts-exhausted',
                    });
                    UI.toast('Move Input Failed', `${from}\u2192${to} did not register; recovery armed.`, 'warn', 3500);
                }
                return false;
            } catch (error) {
                Utils.log(`Input transaction #${transaction.id} failed: ${error?.message || error}`, 'error');
                Humanizer._finishTransaction(transaction, 'failed', { reason: 'input-exception' });
                return false;
            } finally {
                clearTimeout(transaction.watchdog);
                if (State.input.active?.id === transaction.id) State.input.active = null;
            }
        },

        executeMove: (move) => {
            if (!/^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(move || '')) {
                Utils.log(`Move input rejected: invalid UCI move ${move}`, 'error');
                return Promise.resolve(false);
            }
            const active = State.input.active;
            if (active && !active.terminal) {
                if (active.move === move) return active.promise;
                Utils.log(`Move input blocked: transaction #${active.id} is still ${active.state}`, 'warn');
                return Promise.resolve(false);
            }

            const currentFen = Game.getFen();
            if (!Game.isValidFen(currentFen)) {
                Utils.log('Move input postponed: board FEN temporarily unavailable', 'warn');
                return Promise.resolve(false);
            }
            if (currentFen !== State.lastFen && State.moveCount > 1) return Promise.resolve(false);

            const transaction = {
                id: ++State.input.sequence,
                move: move.toLowerCase(),
                startingFen: currentFen,
                initialMoveSignature: Game.moveRecordSignature(Game.getLastMoveRecord()),
                state: 'prepared',
                startedAt: Date.now(),
                updatedAt: Date.now(),
                attempt: 0,
                method: null,
                confirmationSource: null,
                reason: null,
                cancelled: false,
                terminal: false,
                watchdog: null,
                promise: null,
            };
            Humanizer._setTransactionState(transaction, 'prepared');
            transaction.promise = Humanizer._runInputTransaction(transaction);
            State.input.active = transaction;
            return transaction.promise;
        },

        cancelActiveTransaction: (reason = 'cancelled') => {
            const transaction = State.input.active;
            if (!transaction || transaction.terminal) return false;
            transaction.cancelled = true;
            Humanizer._finishTransaction(transaction, 'superseded', { reason });
            Humanizer.resetInputState(transaction.move.substring(0, 2)).catch(() => {});
            return true;
        },

        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: () => {} }));
            }
        },
    };

    // ═══════════════════════════════════════════
    //  AFK SESSION SUPERVISOR
    // ═══════════════════════════════════════════
    const AFKSupervisor = {
        worker: null,
        workerUrl: null,
        fallbackTimer: null,
        queueWakeTimer: null,
        lastTickAt: 0,
        wakeHandler: null,
        rtcPeerA: null,
        rtcPeerB: null,
        rtcChannel: null,
        rtcSink: null,
        rtcPromise: null,
        rtcLastPingAt: 0,
        workerRestartAt: 0,
        lifecycleHandler: null,
        restoredCheckpoint: null,

        startWorker: () => {
            if (AFKSupervisor.worker) return true;
            try {
                if (AFKSupervisor.workerUrl) URL.revokeObjectURL(AFKSupervisor.workerUrl);
                const source = 'setInterval(() => postMessage(Date.now()), 1000);';
                AFKSupervisor.workerUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
                AFKSupervisor.worker = new Worker(AFKSupervisor.workerUrl);
                AFKSupervisor.worker.onmessage = () => AFKSupervisor.tick('worker');
                AFKSupervisor.worker.onerror = () => {
                    State.diagnostics.afk.lastRecovery = 'worker-error-fallback';
                    try { AFKSupervisor.worker?.terminate(); } catch (e) {}
                    AFKSupervisor.worker = null;
                    AFKSupervisor.workerRestartAt = Date.now() + 30000;
                };
                AFKSupervisor.workerRestartAt = 0;
                return true;
            } catch (error) {
                AFKSupervisor.worker = null;
                AFKSupervisor.workerRestartAt = Date.now() + 60000;
                State.diagnostics.afk.lastRecovery = 'worker-unavailable';
                Utils.log(`AFK Guard: worker heartbeat unavailable (${error?.message || error})`, 'warn');
                return false;
            }
        },

        start: () => {
            if (!CONFIG.auto.afkGuard || AFKSupervisor.fallbackTimer) return;
            AFKSupervisor.startWorker();
            if (document.wasDiscarded) {
                State.diagnostics.afk.recoveries++;
                State.diagnostics.afk.lastRecovery = 'discard-restored';
                State.lastFen = null;
                State.lastHandledFen = null;
                try {
                    const raw = GM_getValue('ba_afk_lifecycle_v17_3', null);
                    const checkpoint = raw ? JSON.parse(raw) : null;
                    if (checkpoint && Date.now() - checkpoint.at < 6 * 3600000) {
                        AFKSupervisor.restoredCheckpoint = checkpoint;
                        Main._gameInstanceId = Math.max(Main._gameInstanceId, Number(checkpoint.game) || 0);
                    }
                } catch (e) { AFKSupervisor.restoredCheckpoint = null; }
            }
            AFKSupervisor.startRealtimeKeepAlive();

            AFKSupervisor.fallbackTimer = setInterval(() => AFKSupervisor.tick('interval'), 5000);
            AFKSupervisor.wakeHandler = event => AFKSupervisor.wake(event?.type || 'browser-event');
            window.addEventListener('focus', AFKSupervisor.wakeHandler, true);
            window.addEventListener('pageshow', AFKSupervisor.wakeHandler, true);
            window.addEventListener('online', AFKSupervisor.wakeHandler, true);
            // Listen at window capture phase so the early page visibility guard
            // cannot swallow our own recovery event at document level.
            window.addEventListener('visibilitychange', AFKSupervisor.wakeHandler, true);
            AFKSupervisor.lifecycleHandler = event => {
                if (event.type === 'freeze') {
                    State.diagnostics.afk.lastRecovery = 'page-frozen';
                    // GM_setValue is synchronous in the legacy API and survives
                    // a Memory Saver discard/reload when normal callbacks cannot.
                    try {
                        GM_setValue('ba_afk_lifecycle_v17_3', JSON.stringify({
                            at: Date.now(), fen: State.lastFen, auto: CONFIG.auto.enabled,
                            queue: CONFIG.auto.autoQueue, game: Main._gameInstanceId,
                        }));
                    } catch (e) {}
                    return;
                }
                AFKSupervisor.wake(event.type);
                AFKSupervisor.startWorker();
                AFKSupervisor.startRealtimeKeepAlive();
            };
            document.addEventListener('freeze', AFKSupervisor.lifecycleHandler, true);
            document.addEventListener('resume', AFKSupervisor.lifecycleHandler, true);
            AFKSupervisor.tick('start');
        },

        // Chrome reduces chained timers in a silent hidden tab to one batch per
        // minute after prolonged backgrounding. A local WebRTC data channel is
        // a best-effort real-time signal, keeping eligible Chromium tabs in the
        // gentler one-second timer tier without playing audio or using a server.
        startRealtimeKeepAlive: () => {
            if (AFKSupervisor.rtcChannel?.readyState === 'open') return Promise.resolve(true);
            if (AFKSupervisor.rtcPromise) return AFKSupervisor.rtcPromise;
            const RTC = unsafeWindow.RTCPeerConnection || window.RTCPeerConnection;
            if (!RTC) {
                State.diagnostics.afk.realtime = 'unsupported';
                return Promise.resolve(false);
            }
            State.diagnostics.afk.realtime = 'connecting';
            AFKSupervisor.rtcPromise = (async () => {
                let timeoutId = null;
                try {
                    const a = new RTC({ iceServers: [] });
                    const b = new RTC({ iceServers: [] });
                    AFKSupervisor.rtcPeerA = a;
                    AFKSupervisor.rtcPeerB = b;
                    const candidatesForA = [];
                    const candidatesForB = [];
                    const addCandidate = (peer, queue, candidate) => {
                        if (!candidate) return;
                        if (peer.remoteDescription?.type) peer.addIceCandidate(candidate).catch(() => {});
                        else queue.push(candidate);
                    };
                    const flushCandidates = (peer, queue) => {
                        for (const candidate of queue.splice(0)) peer.addIceCandidate(candidate).catch(() => {});
                    };
                    a.onicecandidate = event => {
                        addCandidate(b, candidatesForB, event.candidate);
                    };
                    b.onicecandidate = event => {
                        addCandidate(a, candidatesForA, event.candidate);
                    };
                    b.ondatachannel = event => {
                        AFKSupervisor.rtcSink = event.channel;
                        AFKSupervisor.rtcSink.onmessage = () => {};
                    };
                    const channel = a.createDataChannel('rexxx-afk', { ordered: false, maxRetransmits: 0 });
                    AFKSupervisor.rtcChannel = channel;
                    const opened = new Promise(resolve => {
                        timeoutId = setTimeout(() => resolve(false), 8000);
                        channel.onopen = () => resolve(true);
                        channel.onerror = () => resolve(false);
                        channel.onclose = () => {
                            State.diagnostics.afk.realtime = CONFIG.auto.afkGuard ? 'disconnected' : 'off';
                        };
                    });
                    const offer = await a.createOffer();
                    await a.setLocalDescription(offer);
                    await b.setRemoteDescription(offer);
                    flushCandidates(b, candidatesForB);
                    const answer = await b.createAnswer();
                    await b.setLocalDescription(answer);
                    await a.setRemoteDescription(answer);
                    flushCandidates(a, candidatesForA);
                    const ok = await opened;
                    clearTimeout(timeoutId);
                    if (!ok) throw new Error('local data channel timed out');
                    State.diagnostics.afk.realtime = 'connected';
                    AFKSupervisor.rtcLastPingAt = Date.now();
                    Utils.log('AFK Guard: local real-time keepalive connected', 'debug');
                    return true;
                } catch (error) {
                    clearTimeout(timeoutId);
                    Utils.log(`AFK Guard: real-time keepalive unavailable (${error?.message || error})`, 'debug');
                    try { AFKSupervisor.rtcChannel?.close(); } catch (e) {}
                    try { AFKSupervisor.rtcSink?.close(); } catch (e) {}
                    try { AFKSupervisor.rtcPeerA?.close(); } catch (e) {}
                    try { AFKSupervisor.rtcPeerB?.close(); } catch (e) {}
                    AFKSupervisor.rtcChannel = null;
                    AFKSupervisor.rtcSink = null;
                    AFKSupervisor.rtcPeerA = null;
                    AFKSupervisor.rtcPeerB = null;
                    State.diagnostics.afk.realtime = 'fallback-timers';
                    return false;
                } finally {
                    AFKSupervisor.rtcPromise = null;
                }
            })();
            return AFKSupervisor.rtcPromise;
        },

        stop: () => {
            clearInterval(AFKSupervisor.fallbackTimer);
            clearTimeout(AFKSupervisor.queueWakeTimer);
            AFKSupervisor.fallbackTimer = null;
            AFKSupervisor.queueWakeTimer = null;
            try { AFKSupervisor.worker?.terminate(); } catch (e) {}
            AFKSupervisor.worker = null;
            if (AFKSupervisor.workerUrl) URL.revokeObjectURL(AFKSupervisor.workerUrl);
            AFKSupervisor.workerUrl = null;
            try { AFKSupervisor.rtcChannel?.close(); } catch (e) {}
            try { AFKSupervisor.rtcSink?.close(); } catch (e) {}
            try { AFKSupervisor.rtcPeerA?.close(); } catch (e) {}
            try { AFKSupervisor.rtcPeerB?.close(); } catch (e) {}
            AFKSupervisor.rtcChannel = null;
            AFKSupervisor.rtcSink = null;
            AFKSupervisor.rtcPeerA = null;
            AFKSupervisor.rtcPeerB = null;
            AFKSupervisor.rtcPromise = null;
            State.diagnostics.afk.realtime = 'off';
            if (AFKSupervisor.wakeHandler) {
                window.removeEventListener('focus', AFKSupervisor.wakeHandler, true);
                window.removeEventListener('pageshow', AFKSupervisor.wakeHandler, true);
                window.removeEventListener('online', AFKSupervisor.wakeHandler, true);
                window.removeEventListener('visibilitychange', AFKSupervisor.wakeHandler, true);
            }
            if (AFKSupervisor.lifecycleHandler) {
                document.removeEventListener('freeze', AFKSupervisor.lifecycleHandler, true);
                document.removeEventListener('resume', AFKSupervisor.lifecycleHandler, true);
            }
            AFKSupervisor.wakeHandler = null;
            AFKSupervisor.lifecycleHandler = null;
            AFKSupervisor.restoredCheckpoint = null;
            AFKSupervisor.workerRestartAt = 0;
            AFKSupervisor.lastTickAt = 0;
        },

        armQueueWake: (delayMs) => {
            clearTimeout(AFKSupervisor.queueWakeTimer);
            AFKSupervisor.queueWakeTimer = setTimeout(() => AFKSupervisor.tick('queue-wake'), Math.max(250, delayMs));
        },

        wake: (reason = 'wake') => {
            if (!CONFIG.auto.afkGuard) return;
            State.cache.board = null;
            State.diagnostics.afk.lastRecovery = reason;
            if (!AFKSupervisor.worker) AFKSupervisor.startWorker();
            if (AFKSupervisor.rtcChannel?.readyState !== 'open') AFKSupervisor.startRealtimeKeepAlive();
            Main._scheduleLoop();
            if (CONFIG.auto.enabled && CONFIG.auto.autoQueue && GameContext._visibleAny(SELECTORS.gameOver)) {
                Main.checkAutoQueue();
            }
        },

        tick: (source = 'heartbeat') => {
            if (!CONFIG.auto.afkGuard) return;
            const now = Date.now();
            const gap = AFKSupervisor.lastTickAt ? now - AFKSupervisor.lastTickAt : 0;
            AFKSupervisor.lastTickAt = now;
            State.diagnostics.afk.heartbeats++;
            State.diagnostics.afk.lastBeatAt = now;

            if (!AFKSupervisor.worker && now >= AFKSupervisor.workerRestartAt) {
                AFKSupervisor.startWorker();
            }

            if (AFKSupervisor.rtcChannel?.readyState === 'open' && now - AFKSupervisor.rtcLastPingAt > 15000) {
                try {
                    AFKSupervisor.rtcChannel.send(String(now));
                    AFKSupervisor.rtcLastPingAt = now;
                } catch (e) {
                    State.diagnostics.afk.realtime = 'disconnected';
                }
            } else if (AFKSupervisor.rtcChannel && AFKSupervisor.rtcChannel.readyState === 'closed' &&
                State.diagnostics.afk.realtime === 'disconnected') {
                try { AFKSupervisor.rtcSink?.close(); } catch (e) {}
                try { AFKSupervisor.rtcPeerA?.close(); } catch (e) {}
                try { AFKSupervisor.rtcPeerB?.close(); } catch (e) {}
                AFKSupervisor.rtcChannel = null;
                AFKSupervisor.rtcSink = null;
                AFKSupervisor.rtcPeerA = null;
                AFKSupervisor.rtcPeerB = null;
                AFKSupervisor.startRealtimeKeepAlive();
            }

            // A large heartbeat gap means the tab or entire machine was
            // suspended. Invalidate DOM handles and restart from observed state.
            if (gap > 15000) {
                State.diagnostics.afk.recoveries++;
                State.diagnostics.afk.lastRecovery = `resume-after-${Math.round(gap / 1000)}s`;
                State.cache.board = null;
                State.lastFen = null;
                State.analysisId++;
                State.isThinking = false;
                APIEngine.abortAll();
                LocalEngine.cancel();
                Humanizer.cancelActiveTransaction('afk-resume');
                Utils.log(`AFK Guard: recovered after ${Math.round(gap / 1000)}s timer gap`, 'warn');
            }

            if (Main._loopScheduled && now - (Main._loopScheduledAt || now) > 5000) {
                Main._loopScheduled = false;
                State.diagnostics.afk.recoveries++;
                State.diagnostics.afk.lastRecovery = 'stale-loop-schedule';
            }
            Main._scheduleLoop();

            if (Main._queueing && now - (Main._queueLockSetAt || now) > Main._LOCK_MAX_HOLD_MS) {
                Main._queueing = false;
                Main._queueLockSetAt = 0;
                State.diagnostics.afk.recoveries++;
                State.diagnostics.afk.lastRecovery = 'stale-queue-lock';
            }
            if (CONFIG.auto.enabled && CONFIG.auto.autoQueue &&
                now >= Main._queueDeferredUntil && GameContext._visibleAny(SELECTORS.gameOver)) {
                Main.checkAutoQueue();
            }
            if (source === 'queue-wake') State.diagnostics.afk.queueRetries++;
        },
    };

    // ═══════════════════════════════════════════
    //  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: () => {
            const welcomeKey = 'ba_welcome_shown_v17_3';
            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 v17.3.0
                    </div>
                    <div style="font-size: 12px; color: #888; margin-bottom: 16px;">Calibrated skill modeling, clock budgets, and lifecycle recovery</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 v17.3.0:</strong>
                        <ul style="margin: 4px 0 0 16px; padding: 0; color: #cfd8dc;">
                            <li><strong>One calibrated move model:</strong> CP loss, skill, complexity, time control, human popularity, calculation depth, repertoire, and recent context feed one probability distribution.</li>
                            <li><strong>Time-control conditioning:</strong> Chess.com’s 40-move classification, sustainable clock budgets, increment credit, and flag-safe clamps for bullet through classical.</li>
                            <li><strong>Short-history context:</strong> planned continuations, capture flow, recent think category, and bounded move history now influence choices without stacking random rolls.</li>
                            <li><strong>AFK lifecycle recovery:</strong> self-healing Worker, local WebRTC keepalive, freeze/resume/discard awareness, and synchronous recovery snapshots.</li>
                            <li><strong>Verified local engine:</strong> pinned Stockfish 18 JS/WASM SHA-256 hashes and automatic restart on corrupt worker messages.</li>
                            <li><strong>Release laboratory:</strong> deterministic rating calibration plus 1,000 input, 300 provider-failover, and 100 lifecycle recovery simulations.</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 () => {
            // Load configuration before constructing controls so their initial
            // values and checked states reflect persisted settings.
            Settings.init(CONFIG);
            UI.injectStyles();
            UI.createInterface();

            // 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);
            UI.refreshSliders();

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

            // Initialize independently of the board so opening the script on the
            // home/play page still activates SPA observers and AFK recovery.
            AFKSupervisor.start();
            Utils.log('Starting engine and board lifecycle...');
            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) => {
            const now = Date.now();
            CONFIG.session.gamesPlayed++;
            CONFIG.session.lastGameAt = now;
            CONFIG.session.gameTimestamps = (CONFIG.session.gameTimestamps || [])
                .filter(ts => now - ts < 3600000);
            CONFIG.session.gameTimestamps.push(now);
            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;
            const fidelity = CONFIG.humanization.ratingFidelity || { enabled: false };
            const canAutoLose = CONFIG.autoLose.enabled && !(fidelity.enabled && fidelity.suppressIntentionalLoss);

            let warningLevel = null;
            let message = '';

            if (streak >= CONFIG.session.maxWinStreak) {
                warningLevel = 'critical';
                message = canAutoLose
                    ? `WIN STREAK: ${streak} in a row! Auto-Lose mode will activate next game to avoid detection.`
                    : fidelity.enabled
                        ? `WIN STREAK: ${streak} in a row. Rating Fidelity will keep the selected skill profile active.`
                    : `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);
            }

            // Fidelity mode keeps unattended sessions moving without replacing a
            // coherent skill profile with an artificial intentional-loss game.
            return warningLevel === 'critical' && !canAutoLose && !fidelity.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: 3 * 60 * 1000,
        _queueDeferredUntil: 0,
        _queueDeferredReason: null,

        _deferAutoQueue: (delayMs, reason) => {
            Main._queueDeferredUntil = Date.now() + Math.max(1000, delayMs);
            Main._queueDeferredReason = reason;
            Main._queueing = false;
            Main._queueLockSetAt = 0;
            AFKSupervisor.armQueueWake(Main._queueDeferredUntil - Date.now());
        },

        // 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;
            if (Date.now() < Main._queueDeferredUntil) return;
            Main._queueDeferredUntil = 0;
            Main._queueDeferredReason = null;

            // 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 (!Utils.isVisible(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).
            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);
                Main._deferAutoQueue(waitMs, 'hourly-limit');
                Settings.save(CONFIG);
                return;
            }

            // 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');
                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();
                Main._deferAutoQueue(breakMs, 'session-break');
                Settings.save(CONFIG);
                return;
            }

            // 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 (!Utils.isVisible(stillOpen)) {
                        State.diagnostics.afk.lastRecovery = 'new-game-requested';
                        Main._scheduleLoop();
                        return;
                    }

                    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);
            Main._deferAutoQueue(15000, 'new-game-button-not-found');
        },

        _loopScheduled: false,
        _loopScheduledAt: 0,
        _scheduleLoop: () => {
            if (Main._loopScheduled && Date.now() - Main._loopScheduledAt < 5000) return;
            Main._loopScheduled = true;
            Main._loopScheduledAt = Date.now();
            setTimeout(() => {
                Main._loopScheduled = false;
                Main._loopScheduledAt = 0;
                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(() => {
                Main._scheduleLoop();
                if (gameOverCheckScheduled) return;
                gameOverCheckScheduled = true;
                setTimeout(() => {
                    gameOverCheckScheduled = false;
                    if (GameContext._visibleAny(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
                        Main.checkAutoQueue();
                    }
                }, 500);
            });
            gameOverObserver.observe(document.body, { childList: true, subtree: true });

            // Chess.com is a SPA. Tampermonkey's native urlchange event catches
            // route transitions that do not necessarily mutate the existing move list.
            if (window.onurlchange === null) {
                window.addEventListener('urlchange', () => {
                    State.cache.board = null;
                    State.lastFen = null;
                    State.analysisId++;
                    State.isThinking = false;
                    Main._queueDeferredUntil = 0;
                    Main._queueDeferredReason = null;
                    Humanizer.cancelActiveTransaction('route-change');
                    APIEngine.abortAll();
                    Main._scheduleLoop();
                });
            }

            setInterval(Main.gameLoop, CONFIG.pollInterval);
            setInterval(() => {
                if (GameContext._visibleAny(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;

            const context = GameContext.snapshot();
            State.diagnostics.context = { ...context };
            if (!context.active) {
                if (context.gameOver && CONFIG.auto.enabled && CONFIG.auto.autoQueue && !Main._autoQueueScheduled) {
                    Main._autoQueueScheduled = true;
                    setTimeout(() => {
                        Main._autoQueueScheduled = false;
                        Main.checkAutoQueue();
                    }, 500);
                }
                if (State.isThinking && context.reason !== 'board-not-ready') {
                    State.analysisId++;
                    State.isThinking = false;
                    APIEngine.abortAll();
                    LocalEngine.cancel();
                }
                if (context.reason !== 'board-not-ready' && context.reason !== 'game-over') {
                    Humanizer.cancelActiveTransaction(context.reason);
                }
                UI.updateStatus('#666');
                return;
            }

            // Run background telemetry noise generator
            TelemetryNoise.tick();
            // (B10 + B11) Run idle mouse / annotation behavior during our long thinks
            IdleBehavior.tick();

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

            // A request for the previous position is now stale. Invalidate its
            // token and abort network work immediately instead of letting it race
            // the new board state.
            if (State.isThinking) {
                State.analysisId++;
                State.isThinking = false;
                State.diagnostics.staleAborts++;
                APIEngine.abortAll();
                LocalEngine.cancel();
            }

            const previousEval = State.currentEval ? { ...State.currentEval } : null;

            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 (GameContext._visibleAny(SELECTORS.gameOver)) {
                if (CONFIG.auto.autoQueue && !Main._autoQueueScheduled) {
                    Main._autoQueueScheduled = true;
                    setTimeout(() => {
                        Main._autoQueueScheduled = false;
                        Main.checkAutoQueue();
                    }, 2000);
                }
                return;
            }

            if (Game.isMyTurn(fen)) {
                const capture = Game.getLastCaptureInfo(previousFen, fen);
                State.lastMoveWasCapture = capture.isCapture;
                State.lastCaptureSquare = capture.square;
                const lastMove = Game.getLastMoveRecord();
                const actualUci = lastMove?.from && lastMove?.to ? `${lastMove.from}${lastMove.to}` :
                    (/^[a-h][1-8][a-h][1-8][qrbn]?$/i.test(lastMove?.notation || '') ? lastMove.notation : null);
                if (actualUci) {
                    HumanMoveModel.recordHistory({
                        role: 'opponent',
                        move: actualUci,
                        fen,
                        capture: capture.isCapture,
                        predicted: State.human.predictedReply
                            ? actualUci.substring(0, 4) === State.human.predictedReply.substring(0, 4)
                            : null,
                    });
                }
                if (State.human.predictedReply) {
                    State.human.predictedReplyMatched = actualUci
                        ? actualUci.substring(0, 4) === State.human.predictedReply.substring(0, 4)
                        : (capture.isCapture && capture.square === State.human.predictedReply.substring(2, 4));
                } else {
                    State.human.predictedReplyMatched = null;
                }
                if (capture.isCapture) {
                    Utils.log(`Opponent capture detected${capture.square ? ` on ${capture.square}` : ''} (${capture.source})`, 'debug');
                }

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

    Main.init();
})();