MoDuL's Lap Recorder

Records Lap Times

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

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

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

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

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

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

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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

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

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

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

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

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

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

// ==UserScript==
// @name         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_);

})();