MoDuL's Lap Recorder

Records Lap Times

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         MoDuL's Lap Recorder
// @namespace    modul.torn.racing
// @version      1.6.5
// @description  Records Lap Times
// @author       MoDuL
// @license      MIT
// @match        https://www.torn.com/page.php?sid=racing*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-idle
// ==/UserScript==

(function () {
    "use strict";

    const RT_LAP_REC_VERSION = "1.6.5";
    var TAG = "[MoDuL's Lap Recorder v" + RT_LAP_REC_VERSION + "]";
    try { console.log(TAG, "Loaded ✅"); } catch (e) {}

    /* ============================
     * STORAGE
     * ============================ */
    const STORE_LAPS_KEY = "RT_TORN_LAPS_V2";
    const STORE_REC_KEY = "RT_TORN_REC_ENABLED";   // 1 = ON, 0 = OFF
    const STORE_UI_KEY = "RT_TORN_LAP_UI_POS";    // {btn:{left,top}, win:{left,top,width,height}}
    const STORE_META_KEY = "RT_TORN_RACE_META_V3";  // {track,car,detectedAtIso,detectedAtLocal,metaKey,countdown}
    const STORE_THEME_KEY = "RT_TORN_LAP_THEME";     // theme key
    const STORE_RECORDS_KEY = "RT_TORN_TRACK_RECORDS_V1";
    const STORE_WIN_OPEN_KEY = "RT_TORN_LAP_WIN_OPEN";
    const STORE_LAST_RACE_ID_KEY = "RT_TORN_LAST_RACE_ID";
    const STORE_CLEAR_ON_RACE_CHANGE_KEY = "RT_TORN_CLEAR_ON_RACE_CHANGE";
    const STORE_RECORDS_MAX = 2500;
    const STORE_MAX = 5000;

    /* ============================
   * SELECTORS / FORMATS
   * ============================ */
    const SEL_LAP_UI = "#racingdetails .pd-val.pd-lap";        // "3/15"
    const SEL_LAST_UI = "#racingdetails .pd-val.pd-laptime";    // "01:04"
    const SEL_COMP_UI = "#racingdetails .pd-val.pd-completion"; // "17.33%" (can be hidden)
    const SEL_POS_UI = "#racingdetails .pd-val.pd-position";   // "8/8"
    const SEL_PILOT_NAME = "#racingdetails .pd-val.pd-pilotname, li.pd-val.pd-pilotname, .pd-val.pd-pilotname";
    const SEL_INFOSPOT = "#infoSpot"; // status: "Race paused" / "Race started"
    const SEL_SPEED_VALUE = "#speed-value"; // replay speed indicator like "x4"
    const SEL_PLAY_PAUSE = "#play-pause-btn"; // replay play/pause button (class play/pause)
    const SEL_LEADERBOARD = "#leaderBoard"; // leaderboard list (spectate)
    const TIME_ANY_RE = /^(\d{1,3}:\d{2}:\d{2}(?:\.\d{1,3})?|\d{1,3}:\d{2}(?:\.\d{1,3})?|\d{1,3}\.\d{1,3})$/;

    /* ============================
   * THEMES
   * ============================ */
    const THEMES = [
        { key: "classic", name: "Classic" },
        { key: "dark", name: "Dark" },
        { key: "ice", name: "Ice" },
        { key: "neon", name: "Neon" },
        { key: "class_a", name: "Class A" },
        { key: "class_b", name: "Class B" },
        { key: "class_c", name: "Class C" },
        { key: "class_d", name: "Class D" },
        { key: "class_e", name: "Class E" },
    ];

    /* ============================
   * STATE
   * ============================ */
    let recordingEnabled = true;
    let lastLapTimeSeen = "";
    let lastLapTimeSeenToken = 0;
    let lastLapTimeUsedToken = 0;
    let lastLapCompleted = 0;
    let lastRecordedKey = "";
    let clearOnRaceChange = false;
    let theme = "classic";

    // Track records UI
    let recordsOpen = true;
    let recordsMode = "lap"; // "lap" or "race"
    let recordsTrack = "";


    let spectateName = "";
    let spectateCar = "";
    let spectateCarImg = "";
    let playerName = "";
    let spectateDriverId_ = "";
    let playerId = "";
    let lastReplaySelectAttemptAtMs = 0;
    let lastReplaySelectAttemptName = "";

    let completionPct = null;              // number or null
    let completionFinishTime = "";     // when completion switches from % to total time
    let finishCapture = null;              // {metaKey, atIso, speed, pause}
    let observersReady = false;
    let uiDirty = true;
    let finishRecordedForMetaKey = "";     // guard
    let startRecordedForMetaKey = "";     // guard (lap 0 baseline)
    let pendingCompletedTarget = 0;        // Lap counter can move before Last lap text; wait for a fresh time.
    let pendingFinalLap = 0;               // if finish detected but time not valid yet

    // Replay pause accounting (for replays)
    let replayIsPlaying = false;
    let replayPauseStartMs = null;
    let replayPauseAccumMs = 0;
    /** @type {{text:string, at:string}[]} */
    let replayEvents = [];

    /** @type {{id:string,lap:number,position:string,time:string,at:string,ms:number|null,ui?:string,track?:string,car?:string,raceDetectedAtIso?:string}[]} */
    let laps = [];

    /** @type {{id:string,track:string,mode:"lap"|"race",car:string,carImg?:string,carClass?:string,driverName?:string,driverId?:string,timeText:string,ms:number,atIso:string}[]} */
    let records = [];

    /** @type {{track:string,car:string,detectedAtIso:string,detectedAtLocal:string,metaKey:string,countdown?:string} | null} */
    let raceMeta = null;

    /* ============================
   * STORAGE HELPERS
   * ============================ */
    function loadWinOpen_() {
        const v = GM_getValue(STORE_WIN_OPEN_KEY, 0);
        return v === 1 || v === "1" || v === true || v === "true";
    }

    function saveWinOpen_(isOpen) {
        GM_setValue(STORE_WIN_OPEN_KEY, isOpen ? 1 : 0);
    }

    function loadLastRaceId_() {
        return String(GM_getValue(STORE_LAST_RACE_ID_KEY, "") || "").trim();
    }

    function loadClearOnRaceChange_() {
        const v = GM_getValue(STORE_CLEAR_ON_RACE_CHANGE_KEY, 1);
        return v === 1 || v === "1" || v === true || v === "true";
    }

    function saveClearOnRaceChange_(enabled) {
        GM_setValue(STORE_CLEAR_ON_RACE_CHANGE_KEY, enabled ? 1 : 0);
    }

    function saveLastRaceId_(raceId) {
        GM_setValue(STORE_LAST_RACE_ID_KEY, String(raceId || "").trim());
    }

    function loadJson_(key, fallback) {
        try {
            const raw = GM_getValue(key, "");
            if (!raw) return fallback;
            const obj = JSON.parse(raw);
            return obj ?? fallback;
        } catch {
            return fallback;
        }
    }
    function saveJson_(key, obj) { GM_setValue(key, JSON.stringify(obj)); }

    function loadLaps_() {
        const data = loadJson_(STORE_LAPS_KEY, []);
        return Array.isArray(data) ? data : [];
    }
    function saveLaps_() {
        if (laps.length > STORE_MAX) laps = laps.slice(laps.length - STORE_MAX);
        saveJson_(STORE_LAPS_KEY, laps);
    }

    function loadRecords_() {
        const data = loadJson_(STORE_RECORDS_KEY, []);
        return Array.isArray(data) ? data : [];
    }
    function saveRecords_() {
        if (records.length > STORE_RECORDS_MAX) records = records.slice(records.length - STORE_RECORDS_MAX);
        saveJson_(STORE_RECORDS_KEY, records);
    }

    function loadRecEnabled_() {
        const v = GM_getValue(STORE_REC_KEY, 1);
        if (v === 1 || v === "1" || v === true || v === "true") return true;
        if (v === 0 || v === "0" || v === false || v === "false") return false;
        return true;
    }
    function saveRecEnabled_() { GM_setValue(STORE_REC_KEY, recordingEnabled ? 1 : 0); }

    function loadUiState_() { return loadJson_(STORE_UI_KEY, { btn: null, win: null }); }
    function saveUiState_(state) { saveJson_(STORE_UI_KEY, state); }

    function loadRaceMeta_() {
        const m = loadJson_(STORE_META_KEY, null);
        if (!m || typeof m !== "object") return null;
        if (!m.track || !m.car || !m.detectedAtIso || !m.metaKey) return null;
        return m;
    }
    function saveRaceMeta_(m) { saveJson_(STORE_META_KEY, m); }

    function loadTheme_() {
        const t = GM_getValue(STORE_THEME_KEY, "classic");
        return THEMES.some(x => x.key === t) ? t : "classic";
    }

    function readText_(sel) {
        try {
            const el = document.querySelector(sel);
            return (el?.textContent || "").trim();
        } catch {
            return "";
        }
    }

    function saveTheme_() { GM_setValue(STORE_THEME_KEY, theme); }

    /* ============================
   * TIME / FORMAT HELPERS
   * ============================ */
    const nowIso_ = () => new Date().toISOString();

    function makeRaceMetaKey_(track, car, raceId, detectedAtIso) {
        const rid = String(raceId || "").trim();
        const seen = String(detectedAtIso || "").trim();
        const racePart = rid ? `race:${rid}` : `seen:${seen || nowIso_()}`;
        return `${racePart}__${track || "UnknownTrack"}__${car || "Replay"}`;
    }

    function lapBelongsToMeta_(r, metaKey = raceMeta?.metaKey || "") {
        if (!r) return false;
        if (!metaKey) return true;
        const id = String(r.id || "");
        if (id.startsWith(`${metaKey}|`)) return true;

        const scopePrefix = String(metaKey).match(/^(?:race|seen):.+?__/);
        if (scopePrefix && id.startsWith(scopePrefix[0])) return true;

        // Migration path for rows saved before race id / detected-at were part of the key.
        return !!(
            raceMeta &&
            String(r.raceDetectedAtIso || "") &&
            String(r.raceDetectedAtIso || "") === String(raceMeta.detectedAtIso || "")
        );
    }

    function currentRaceLaps_(includeLap0 = false) {
        const metaKey = raceMeta?.metaKey || "";
        const rows = metaKey ? laps.filter(r => lapBelongsToMeta_(r, metaKey)) : laps.slice();
        return includeLap0 ? rows : rows.filter(r => (r.lap || 0) > 0);
    }

    function refreshLastLapCompleted_() {
        lastLapCompleted = currentRaceLaps_(true).reduce((m, r) => Math.max(m, Number(r.lap) || 0), 0);
    }

    function resetSeenLapTime_() {
        lastLapTimeSeen = "";
        lastLapTimeSeenToken = 0;
        lastLapTimeUsedToken = 0;
    }

    function noteLastLapTime_(timeText) {
        const s = String(timeText || "").trim();
        if (!TIME_ANY_RE.test(s)) return false;

        const pendingNeedsTime = (pendingCompletedTarget > lastLapCompleted) || !!pendingFinalLap;
        if (s !== lastLapTimeSeen || lastLapTimeSeenToken === 0 || (pendingNeedsTime && lastLapTimeSeenToken <= lastLapTimeUsedToken)) {
            lastLapTimeSeenToken++;
        }

        lastLapTimeSeen = s;
        return true;
    }

    function hasFreshLastLapTime_() {
        return TIME_ANY_RE.test(lastLapTimeSeen) && lastLapTimeSeenToken > lastLapTimeUsedToken;
    }

    function markLastLapTimeUsed_() {
        lastLapTimeUsedToken = lastLapTimeSeenToken;
    }

    function readPlayerFromSidebar_() {
        try {
            const a = document.querySelector('#sidebarroot a[href^="/profiles.php?XID="], #sidebar a[href^="/profiles.php?XID="], a[href^="/profiles.php?XID="]');
            if (!a) return null;
            const name = (a.textContent || '').trim();
            const href = a.getAttribute('href') || '';
            const idm = href.match(/XID=(\d+)/i);
            const xid = idm ? idm[1] : '';
            if (!name) return null;
            return { name, xid };
        } catch (_) { return null; }
    }

    function readPilotName_() {
        try {
            const el = document.querySelector(SEL_PILOT_NAME);
            const name = (el?.textContent || "").trim();
            return name || "";
        } catch (_) {
            return "";
        }
    }

    function normalizeDriverName_(name) {
        return String(name || "").trim().replace(/\s+/g, " ").toLowerCase();
    }

    function currentDriverName_() {
        return String(spectateName || playerName || raceMeta?.driver || "").trim();
    }

    function currentDriverId_() {
        return String(spectateDriverId_ || playerId || raceMeta?.driverId || "").trim();
    }

    function ensurePlayer_() {
        const sidebar = readPlayerFromSidebar_();
        if (sidebar?.xid) playerId = String(sidebar.xid).trim();

        const detectedName = readPilotName_() || String(sidebar?.name || "").trim();
        if (!detectedName) return !!playerName;

        if (detectedName !== playerName) {
            playerName = detectedName;
            uiDirty = true;
            scheduleRender_();
        }
        return true;
    }
    function resetForRaceChange_(newRaceId) {
        laps = [];
        resetSeenLapTime_();
        lastLapCompleted = 0;
        lastRecordedKey = "";
        completionPct = null;
        completionFinishTime = "";
        finishRecordedForMetaKey = "";
        startRecordedForMetaKey = "";
        pendingCompletedTarget = 0;
        pendingFinalLap = 0;

        replayIsPlaying = false;
        replayPauseStartMs = null;
        replayPauseAccumMs = 0;
        replayEvents = [];

        clearRaceMeta_();
        saveLaps_();
        saveLastRaceId_(newRaceId || "");

        uiDirty = true;
        scheduleRender_();
    }
    function bestRaceAtIso_() {
        // Best-effort: for replays prefer "Time started" from the Race info panel.
        try {
            const wrap = document.querySelector('.race-info-wrap');
            if (wrap) {
                const rows = Array.from(wrap.querySelectorAll('.info-row'));
                for (const r of rows) {
                    const t = (r.querySelector('.info-title')?.textContent || '').trim();
                    const v = (r.querySelector('.info-value')?.textContent || '').trim();
                    if (/^time started/i.test(t) && v) {
                        const iso = toIsoFromAt_(v);
                        if (iso) return iso;
                    }
                }
                for (const r of rows) {
                    const t = (r.querySelector('.info-title')?.textContent || '').trim();
                    const v = (r.querySelector('.info-value')?.textContent || '').trim();
                    if ((/^time created/i.test(t) || /^time ended/i.test(t)) && v) {
                        const iso = toIsoFromAt_(v);
                        if (iso) return iso;
                    }
                }
            }
        } catch { }

        // Fall back to stored meta if available.
        if (raceMeta) {
            const cand =
                  raceMeta.startAtIso ||
                  raceMeta.raceAtIso ||
                  raceMeta.detectedAtIso ||
                  raceMeta.createdAtIso ||
                  "";
            if (cand) return String(cand);
        }

        return nowIso_();
    }

    function pad2_(n) { return String(n).padStart(2, "0"); }

    function formatLocalStamp_(d) {
        const yy = String(d.getFullYear()).slice(-2);
        const mm = pad2_(d.getMonth() + 1);
        const dd = pad2_(d.getDate());
        const HH = pad2_(d.getHours());
        const MM = pad2_(d.getMinutes());
        return `${yy}-${mm}-${dd}.${HH}${MM}`;
    }

    function formatLocalDateTime_(d) {
        const dd = pad2_(d.getDate());
        const mm = pad2_(d.getMonth() + 1);
        const yyyy = d.getFullYear();
        const HH = pad2_(d.getHours());
        const MM = pad2_(d.getMinutes());
        const SS = pad2_(d.getSeconds());
        return `${dd}/${mm}/${yyyy}, ${HH}:${MM}:${SS}`;
    }

    function parseCountdownToSeconds_(s) {
        const txt = String(s || "").toLowerCase();
        const h = (txt.match(/(\d+)\s*hour/) || [])[1];
        const m = (txt.match(/(\d+)\s*minute/) || [])[1];
        const sec = (txt.match(/(\d+)\s*second/) || [])[1];
        const hours = h ? parseInt(h, 10) : 0;
        const mins = m ? parseInt(m, 10) : 0;
        const secs = sec ? parseInt(sec, 10) : 0;
        const total = hours * 3600 + mins * 60 + secs;
        return Number.isFinite(total) ? total : 0;
    }

    function formatShortCountdown_(totalSeconds) {
        totalSeconds = Math.max(0, Math.floor(totalSeconds));
        const h = Math.floor(totalSeconds / 3600);
        const m = Math.floor((totalSeconds % 3600) / 60);
        const s = totalSeconds % 60;
        if (h > 0) return `${h}h ${String(m).padStart(2, "0")}m ${String(s).padStart(2, "0")}s`;
        if (m > 0) return `${m}m ${String(s).padStart(2, "0")}s`;
        return `${s}s`;
    }

    // ============================
    // TAB TITLE TIMER (countdown / elapsed)
    // ============================
    const RT_TAB_SUFFIX = " | Torn";
    let rtOrigTitle_ = null;
    let rtStartEpochMs_ = null;

    const RT_ORIG_TITLE = document.title || "Torn";

    function isReplay_() {
        // Strict: only treat as replay when Torn replay controls exist.
        // (Prevents pause/speed logic from running in live races.)
        return !!(document.querySelector(SEL_SPEED_VALUE) || document.querySelector(SEL_PLAY_PAUSE) || isReplayPage_());
    }

    function getReplaySpeedFactor_() {
        // Reads replay speed from the in-game control: <span id="speed-value">x4</span>
        // Returns 1 when not available.
        try {
            const el = document.querySelector(SEL_SPEED_VALUE);
            const raw = (el?.textContent || "").trim();
            const m = raw.match(/x\s*(\d+(?:\.\d+)?)/i);
            const v = m ? parseFloat(m[1]) : NaN;
            return (Number.isFinite(v) && v > 0) ? v : 1;
        } catch {
            return 1;
        }
    }

    function getReplayPauseAccumMs_() {
        // Total paused milliseconds so far (replay only). While currently paused, includes the ongoing pause duration.
        if (!isReplay_()) return 0;
        const now = Date.now();
        if (replayPauseStartMs != null) return replayPauseAccumMs + Math.max(0, now - replayPauseStartMs);
        return replayPauseAccumMs;
    }

    function getReplayPauseCaptureMs_() {
        // For lap capture snapshots we want only completed pause time.
        // If the replay has just auto-paused at the finish, including the ongoing pause
        // can collapse the last lap to 0ms.
        if (!isReplay_()) return 0;
        return replayPauseAccumMs;
    }

    function fmtLocalMs_(d) {
        const pad = (n, w = 2) => String(n).padStart(w, "0");
        return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
    }

    function logReplayEvent_(text) {
        const at = fmtLocalMs_(new Date());
        replayEvents.push({ text, at });
        if (replayEvents.length > 50) replayEvents.shift();
        uiDirty = true;
        scheduleRender_();
    }

    function isReplayPlaying_() {
        const btn = document.querySelector(SEL_PLAY_PAUSE);
        if (!btn) return false;
        // Torn uses class "play" when paused/stopped, and "pause" when actually playing.
        if (btn.classList && btn.classList.contains("pause")) return true;
        const cls = String(btn.className || "").toLowerCase();
        return cls.includes("pause");
    }


    function hookReplayPlayObserver_() {
        try {
            const btn = document.querySelector(SEL_PLAY_PAUSE);
            if (!btn) return false;

            // Ensure we only hook once per page load
            if (btn.__tlrHooked) return true;
            btn.__tlrHooked = true;

            const onChange = () => {
                const nowPlaying = isReplay_() && isReplayPlaying_();

                // Track pause/resume and exclude paused time from chrono.
                if (isReplay_()) {
                    if (nowPlaying && !replayIsPlaying) {
                        // resumed
                        if (replayPauseStartMs != null) {
                            const delta = Math.max(0, Date.now() - replayPauseStartMs);
                            replayPauseAccumMs += delta;
                            replayPauseStartMs = null;
                            logReplayEvent_(`Replay resumed`);
                        } else {
                            // first time play, just note
                            logReplayEvent_(`Replay started`);
                        }
                    } else if (!nowPlaying && replayIsPlaying) {
                        // paused
                        replayPauseStartMs = Date.now();
                        logReplayEvent_(`Replay paused`);
                    }
                    replayIsPlaying = nowPlaying;
                } else {
                    // not in replay context
                    replayIsPlaying = false;
                    replayPauseStartMs = null;
                    replayPauseAccumMs = 0;
                }

                // When user presses Play on a replay, Torn swaps class from "play" -> "pause".
                if (isReplay_() && nowPlaying) {
                    // If init disabled recording earlier (older builds), force it ON now.
                    if (!recordingEnabled) {
                        recordingEnabled = true;
                        saveRecEnabled_();
                    }
                    // Establish Lap 0 immediately so Lap 1 chrono can be computed.
                    maybeRecordLap0Baseline_();
                    maybeMarkRaceStarted_();
                }
            };

            new MutationObserver(onChange).observe(btn, { attributes: true, attributeFilter: ["class"] });
            // Run once in case we're already playing when the script attaches.
            onChange();
            return true;
        } catch (e) {
            console.warn("[Lap Recorder] hookReplayPlayObserver_ failed", e);
            return false;
        }
    }


    function hookReplaySpeedObserver_() {
        try {
            const el = document.querySelector(SEL_SPEED_VALUE);
            if (!el) return false;

            if (el.__tlrSpeedHooked) return true;
            el.__tlrSpeedHooked = true;

            const onChange = () => {
                // The render reads the current speed label.
                uiDirty = true;
                scheduleRender_();
            };

            new MutationObserver(onChange).observe(el, { characterData: true, childList: true, subtree: true });
            onChange();
            return true;
        } catch (e) {
            console.warn("[MoDuL's Lap Recorder] hookReplaySpeedObserver_ failed", e);
            return false;
        }
    }

    function getLeaderboardEntries_() {
        try {
            const root = document.querySelector(SEL_LEADERBOARD);
            if (!root) return [];

            const rows = Array.from(root.querySelectorAll("li"))
                .filter(row => row.querySelector("li.name span, li.name, .name span, .name") && row.querySelector("li.car img, .car img"));

            return rows.map(row => {
                const name = (row.querySelector("li.name span, li.name, .name span, .name")?.textContent || "").trim();
                const imgEl = row.querySelector("li.car img, .car img");
                const car = (imgEl?.getAttribute("title") || "").trim();
                const img = (imgEl?.getAttribute("src") || "").trim();
                const href = row.querySelector('a[href*="XID="]')?.getAttribute("href") || "";
                const idMatch = href.match(/XID=(\d+)/i);
                const driverId = idMatch ? idMatch[1] : "";
                const selected = /\bselected\b/i.test(String(row.className || ""));
                return { row, name, car, img, driverId, selected };
            }).filter(entry => entry.name);
        } catch {
            return [];
        }
    }

    function readSelectedDriver_() {
        const entry = getLeaderboardEntries_().find(item => item.selected);
        if (!entry) return { name: "", car: "", img: "", driverId: "" };
        return entry;
    }

    function clickReplayDriverEntry_(entry) {
        const row = entry?.row;
        if (!row) return false;

        const targets = [
            row.querySelector("li.name a, .name a"),
            row.querySelector("li.name, .name"),
            row.querySelector("a"),
            row
        ].filter(Boolean);

        for (const target of targets) {
            try {
                if (typeof target.click === "function") target.click();
                target.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
                target.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
                target.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
                return true;
            } catch { }
        }
        return false;
    }

    function trySelectReplayDriver_() {
        if (!isReplay_()) return false;

        const wanted = normalizeDriverName_(playerName);
        if (!wanted) return false;

        const selected = readSelectedDriver_();
        if (selected.name) return false;

        const now = Date.now();
        if (lastReplaySelectAttemptName === wanted && (now - lastReplaySelectAttemptAtMs) < 1000) return false;

        const entry = getLeaderboardEntries_().find(item => normalizeDriverName_(item.name) === wanted);
        if (!entry) return false;

        lastReplaySelectAttemptName = wanted;
        lastReplaySelectAttemptAtMs = now;
        return clickReplayDriverEntry_(entry);
    }


    let playerProbeTimer_ = null;
    function startPlayerProbe_() {
        if (playerProbeTimer_) return;
        let tries = 0;
        playerProbeTimer_ = setInterval(() => {
            tries++;
            ensurePlayer_();
            if ((playerName && playerName !== '—') || tries > 30) {
                clearInterval(playerProbeTimer_);
                playerProbeTimer_ = null;
            }
        }, 500);
    }

    function refreshSpectate_() {
        ensurePlayer_();
        trySelectReplayDriver_();

        const { name, car, img, driverId } = readSelectedDriver_();
        const changed =
              (name && name !== spectateName) ||
              (car && car !== spectateCar) ||
              (img && img !== spectateCarImg) ||
              (driverId && driverId !== spectateDriverId_);

        if (name) spectateName = name;
        if (car) spectateCar = car;
        if (img) spectateCarImg = img;
        if (driverId) spectateDriverId_ = driverId;

        // If meta car is unknown (e.g., "Replay"), use the selected driver's car.
        if (raceMeta && spectateCar && (!raceMeta.car || /^replay(?:\s+driver)?$/i.test(String(raceMeta.car || "")))) {
            raceMeta.car = spectateCar;
            if (spectateCarImg && !raceMeta.carImg) raceMeta.carImg = spectateCarImg;
            raceMeta.driver = currentDriverName_();
            raceMeta.driverId = currentDriverId_();
            saveRaceMeta_(raceMeta);
        }

        // Keep car image fresh if we have it.
        if (raceMeta && spectateCarImg && raceMeta.car === spectateCar && raceMeta.carImg !== spectateCarImg) {
            raceMeta.carImg = spectateCarImg;
            raceMeta.driver = currentDriverName_();
            raceMeta.driverId = currentDriverId_();
            saveRaceMeta_(raceMeta);
        }

        if (raceMeta) {
            const nextDriver = currentDriverName_();
            const nextDriverId = currentDriverId_();
            if ((nextDriver && raceMeta.driver !== nextDriver) || (nextDriverId && raceMeta.driverId !== nextDriverId)) {
                if (nextDriver) raceMeta.driver = nextDriver;
                if (nextDriverId) raceMeta.driverId = nextDriverId;
                saveRaceMeta_(raceMeta);
            }
        }

        if (changed) { uiDirty = true; scheduleRender_(); }
    }
    function raceIsFinished_() {
        const info = document.querySelector(SEL_INFOSPOT);
        const t = (info?.textContent || "").toLowerCase();
        if (t.includes("finished")) return true;
        // Some layouts show it in the video bar / title.
        const bodyTxt = (document.body?.textContent || "").toLowerCase();
        return bodyTxt.includes("race finished");
    }


    // ============================
    // TRACK RECORDS (per track, unique cars)
    // ============================
    function makeRecId_(mode, track, car, atIso) {
        return `${mode}|${track}|${car}|${atIso}`;
    }

    function msToTimeText_(ms, mode) {
        // lap/chrono/last: m:ss.mmm
        // race: h:mm:ss.mmm (or m:ss.mmm when < 1h)
        ms = Math.max(0, Math.floor(ms || 0));
        if (!ms) return '';
        if (mode === "race") {
            const totalSec = Math.floor(ms / 1000);
            const h = Math.floor(totalSec / 3600);
            const m = Math.floor((totalSec % 3600) / 60);
            const s = totalSec % 60;
            const mm = ms % 1000;
            if (h > 0) return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(mm).padStart(3, "0")}`;
            return `${m}:${String(s).padStart(2, "0")}.${String(mm).padStart(3, "0")}`;
        }
        const totalSec = Math.floor(ms / 1000);
        const m = Math.floor(totalSec / 60);
        const s = totalSec % 60;
        const mm = ms % 1000;
        return `${m}:${String(s).padStart(2, "0")}.${String(mm).padStart(3, "0")}`;
    }

    function finishRaceRecording_(metaKey) {
        const key = metaKey || raceMeta?.metaKey || "";
        if (!key || finishRecordedForMetaKey === key) return;
        finishRecordedForMetaKey = key;
        maybeSaveTrackRecordsForRace_();
        uiDirty = true;
        scheduleRender_();
    }

    function clearFinishCapture_() {
        finishCapture = null;
    }

    function maybeCaptureFinishMoment_(parsed) {
        try {
            ensureRaceMeta_();
            const metaKey = raceMeta?.metaKey || "";
            if (!metaKey) return;

            const comp = parsed || ((completionPct == null && !completionFinishTime)
                ? parseCompletionOrTime_()
                : { pct: completionPct, time: completionFinishTime });

            const finished = !!(comp && comp.time) || (typeof comp?.pct === "number" && isFinite(comp.pct) && comp.pct >= 99.9);
            if (!finished) return;
            if (finishCapture && finishCapture.metaKey === metaKey) return;

            finishCapture = {
                metaKey,
                atIso: nowIso_(),
                speed: isReplay_() ? getReplaySpeedFactor_() : 1,
                pause: isReplay_() ? getReplayPauseCaptureMs_() : 0
            };
        } catch { }
    }

    function deriveFinalLapCapture_(lapNum, timeText) {
        try {
            const lapMs = timeTextToMs_(timeText);
            if (!(typeof lapMs === "number" && isFinite(lapMs) && lapMs > 0)) return null;

            const prevLapNum = Math.max(0, (Number(lapNum) || 0) - 1);
            const prevRows = currentRaceLaps_(true).filter(r => (r.lap || 0) === prevLapNum);
            if (!prevRows.length) return null;

            const prev = prevRows
                .slice()
                .sort((a, b) => tsMs_(b) - tsMs_(a))[0];
            const prevTs = tsMs_(prev);
            if (!isFinite(prevTs)) return null;

            if (isReplay_()) {
                const prevPause = (typeof prev.pause === "number" && isFinite(prev.pause)) ? prev.pause : 0;
                const speedNow = getReplaySpeedFactor_();
                const pauseNow = getReplayPauseCaptureMs_();
                const speed = (Number.isFinite(speedNow) && speedNow > 0)
                    ? speedNow
                    : ((typeof prev.speed === "number" && isFinite(prev.speed) && prev.speed > 0)
                        ? prev.speed
                        : ((typeof raceMeta?.replaySpeed === "number" && isFinite(raceMeta.replaySpeed) && raceMeta.replaySpeed > 0) ? raceMeta.replaySpeed : 1));

                if (finishCapture && finishCapture.metaKey === (raceMeta?.metaKey || "")) {
                    const finishTs = Date.parse(finishCapture.atIso || "");
                    const finishPause = (typeof finishCapture.pause === "number" && isFinite(finishCapture.pause))
                        ? finishCapture.pause
                        : pauseNow;
                    const finishSpeed = (typeof finishCapture.speed === "number" && isFinite(finishCapture.speed) && finishCapture.speed > 0)
                        ? finishCapture.speed
                        : speed;
                    const finishCalcMs = isFinite(finishTs)
                        ? Math.max(0, (finishTs - prevTs - (finishPause - prevPause)) * finishSpeed)
                        : NaN;
                    const finishLooksSane = isFinite(finishCalcMs)
                        && finishCalcMs > 0
                        && Math.abs(finishCalcMs - lapMs) <= 1500;

                    if (finishLooksSane) {
                        return {
                            atIso: finishCapture.atIso,
                            speed: finishSpeed,
                            pause: finishPause
                        };
                    }
                }

                return {
                    atIso: new Date(prevTs + Math.max(0, pauseNow - prevPause) + (lapMs / speed)).toISOString(),
                    speed,
                    pause: pauseNow
                };
            }

            return {
                atIso: new Date(prevTs + lapMs).toISOString()
            };
        } catch {
            return null;
        }
    }

    function fallbackLapChronoMs_(row) {
        const ms = timeTextToMs_(row && row.time);
        return (typeof ms === "number" && isFinite(ms) && ms > 0) ? ms : null;
    }

    function computeLapChronoMs_(cur, prev) {
        if (!cur || (cur.lap || 0) <= 0) return null;

        const tCur = Date.parse(lapAtIso_(cur));
        const tPrev = prev ? Date.parse(lapAtIso_(prev)) : Date.parse((raceMeta && raceMeta.startAtIso) ? raceMeta.startAtIso : "");
        if (isFinite(tCur) && isFinite(tPrev)) {
            const spd = (typeof cur.speed === "number" && isFinite(cur.speed) && cur.speed > 0)
                ? cur.speed
                : (typeof raceMeta?.replaySpeed === "number" && isFinite(raceMeta.replaySpeed) && raceMeta.replaySpeed > 0 ? raceMeta.replaySpeed : 1);
            const pCur = (cur.pause || 0);
            const pPrev = prev ? (prev.pause || 0) : 0;
            const calcMs = Math.max(0, (tCur - tPrev - (pCur - pPrev)) * spd);
            if (calcMs > 0) return calcMs;
        }

        return fallbackLapChronoMs_(cur);
    }

    function maybeRecordFinalLapOnFinish_() {
        if (!recordingEnabled) return;
        ensureRaceMeta_();

        const metaKey = raceMeta?.metaKey || "";
        if (!metaKey) return;
        if (finishRecordedForMetaKey === metaKey) return;

        const { cur, tot, raw } = parseLapUi_();
        if (!cur || !tot) return;
        if (cur !== tot) return;

        const parsed = (completionPct == null && !completionFinishTime)
        ? parseCompletionOrTime_()
        : { pct: completionPct, time: completionFinishTime };
        maybeCaptureFinishMoment_(parsed);

        const pct = parsed.pct;
        const finishTime = parsed.time;
        if (!finishTime && (pct == null || pct < 99.9)) return;

        if (finishTime) {
            if (raceMeta && (raceMeta.totalTime || "") !== finishTime) {
                raceMeta.totalTime = finishTime;
                saveRaceMeta_(raceMeta);
            }
        }

        if (lastLapCompleted === tot) {
            finishRaceRecording_(metaKey);
            return;
        }
        if (lastLapCompleted !== tot - 1) return;

        const lastNow = (document.querySelector(SEL_LAST_UI)?.textContent || "").trim();

        // Do not reuse the previous lap's time for the finish.
        if (!noteLastLapTime_(lastNow) || !hasFreshLastLapTime_()) {
            pendingFinalLap = tot;
            return;
        }

        const posNow = getPositionText_();
        const finalCapture = deriveFinalLapCapture_(tot, lastLapTimeSeen) || {};
        recordLap_(tot, lastLapTimeSeen, raw, posNow, finalCapture);
        markLastLapTimeUsed_();

        lastLapCompleted = tot;
        finishRaceRecording_(metaKey);
    }

    function computeChronoByLap_(metaKey) {
        try {
            const asc = (laps || [])
                .filter(r => r && (r.lap || 0) > 0 && lapBelongsToMeta_(r, metaKey))
            .slice()
            .sort((a, b) => (a.lap || 0) - (b.lap || 0));

            const out = new Map();
            if (!asc.length) return out;

            for (let i = 0; i < asc.length; i++) {
                const cur = asc[i];
                const prev = i > 0 ? asc[i - 1] : null;
                out.set(cur.lap, computeLapChronoMs_(cur, prev));
            }

            return out;
        } catch {
            return new Map();
        }
    }

    function maybeSaveTrackRecordsForRace_() {
        try {
            ensureRaceMeta_();
            if (!raceMeta?.metaKey) return;
            const track = raceMeta.track || "UnknownTrack";
            const car = raceMeta.car || "Replay";
            const carImg = raceMeta.carImg || findCarImageUrl_(car) || spectateCarImg || "";
            const driverName = currentDriverName_();
            const driverId = currentDriverId_();
            const carClass = String(raceMeta.carClass || "").trim();
            const atIso = raceMeta.detectedAtIso || nowIso_();

            // 1) Best lap chrono (calculated)
            const chrono = computeChronoByLap_(raceMeta.metaKey);
            const msBestLap = Math.min(...Array.from(chrono.values()).filter(v => typeof v === "number" && v > 0));
            if (isFinite(msBestLap) && msBestLap > 0) {
                const id = makeRecId_("lap", track, car, atIso);
                records = records.filter(r => r.id !== id);
                records.push({
                    id,
                    track,
                    mode: "lap",
                    car,
                    carImg,
                    carClass,
                    driverName,
                    driverId,
                    raceId: raceMeta.raceId || "",
                    timeText: msToTimeText_(msBestLap, "lap"),
                    ms: msBestLap,
                    atIso
                });
            }

            // 2) Race total time
            // Torn completion total can be rounded to centiseconds, while our Chrono sum can be more precise.
            // Use the FASTEST positive value so records update correctly.
            const tornTxt = String(raceMeta.totalTime || "").trim();
            const msTorn = timeTextToMs_(tornTxt);

            const chronoAll = computeChronoByLap_(raceMeta.metaKey);
            const vals = Array.from(chronoAll.values()).filter(v => typeof v === "number" && isFinite(v) && v > 0);
            const msChronoSum = vals.length ? vals.reduce((a, b) => a + b, 0) : 0;

            let msRace = 0;
            if (msTorn > 0 && msChronoSum > 0) msRace = Math.min(msTorn, msChronoSum);
            else msRace = msTorn > 0 ? msTorn : msChronoSum;

            if (msRace && msRace > 0) {
                const id = makeRecId_("race", track, car, atIso);
                records = records.filter(r => r.id !== id);
                records.push({
                    id,
                    track,
                    mode: "race",
                    car,
                    carImg,
                    carClass,
                    driverName,
                    driverId,
                    raceId: raceMeta.raceId || "",
                    timeText: msToTimeText_(msRace, "race"),
                    ms: msRace,
                    atIso
                });
            }

            saveRecords_();
        } catch (e) {
            console.warn("[MoDuL's Lap Recorder] maybeSaveTrackRecordsForRace_ failed", e);
        }
    }

    function formatHMS_(ms) {
        ms = Math.max(0, Math.floor(ms));
        const totalSec = Math.floor(ms / 1000);
        const h = Math.floor(totalSec / 3600);
        const m = Math.floor((totalSec % 3600) / 60);
        const s = totalSec % 60;
        return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
    }

    function setTabTitle_(ms) {
        if (!rtOrigTitle_) rtOrigTitle_ = document.title || "Torn";
        document.title = `R${formatHMS_(ms)}${RT_TAB_SUFFIX}`;
    }

    function updateTabTitle_() {
        // Freeze at finish time if known
        const finishTxt = (completionFinishTime || raceMeta?.totalTime || "");
        if (finishTxt) {
            const ms = timeTextToMs_(finishTxt);
            if (ms) { setTabTitle_(ms); return; }
        }

        // Countdown (pre-race)
        if (!raceHasStarted_() && (raceMeta?.countdown || "")) {
            const secs = parseCountdownToSeconds_(raceMeta.countdown);
            setTabTitle_(secs * 1000);
            // Use planned start as baseline so elapsed is stable when race begins
            rtStartEpochMs_ = Date.now() + secs * 1000;
            return;
        }

        // Elapsed (race running)
        if (raceHasStarted_()) {
            if (!rtStartEpochMs_) rtStartEpochMs_ = Date.now();
            setTabTitle_(Date.now() - rtStartEpochMs_);
            return;
        }

        // No race context; restore original title
        if (rtOrigTitle_) document.title = rtOrigTitle_;
    }


    function timeTextToMs_(t) {
        const s = String(t || "").trim().toLowerCase();
        if (!s) return null;

        // h:mm:ss(.ms)
        let m1 = s.match(/^(\d{1,3}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/);
        if (m1) {
            return ((parseInt(m1[1], 10) * 3600) + (parseInt(m1[2], 10) * 60) + parseInt(m1[3], 10)) * 1000 +
                (m1[4] ? parseInt(m1[4].padEnd(3, "0"), 10) : 0);
        }

        // mm:ss(.ms)
        m1 = s.match(/^(\d{1,3}):(\d{2})(?:\.(\d{1,3}))?$/);
        if (m1) {
            return (parseInt(m1[1], 10) * 60 + parseInt(m1[2], 10)) * 1000 +
                (m1[3] ? parseInt(m1[3].padEnd(3, "0"), 10) : 0);
        }

        // ss.ms (e.g. 50.825)
        m1 = s.match(/^(\d{1,3})\.(\d{1,3})$/);
        if (m1) return parseInt(m1[1], 10) * 1000 + parseInt(m1[2].padEnd(3, "0"), 10);

        // "14 minutes, 27 seconds" / "1 hour, 2 minutes, 3 seconds" / "14m 27s"
        const h = (s.match(/(\d+)\s*(?:h|hour|hours)\b/) || [])[1];
        const min = (s.match(/(\d+)\s*(?:m|min|mins|minute|minutes)\b/) || [])[1];
        const sec = (s.match(/(\d+)\s*(?:s|sec|secs|second|seconds)\b/) || [])[1];

        if (h || min || sec) {
            const hh = h ? parseInt(h, 10) : 0;
            const mm = min ? parseInt(min, 10) : 0;
            const ss = sec ? parseInt(sec, 10) : 0;
            if ([hh, mm, ss].some(n => !Number.isFinite(n))) return null;
            return ((hh * 3600) + (mm * 60) + ss) * 1000;
        }

        return null;
    }

    function msToDeltaText_(ms) {
        if (ms == null || !isFinite(ms)) return "";
        const s = ms / 1000;
        const abs = Math.abs(s).toFixed(3);
        const sign = s > 0 ? "+" : (s < 0 ? "-" : "+");
        return `${sign}${abs}`;
    }

    function msToGapText_(ms) {
        if (ms == null) return "";
        const rounded = Math.round(ms / 1000);
        if (rounded === 0) return "0";
        const sign = rounded > 0 ? "+" : "-";
        const abs = Math.abs(rounded);
        const m = Math.floor(abs / 60);
        const s = abs % 60;
        return `${sign}${m}:${String(s).padStart(2, "0")}`;
    }


    function msToCalcText_(ms) {
        if (ms == null || !isFinite(ms) || ms <= 0) return "";
        const totalMs = Math.round(ms);
        const msPart = totalMs % 1000;
        const totalSec = Math.floor(totalMs / 1000);
        const s = totalSec % 60;
        const m = Math.floor(totalSec / 60);
        return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}.${String(msPart).padStart(3, "0")}`;
    }



    function tsMs_(r) {
        // Prefer ISO timestamp with milliseconds for stable precision across replay multipliers.
        if (!r) return NaN;
        if (r.atIso) {
            const v = Date.parse(r.atIso);
            if (isFinite(v)) return v;
        }
        const s = String(r.at || "");
        if (!s) return NaN;
        const isoLike = s.replace(" ", "T"); // "YYYY-MM-DD HH:MM:SS.mmm" -> ISO-like
        const v2 = Date.parse(isoLike);
        return v2;
    }

    function gapClass_(ms) {
        if (ms == null) return "gap-zero";
        if (ms < 0) return "gap-neg";
        if (ms > 0) return "gap-pos";
        return "gap-zero";
    }

    function parseLapUi_() {
        const el = document.querySelector(SEL_LAP_UI);
        const m = (el?.textContent || "").match(/^(\d+)\s*\/\s*(\d+)$/);
        return m ? { cur: +m[1], tot: +m[2], raw: `${m[1]}/${m[2]}` } : { cur: null, tot: null, raw: "" };
    }


    function getPositionText_() {
        const el = document.querySelector(SEL_POS_UI);
        return (el?.textContent || "").trim(); // e.g. "3/6" or "8/8"
    }

    function parseCompletionOrTime_() {
        const el = document.querySelector(SEL_COMP_UI);
        const t = (el?.textContent || "").trim();
        const m = t.match(/(\d+(?:\.\d+)?)\s*%/);
        if (m) {
            const v = parseFloat(m[1]);
            return { pct: Number.isFinite(v) ? v : null, time: "" };
        }
        // On some pages, completion switches from % to total time at finish.
        if (TIME_ANY_RE.test(t)) return { pct: null, time: t };
        return { pct: null, time: "" };
    }

    function esc_(s) {
        return String(s ?? "").replace(/[&<>"]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
    }

    function escAttr_(s) { return esc_(s); }

    function fmtWhen_(iso) {
        if (!iso) return "—";
        const s = String(iso);
        if (/^\d{4}-\d{2}-\d{2}T/.test(s)) return s.replace("T", " ").replace("Z", "").slice(0, 16);
        return s.slice(0, 32);
    }

    // Alias used by some render/export paths
    function escapeHtml_(s) { return esc_(s); }


    function toIsoFromAt_(atStr) {
        // Convert "YYYY-MM-DD HH:MM:SS.mmm" -> "YYYY-MM-DDTHH:MM:SS.mmmZ"
        const s = String(atStr || "").trim();
        if (!s) return "";
        if (s.includes("T")) return s;
        const m = s.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})(?:\.(\d{1,3}))?$/);
        if (!m) return "";
        const ms = m[3] ? "." + m[3].padEnd(3, "0") : "";
        return `${m[1]}T${m[2]}${ms}Z`;
    }

    function lapAtIso_(r) {
        return (r && r.atIso) ? String(r.atIso) : toIsoFromAt_(r && r.at);
    }

    function sanitizeFilePart_(s) {
        return String(s || "")
            .trim()
            .replace(/[\/\\:*?"<>|]/g, "")
            .replace(/\s+/g, " ")
            .replace(/\s/g, "_")
            .replace(/_+/g, "_")
            .slice(0, 60) || "Unknown";
    }

    function buildBaseFilename_() {
        const m = raceMeta || { track: "UnknownTrack", car: "Replay", detectedAtIso: nowIso_(), startAtIso: "" };

        // Prefer true race start time:
        // 1) replay info "Time started" (race log)
        // 2) our recorded startAtIso (live/replay playback baseline)
        // 3) detectedAtIso
        let whenIso = "";
        const tStarted = (m.replayInfo && (m.replayInfo.time_started || m.replayInfo.timeStarted)) ? String(m.replayInfo.time_started || m.replayInfo.timeStarted) : "";
        if (tStarted) {
            // "YYYY-MM-DD HH:MM:SS" -> ISO-ish
            const mm = tStarted.match(/^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})$/);
            whenIso = mm ? `${mm[1]}T${mm[2]}Z` : "";
        }
        if (!whenIso && m.startAtIso) whenIso = String(m.startAtIso);
        if (!whenIso) whenIso = String(m.detectedAtIso || nowIso_());

        const d = new Date(whenIso);
        const stamp = formatLocalStamp_(d);
        const track = sanitizeFilePart_(m.track);
        const car = sanitizeFilePart_(m.car);

        // Position tag (P1, P6, etc.) — use latest known position if available.
        let pTag = "";
        try {
            const asc = currentRaceLaps_().slice().sort((a, b) => (a.lap || 0) - (b.lap || 0));
            const last = asc.length ? asc[asc.length - 1] : null;
            const posTxt = String((last && last.position) || getPositionText_() || "").trim();
            const pm = posTxt.match(/^(\d+)\s*\/\s*\d+$/);
            if (pm) pTag = `_P${pm[1]}`;
        } catch { }

        return `${stamp}.${track}.${car}${pTag}`;
    }

    function raceHasStarted_() { return !!(raceMeta && raceMeta.startAtIso); }

    function clearRaceMeta_() {
        raceMeta = null;
        spectateName = "";
        spectateCar = "";
        spectateCarImg = "";
        spectateDriverId_ = "";
        lastReplaySelectAttemptAtMs = 0;
        lastReplaySelectAttemptName = "";
        GM_setValue(STORE_META_KEY, "");
        startRecordedForMetaKey = "";
        finishRecordedForMetaKey = "";
        clearFinishCapture_();
        rtOrigTitle_ = RT_ORIG_TITLE;
        document.title = RT_ORIG_TITLE;
        rtStartEpochMs_ = null;
    }

    function ensureLap0_(whenIso) {
        ensureRaceMeta_();
        if (!raceMeta || !raceMeta.metaKey) return;
        if (raceHasStarted_()) return;
        raceMeta.startAtIso = whenIso || new Date().toISOString();
        startRecordedForMetaKey = raceMeta.metaKey;
        saveRaceMeta_(raceMeta);

        const at0Iso = raceMeta.startAtIso;
        const at0 = at0Iso.replace("T", " ").replace("Z", "");
        const exists = laps.some(r => r && r.lap === 0 && lapBelongsToMeta_(r));
        if (!exists) {
            laps.push({
                id: `${raceMeta.metaKey}|0|${at0}`,
                lap: 0,
                position: "",
                time: "",
                at: at0,
                atIso: at0Iso,
                ms: null,
                speed: getReplaySpeedFactor_(),
                pause: (isReplay_() ? getReplayPauseAccumMs_() : 0),
                ui: "0",
                track: raceMeta.track || "",
                car: raceMeta.car || "",
                raceDetectedAtIso: raceMeta.detectedAtIso || ""
            });
            saveLaps_();
        }
    }

    function maybeMarkRaceStarted_() {
        try {
            if (!recordingEnabled) return;
            ensureRaceMeta_();
            if (!raceMeta || !raceMeta.metaKey) return;
            if (raceHasStarted_()) return;
            if (raceIsFinished_() && !laps.length) return; // don't fabricate lap0 on finished screen
            // Extra guard: on a finished replay screen (before pressing play), never mark started.
            if (raceIsFinished_() && isReplay_() && !isReplayPlaying_()) return;

            const info = document.querySelector(SEL_INFOSPOT);
            const txt = info ? String(info.textContent || "").trim().toLowerCase() : "";

            // Primary trigger: completion starts moving (replay/live)
            const comp = (completionPct == null && !completionFinishTime) ? parseCompletionOrTime_() : { pct: completionPct, time: completionFinishTime };
            const pct = comp.pct;
            const moving = (typeof pct === "number" && isFinite(pct) && pct > 0);

            // Secondary trigger: Torn explicitly says started
            const saysReplay = (txt.includes("race replaying") || txt.includes("replaying"));
            const saysStarted = txt.includes("race started") || (txt.includes("started") && !txt.includes("paused")) || (saysReplay && isReplayPlaying_());

            if (!moving && !saysStarted) return;

            // Save replay speed at start (if any) so calc can be scaled correctly.
            const spd = getReplaySpeedFactor_();
            raceMeta.replaySpeed = spd;
            ensureLap0_(new Date().toISOString());

            // If the replay is already on lap 2+ by the time we mark start, catch up
            // pending lap(s) immediately using the last known "Last lap" time.
            const { cur, raw } = parseLapUi_();
            if (cur && cur > 1) {
                pendingCompletedTarget = Math.max(pendingCompletedTarget || 0, cur - 1);
                flushPending_(raw, getPositionText_());
            }
            uiDirty = true;
            scheduleRender_();
        } catch (e) {
            console.warn("[MoDuL's Lap Recorder] maybeMarkRaceStarted_ failed", e);
        }
    }

    /* ============================
   * META DETECTION
   * ============================ */
    function parseRaceTitle_(raw) {
        if (!raw) return { title: "", countdown: "" };
        const cleanPart = (text) => String(text || "")
            .replace(/\s+/g, " ")
            .replace(/\s*[-—]+\s*$/, "")
            .trim();

        const parts = String(raw)
            .replace(/\s+/g, " ")
            .split(" - ")
            .map(cleanPart)
            .filter(Boolean);
        return {
            title: cleanPart(parts.slice(0, 2).join(" - ")),
            countdown: cleanPart(parts.length > 2 ? parts.slice(2).join(" - ") : "")
        };
    }

    function getRaceTitleParts_() {
        const el =
              document.querySelector(".drivers-list.right .title-black") ||
              document.querySelector(".drivers-list.right .title-black.top-round") ||
              document.querySelector("#racingupdatesnew .drivers-list.right .title-black") ||
              document.querySelector("#racingupdates .drivers-list.right .title-black");
        const raw = (el?.textContent || "").trim();
        return parseRaceTitle_(raw);
    }


    function getAllTrackNames_() {
        // Harvest track names from any track selectors on the page + saved records.
        const out = new Set();
        try {
            for (const sel of Array.from(document.querySelectorAll("select"))) {
                const id = (sel.id || "").toLowerCase();
                const name = (sel.getAttribute("name") || "").toLowerCase();
                if (!id.includes("track") && !name.includes("track")) continue;
                for (const opt of Array.from(sel.querySelectorAll("option"))) {
                    const t = (opt.textContent || "").trim();
                    if (t && t.length >= 3 && !/^[-—]+$/.test(t)) out.add(t);
                }
            }
            for (const el of Array.from(document.querySelectorAll("[class*='track']"))) {
                const raw = (el.textContent || "").trim();
                if (!raw || raw.length > 50) continue;
                const parts = raw.split(" - ").map(x => x.trim()).filter(Boolean);
                if (!parts.length) continue;
                const t = parts[0];
                if (t.length >= 3 && t.length <= 40) out.add(t);
            }
        } catch { }
        try { for (const r of (records || [])) if (r && r.track) out.add(String(r.track)); } catch { }
        const cur = (raceMeta?.track || "").trim();
        if (cur) out.add(cur);

        // Fallback: common track names (if the current page view doesn't expose a track selector).
        if (out.size < 3) {
            ["Uptown", "Parkland", "Docks", "Commerce", "Industrial", "Stone Park", "Underground", "Airport", "Harbor", "City", "Mountain", "Coastal", "Suburbs"]
                .forEach(t => out.add(t));
        }

        return Array.from(out).sort((a, b) => a.localeCompare(b));
    }

    function getCarName_() {
        const candidates = [
            ".model p",
            ".model-wrap .model p",
            "#racingupdatesnew .model p",
            "#racingupdates .model p",
            "#racing .model p",
            ".car-selected-wrap .img-title",
            ".car-selected-wrap span.img-title",
            "#racingupdatesnew .car-selected-wrap .img-title",
            "#racingupdates .car-selected-wrap .img-title",
            ".model-wrap .img-title",
            ".model-wrap span.img-title",
            ".car-selected .img-title",
            "#racingupdatesnew .img-title",
            "#racingupdates .img-title",
            "#racing .img-title"
        ];
        for (const sel of candidates) {
            const el = document.querySelector(sel);
            const t = (el?.textContent || "").trim();
            if (/^replay(?:\s+driver)?$/i.test(t)) continue;
            if (t) return t;
        }
        return "";
    }

    function findCarImageUrl_(carName) {
        try {
            const wanted = String(carName || "").trim();
            if (!wanted) return "";

            // Prefer the selected driver card (leaderboard)
            const lb = document.querySelector(SEL_LEADERBOARD);
            if (lb) {
                const sel = lb.querySelector("li.selected") || lb.querySelector("li[class*='selected']");
                const imgEl = sel?.querySelector("li.car img[title]");
                const title = (imgEl?.getAttribute("title") || "").trim();
                const src = (imgEl?.getAttribute("src") || "").trim();
                if (src && title === wanted) return src;
            }

            // Fallback: search for any <img title="car"> using the Torn item image path.
            const imgs = document.querySelectorAll("img[title]");
            for (const el of imgs) {
                const title = (el.getAttribute("title") || "").trim();
                if (title !== wanted) continue;
                const src = (el.getAttribute("src") || "").trim();
                if (src && /\/images\/items\//.test(src)) return src;
            }
            return "";
        } catch {
            return "";
        }
    }



    function getRaceId_() {
        try {
            const u = new URL(location.href);
            const p = u.searchParams;
            return (p.get("raceID") || p.get("raceId") || p.get("raceid") || "").trim();
        } catch (e) {
            return "";
        }
    }

    function isReplayPage_() {
        try {
            const u = new URL(location.href);
            const tab = (u.searchParams.get("tab") || "").toLowerCase();
            return tab === "log" || tab === "replay";
        } catch (e) {
            return false;
        }
    }

    function getReplayInfo_() {
        // Only meaningful on race log/replay pages where the left panel shows "Race info"
        try {
            // Find an element whose text is exactly "Race info"
            const header = Array.from(document.querySelectorAll("div,span,h1,h2,h3"))
            .find(el => (el.textContent || "").trim() === "Race info");
            if (!header) return null;

            // The content is usually within the same card/panel as the header.
            // We'll grab a reasonably small container around it.
            let panel = header.closest("div");
            for (let i = 0; i < 6 && panel; i++) {
                const t = (panel.innerText || "");
                // heuristic: panel must contain at least one "Type:" line
                if (/\bType:\s*/i.test(t) && /\bCars allowed:\s*/i.test(t)) break;
                panel = panel.parentElement;
            }
            if (!panel) return null;

            const lines = (panel.innerText || "")
            .split("\n")
            .map(s => s.trim())
            .filter(Boolean);

            const info = {};
            for (const line of lines) {
                const m = line.match(/^([A-Za-z ]+):\s*(.+)$/);
                if (!m) continue;
                const key = m[1].trim().toLowerCase().replace(/\s+/g, "_");
                const val = m[2].trim();
                info[key] = val;
            }

            // Keep only useful replay fields (avoid pulling random sidebar stats)
            const allow = ["type", "cars_allowed", "upgrades_allowed", "bet_amount", "time_started", "name", "track", "laps"];
            const out = {};
            for (const k of allow) if (k in info) out[k] = info[k];

            const has = ["type", "cars_allowed", "upgrades_allowed", "bet_amount"].some(k => k in out);
            return has ? out : null;
        } catch (e) {
            return null;
        }
    }

    function ensureRaceMeta_() {
        const { title: trackFound, countdown } = getRaceTitleParts_();
        const carFound = getCarName_();

        if (!trackFound && !carFound && !raceMeta) return;

        if (!raceMeta) {
            const d = new Date();
            const detectedAtIso = d.toISOString();
            const rid = getRaceId_() || "";
            raceMeta = {
                track: trackFound || "UnknownTrack",
                car: carFound || "Replay",
                carImg: findCarImageUrl_(carFound) || spectateCarImg || "",
                driver: currentDriverName_(),
                driverId: currentDriverId_(),
                raceId: rid,
                replayInfo: (isReplayPage_() && rid) ? getReplayInfo_() : null,
                theme: theme,
                totalTime: "",
                detectedAtIso,
                detectedAtLocal: formatLocalDateTime_(d),
                metaKey: makeRaceMetaKey_(trackFound || "UnknownTrack", carFound || "Replay", rid, detectedAtIso),
                countdown: countdown || "",
                startAtIso: ""
            };
            finishRecordedForMetaKey = "";
            startRecordedForMetaKey = "";
            clearFinishCapture_();
            saveRaceMeta_(raceMeta);
            refreshLastLapCompleted_();
            return;
        }

        let changed = false;
        if (trackFound && trackFound !== raceMeta.track) { raceMeta.track = trackFound; changed = true; }
        if (carFound && carFound !== raceMeta.car) { raceMeta.car = carFound; raceMeta.carImg = findCarImageUrl_(carFound) || raceMeta.carImg || ""; changed = true; }
        if (raceMeta && raceMeta.car && !raceMeta.carImg) { const u = findCarImageUrl_(raceMeta.car) || spectateCarImg || ""; if (u) { raceMeta.carImg = u; changed = true; } }
        const nextDriver = currentDriverName_();
        const nextDriverId = currentDriverId_();
        if (nextDriver && raceMeta.driver !== nextDriver) { raceMeta.driver = nextDriver; changed = true; }
        if (nextDriverId && raceMeta.driverId !== nextDriverId) { raceMeta.driverId = nextDriverId; changed = true; }

        const oldRaceId = raceMeta.raceId || "";
        const rid = getRaceId_() || "";
        const raceIdChanged = oldRaceId !== rid;
        if (raceIdChanged) { raceMeta.raceId = rid; changed = true; }

        if (isReplayPage_() && rid && !raceMeta.replayInfo) {
            const rinfo = getReplayInfo_();
            if (rinfo) { raceMeta.replayInfo = rinfo; changed = true; }
        }

        const th = theme;
        if ((raceMeta.theme || "dark") !== th) { raceMeta.theme = th; changed = true; }

        if (!raceHasStarted_()) {
            const cd = countdown || "";
            if ((raceMeta.countdown || "") !== cd) { raceMeta.countdown = cd; changed = true; }
        }

        const newKey = makeRaceMetaKey_(raceMeta.track || "UnknownTrack", raceMeta.car || "Replay", raceMeta.raceId || "", raceMeta.detectedAtIso || "");
        if (raceMeta.metaKey !== newKey) {
            const wasLegacyKey = !/^(race|seen):/.test(String(raceMeta.metaKey || ""));
            if (raceIdChanged || !wasLegacyKey) {
                const d = new Date();
                raceMeta.detectedAtIso = d.toISOString();
                raceMeta.detectedAtLocal = formatLocalDateTime_(d);
            }
            raceMeta.metaKey = makeRaceMetaKey_(raceMeta.track || "UnknownTrack", raceMeta.car || "Replay", raceMeta.raceId || "", raceMeta.detectedAtIso);
            resetSeenLapTime_();
            lastLapCompleted = 0;
            lastRecordedKey = "";
            startRecordedForMetaKey = "";
            finishRecordedForMetaKey = "";
            clearFinishCapture_();
            pendingCompletedTarget = 0;
            pendingFinalLap = 0;
            changed = true;
        }

        if (changed) {
            saveRaceMeta_(raceMeta);
            refreshLastLapCompleted_();
        }
    }

    function maybeResetOnRaceIdChange_() {
        const currentRaceId = getRaceId_();
        if (!currentRaceId) return;

        const storedRaceId = loadLastRaceId_();
        if (!storedRaceId) {
            saveLastRaceId_(currentRaceId);
            return;
        }

        if (storedRaceId !== currentRaceId) {
            if (clearOnRaceChange) {
                resetForRaceChange_(currentRaceId);
            } else {
                saveLastRaceId_(currentRaceId);
            }
        }
    }

    /* ============================
   * THEME APPLICATION
   * ============================ */
    function applyTheme_() {
        const win = document.getElementById("rtLapWin");
        const btn = document.getElementById("rtLapBtn");
        if (!win || !btn) return;
        win.dataset.theme = theme;
        btn.dataset.theme = theme;

        const label = document.getElementById("rtThemeLabel");
        if (label) {
            const t = THEMES.find(x => x.key === theme);
            label.textContent = t ? t.name : "Theme";
        }
        updateHeaderCompact_(win);
    }

    function cycleTheme_() {
        const i = THEMES.findIndex(x => x.key === theme);
        const next = (i >= 0) ? THEMES[(i + 1) % THEMES.length].key : THEMES[0].key;
        theme = next;
        saveTheme_();
        applyTheme_();

        uiDirty = true;
        scheduleRender_();
    }

    /* ============================
   * UI STYLES
   * ============================ */
    GM_addStyle(`
/* ============================
 * UI STYLES (Lap Recorder)
 * ============================ */

#rtLapWin, #rtLapBtn{
  /* defaults (dark) */
  --bg:#161616;
  --panel:#121212;
  --border:#333;
  --text:#ffffff;
  --muted:#bbbbbb;
  --header:#1a1a1a;
  --hover:rgba(255,255,255,.04);
  --pill:#1c1c1c;
  --pillHover:#232323;
  --tableBg:#0f0f0f;
  --thBg:#1a1a1a;

  --scrollTrack:#1c1c1c;
  --scrollThumb:#9a9a9a;
  --scrollThumbBorder:#2b2b2b;

  --carIconBg: rgba(255,255,255,.08);

  /* semantic */
  --time: var(--text);
  --gapNeg: #00FF00;
  --gapPos: #FFC83D;
  --gapZero: var(--muted);

  --bestRowBg: rgba(180, 120, 255, 0.22);
  --bestRowBorder: rgba(180, 120, 255, 0.55);
  --bestText: #f2e9ff;
}

/* ========== Base icon styles ========== */
img.carIcon{
  width:38px;
  height:19px;
  object-fit:contain;
  border-radius:8px;
  background: var(--carIconBg);
  border:1px solid rgba(255,255,255,.12);
  display:inline-block;
  vertical-align:middle;
}
#rtCarImg.carIcon{
  width:46px;
  height:23px;
  border-radius:10px;
  border:1px solid rgba(255,255,255,.14);
}

/* ========== Force all UI text to follow vars (prevents black leaks) ========== */
#rtLapWin, #rtLapBtn,
#rtHdr, #rtBody, #rtMeta, #rtCountdown,
#rtTable, #rtTrack, #rtCar, #rtDriver, #rtRaceTime{
  color: var(--text) !important;
}
.muted{ color: var(--muted) !important; }

/* Race ID link visibility */
#rtRaceId{ color: var(--gapPos) !important; text-decoration: underline; font-weight: 900; }
#rtRaceId:hover{ opacity:.9; }

/* ========== Floating Button ========== */
#rtLapBtn{
  position:fixed;right:16px;bottom:16px;z-index:999999;
  background:var(--panel);
  border:1px solid var(--border);
  color:var(--text);
  border-radius:14px;
  padding:10px 12px;
  font:12px system-ui;
  cursor:pointer;
  display:flex;
  gap:10px;
  align-items:center;
  box-shadow:0 12px 30px rgba(0,0,0,.35);
  user-select:none;
}
#rtLapBtn .recDot{width:10px;height:10px;border-radius:50%;background:#ff3b3b}
#rtLapBtn .recDot.off{background:#777}
#rtLapBtn .badge{
  padding:4px 8px;
  border-radius:999px;
  background:rgba(255,255,255,.10);
  border:1px solid rgba(0,0,0,.25);
  font-weight:700;
}

/* ========== Window ========== */
#rtLapWin{
  position:fixed;
  right:16px;
  bottom:16px;
  width:720px;
  height:420px;
  min-width:550px;
  min-height:300px;
  background:var(--bg);
  border:1px solid var(--border);
  border-radius:16px;
  box-shadow:0 18px 46px rgba(0,0,0,.45);
  display:none;
  z-index:1000000;
  overflow:hidden;
  resize:both;
}

/* Cursor sanity */
#rtLapWin{ cursor:default; }
#rtLapWin button, #rtLapWin a, #rtLapWin input, #rtLapWin textarea, #rtLapWin select,
#rtLapWin .delBtn{ cursor:default; }

.totalCell{font-weight:800;opacity:.95}

/* Header / drag handle */
#rtHdr{
  height:52px;
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:10px;
  padding:0 12px;
  border-bottom:1px solid var(--border);
  cursor:move;
  user-select:none;
  background:var(--header);
  color:var(--text);
  position:relative;
  z-index:200;
  box-sizing:border-box;
  overflow:hidden;
}
#rtHdr .left{
  display:flex;
  flex-direction:column;
  align-items:flex-start;
  gap:2px;
  flex:1 1 auto;
  min-width:115px;
  overflow:hidden;
}
#rtHdr .title{font-weight:900;letter-spacing:.2px}
#rtHdr .count{opacity:.9;font-weight:800}
#rtVer{
  max-width:100%;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}
#rtHdr .right{
  display:flex;
  gap:8px;
  align-items:center;
  justify-content:flex-end;
  flex:0 1 auto;
  min-width:0;
  flex-wrap:nowrap;
  overflow:hidden;
}

/* Pills */
.pill{
  border:1px solid rgba(20,20,20,.55);
  background:var(--pill);
  border-radius:999px;
  padding:7px 10px;
  color:var(--text) !important;
  cursor:pointer;
  font-size:12px;
  display:flex;
  align-items:center;
  gap:8px;
  flex:0 0 auto;
  white-space:nowrap;
  box-sizing:border-box;
}
.pill:hover{background:var(--pillHover)}
.pill.on{box-shadow: inset 0 0 0 2px var(--gapPos);}

#rtLapWin.rtCompact #rtHdr .right{gap:6px}
#rtLapWin.rtCompact .pill{
  width:36px;
  min-width:36px;
  height:32px;
  justify-content:center;
  padding:7px 0;
  gap:0;
}
#rtLapWin.rtCompact .pill span:nth-child(2){display:none !important}
#rtLapWin.rtCompact #rtRecText{display:none !important}
#rtLapWin.rtCompact #rtThemeLabel{display:none !important}
#rtRecDotInline{width:10px;height:10px;border-radius:50%;background:#ff3b3b;display:inline-block}
#rtRecDotInline.off{background:#777}

/* Body layout */
#rtBody{
  position:relative;
  z-index:1;
  padding:12px 12px 22px 12px;
  height:calc(100% - 52px);
  display:flex;
  flex-direction:column;
  gap:10px;
  box-sizing:border-box;
}

/* Meta */
#rtMeta{
  display:flex;
  align-items:flex-start;
  justify-content:space-between;
  gap:14px;
  color:var(--muted) !important;
  font:12px system-ui;
}
#rtMeta>div{
  display:flex;
  align-items:center;
  gap:10px;
  flex-wrap:wrap;
}
#rtMeta img{vertical-align:middle;}
#rtMeta .muted{line-height:1;}
#rtMeta .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
#rtCountdown{
  color:var(--muted) !important;
  font:12px system-ui;
  margin-top:-6px;
  padding-left:2px;
}
#rtCarWrap{display:inline-flex;align-items:center;gap:8px;}

/* ========== Main laps table wrapper ========== */
#rtTableWrap{
  flex:1;
  overflow:auto;
  border:1px solid rgba(0,0,0,.25);
  border-radius:12px;
  background:var(--tableBg);
  padding-bottom:14px;
  box-sizing:border-box;
}

/* Scrollbar (main) */
#rtTableWrap::-webkit-scrollbar{ width:12px; height:12px; }
#rtTableWrap::-webkit-scrollbar-track{ background:var(--scrollTrack); border-radius:10px; }
#rtTableWrap::-webkit-scrollbar-thumb{
  background:var(--scrollThumb);
  border-radius:10px;
  border:3px solid var(--scrollThumbBorder);
}

/* Main table */
#rtTable{
  width:100%;
  border-collapse:collapse;
  font:12px system-ui;
  color:var(--text);
}
#rtTable th,#rtTable td{
  cursor:text;
  user-select:text;
  padding:9px;
  border-bottom:1px solid rgba(0,0,0,.25);
  text-align:left;
  white-space:nowrap;
}
#rtTable th{
  background:var(--thBg);
  position:sticky;
  top:0;
  z-index:5;
  color:var(--text);
}
#rtTable tr:hover td{background:var(--hover)}
#rtLapWin #rtTable tbody td:not(.gap-neg):not(.gap-pos):not(.gap-zero){
  color: var(--text) !important;
}
#rtTable tbody td:nth-child(1),
#rtTable tbody td:nth-child(2),
#rtTable tbody td:nth-child(3){
  font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
  font-weight:900;
  color:var(--time) !important;
}

/* Delta colors */
.gap-neg { color: var(--gapNeg) !important; font-weight: 900; }
.gap-pos { color: var(--gapPos) !important; font-weight: 900; }
.gap-zero{ color: var(--gapZero) !important; }

/* Fastest row */
.fastest { background: var(--bestRowBg) !important; }
.fastest td { border-bottom-color: var(--bestRowBorder) !important; }
.fastest td:nth-child(4){ color:#9C6DFF !important; font-weight:900; }
.flBadge{
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:2px 8px;
  border-radius:999px;
  border:1px solid var(--bestRowBorder);
  background: var(--bestRowBg);
  color: var(--bestText);
  font-weight:900;
  font-size:11px;
  margin-left:8px;
}

/* Delete button (laps) */
.delBtn{
  border:1px solid rgba(0,0,0,.25);
  background:rgba(255,255,255,.10);
  color:var(--text) !important;
  border-radius:10px;
  padding:4px 8px;
  cursor:pointer;
  font:12px system-ui;
  display:inline-flex;
  align-items:center;
  justify-content:center;
}
.delBtn:hover{ background:rgba(255,255,255,.16); }

/* ========== RECORDS PANEL (bottom) ========== */
#rtRecords{
  margin-top:10px;
  color: var(--text);
  border:1px solid var(--border);
  border-radius:14px;
  background:var(--panel);
  overflow:hidden;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.03);
}
#rtRecords .recordsBar{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:12px;
  padding:10px 10px;
  background: var(--header);
  border-bottom:1px solid var(--border);
  font-size:12px;
}
#rtRecords .recordsLeft{
  display:flex;
  align-items:center;
  gap:8px;
  flex-wrap:wrap;
}
#rtRecords .select{
  background: var(--pill);
  color: var(--text);
  border:1px solid var(--border);
  border-radius:10px;
  padding:6px 10px;
  font-size:12px;
  outline:none;
}

/* Scroll container: shows scrollbar only when needed (e.g., >3 rows) */
#rtRecScroll{
  max-height:168px;      /* header + ~3 rows */
  overflow-y:scroll;
  overflow-x:hidden;
  scrollbar-gutter: stable;
}

/* Scrollbar (records) – same as main */
#rtRecScroll::-webkit-scrollbar{ width:12px; height:12px; }
#rtRecScroll::-webkit-scrollbar-track{ background:var(--scrollTrack); border-radius:10px; }
#rtRecScroll::-webkit-scrollbar-thumb{
  background:var(--scrollThumb);
  border-radius:10px;
  border:3px solid var(--scrollThumbBorder);
}

/* Records table */
#rtRecTable{
  width:100%;
  border-collapse:collapse;
  table-layout:fixed;
  font-size:12px;
}
#rtRecTable thead th{
  position:sticky;
  top:0;
  z-index:5;
  text-align:left;
  padding:9px 10px;
  background: var(--thBg);
  border-bottom:1px solid var(--border);
  color: var(--muted);
  font-weight:800;
}
#rtRecTable tbody td{
  padding:9px 10px;
  border-bottom:1px solid rgba(255,255,255,.06);
  vertical-align:middle;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
  color: var(--text);
}
#rtRecTable tbody tr:hover{ background: var(--hover); }

/* Column widths (balanced + prevents blending/cutoff) */
#rtRecTable .colNum{ width:4%;  text-align:center; }
#rtRecTable .colImg{ width:9%;  text-align:center; }
#rtRecTable .colCar{ width:28%; }
#rtRecTable .colTime{ width:18%; }
#rtRecTable .colDriver{ width:13%; }
#rtRecTable .colRace{ width:17%; text-align:left; }
.recRace{
  font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
  color: var(--muted);
}
.recRaceLink{
  color: var(--gapPos);
  text-decoration:none;
  font-weight:700;
}
.recRaceLink:hover{
  text-decoration:underline;
  opacity:.9;
}
#rtRecTable .colWhen{ width:18%; }
#rtRecTable .colAct{ width:8%;  text-align:center; }

/* Cells */
.recNum{ text-align:center; color:var(--muted); }
.recImg{ text-align:center; padding:6px 6px; }
.recImg .carIcon{
  width:34px !important;
  height:20px !important;
  border-radius:8px;
  display:block;
  margin:0 auto;
  object-fit:contain;
  background:rgba(255,255,255,0.06);
  border:1px solid rgba(255,255,255,0.10);
}
.recCar{ font-weight:700; min-width:0; }
.recCar span{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block; }
.recWhen{ white-space:nowrap; color: var(--muted); font-size:12px; opacity:.9; }
.recAct{ text-align:center; }

/* Delete (records) */
.recDelBtn{
  border:1px solid var(--border);
  background: var(--pill);
  color: var(--text);
  border-radius:10px;
  padding:4px 8px;
  cursor:pointer;
}
.recDelBtn:hover{ background: var(--pillHover); }

/* ========== Toast ========== */
.rtToast{
  position:fixed;
  left:50%;
  bottom:18px;
  transform:translateX(-50%) translateY(10px);
  opacity:0;
  z-index:999999;
  background:rgba(0,0,0,.75);
  color:#fff;
  padding:10px 12px;
  border-radius:12px;
  border:1px solid rgba(255,255,255,.15);
  backdrop-filter:blur(6px);
  font-size:13px;
  max-width:min(720px,92vw);
  text-align:center;
  transition:opacity .22s ease,transform .22s ease;
}
.rtToast.show{opacity:1;transform:translateX(-50%) translateY(0);}

/* ============================
 * THEMES
 * ============================ */

/* Classic */
#rtLapWin[data-theme="classic"], #rtLapBtn[data-theme="classic"], #rtRecTable[data-theme="classic"]{
  --bg:#2a2a2a; --panel:#2f2f2f; --border:#4a4a4a;
  --text:#f5f5f5; --muted:#d0d0d0; --header:#343434;
  --hover:rgba(255,255,255,.05); --pill:#3a3a3a; --pillHover:#444;
  --tableBg:#262626; --thBg:#313131; --scrollTrack:#2b2b2b; --scrollThumb:#bdbdbd;
  --scrollThumbBorder:#2b2b2b;
  --time:#ffffff;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(180, 120, 255, 0.22);
  --bestRowBorder: rgba(180, 120, 255, 0.55);
  --bestText:#f2e9ff;
}

/* Dark */
#rtLapWin[data-theme="dark"], #rtLapBtn[data-theme="dark"], #rtRecTable[data-theme="dark"]{
  --bg:#141414; --panel:#101010; --border:#2f2f2f; --header:#171717;
  --tableBg:#0d0d0d; --thBg:#171717; --pill:#1a1a1a; --pillHover:#222;
  --scrollTrack:#141414; --scrollThumb:#8a8a8a; --scrollThumbBorder:#141414;
  --time:#ffffff;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(180, 120, 255, 0.22);
  --bestRowBorder: rgba(180, 120, 255, 0.55);
  --bestText:#f2e9ff;
}

/* Ice */
#rtLapWin[data-theme="ice"], #rtLapBtn[data-theme="ice"], #rtRecTable[data-theme="ice"]{
  --bg:#e6eef7;
  --panel:#f1f6fb;
  --border:#b7c7d8;
  --header:#d9e6f3;
  --tableBg:#eef4fb;
  --thBg:#d9e6f3;
  --pill:#d4e2f1;
  --pillHover:#c7d9eb;
  --text:#1b1f24;
  --muted:#33414f;
  --hover:#e7f0fa;
  --scrollTrack:#d9e6f3;
  --scrollThumb:#8aa3bb;
  --scrollThumbBorder:#d9e6f3;
  --time:#0b0f14;
  --gapNeg:#00AA00; --gapPos:#b18630; --gapZero:var(--muted);
  --bestRowBg: rgba(180, 120, 255, 0.22);
  --bestRowBorder: rgba(180, 120, 255, 0.55);
  --bestText:#0b0f14;
}
#rtLapWin[data-theme="ice"] td:nth-child(4){
  -webkit-text-stroke: 0.15px rgba(0,0,0,0.60);
}

/* Neon */
#rtLapWin[data-theme="neon"], #rtLapBtn[data-theme="neon"], #rtRecTable[data-theme="neon"]{
  --bg:#0b0c10; --panel:#11131a; --border:#2d2f3a; --header:#151824;
  --tableBg:#0b0c10; --thBg:#151824; --pill:#1c2030; --pillHover:#232944;
  --text:#eaf0ff; --hover:rgba(255,255,255,.06);
  --muted:#aab3d8;
  --scrollTrack:#11131a; --scrollThumb:#7f8cff; --scrollThumbBorder:#11131a;
  --time:#eaf0ff;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(180, 120, 255, 0.22);
  --bestRowBorder: rgba(180, 120, 255, 0.55);
  --bestText:#f2e9ff;
}

/* Torn Blue */
#rtLapWin[data-theme="class_a"], #rtLapBtn[data-theme="class_a"], #rtRecTable[data-theme="class_a"]{
  --bg:#0d1218; --panel:#121a24; --border:#203042; --header:#162030;
  --tableBg:#0b1016; --thBg:#162030; --pill:#17263a; --pillHover:#1d324c;
  --text:#eaf3ff; --muted:#b8cbe2; --hover:rgba(255,255,255,.06);
  --scrollTrack:#121a24; --scrollThumb:#3aa0ff; --scrollThumbBorder:#121a24;
  --time:#eaf3ff;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(58, 160, 255, 0.18);
  --bestRowBorder: rgba(58, 160, 255, 0.55);
  --bestText:#eaf3ff;
}

/* Torn Red */
#rtLapWin[data-theme="class_b"], #rtLapBtn[data-theme="class_b"], #rtRecTable[data-theme="class_b"]{
  --bg:#150b0c; --panel:#1d1012; --border:#3a1a1e; --header:#241215;
  --tableBg:#12090a; --thBg:#241215; --pill:#2b1417; --pillHover:#35191d;
  --text:#fff0f1; --muted:#e4b9bd; --hover:rgba(255,255,255,.06);
  --scrollTrack:#1d1012; --scrollThumb:#ff4b57; --scrollThumbBorder:#1d1012;
  --time:#fff0f1;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(255, 75, 87, 0.16);
  --bestRowBorder: rgba(255, 75, 87, 0.55);
  --bestText:#fff0f1;
}

/* Torn Green */
#rtLapWin[data-theme="class_c"], #rtLapBtn[data-theme="class_c"], #rtRecTable[data-theme="class_c"]{
  --bg:#0c120e; --panel:#101a14; --border:#203826; --header:#132018;
  --tableBg:#0a0f0c; --thBg:#132018; --pill:#163022; --pillHover:#1c3a29;
  --text:#effff5; --muted:#bfe2cc; --hover:rgba(255,255,255,.06);
  --scrollTrack:#101a14; --scrollThumb:#33ff7d; --scrollThumbBorder:#101a14;
  --time:#effff5;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(51, 255, 125, 0.14);
  --bestRowBorder: rgba(51, 255, 125, 0.55);
  --bestText:#effff5;
}

/* Torn Purple */
#rtLapWin[data-theme="class_d"], #rtLapBtn[data-theme="class_d"], #rtRecTable[data-theme="class_d"]{
  --bg:#120b16; --panel:#181020; --border:#2f1f3b; --header:#1e1427;
  --tableBg:#0f0913; --thBg:#1e1427; --pill:#251730; --pillHover:#2d1d3a;
  --text:#f7efff; --muted:#d7c2ea; --hover:rgba(255,255,255,.06);
  --scrollTrack:#181020; --scrollThumb:#c056ff; --scrollThumbBorder:#181020;
  --time:#f7efff;
  --gapNeg:#00FF00; --gapPos:#FFC83D; --gapZero:var(--muted);
  --bestRowBg: rgba(192, 86, 255, 0.16);
  --bestRowBorder: rgba(192, 86, 255, 0.55);
  --bestText:#f7efff;
}

/* Torn Gold */
#rtLapWin[data-theme="class_e"], #rtLapBtn[data-theme="class_e"], #rtRecTable[data-theme="class_e"]{
  --bg:#17110a; --panel:#23180b; --border:#5a3c12; --header:#2b1e0d;
  --tableBg:#130e08; --thBg:#261b10; --pill:#2d2013; --pillHover:#372818;
  --text:#fff6e6; --muted:#e8d2ac; --hover:rgba(255,255,255,.06);
  --scrollTrack:#1f160d; --scrollThumb:#FFD700; --scrollThumbBorder:#1f160d;
  --time:#fff6e6;
  --gapNeg:#00FF00; --gapPos:#ffd24a; --gapZero:var(--muted);
  --bestRowBg: rgba(255, 210, 74, 0.18);
  --bestRowBorder: rgba(255, 210, 74, 0.70);
  --bestText:#fff6e6;
}
`);


    /* ============================
   * EXPORTS
   * ============================ */
    function buildHtmlReport_() {
        const asc = currentRaceLaps_().slice().sort((a, b) => (a.lap || 0) - (b.lap || 0));


        const fastestUiMs = Math.min(...asc.map(r => r.ms).filter(ms => typeof ms === "number" && ms > 0));

        // Calc lap time based on capture timestamps (centiseconds precision)
        const calcByLap = new Map();
        for (let i = 0; i < asc.length; i++) {
            const cur = asc[i];
            const prev = i > 0 ? asc[i - 1] : null;
            calcByLap.set(cur.lap, computeLapChronoMs_(cur, prev));
        }

        // Total (cumulative) based on calculated lap times
        const cumByLap = new Map();
        let cum = 0;
        let ok = true;
        let maxLapShown = 0;
        for (const r of asc) {
            const lapNo = (r && r.lap) ? r.lap : 0;
            if (lapNo > maxLapShown) maxLapShown = lapNo;
            const v = calcByLap.get(lapNo);
            if (typeof v === "number" && isFinite(v) && v >= 0 && ok) {
                cum += v;
                cumByLap.set(lapNo, cum);
            } else {
                ok = false;
                cumByLap.set(lapNo, null);
            }
        }

        const calcVals = [...calcByLap.entries()].filter(([, v]) => typeof v === "number" && isFinite(v) && v > 0);
        const fastestCalcMs = calcVals.length ? Math.min(...calcVals.map(([, v]) => v)) : Infinity;
        const fastestLap = calcVals.length ? Math.min(...calcVals.filter(([, v]) => v === fastestCalcMs).map(([k]) => k)) : null;

        const created = new Date().toLocaleString();

        let raceTimeLocal = raceMeta?.detectedAtLocal || "—";
        if (!raceHasStarted_() && (raceMeta?.countdown || "")) {
            const secs = parseCountdownToSeconds_(raceMeta.countdown);
            raceTimeLocal = formatLocalDateTime_(new Date(Date.now() + secs * 1000));
        }

        const rows = asc.filter(x => (x.lap || 0) > 0).map((r, i) => {
            const prev = i > 0 ? asc[i - 1] : null;
            const curCalc = calcByLap.get(r.lap);
            const prevCalc = prev ? calcByLap.get(prev.lap) : null;
            const deltaMs = (r.lap <= 1) ? 0 : ((typeof curCalc === "number" && typeof prevCalc === "number") ? (curCalc - prevCalc) : 0);
            const isFastest = (fastestLap != null && r.lap === fastestLap);
            const tip = [r.driverName, r.driverCar].filter(Boolean).join(" — ");
            const tipAttr = tip ? ` title="${escAttr_(tip)}"` : "";
            const totalTxt = (r.lap === maxLapShown) ? msToCalcText_(cumByLap.get(r.lap)) : "";
            return `
        <tr class="${isFastest ? "fastest" : ""}"${tipAttr}>
          <td>${r.lap}</td>
          <td>${esc_(r.position || "")}</td>
          <td class="muted mono">${esc_(msToCalcText_(calcByLap.get(r.lap)))}${isFastest ? ` <span class="flBadge">🏁FL</span>` : ""}</td>
          <td class="${gapClass_(deltaMs)} mono ${isFastest ? "delta-best" : ""}">${esc_(msToDeltaText_(deltaMs))}</td>
          <td>${esc_(r.time)}</td>
          <td class="mono">${esc_(totalTxt)}</td>
          <td></td>
        </tr>`;
        }).join("");

        const track = raceMeta?.track || "—";
        const car = raceMeta?.car || "—";
        const carImg = raceMeta?.carImg || "";

        const rid = (raceMeta?.raceId || "").trim();
        const raceLink = rid ? `https://www.torn.com/page.php?sid=racing&raceID=${encodeURIComponent(rid)}` : "";
        const raceAtIso = bestRaceAtIso_();
        const themeKey = (theme || raceMeta?.theme || "classic");
        const THEME_MAP = {
            classic: {
                bg: "#2a2a2a", card: "#2f2f2f", border: "#4a4a4a", th: "#313131", table: "#262626", hover: "#2f2f2f", accent: "#6aa6ff", muted: "#d0d0d0", text: "#f5f5f5",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#d0d0d0"
            },
            dark: {
                bg: "#141414", card: "#101010", border: "#2f2f2f", th: "#171717", table: "#0d0d0d", hover: "#151515", accent: "#6aa6ff", muted: "#bbbbbb", text: "#ffffff",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#bbbbbb"
            },
            ice: {
                bg: "#e6eef7", card: "#f1f6fb", border: "#b7c7d8", th: "#d9e6f3", table: "#eef4fb", hover: "#e7f0fa", accent: "#2b6fff", muted: "#33414f", text: "#1b1f24",
                gapNeg: "#00AA00", gapPos: "#b18630", gapZero: "#33414f"
            },
            neon: {
                bg: "#0b0c10", card: "#11131a", border: "#2d2f3a", th: "#151824", table: "#0b0c10", hover: "#0f1320", accent: "#7f8cff", muted: "#aab3d8", text: "#eaf0ff",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#aab3d8"
            },

            // Class themes (in-game colourways)
            class_a: {
                bg: "#0d1218", card: "#121a24", border: "#203042", th: "#162030", table: "#0b1016", hover: "#111a26", accent: "#3aa0ff", muted: "#b8cbe2", text: "#eaf3ff",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#b8cbe2"
            },
            class_b: {
                bg: "#150b0c", card: "#1d1012", border: "#3a1a1e", th: "#241215", table: "#12090a", hover: "#1a0d0f", accent: "#ff4b57", muted: "#e4b9bd", text: "#fff0f1",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#e4b9bd"
            },
            class_c: {
                bg: "#0c120e", card: "#101a14", border: "#203826", th: "#132018", table: "#0a0f0c", hover: "#0e1510", accent: "#33ff7d", muted: "#bfe2cc", text: "#effff5",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#bfe2cc"
            },
            class_d: {
                bg: "#120b16", card: "#181020", border: "#2f1f3b", th: "#1e1427", table: "#0f0913", hover: "#140d18", accent: "#c056ff", muted: "#d7c2ea", text: "#f7efff",
                gapNeg: "#00FF00", gapPos: "#FFC83D", gapZero: "#d7c2ea"
            },
            class_e: {
                bg: "#17110a", card: "#23180b", border: "#5a3c12", th: "#2b1e0d", table: "#130e08", hover: "#1d150b", accent: "#FFD700", muted: "#e8d2ac", text: "#fff6e6",
                gapNeg: "#00FF00", gapPos: "#FFD700", gapZero: "#e8d2ac"
            }
        };
        const THEME = THEME_MAP[themeKey] || THEME_MAP.classic;
        const replayInfo = (isReplay_() ? (raceMeta?.replayInfo || null) : null);
        const replayRows = replayInfo ? (() => {
            const LABEL = {
                name: "Name",
                type: "Type",
                cars_allowed: "Cars allowed",
                upgrades_allowed: "Upgrades allowed",
                bet_amount: "Bet amount"
            };
            const ORDER = ["name", "type", "cars_allowed", "upgrades_allowed", "bet_amount"];
            return ORDER
                .filter(k => replayInfo[k] != null && String(replayInfo[k]).trim() !== "")
                .map(k => `<div class="muted"><b>${esc_(LABEL[k])}:</b> ${esc_(replayInfo[k])}</div>`)
                .join("");
        })() : "";
        return `<!doctype html>
<html><head><meta charset="utf-8"><meta name="rt-track" content="${escAttr_(track)}"><meta name="rt-car" content="${escAttr_(car)}"><meta name="rt-car-img" content="${escAttr_(carImg)}"><meta name="rt-race-at" content="${escAttr_(raceAtIso)}"><meta name="rt-race-id" content="${escAttr_(rid)}"><title>Torn Lap Times</title>
<style>
  body{font-family:system-ui;margin:20px;background:${THEME.bg};color:${THEME.text}}
  .card{background:${THEME.card};border:1px solid ${THEME.border};border-radius:14px;padding:14px;box-shadow:0 12px 28px rgba(0,0,0,.35)}
  .mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace}
  table{width:100%;border-collapse:collapse;margin-top:12px}
  th,td{padding:10px;border-bottom:1px solid rgba(0,0,0,.25);text-align:left;white-space:nowrap}
  th{background:${THEME.th};position:sticky;top:0;z-index:5;color:${THEME.text}}
  tr:hover td{background:${THEME.hover}}
  .muted{opacity:.8}
  .mono{font-family:ui-monospace,Menlo,Consolas,monospace}
  a{color:${THEME.accent};text-decoration:none;font-weight:600}
  a:hover{text-decoration:underline}
  .gap-neg { color: ${THEME.gapNeg}; font-weight: 900; }
  .gap-pos { color: ${THEME.gapPos}; font-weight: 900; }
  .gap-zero { color: ${THEME.gapZero}; }
  .delta-best{ color:#9C6DFF; font-weight:900; }
  .delta-best { color:#9C6DFF; font-weight:900; }
  .fastest { background: rgba(180, 120, 255, 0.22); }
  .fastest td:nth-child(4){ color:#9C6DFF !important; font-weight:900; }
  .flBadge{
  display:inline-flex;
  align-items:center;
  gap:6px;
  padding:2px 8px;
  border-radius:999px;
  border:1px solid rgba(180,120,255,.65);
  background: rgba(180,120,255,.22);
  color:#f2e9ff;
  font-weight:900;
  font-size:11px;margin-left:8px;
  }
</style></head>
<body>
  <div class="card">
    <h2 style="margin:0 0 8px 0">Torn Lap Times</h2>
    <div class="muted"><b>Generated:</b> ${esc_(created)}</div>
    <div style="margin-top:8px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;">
      <div><b>Track:</b> ${esc_(track)} &nbsp; <b>Car:</b> ${esc_(car)}</div>
      ${carImg ? `<img src="${escAttr_(carImg)}" alt="" style="height:22px;width:auto;border-radius:6px;vertical-align:middle;opacity:.95;">` : ""}
    </div>
    <div class="mono"><b>Race:</b> ${esc_(raceTimeLocal)}${raceLink ? ` &nbsp; <a href="${raceLink}" target="_blank" rel="noopener noreferrer">(Replay link)</a>` : ""}</div>${rid ? `<div class="muted"><b>Race ID:</b> ${esc_(rid)}</div>` : ""}${replayRows ? `<div style="margin-top:8px">${replayRows}</div>` : ""}
    <div class="muted" style="margin-top:6px">Gap = vs previous lap • 🏁FL = Fastest Lap</div>

    <div style="max-height:70vh;overflow:auto;border:1px solid rgba(0,0,0,.25);border-radius:12px;margin-top:12px">
      <table>
        <thead><tr><th>Lap</th><th>Pos</th><th>Chrono</th><th>Delta</th><th>Last lap</th><th>Total</th><th></th></tr></thead>
        <tbody>${rows || `<tr><td colspan="7" class="muted">No laps recorded.</td></tr>`}</tbody>
      </table>
    </div>
  </div>
</body></html>`;
    }

    function exportHtml_() {
        try {
            ensureRaceMeta_();
            // ensure we export with the currently selected theme
            if (raceMeta) { raceMeta.theme = theme; saveRaceMeta_(raceMeta); }
            const html = buildHtmlReport_();
            const base = buildBaseFilename_();

            // Download first (keeps the browser "user gesture" even on stricter download policies)
            const blob = new Blob([html], { type: "text/html;charset=utf-8" });
            const url = URL.createObjectURL(blob);
            const a = document.createElement("a");
            a.href = url;
            a.download = `${base}.html`;
            document.body.appendChild(a);
            a.click();
            a.remove();
            setTimeout(() => URL.revokeObjectURL(url), 1500);

            // Copy to clipboard (best effort)
            try {
                if (navigator.clipboard && window.isSecureContext) {
                    navigator.clipboard.writeText(html).catch(() => { });
                } else {
                    const ta = document.createElement("textarea");
                    ta.value = html;
                    ta.style.position = "fixed";
                    ta.style.left = "-9999px";
                    document.body.appendChild(ta);
                    ta.select();
                    document.execCommand("copy");
                    document.body.removeChild(ta);
                }
            } catch { }

            // Export complete.
        } catch (e) {
            console.warn("[MoDuL's Lap Recorder] exportHtml_ failed", e);
            toast_("HTML export failed — check console.");
        }
    }

    /* ============================
   * HTML IMPORT (Records)
   * ============================ */
    function toast_(msg) {
        try {
            const t = document.createElement('div');
            t.className = 'rtToast';
            t.textContent = String(msg || '');
            document.body.appendChild(t);
            setTimeout(() => t.classList.add('show'), 10);
            setTimeout(() => { t.classList.remove('show'); setTimeout(() => t.remove(), 250); }, 2600);
        } catch { }
    }

    function readFileText_(file) {
        return new Promise((resolve, reject) => {
            try {
                const fr = new FileReader();
                fr.onload = () => resolve(String(fr.result || ''));
                fr.onerror = () => reject(fr.error || new Error('read failed'));
                fr.readAsText(file);
            } catch (e) { reject(e); }
        });
    }

    function hash32_(s) {
        // FNV-1a 32-bit
        s = String(s || '');
        let h = 0x811c9dc5;
        for (let i = 0; i < s.length; i++) {
            h ^= s.charCodeAt(i);
            h = Math.imul(h, 0x01000193);
        }
        return (h >>> 0).toString(16).padStart(8, '0');
    }

    function makeBestRecId_(mode, track, car) {
        return `best:${mode}:${hash32_(track)}:${hash32_(car)}`;
    }

    function updateBestRecord_(mode, track, car, ms, timeText, extra) {
        if (!mode || !track || !car) return false;
        if (!(typeof ms === 'number' && isFinite(ms) && ms > 0)) return false;

        const id = makeBestRecId_(mode, track, car);
        const prev = records.find(r => r && r.id === id);
        if (prev && (prev.ms || 0) <= ms) return false; // existing is better or equal

        // remove any older entries for this tuple (including legacy ids)
        records = records.filter(r => !(r && r.mode === mode && r.track === track && r.car === car));

        const atIso = (extra && extra.atIso) ? extra.atIso : bestRaceAtIso_();
        records.push({
            id,
            track,
            mode,
            car,
            carImg: (extra && extra.carImg) ? extra.carImg : '',
            carClass: (extra && extra.carClass) ? extra.carClass : '',
            driverName: (extra && extra.driverName) ? extra.driverName : '',
            driverId: (extra && extra.driverId) ? extra.driverId : '',
            raceId: (extra && extra.raceId) ? extra.raceId : (raceMeta?.raceId || ""),
            timeText: timeText || msToTimeText_(ms, mode),
            ms,
            atIso
        });

        return true;
    }

    function importHtmlReportText_(htmlText, filename) {
        try {
            const doc = new DOMParser().parseFromString(String(htmlText || ''), 'text/html');
            if (!doc || !doc.querySelector) return null;
            const title = (doc.querySelector('title')?.textContent || '').trim();
            if (!title.toLowerCase().includes('torn lap times')) return null;

            const metaCarImg = doc.querySelector('meta[name="rt-car-img"]')?.getAttribute('content') || '';
            const metaRaceAt = doc.querySelector('meta[name="rt-race-at"]')?.getAttribute('content') || '';
            let raceId = (doc.querySelector('meta[name="rt-race-id"]')?.getAttribute('content') || '').trim();

            // Track + Car
            let track = '';
            let car = '';
            const tcEl = Array.from(doc.querySelectorAll('div')).find(d => {
                const t = (d.textContent || '').replace(/\s+/g, ' ').trim();
                return /Track:/i.test(t) && /Car:/i.test(t);
            });
            if (tcEl) {
                const t = (tcEl.textContent || '').replace(/\s+/g, ' ').trim();
                const m = t.match(/Track:\s*(.*?)\s*Car:\s*(.*?)(?:\s+Race:|\s+Driver:|\s+Generated:|$)/i);
                if (m) { track = (m[1] || '').trim(); car = (m[2] || '').trim(); }
            }

            // Fallback from filename: "Commerce_-_15_laps.Honda_NSX_P3.html"
            if ((!track || !car) && filename) {
                const fn = String(filename);
                const m = fn.match(/\.([^.]*)\.([^.]*)\./);
                if (m && !track) track = (m[1] || '').replace(/_/g, ' ').trim();
                if (m && !car) car = (m[2] || '').replace(/_/g, ' ').trim();
            }

            track = track || 'UnknownTrack';
            car = car || 'UnknownCar';

            // Race datetime -> ISO (best effort)
            let atIso = '';
            if (metaRaceAt) atIso = metaRaceAt;
            const raceEl = Array.from(doc.querySelectorAll('div')).find(d => /Race:/i.test((d.textContent || '')));
            if (!atIso && raceEl) {
                const t = (raceEl.textContent || '').replace(/\s+/g, ' ').trim();
                const m = t.match(/Race:\s*(\d{1,2})\/(\d{1,2})\/(\d{4}),\s*(\d{1,2}):(\d{2}):(\d{2})/);
                if (m) {
                    const dd = parseInt(m[1], 10), mm = parseInt(m[2], 10), yyyy = parseInt(m[3], 10);
                    const HH = parseInt(m[4], 10), MI = parseInt(m[5], 10), SS = parseInt(m[6], 10);
                    const d = new Date(yyyy, mm - 1, dd, HH, MI, SS);
                    if (!isNaN(d.getTime())) atIso = d.toISOString();
                }
            }
            if (!atIso) atIso = nowIso_();

            if (!raceId) {
                const raceLink = doc.querySelector('a[href*="raceID="], a[href*="raceId="], a[href*="raceid="]');
                if (raceLink) {
                    try {
                        const u = new URL(raceLink.getAttribute('href') || '', location.href);
                        raceId = (u.searchParams.get('raceID') || u.searchParams.get('raceId') || u.searchParams.get('raceid') || '').trim();
                    } catch { }
                }
            }
            if (!raceId) {
                const raceIdEl = Array.from(doc.querySelectorAll('div')).find(d => /Race ID:/i.test((d.textContent || '')));
                const m = raceIdEl ? String(raceIdEl.textContent || '').match(/Race ID:\s*(\d+)/i) : null;
                if (m) raceId = m[1];
            }

            // Driver name from row title="Driver — Car"
            let driverName = '';
            const trAny = doc.querySelector('tbody tr[title]');
            if (trAny) {
                const t = String(trAny.getAttribute('title') || '').trim();
                const parts = t.split('—').map(x => x.trim()).filter(Boolean);
                if (parts.length) driverName = parts[0];
            }

            // Collect all chrono lap times
            const chronoMs = [];
            const rows = Array.from(doc.querySelectorAll('tbody tr'));
            for (const tr of rows) {
                const tds = tr.querySelectorAll('td');
                if (!tds || tds.length < 3) continue;
                const chronoTxt = String(tds[2].textContent || '').replace(/\s+/g, '').trim();
                const ms = timeTextToMs_(chronoTxt);
                if (ms && ms > 0) chronoMs.push(ms);
            }
            if (!chronoMs.length) return null;

            // Best lap = fastest row (class fastest) else minimum chrono
            let bestMs = 0;
            const trFast = doc.querySelector('tbody tr.fastest');
            if (trFast) {
                const tds = trFast.querySelectorAll('td');
                const chronoTxt = tds && tds[2] ? String(tds[2].textContent || '').replace(/\s+/g, '').trim() : '';
                const ms = timeTextToMs_(chronoTxt);
                if (ms && ms > 0) bestMs = ms;
            }
            if (!bestMs) bestMs = Math.min(...chronoMs);

            const raceMs = chronoMs.reduce((a, b) => a + b, 0);

            const extra = { atIso, carImg: metaCarImg || '', carClass: '', driverName, driverId: '', raceId };

            const changedLap = updateBestRecord_('lap', track, car, bestMs, msToTimeText_(bestMs, 'lap'), extra);
            const changedRace = updateBestRecord_('race', track, car, raceMs, msToTimeText_(raceMs, 'race'), extra);
            return (changedLap || changedRace) ? true : false;
        } catch (e) {
            console.warn("[MoDuL's Lap Recorder] importHtmlReportText_ failed", e);
            return null;
        }
    }


    /* ============================
   * RECORDING
   * ============================ */
    function maybeRecordLap0Baseline_() {
        try {
            ensureRaceMeta_();
            if (!raceMeta || !raceMeta.metaKey) return;

            const info = document.querySelector(SEL_INFOSPOT);
            const txt = info ? String(info.textContent || "").trim().toLowerCase() : "";

            const isReplay = isReplay_(); // replay controls present
            const lapNow = (() => {
                const el = document.querySelector(SEL_LAP_UI);
                const t = el ? String(el.textContent || "").trim() : "";
                const m = t.match(/^(\d+)\s*\/\s*(\d+)/);
                return m ? parseInt(m[1], 10) : 0;
            })();

            // When NOT in replay: require "started" wording.
            // When in replay: infoSpot might be "Race replaying" or even "Race finished" while replay controls run,
            // so allow baseline once playback has progressed (completion > 0 or lap counter > 0 or a last-lap time is visible).
            const playbackHasProgressed =
                  (typeof completionPct === "number" && completionPct > 0) ||
                  (lapNow > 0) ||
                  !!readText_(SEL_LAST_UI);

            if (!isReplay) {
                if (!(txt.includes("started") || txt.includes("race started"))) return;
            } else {
                // For replay baseline, trigger immediately when Torn flips to "replaying" AND playback is actually running.
                if (!(txt.includes("replaying") && isReplayPlaying_())) {
                    if (!playbackHasProgressed) return;
                }
            }

            // Already captured baseline for this race?
            if (startRecordedForMetaKey === raceMeta.metaKey && raceMeta.startAtIso) return;

            const d = new Date();
            raceMeta.startAtIso = d.toISOString();
            startRecordedForMetaKey = raceMeta.metaKey;
            saveRaceMeta_(raceMeta);

            // Add a visible Lap 0 row so:
            // - UI doesn't stay on "Waiting for lap times…"
            // - Calc Chrono for Lap 1 has a baseline
            const at0Iso = raceMeta.startAtIso;
            const at0 = at0Iso.replace("T", " ").replace("Z", "");
            const id0 = `${raceMeta.metaKey}|0|${at0}`;
            const exists = laps.some(r => r && r.lap === 0 && lapBelongsToMeta_(r));
            if (!exists) {
                laps.push({
                    id: id0,
                    lap: 0,
                    position: "",
                    time: "", // "Last lap" blank for Lap 0
                    at: at0, // captured baseline time
                    atIso: at0Iso,
                    ms: null,
                    speed: getReplaySpeedFactor_(),
                    ui: "0",
                    track: raceMeta.track || "",
                    car: raceMeta.car || "",
                    raceDetectedAtIso: raceMeta.detectedAtIso || "",
                    driverName: currentDriverName_(),
                    driverCar: spectateCar || ""
                });
                saveLaps_();
            }

            uiDirty = true;
            scheduleRender_();
        } catch (e) {
            console.warn("[MoDuL's Lap Recorder] baseline capture failed", e);
        }
    }


    function recordLap_(lapNum, time, lapUi, positionText, opts = {}) {
        ensureRaceMeta_();

        const key = `${lapNum}|${time}|${lapUi || ""}|${raceMeta?.metaKey || ""}`;
        if (key === lastRecordedKey) return;
        lastRecordedKey = key;

        refreshSpectate_();
        const ms = timeTextToMs_(time);
        const atIso = String(opts.atIso || "").trim() || nowIso_();
        const capturedAt = atIso.replace("T", " ").replace("Z", "");
        const id = `${raceMeta?.metaKey || "meta"}|${lapNum}|${capturedAt}`;
        const speed = (typeof opts.speed === "number" && isFinite(opts.speed) && opts.speed > 0)
            ? opts.speed
            : getReplaySpeedFactor_();
        const pause = (typeof opts.pause === "number" && isFinite(opts.pause) && opts.pause >= 0)
            ? opts.pause
            : (isReplay_() ? getReplayPauseAccumMs_() : 0);

        laps.push({
            id,
            lap: lapNum,
            position: String(positionText || "").trim(),
            time,
            at: capturedAt,
            atIso: atIso,
            ms,
            speed,
            pause,
            ui: lapUi || "",
            track: raceMeta?.track || "",
            car: raceMeta?.car || "",
            raceDetectedAtIso: raceMeta?.detectedAtIso || "",
            driverName: currentDriverName_(),
            driverCar: spectateCar || ""
        });

        saveLaps_();
        uiDirty = true;
        scheduleRender_();
    }

    function flushPending_(lapUiRaw, positionText) {
        if (!recordingEnabled) return;
        if (!raceHasStarted_()) return;
        if (!hasFreshLastLapTime_()) return;

        const pos = String(positionText || getPositionText_() || "").trim();

        if (pendingCompletedTarget > lastLapCompleted) {
            for (let l = lastLapCompleted + 1; l <= pendingCompletedTarget; l++) {
                recordLap_(l, lastLapTimeSeen, lapUiRaw || "", pos);
            }
            lastLapCompleted = pendingCompletedTarget;
            pendingCompletedTarget = 0;
            markLastLapTimeUsed_();
        }

        if (pendingFinalLap && pendingFinalLap > 0) {
            if (!hasFreshLastLapTime_()) return;
            const finalCapture = deriveFinalLapCapture_(pendingFinalLap, lastLapTimeSeen) || {};
            recordLap_(pendingFinalLap, lastLapTimeSeen, lapUiRaw || "", pos, finalCapture);
            lastLapCompleted = Math.max(lastLapCompleted, pendingFinalLap);
            pendingFinalLap = 0;
            markLastLapTimeUsed_();
            finishRaceRecording_();
        }
    }

    function onLapUiChanged_() {
        if (!recordingEnabled) return;

        // Establish Lap 0 baseline as soon as the race actually starts moving (replays)
        // so Lap 1 calculated time is sane.
        maybeMarkRaceStarted_();
        if (!raceHasStarted_()) return;

        const { cur, raw } = parseLapUi_();
        if (!cur) return;

        const completed = Math.max(0, cur - 1);
        if (completed <= lastLapCompleted) return;

        pendingCompletedTarget = Math.max(pendingCompletedTarget || 0, completed);
        flushPending_(raw, getPositionText_());
    }



    function updateSeenLastLap_(t) {
        if (!noteLastLapTime_(t)) return;

        maybeMarkRaceStarted_();

        // Flush any lap changes that arrived before the Last lap text.
        const { raw } = parseLapUi_();
        flushPending_(raw, getPositionText_());

        // finish check
        maybeRecordFinalLapOnFinish_();
    }

    /* ============================
   * OBSERVERS
   * ============================ */
    function hookObservers_() {
        if (observersReady) return true;
        const lapEl = document.querySelector(SEL_LAP_UI);
        const lastEl = document.querySelector(SEL_LAST_UI);
        if (!lapEl || !lastEl) return false;

        new MutationObserver(() => updateSeenLastLap_((lastEl.textContent || "").trim()))
            .observe(lastEl, { childList: true, subtree: true, characterData: true });

        new MutationObserver(() => {
            onLapUiChanged_();
            maybeRecordFinalLapOnFinish_();
        }).observe(lapEl, { childList: true, subtree: true, characterData: true });

        const compEl = document.querySelector(SEL_COMP_UI);
        if (compEl) {
            new MutationObserver(() => {
                ({ pct: completionPct, time: completionFinishTime } = parseCompletionOrTime_());
                maybeCaptureFinishMoment_({ pct: completionPct, time: completionFinishTime });
                maybeMarkRaceStarted_();
                maybeRecordFinalLapOnFinish_();
            }).observe(compEl, { childList: true, subtree: true, characterData: true });
            ({ pct: completionPct, time: completionFinishTime } = parseCompletionOrTime_());
            maybeCaptureFinishMoment_({ pct: completionPct, time: completionFinishTime });
        }

        const infoEl = document.querySelector(SEL_INFOSPOT);
        if (infoEl) {
            new MutationObserver(() => {
                // Start baseline as soon as Torn flips "Race replaying"/"Race started".
                maybeMarkRaceStarted_();
            }).observe(infoEl, { childList: true, subtree: true, characterData: true });
        }

        // Replay play/pause button is the most reliable 'playing' signal.
        hookReplayPlayObserver_();
        hookReplaySpeedObserver_();


        const lb = document.querySelector(SEL_LEADERBOARD);
        if (lb) {
            // Track spectated driver changes (li.selected moves)
            new MutationObserver(() => refreshSpectate_())
                .observe(lb, { attributes: true, childList: true, subtree: true, attributeFilter: ["class"] });
            refreshSpectate_();
        }

        updateSeenLastLap_((lastEl.textContent || "").trim());
        onLapUiChanged_();
        maybeRecordFinalLapOnFinish_();

        observersReady = true;
        return true;
    }

    /* ============================
   * UI
   * ============================ */
    function updateHeaderCompact_(win) {
        try {
            if (!win) return;
            const hdr = win.querySelector("#rtHdr");
            const left = hdr?.querySelector(".left");
            const right = hdr?.querySelector(".right");
            if (!hdr || !left || !right) return;

            win.classList.remove("rtCompact");

            const headerWidth = hdr.clientWidth || 0;
            const buttons = Array.from(right.querySelectorAll(".pill"));
            const buttonWidth = buttons.reduce((sum, btn) => sum + (btn.scrollWidth || btn.getBoundingClientRect().width || 0), 0);
            const buttonGaps = Math.max(0, buttons.length - 1) * 8;
            const titleMinWidth = 170;
            const headerPaddingAndGap = 34;
            const needsCompact = headerWidth > 0 && (titleMinWidth + buttonWidth + buttonGaps + headerPaddingAndGap > headerWidth);

            win.classList.toggle("rtCompact", needsCompact);
        } catch { }
    }

    function restorePositions_() {
        const st = loadUiState_();
        const btn = document.getElementById("rtLapBtn");
        const win = document.getElementById("rtLapWin");

        if (btn && st.btn && Number.isFinite(st.btn.left) && Number.isFinite(st.btn.top)) {
            btn.style.right = "auto"; btn.style.bottom = "auto";
            btn.style.left = `${st.btn.left}px`;
            btn.style.top = `${st.btn.top}px`;
        }
        if (win && st.win && Number.isFinite(st.win.left) && Number.isFinite(st.win.top)) {
            win.style.right = "auto"; win.style.bottom = "auto";
            win.style.left = `${st.win.left}px`;
            win.style.top = `${st.win.top}px`;
            if (Number.isFinite(st.win.width) && Number.isFinite(st.win.height)) {
                win.style.width = `${st.win.width}px`;
                win.style.height = `${st.win.height}px`;
            }
        }
    }

    function openWin_() {
        document.getElementById("rtLapWin").style.display = "block";
        document.getElementById("rtLapBtn").style.display = "none";
        saveWinOpen_(true);
    }

    function closeWin_() {
        document.getElementById("rtLapWin").style.display = "none";
        document.getElementById("rtLapBtn").style.display = "flex";
        saveWinOpen_(false);
    }


    function clearLaps_(opts = {}) {
        laps = [];
        resetSeenLapTime_();
        lastLapCompleted = 0;
        lastRecordedKey = "";
        finishRecordedForMetaKey = "";
        pendingCompletedTarget = 0;
        pendingFinalLap = 0;
        completionPct = null;
        completionFinishTime = "";
        clearFinishCapture_();
        replayIsPlaying = false;
        replayPauseStartMs = null;
        replayPauseAccumMs = 0;
        replayEvents = [];
        if (!opts.keepMeta) clearRaceMeta_();
        saveLaps_();
        uiDirty = true;
        scheduleRender_();
    }

    function ensureUi_() {
        if (!document.getElementById("rtLapBtn")) {
            const b = document.createElement("div");
            b.id = "rtLapBtn";
            b.innerHTML = `
        <span id="rtBtnDot" class="recDot"></span>
        <b>Lap Recorder</b>
        <span id="rtBtnCount" class="badge">(0)</span>
      `;
            b.addEventListener("click", (e) => {
                if (b.dataset.dragging === "1") { e.preventDefault(); e.stopPropagation(); return; }
                openWin_();
            });
            document.body.appendChild(b);

            makeDraggable_(b, b, { storeKey: "btn", clickGuardAttr: "dragging", noDragSelector: null });
        }

        if (!document.getElementById("rtLapWin")) {
            const w = document.createElement("div");
            w.id = "rtLapWin";
            w.innerHTML = `
        <div id="rtHdr">
          <div class="left">
            <div style="display:flex;align-items:center;gap:10px">
              <span class="title">Lap Recorder</span><span id="rtCount" class="count">(0)</span>
            </div>
            <div id="rtVer" class="muted" style="font-size:11px;opacity:.85">v${RT_LAP_REC_VERSION} <span style="opacity:.8">· Powered by <a href="/profiles.php?XID=4022159" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">MoDuL</a></span></div>
          </div>
          <div class="right">
            <button id="rtToggle" class="pill" title="Toggle recording">
              <span id="rtRecDotInline"></span>
              <span id="rtRecText">Recording</span>
            </button>
            <button id="rtTheme" class="pill" title="Cycle themes">
              <span>🎨</span><span id="rtThemeLabel">Theme</span>
            </button>
            <button id="rtExportHtml" class="pill" title="Export HTML">
              <span>🧾</span><span>HTML</span>
            </button>
            <button id="rtImportHtml" class="pill" title="Import Lap Recorder HTML exports into Records">
              <span>📄</span><span>Import</span>
            </button>
            <button id="rtToggleRecords" class="pill" title="Show/hide per-track records">
              <span>🏆</span><span>Records</span>
            </button>
            <button id="rtClear" class="pill" title="Clear laps (keeps REC state)">
              <span>🧹</span><span>Clear</span>
            </button>
            <button id="rtAutoClear" class="pill" title="Toggle auto-clear when race/replay ID changes">
              <span>🧼</span><span id="rtAutoClearText">Auto-clear ID</span>
            </button>
            <button id="rtClose" class="pill" title="Close">
              <span>✕</span>
            </button>
          </div>
        </div>

        <div id="rtBody">
          <div id="rtMeta">
            <div class="rtMetaLeft">
              <div class="rtMetaTop">
                <div><span class="muted rtMetaLbl">Track:</span> <span id="rtTrack">—</span></div>
                <div id="rtRaceIdWrap"><span class="muted rtMetaLbl">Race ID:</span> <a id="rtRaceId" href="#" target="_blank" rel="noopener noreferrer">—</a></div>
              </div>
              <div class="rtMetaLine">
                <span class="muted rtMetaLbl">Car:</span>
                <span class="rtMetaCar">
                  <img id="rtCarImg" class="carIcon" style="display:none" alt="" />
                  <span id="rtCar" class="carName">—</span>
                </span>
                <span class="muted" style="opacity:.85">Driver:</span> <span id="rtDriver">—</span>
              </div>
            </div>
            <div class="rtMetaRight mono">
              <div><span class="muted">Race:</span> <span id="rtRaceTime">—</span></div>
              <div><span class="muted">Total:</span> <span id="rtRaceTotal">—</span></div>
            </div>
          </div>
          <div id="rtCountdown" style="display:none"></div>
          <div id="rtCalcNote" class="muted" style="font-size:11px;opacity:.85;display:none">Calc uses replay speed: <b id="rtSpeedLabel">1×</b>.</div>
          <div id="rtEvents" class="mono muted" style="font-size:11px;opacity:.9;max-height:78px;overflow:auto;display:none;margin-top:6px"></div>

          <div id="rtTableWrap">
            <table id="rtTable">
              <thead>
                <tr>
                  <th>Lap</th>
                  <th>Pos</th>
                  <th>Chrono</th>
                  <th>Delta</th>
                  <th>Last lap</th>
                  <th></th>
                </tr>
              </thead>
              <tbody id="rtTbody"></tbody>
            </table>
          </div>

          <div id="rtRecords" class="recordsWrap" style="display:none">
            <div class="recordsBar">
              <div class="recordsLeft">
                <span class="muted">Track:</span>
                <select id="rtRecTrack" class="select"></select>
                <span class="muted" style="margin-left:8px">Mode:</span>
                <select id="rtRecMode" class="select">
                  <option value="lap">Best Lap</option>
                  <option value="race">Race Time</option>
                </select>
              </div>
              <div class="recordsRight muted">Top 10 • unique cars</div>
            </div>
            <div class="recordsTable" id="rtRecScroll">
              <table id="rtRecTable">
                <thead>
                  <tr>
                    <th class="colNum">#</th>
                    <th class="colImg">Img</th>
                    <th class="colCar">Car</th>
                    <th class="colTime">Time</th>
                    <th class="colDriver">Driver</th>
                    <th class="colRace">Race</th>
                    <th class="colWhen">When</th>
                    <th class="colAct">Delete</th>
                  </tr>
                </thead>
                <tbody id="rtRecTbody"></tbody>
              </table>
            </div>
          </div>

          <input id="rtImportInput" type="file" accept="text/html,.html" multiple style="display:none" />
        </div>
      `;
            document.body.appendChild(w);

            const updateHeaderCompact = () => updateHeaderCompact_(w);

            // Switch the header buttons to icon-only when the title and buttons no longer fit.
            try {
                const ro = new ResizeObserver(updateHeaderCompact);
                ro.observe(w);
            } catch (e) {
                // ResizeObserver not supported (very old browsers) — ignore
            }
            updateHeaderCompact();

            document.getElementById("rtClose").onclick = closeWin_;
            document.getElementById("rtClear").onclick = () => {
                clearLaps_();
                uiDirty = true;
                scheduleRender_();
            };


            document.getElementById("rtToggle").onclick = () => {
                recordingEnabled = !recordingEnabled;
                saveRecEnabled_();
                uiDirty = true;
                scheduleRender_();
            };

            document.getElementById("rtAutoClear").onclick = () => {
                clearOnRaceChange = !clearOnRaceChange;
                saveClearOnRaceChange_(clearOnRaceChange);
                uiDirty = true;
                scheduleRender_();
            };

            document.getElementById("rtTheme").onclick = cycleTheme_;

            document.getElementById("rtExportHtml").onclick = exportHtml_;

            // Import HTML exports into Records
            const impBtn = document.getElementById("rtImportHtml");
            const impInput = document.getElementById("rtImportInput");
            if (impBtn && impInput) {
                impBtn.onclick = () => {
                    try { impInput.value = ""; } catch { }
                    impInput.click();
                };
                impInput.addEventListener("change", async () => {
                    const files = Array.from(impInput.files || []);
                    if (!files.length) return;
                    let ok = 0, skipped = 0, failed = 0;
                    for (const f of files) {
                        try {
                            const txt = await readFileText_(f);
                            const res = importHtmlReportText_(txt, f.name);
                            if (res === true) ok++;
                            else if (res === false) skipped++;
                            else failed++;
                        } catch {
                            failed++;
                        }
                    }
                    saveRecords_();
                    uiDirty = true;
                    scheduleRender_();
                    toast_(`Import complete: ${ok} added/updated, ${skipped} skipped, ${failed} failed.`);
                });
            }

            document.getElementById("rtToggleRecords").onclick = () => {
                recordsOpen = !recordsOpen;
                uiDirty = true;
                scheduleRender_();
            };

            document.getElementById("rtRecMode").onchange = (e) => {
                recordsMode = String(e.target.value || "lap");
                uiDirty = true;
                scheduleRender_();
            };
            document.getElementById("rtRecTrack").onchange = (e) => {
                recordsTrack = String(e.target.value || "");
                uiDirty = true;
                scheduleRender_();
            };

            document.getElementById("rtRecTbody").addEventListener("click", (e) => {
                const btn = e.target.closest(".recDelBtn");
                if (!btn) return;
                const id = btn.getAttribute("data-id");
                if (!id) return;
                records = records.filter(r => r.id !== id);
                saveRecords_();
                uiDirty = true;
                scheduleRender_();
            });

            document.getElementById("rtTbody").addEventListener("click", (e) => {
                const btn = e.target.closest(".delBtn");
                if (!btn) return;
                const id = btn.getAttribute("data-id");
                if (!id) return;

                laps = laps.filter(r => r.id !== id);
                refreshLastLapCompleted_();
                saveLaps_();
                uiDirty = true;
                scheduleRender_();
            });

            makeDraggable_(w, w, { storeKey: "win", clickGuardAttr: null, noDragSelector: "button, a, input, textarea, select, .delBtn, td, th" });

            observeResize_(w, "win");
        }

        restorePositions_();
        applyTheme_();

        const wasOpen = loadWinOpen_();
        const win = document.getElementById("rtLapWin");
        const btn = document.getElementById("rtLapBtn");

        if (win && btn) {
            win.style.display = wasOpen ? "block" : "none";
            btn.style.display = wasOpen ? "none" : "flex";
        }

        uiDirty = true;
        scheduleRender_();
    }

    /* ============================
   * RENDER
   * ============================ */
    let lastRenderKey = "";
    let renderScheduled = false;

    function scheduleRender_() {
        if (renderScheduled) return;
        renderScheduled = true;
        requestAnimationFrame(() => {
            renderScheduled = false;
            render_();
        });
    }
    function render_() {
        ensureRaceMeta_();
        applyTheme_();
        const btnDot = document.getElementById("rtBtnDot");
        const btnCount = document.getElementById("rtBtnCount");
        const count = document.getElementById("rtCount");
        const recDot = document.getElementById("rtRecDotInline");
        const recText = document.getElementById("rtRecText");
        const autoClearBtn = document.getElementById("rtAutoClear");

        if (autoClearBtn) {
            autoClearBtn.onclick = () => {
                clearOnRaceChange = !clearOnRaceChange;
                saveClearOnRaceChange_(clearOnRaceChange);
                uiDirty = true;
                scheduleRender_();
            };
        }
        const autoClearText = document.getElementById("rtAutoClearText");
        const tb = document.getElementById("rtTbody");

        const trackEl = document.getElementById("rtTrack");
        const carEl = document.getElementById("rtCar");
        const carImgEl = document.getElementById("rtCarImg");

        const recWrap = document.getElementById("rtRecords");
        const recTbody = document.getElementById("rtRecTbody");
        const recTrackSel = document.getElementById("rtRecTrack");
        const recModeSel = document.getElementById("rtRecMode");

        const driverEl = document.getElementById("rtDriver");
        const raceTimeEl = document.getElementById("rtRaceTime");
        const raceTotalEl = document.getElementById("rtRaceTotal");
        const raceIdEl = document.getElementById("rtRaceId");
        const raceIdWrap = document.getElementById("rtRaceIdWrap");
        const cdEl = document.getElementById("rtCountdown");
        const raceLaps = currentRaceLaps_(true);
        const shownLapCount = raceLaps.filter(r => (r.lap || 0) > 0).length;

        if (btnDot) btnDot.className = recordingEnabled ? "recDot" : "recDot off";
        if (btnCount) btnCount.textContent = `(${shownLapCount})`;
        if (count) count.textContent = `(${shownLapCount})`;
        if (autoClearBtn) autoClearBtn.classList.toggle("on", !!clearOnRaceChange);
        if (autoClearText) autoClearText.textContent = clearOnRaceChange ? "Auto-clear ID: ON" : "Auto-clear ID: OFF";
        if (recDot) recDot.className = recordingEnabled ? "" : "off";
        if (recText) recText.textContent = recordingEnabled ? "Recording" : "Paused";
        updateHeaderCompact_(document.getElementById("rtLapWin"));
        if (trackEl) trackEl.textContent = raceMeta?.track || "—";
        const selectedDriver = readSelectedDriver_();
        const replayTargetName = String(playerName || "").trim();
        const replayWaitingForTarget = isReplay_() && !!replayTargetName && !selectedDriver.name;
        const replayNeedsSelection = isReplay_() && !selectedDriver.name && !replayTargetName;
        const showReplayPrompt = replayWaitingForTarget || replayNeedsSelection;
        if (carEl) {
            carEl.textContent = showReplayPrompt
                ? (replayTargetName ? `Waiting for ${replayTargetName}` : "Select from replay list")
                : (spectateCar || raceMeta?.car || "—");
        }
        if (carImgEl) {
            const u = showReplayPrompt ? "" : (raceMeta?.carImg || findCarImageUrl_(raceMeta?.car) || spectateCarImg || "").trim();
            if (u) { carImgEl.src = u; carImgEl.style.display = "inline-block"; }
            else { carImgEl.removeAttribute("src"); carImgEl.style.display = "none"; }
        }
        if (driverEl) {
            driverEl.textContent = showReplayPrompt
                ? (replayTargetName ? "Auto-selecting replay driver" : "Choose a driver to follow")
                : (spectateName || playerName || raceMeta?.driver || "—");
        }

        if (raceIdEl) {
            const rid = (raceMeta?.raceId || "").trim();
            if (rid) {
                const href = `https://www.torn.com/page.php?sid=racing&raceID=${encodeURIComponent(rid)}`;
                raceIdEl.textContent = rid;
                raceIdEl.href = href;
                if (raceIdWrap) raceIdWrap.style.display = "";
            } else {
                if (raceIdWrap) raceIdWrap.style.display = "none";
            }
        }

        if (raceTimeEl) {
            if (!raceHasStarted_() && (raceMeta?.countdown || "")) {
                const secs = parseCountdownToSeconds_(raceMeta.countdown);
                const startAt = new Date(Date.now() + secs * 1000);
                raceTimeEl.textContent = formatLocalDateTime_(startAt);
            } else {
                raceTimeEl.textContent = raceMeta?.detectedAtLocal || "—";
            }
        }

        if (cdEl) {
            const show = !raceHasStarted_() && !!(raceMeta?.countdown || "");
            if (!show) {
                cdEl.textContent = "";
                cdEl.style.display = "none";
            } else {
                const secs = parseCountdownToSeconds_(raceMeta.countdown);
                cdEl.textContent = `Race starts in: ${formatShortCountdown_(secs)}`;
                cdEl.style.display = "block";
            }
        }

        // Records panel (per track)
        if (recWrap) {
            recWrap.style.display = recordsOpen ? "" : "none";
        }
        if (recordsOpen && recTbody && recTrackSel && recModeSel) {
            const trackNow = (raceMeta?.track || "").trim();
            const tracks = getAllTrackNames_();
            if (!recordsTrack && trackNow) recordsTrack = trackNow;

            const optKey = tracks.join("||");
            if (recTrackSel.dataset.key !== optKey) {
                recTrackSel.innerHTML = tracks.map(t => `<option value="${escAttr_(t)}">${esc_(t)}</option>`).join("") || `<option value="">—</option>`;
                recTrackSel.dataset.key = optKey;
            }

            if (recordsTrack && tracks.includes(recordsTrack)) recTrackSel.value = recordsTrack;
            else if (tracks.length) { recordsTrack = tracks[0]; recTrackSel.value = recordsTrack; }

            recModeSel.value = recordsMode;

            const mode = recordsMode;
            const track = recordsTrack || trackNow || "";

            const rows = records
            .filter(r => r && r.track === track && r.mode === mode)
            .slice()
            .sort((a, b) => (a.ms || 0) - (b.ms || 0));

            const bestByCar = new Map();
            for (const r of rows) {
                const k = (r.car || "").trim();
                if (!k) continue;
                const prev = bestByCar.get(k);
                if (!prev || (r.ms || 0) < (prev.ms || 0)) bestByCar.set(k, r);
            }

            const top = Array.from(bestByCar.values())
            .sort((a, b) => (a.ms || 0) - (b.ms || 0))
            .slice(0, 10);
            if (!top.length) {
                recTbody.innerHTML = `<tr><td colspan="8" class="muted">No records for this track yet.</td></tr>`;
            } else {
                recTbody.innerHTML = top.map((r, i) => {
                    const imgCell = r.carImg ? `<img class="carIcon" src="${escAttr_(r.carImg)}" alt="">` : `<div class="carIcon" aria-hidden="true"></div>`;
                    const drv = (r.driverName || r.driverId || "—");
                    const recRid = String(r.raceId || "").trim();
                    const raceCell = recRid ? `<a class="recRaceLink" href="https://www.torn.com/page.php?sid=racing&raceID=${encodeURIComponent(recRid)}" target="_blank" title="View Replay">${esc_(recRid)}</a>` : "";
                    return `<tr>
            <td class="recNum">${i + 1}</td>
            <td class="recImg">${imgCell}</td>
            <td class="recCar"><span>${esc_(r.car || "—")}</span></td>
            <td class="recTime">${esc_(r.timeText || "—")}</td>
            <td class="recDrv">${esc_(drv)}</td>
            <td class="recRace mono">${raceCell}</td>
            <td class="recWhen">${esc_(fmtWhen_(r.atIso || r.at || ""))}</td>
            <td class="recAct"><button class="recDelBtn" data-id="${escAttr_(r.id)}" title="Delete">🗑️</button></td>
          </tr>`;
                }).join("");
            }
        }

        if (!tb) return;

        const curKey = `${raceLaps.length}|${raceMeta?.metaKey || ""}|${recordingEnabled ? 1 : 0}|${theme}|${raceMeta?.raceId || ""}|${raceMeta?.totalTime || ""}`;
        const needTable = uiDirty || (curKey !== lastRenderKey);

        if (!shownLapCount) {
            if (raceTotalEl) raceTotalEl.textContent = "—";
            if (needTable) {
                tb.innerHTML = raceIsFinished_()
                    ? `<tr><td colspan="6" class="muted">Race finished — no laps captured (replay not played).</td></tr>`
          : `<tr><td colspan="6" class="muted">Waiting for lap times…</td></tr>`;
            }
            lastRenderKey = curKey;
            uiDirty = false;
            return;
        }

        const fastestUiMs = Math.min(...raceLaps.map(r => r.ms).filter(ms => typeof ms === "number" && ms > 0));
        const asc = raceLaps.slice().sort((a, b) => (a.lap || 0) - (b.lap || 0)).filter(r => (r.lap || 0) > 0);
        const disp = asc.filter(r => (r.lap || 0) > 0); // hide Lap 0 baseline from display
        const byLap = new Map(asc.map(r => [r.lap, r]));

        // Calc lap time based on capture timestamps (centiseconds precision).
        const calcByLap = new Map();
        for (let i = 0; i < asc.length; i++) {
            const cur = asc[i];
            const prev = i > 0 ? asc[i - 1] : null;

            // Lap 0 is the baseline start marker.
            if ((cur.lap || 0) === 0) { calcByLap.set(0, 0); continue; }

            calcByLap.set(cur.lap, computeLapChronoMs_(cur, prev));
        }

        const fastestCalcMs = Math.min(...[...calcByLap.values()].filter(v => typeof v === "number" && isFinite(v) && v > 0));
        const fastestLap = (isFinite(fastestCalcMs) && fastestCalcMs > 0)
        ? Math.min(...[...calcByLap.entries()].filter(([, v]) => v === fastestCalcMs).map(([lap]) => lap))
        : null;


        // Cumulative (total) time up to each lap (using Chrono / calculated ms)
        const cumByLap = new Map();
        let cumMs = 0;
        for (let i = 0; i < asc.length; i++) {
            const lapN = asc[i].lap || 0;
            if (lapN <= 0) continue;
            const v = calcByLap.get(lapN);
            if (typeof v === "number" && isFinite(v) && v > 0) cumMs += v;
            cumByLap.set(lapN, cumMs);
        }
        const maxLapShown = disp.length ? Math.max(...disp.map(x => x.lap || 0)) : 0;

        // Total shown under Race (no longer a column)
        if (raceTotalEl) {
            const tms = maxLapShown ? (cumByLap.get(maxLapShown) || 0) : 0;
            raceTotalEl.textContent = tms > 0 ? msToCalcText_(tms) : "—";
        }

        const calcNote = document.getElementById("rtCalcNote");
        if (calcNote) {
            // Only show the 1× note when this looks like a replay.
            const canCalc = asc.length > 1 && [...calcByLap.values()].some(v => typeof v === "number" && isFinite(v) && v > 0);
            const show = (isReplay_() && canCalc);
            calcNote.style.display = show ? "block" : "none";
            if (show) {
                const spd = getReplaySpeedFactor_();
                const lbl = document.getElementById("rtSpeedLabel");
                if (lbl) lbl.textContent = `${spd}×`;
            }
        }

        const ev = document.getElementById("rtEvents");
        if (ev) {
            const showEv = (isReplay_() && replayEvents && replayEvents.length);
            ev.style.display = showEv ? "block" : "none";
            if (showEv) {
                ev.innerHTML = replayEvents.slice().reverse().map(e =>
                                                                  `<div>${escapeHtml_(e.text)} at <span class="mono">${escapeHtml_(e.at)}</span></div>`
        ).join("");
            } else {
                ev.textContent = "";
            }
        }

        if (needTable) {
            try {
                tb.innerHTML = disp.slice().reverse().map(r => {
                    const prev = byLap.get((r.lap || 0) - 1);
                    const curCalc = calcByLap.get(r.lap);
                    const prevCalc = prev ? calcByLap.get(prev.lap) : null;
                    const deltaMs = (r.lap <= 1) ? 0 : ((typeof curCalc === "number" && typeof prevCalc === "number") ? (curCalc - prevCalc) : 0);
                    const isFastest = (fastestLap != null && r.lap === fastestLap);
                    const tip = [r.driverName, r.driverCar].filter(Boolean).join(" — ");
                    const tipAttr = tip ? ` title="${esc_(tip)}"` : "";

                    return `
        <tr class="${isFastest ? "fastest" : ""}"${tipAttr}>
          <td>${r.lap}</td>
          <td>${esc_(r.position || "")}</td>
          <td class="muted mono">${esc_(msToCalcText_(calcByLap.get(r.lap)))}${isFastest ? `<span class="flBadge">🏁FL</span>` : ""}</td>
          <td class="${gapClass_(deltaMs)} mono ${isFastest ? "delta-best" : ""}">${esc_(msToDeltaText_(deltaMs))}</td>
          <td>${esc_(r.time)}</td>
          <td><button class="delBtn" data-id="${esc_(r.id)}" title="Delete this lap">🗑️</button></td>
        </tr>`;
                }).join("");
            } catch (e) {
                console.warn("[MoDuL's Lap Recorder] render table failed", e);
                tb.innerHTML = `<tr><td colspan="6" class="muted">Render error — check console.</td></tr>`;
            }
        }

        lastRenderKey = curKey;
        uiDirty = false;
    }

    /* ============================
   * DRAGGING
   * ============================ */

    function observeResize_(el, storeKey) {
        if (!el || !storeKey || typeof ResizeObserver === "undefined") return;
        const ro = new ResizeObserver(() => {
            // Persist size (and current position) when the user resizes.
            const st = loadUiState_();
            const rect = el.getBoundingClientRect();
            st[storeKey] = {
                left: Math.round(rect.left),
                top: Math.round(rect.top),
                width: Math.round(rect.width),
                height: Math.round(rect.height)
            };
            saveUiState_(st);
        });
        ro.observe(el);
    }

    function makeDraggable_(targetEl, handleEl, opts) {
        const { storeKey, clickGuardAttr, noDragSelector } = opts || {};
        let down = false, startX = 0, startY = 0, startL = 0, startT = 0;
        let moved = false;

        handleEl.addEventListener("mousedown", (e) => {
            if (noDragSelector && e.target.closest(noDragSelector)) return;
            down = true; moved = false;

            const rect = targetEl.getBoundingClientRect();

            // If user is grabbing the native resize handle, don't start a drag.
            if (e.clientX > rect.right - 18 && e.clientY > rect.bottom - 18) { down = false; return; }
            startX = e.clientX; startY = e.clientY;
            startL = rect.left; startT = rect.top;

            targetEl.style.right = "auto";
            targetEl.style.bottom = "auto";
            targetEl.style.left = `${startL}px`;
            targetEl.style.top = `${startT}px`;

            e.preventDefault();
        });

        window.addEventListener("mousemove", (e) => {
            if (!down) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            if (Math.abs(dx) > 2 || Math.abs(dy) > 2) moved = true;

            targetEl.style.left = `${startL + dx}px`;
            targetEl.style.top = `${startT + dy}px`;

            if (clickGuardAttr) targetEl.dataset[clickGuardAttr] = "1";
        });

        window.addEventListener("mouseup", () => {
            if (!down) return;
            down = false;

            if (storeKey) {
                const st = loadUiState_();
                const rect = targetEl.getBoundingClientRect();
                st[storeKey] = { left: Math.round(rect.left), top: Math.round(rect.top), width: Math.round(rect.width), height: Math.round(rect.height) };
                saveUiState_(st);
            }

            if (clickGuardAttr) {
                setTimeout(() => { targetEl.dataset[clickGuardAttr] = moved ? "1" : "0"; }, 0);
                if (moved) setTimeout(() => { targetEl.dataset[clickGuardAttr] = "0"; }, 60);
            }
        });
    }

    /* ============================
   * INIT
   * ============================ */
    async function init_() {
        laps = loadLaps_();
        records = loadRecords_();
        recordingEnabled = loadRecEnabled_();
        raceMeta = loadRaceMeta_();
        theme = loadTheme_();
        clearOnRaceChange = loadClearOnRaceChange_();
        refreshLastLapCompleted_();

        maybeResetOnRaceIdChange_();
        laps = loadLaps_();
        raceMeta = loadRaceMeta_();
        refreshLastLapCompleted_();

        ensureUi_();
        ensurePlayer_();
        refreshSpectate_();

        for (let i = 0; i < 60; i++) {
            if (hookObservers_()) break;
            await new Promise(r => setTimeout(r, 250));
        }

        setInterval(() => {
            maybeResetOnRaceIdChange_();
            updateTabTitle_();
            maybeMarkRaceStarted_();
            ensurePlayer_();
            refreshSpectate_();

            if (!raceHasStarted_()) {
                ensureRaceMeta_();
                uiDirty = true;
                scheduleRender_();
            } else {
                ({ pct: completionPct, time: completionFinishTime } = parseCompletionOrTime_());
                maybeCaptureFinishMoment_({ pct: completionPct, time: completionFinishTime });
                maybeRecordFinalLapOnFinish_();
            }
        }, 1000);
    }

    function waitForRacingDom_(cb) {
        const startedAt = Date.now();
        const t = setInterval(() => {
            if (document.querySelector("#racingdetails") || document.querySelector(SEL_INFOSPOT)) {
                clearInterval(t);
                cb();
            } else if (Date.now() - startedAt > 30000) {
                // Don't block forever; still try to init.
                clearInterval(t);
                cb();
            }
        }, 300);
    }

    waitForRacingDom_(init_);

})();