Smart Racing

Race history, car suggestions, and upgrade helpers for Torn racing

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 or Violentmonkey 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.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

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         Smart Racing
// @namespace    smart.torn.tools
// @version      1.0.0
// @description  Race history, car suggestions, and upgrade helpers for Torn racing
// @author       Noobler
// @match        https://www.torn.com/page.php?sid=racing*
// @match        https://www.torn.com/loader.php?sid=racing*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @run-at       document-idle
// @license      MIT
// ==/UserScript==


(() => {
  // src/shared/theme.js
  var tokens = {
    accent: "#caa14a",
    accentMuted: "rgba(202, 161, 74, 0.35)",
    accentSubtle: "rgba(202, 161, 74, 0.12)",
    accentText: "#f0e3c0",
    panelBg: "linear-gradient(180deg, #23252b, #1b1d22)",
    panelHeaderBg: "linear-gradient(180deg, #2c2f37, #23252b)",
    panelBorder: "#34373f",
    surface: "#15161a",
    text: "#d7d9de",
    textMuted: "#8a8d96",
    textDim: "#71747d",
    success: "#6fcf86",
    successBg: "#16341f",
    successBorder: "#2c6b3c",
    warning: "#ffd966",
    warningBg: "#332b13",
    warningBorder: "#6b5a1f",
    danger: "#ff6b6b",
    dangerBg: "#341818",
    dangerBorder: "#6b2c2c",
    info: "#8dbdf0",
    infoBg: "#15263d",
    infoBorder: "#2c4f7b",
    hover: "#d8b25c",
    font: "'Trebuchet MS', Verdana, sans-serif",
    radius: "8px",
    shadow: "0 2px 10px rgba(0, 0, 0, 0.35)",
    statDex: "#8a7ff0",
    statDef: "#e07a4f",
    statStr: "#3fae84",
    statSpd: "#4a97e6"
  };
  var STYLE_ID = "hf-torn-theme";
  function injectStyles(scopeClass = "hf-torn") {
    if (document.getElementById(STYLE_ID)) {
      return;
    }
    const css = `
    .${scopeClass} {
      --hf-accent: ${tokens.accent};
      --hf-accent-muted: ${tokens.accentMuted};
      --hf-accent-subtle: ${tokens.accentSubtle};
      --hf-accent-text: ${tokens.accentText};
      --hf-panel-bg: ${tokens.panelBg};
      --hf-panel-header-bg: ${tokens.panelHeaderBg};
      --hf-panel-border: ${tokens.panelBorder};
      --hf-surface: ${tokens.surface};
      --hf-text: ${tokens.text};
      --hf-text-muted: ${tokens.textMuted};
      --hf-text-dim: ${tokens.textDim};
      --hf-success: ${tokens.success};
      --hf-success-bg: ${tokens.successBg};
      --hf-success-border: ${tokens.successBorder};
      --hf-warning: ${tokens.warning};
      --hf-warning-bg: ${tokens.warningBg};
      --hf-warning-border: ${tokens.warningBorder};
      --hf-danger: ${tokens.danger};
      --hf-danger-bg: ${tokens.dangerBg};
      --hf-danger-border: ${tokens.dangerBorder};
      --hf-info: ${tokens.info};
      --hf-info-bg: ${tokens.infoBg};
      --hf-info-border: ${tokens.infoBorder};
      --hf-hover: ${tokens.hover};
      --hf-radius: ${tokens.radius};
      --hf-shadow: ${tokens.shadow};
      --hf-font: ${tokens.font};
      --hf-stat-dex: ${tokens.statDex};
      --hf-stat-def: ${tokens.statDef};
      --hf-stat-str: ${tokens.statStr};
      --hf-stat-spd: ${tokens.statSpd};
      color: var(--hf-text);
      font-family: var(--hf-font);
      font-size: 12px;
      line-height: 1.4;
      box-sizing: border-box;
    }

    .${scopeClass} *, .${scopeClass} *::before, .${scopeClass} *::after {
      box-sizing: border-box;
    }

    .${scopeClass} .hf-btn {
      appearance: none;
      border: 1px solid var(--hf-panel-border);
      border-radius: 5px;
      background: var(--hf-surface);
      color: var(--hf-accent-text);
      cursor: pointer;
      padding: 6px 10px;
      font: inherit;
      transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
    }

    .${scopeClass} .hf-btn:hover {
      background: var(--hf-accent-subtle);
      border-color: var(--hf-accent);
      color: #fff;
    }

    .${scopeClass} .hf-btn:disabled {
      opacity: 0.65;
      cursor: wait;
      pointer-events: none;
    }

    .${scopeClass} .hf-btn.is-active,
    .${scopeClass} .hf-btn--primary {
      background: var(--hf-accent);
      border-color: var(--hf-accent);
      color: #1b1d22;
      font-weight: 700;
    }

    .${scopeClass} .hf-btn.is-active:hover,
    .${scopeClass} .hf-btn--primary:hover {
      background: var(--hf-hover);
      border-color: var(--hf-hover);
    }

    .${scopeClass} .hf-muted {
      color: var(--hf-text-muted);
    }

    .${scopeClass} .hf-dim {
      color: var(--hf-text-dim);
    }

    .${scopeClass} .hf-badge {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 4px;
      background: var(--hf-accent);
      color: #1b1d22;
      font-size: 11px;
      font-weight: 700;
      letter-spacing: 0.08em;
      text-transform: uppercase;
    }

    .${scopeClass} .hf-input,
    .${scopeClass} .hf-select {
      width: 100%;
      box-sizing: border-box;
      padding: 6px 8px;
      background: var(--hf-surface);
      border: 1px solid var(--hf-panel-border);
      color: var(--hf-accent-text);
      border-radius: 5px;
      font: inherit;
      font-size: 12px;
    }

    .${scopeClass} .hf-label {
      display: block;
      font-size: 11px;
      color: var(--hf-text-muted);
      margin: 12px 0 4px;
    }

    .${scopeClass} .hf-table {
      width: 100%;
      border-collapse: collapse;
    }

    .${scopeClass} .hf-table th,
    .${scopeClass} .hf-table td {
      padding: 4px 6px;
      text-align: left;
      border-bottom: 1px solid var(--hf-panel-border);
      color: var(--hf-text) !important;
      background: transparent !important;
    }

    .${scopeClass} .hf-table th {
      font-weight: bold;
      color: var(--hf-accent-text) !important;
    }

    .${scopeClass} .hf-table tbody tr:hover td {
      background: var(--hf-accent-subtle) !important;
    }

    .${scopeClass} .hf-table .hf-pos--1 {
      color: #e8c547 !important;
      font-weight: 700;
    }

    .${scopeClass} .hf-table .hf-pos--2 {
      color: #c4c9d4 !important;
      font-weight: 700;
    }

    .${scopeClass} .hf-table .hf-pos--3 {
      color: #c98a5a !important;
      font-weight: 700;
    }

    .${scopeClass} .hf-racing-log-toolbar {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 8px;
      margin-bottom: 10px;
    }

    .${scopeClass} .hf-racing-log-hint {
      font-size: 11px;
      color: var(--hf-text-muted);
    }

    .${scopeClass} strong {
      color: var(--hf-accent-text);
    }

    .${scopeClass} hr {
      border: none;
      border-top: 1px solid var(--hf-panel-border);
      margin: 10px 0;
    }

    .${scopeClass} .hf-section-break {
      box-sizing: border-box;
      border-top: 1px solid var(--hf-panel-border);
      margin-top: 10px;
      padding-top: 12px;
    }

    .${scopeClass} .hf-guide-row {
      margin: 0 0 4px;
    }

    .${scopeClass} .hf-guide-row:first-child {
      margin-top: 0;
    }

    .${scopeClass} .hf-racing-car-row {
      margin-bottom: 10px;
    }

    .${scopeClass} .hf-racing-live {
      margin-bottom: 10px;
      padding: 6px 8px;
      border-radius: 5px;
      background: var(--hf-accent-subtle);
      border: 1px solid var(--hf-accent-muted);
      color: var(--hf-accent-text);
      font-size: 11px;
    }

    .${scopeClass} .hf-placement-row {
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      gap: 6px;
      margin: 4px 0 2px;
    }

    .${scopeClass} .hf-placement-chip {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      width: auto;
      max-width: none;
      flex: 0 0 auto;
      padding: 3px 7px 3px 5px;
      border-radius: 5px;
      border: 1px solid var(--hf-panel-border);
      background: var(--hf-surface);
      font-size: 11px;
      font-weight: 700;
      font-variant-numeric: tabular-nums;
      line-height: 1;
    }

    .${scopeClass} .hf-placement-chip.is-zero {
      opacity: 0.35;
    }

    .${scopeClass} .hf-placement-chip svg.hf-placement-icon {
      width: 14px !important;
      height: 14px !important;
      min-width: 14px !important;
      max-width: 14px !important;
      min-height: 14px !important;
      max-height: 14px !important;
      flex: 0 0 14px !important;
      display: block !important;
      overflow: visible !important;
      vertical-align: middle;
    }

    .${scopeClass} .hf-placement-chip--gold {
      color: #e8c547;
      border-color: rgba(232, 197, 71, 0.35);
      background: rgba(232, 197, 71, 0.1);
    }

    .${scopeClass} .hf-placement-chip--silver {
      color: #c4c9d4;
      border-color: rgba(196, 201, 212, 0.35);
      background: rgba(196, 201, 212, 0.08);
    }

    .${scopeClass} .hf-placement-chip--bronze {
      color: #c98a5a;
      border-color: rgba(201, 138, 90, 0.35);
      background: rgba(201, 138, 90, 0.1);
    }

    .${scopeClass} .hf-placement-chip--out {
      color: var(--hf-text-muted);
      border-color: var(--hf-panel-border);
      background: rgba(0, 0, 0, 0.15);
    }

    .${scopeClass}.hf-panel {
      margin: 0 0 12px;
      border-radius: var(--hf-radius);
      background: var(--hf-panel-bg);
      border: 1px solid var(--hf-panel-border);
      box-shadow: var(--hf-shadow);
      overflow: hidden;
    }

    .${scopeClass}.hf-panel--page {
      width: 100%;
    }

    .${scopeClass}.hf-panel--fixed {
      position: fixed;
      z-index: 9999;
    }

    .${scopeClass} .hf-panel-header {
      background: var(--hf-panel-header-bg);
      border-bottom: 1px solid var(--hf-panel-border);
      border-radius: var(--hf-radius) var(--hf-radius) 0 0;
    }

    .${scopeClass} .hf-panel-tabs {
      border-bottom: 1px solid var(--hf-panel-border);
    }

    .${scopeClass} .hf-panel-body {
      padding: 12px 13px;
    }

    .${scopeClass}.hf-panel--page .hf-panel-body {
      max-height: min(70vh, 640px);
      overflow-y: auto;
    }

    .${scopeClass}.hf-panel--page.hf-panel--grow .hf-panel-body {
      max-height: none;
      overflow: visible;
    }

    .${scopeClass}.hf-overlay,
    .${scopeClass} .hf-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0, 0, 0, 0.6);
      z-index: 99999;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .${scopeClass}.hf-modal,
    .${scopeClass} .hf-modal {
      background: #1f2127;
      border: 1px solid var(--hf-panel-border);
      border-radius: var(--hf-radius);
      width: min(440px, 92vw);
      max-height: 84vh;
      overflow-y: auto;
      padding: 18px;
      color: var(--hf-text);
    }

    .${scopeClass}.hf-modal h2,
    .${scopeClass} .hf-modal h2 {
      margin: 0 0 14px;
      font-size: 15px;
      color: var(--hf-accent-text);
      display: flex;
      justify-content: space-between;
      align-items: center;
    }

    .${scopeClass} .hf-mini {
      font-size: 10px;
      color: var(--hf-text-dim);
      margin-top: 3px;
      line-height: 1.4;
    }
  `;
    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = css;
    document.head.appendChild(style);
  }
  var theme = {
    tokens,
    injectStyles
  };

  // src/shared/storage.js
  var PREFIX = "smart.torn";
  var LEGACY_PREFIX = "hf.torn";
  function namespaced(key) {
    return `${PREFIX}.${key}`;
  }
  function legacyNamespaced(key) {
    return `${LEGACY_PREFIX}.${key}`;
  }
  function getLocal(key, fallback = null) {
    try {
      const raw = localStorage.getItem(namespaced(key));
      if (raw !== null) {
        return JSON.parse(raw);
      }
      const legacyRaw = localStorage.getItem(legacyNamespaced(key));
      if (legacyRaw !== null) {
        localStorage.setItem(namespaced(key), legacyRaw);
        return JSON.parse(legacyRaw);
      }
      return fallback;
    } catch {
      return fallback;
    }
  }
  function setLocal(key, value) {
    localStorage.setItem(namespaced(key), JSON.stringify(value));
  }
  function removeLocal(key) {
    localStorage.removeItem(namespaced(key));
    localStorage.removeItem(legacyNamespaced(key));
  }
  var DB_NAME = "smart-torn";
  var LEGACY_DB_NAME = "heartflower-torn";
  var DB_VERSION = 1;
  var dbPromise = null;
  function openDbByName(name) {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(name, DB_VERSION);
      request.onupgradeneeded = () => {
        const db = request.result;
        if (!db.objectStoreNames.contains("races")) {
          const store = db.createObjectStore("races", { keyPath: "id" });
          store.createIndex("timestamp", "timestamp", { unique: false });
          store.createIndex("track", "track", { unique: false });
          store.createIndex("carKey", "carKey", { unique: false });
        }
      };
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error ?? new Error("IndexedDB open failed"));
    });
  }
  async function migrateLegacyDb(target) {
    if (typeof indexedDB.databases !== "function") {
    }
    let legacy;
    try {
      legacy = await openDbByName(LEGACY_DB_NAME);
    } catch {
      return;
    }
    if (!legacy.objectStoreNames.contains("races") || !target.objectStoreNames.contains("races")) {
      legacy.close();
      return;
    }
    const rows = await new Promise((resolve, reject) => {
      const tx = legacy.transaction("races", "readonly");
      const request = tx.objectStore("races").getAll();
      request.onsuccess = () => resolve(
        /** @type {Record<string, unknown>[]} */
        request.result ?? []
      );
      request.onerror = () => reject(request.error ?? new Error("Legacy IDB read failed"));
    });
    if (rows.length > 0) {
      await new Promise((resolve, reject) => {
        const tx = target.transaction("races", "readwrite");
        const store = tx.objectStore("races");
        for (const row of rows) {
          store.put(row);
        }
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error ?? new Error("Legacy IDB migrate failed"));
      });
    }
    legacy.close();
  }
  function openDb() {
    if (dbPromise) {
      return dbPromise;
    }
    dbPromise = (async () => {
      const db = await openDbByName(DB_NAME);
      const migratedKey = "idb.migratedFromHeartflower";
      if (getLocal(migratedKey, false) !== true) {
        try {
          await migrateLegacyDb(db);
        } catch {
        }
        setLocal(migratedKey, true);
      }
      return db;
    })();
    return dbPromise;
  }
  async function withStore(storeName, mode, run) {
    const db = await openDb();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(storeName, mode);
      const store = tx.objectStore(storeName);
      const request = run(store);
      request.onsuccess = () => resolve(request.result);
      request.onerror = () => reject(request.error ?? new Error("IndexedDB request failed"));
    });
  }
  async function replaceAllRecords(storeName, records) {
    const db = await openDb();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(storeName, "readwrite");
      const store = tx.objectStore(storeName);
      store.clear();
      for (const record of records) {
        store.put(record);
      }
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error ?? new Error("IndexedDB replaceAll failed"));
    });
  }
  async function putRecord(storeName, record) {
    await withStore(storeName, "readwrite", (store) => store.put(record));
  }
  async function deleteRecord(storeName, key) {
    await withStore(storeName, "readwrite", (store) => store.delete(key));
  }
  async function getRecord(storeName, key) {
    return withStore(storeName, "readonly", (store) => store.get(key));
  }
  async function getAllRecords(storeName, query, limit) {
    const db = await openDb();
    return new Promise((resolve, reject) => {
      const tx = db.transaction(storeName, "readonly");
      const store = tx.objectStore(storeName);
      const request = store.getAll(query, limit);
      request.onsuccess = () => resolve(
        /** @type {Record<string, unknown>[]} */
        request.result ?? []
      );
      request.onerror = () => reject(request.error ?? new Error("IndexedDB getAll failed"));
    });
  }
  async function getByIndex(storeName, indexName, key) {
    return withStore(storeName, "readonly", (store) => store.index(indexName).getAll(key));
  }
  var storage = {
    PREFIX,
    LEGACY_PREFIX,
    getLocal,
    setLocal,
    removeLocal,
    putRecord,
    deleteRecord,
    getRecord,
    getAllRecords,
    getByIndex,
    replaceAllRecords
  };

  // src/shared/dom.js
  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }
  async function waitFor(selector, options = {}) {
    const { timeout = 1e4, interval = 100, root = document } = options;
    const start = Date.now();
    while (Date.now() - start < timeout) {
      const el = root.querySelector(selector);
      if (el) {
        return el;
      }
      await sleep(interval);
    }
    throw new Error(`waitFor timeout: ${selector}`);
  }
  async function waitForOptional(selector, options = {}) {
    try {
      return await waitFor(selector, options);
    } catch {
      return null;
    }
  }
  function isHeaderBarNode(node) {
    if (!(node instanceof HTMLElement)) {
      return node.parentElement ? isHeaderBarNode(node.parentElement) : false;
    }
    return Boolean(node.closest(
      'a[class*="bar-link"], [class*="bar-stats"], #header, header, [class*="topBar"], [class*="top-bar"], [class*="status-bar"]'
    ));
  }
  function isHeaderBarOnlyMutations(mutations) {
    return mutations.length > 0 && mutations.every((mutation) => isHeaderBarNode(mutation.target));
  }
  function observeMutations(root, callback, options = {}) {
    const { debounce = 50, ignoreHeaderBars = true } = options;
    let timer = null;
    const observer = new MutationObserver((mutations) => {
      if (ignoreHeaderBars && isHeaderBarOnlyMutations(mutations)) {
        return;
      }
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(callback, debounce);
    });
    observer.observe(root, { childList: true, subtree: true });
    callback();
    return () => {
      if (timer) {
        clearTimeout(timer);
      }
      observer.disconnect();
    };
  }
  function onAnchorClick(callback) {
    const handler = (event) => {
      const target = (
        /** @type {Element} */
        event.target
      );
      if (target.tagName === "A" || target.closest("a")) {
        setTimeout(callback, 150);
      }
    };
    document.body.addEventListener("click", handler);
    return () => document.body.removeEventListener("click", handler);
  }
  function normalizeText(text) {
    return text.replace(/\s+/g, " ").trim();
  }
  var dom = {
    sleep,
    waitFor,
    waitForOptional,
    observeMutations,
    onAnchorClick,
    normalizeText
  };

  // src/shared/mount.js
  var MOUNT_PRESETS = {
    racing: {
      waitFor: ["#racingMainContainer"],
      target: "#racingMainContainer",
      position: "before"
    },
    gym: {
      waitFor: ["#gymroot"],
      target: "#gymroot",
      position: "prepend"
    },
    gymBar: {
      waitFor: ["#gymroot"],
      target: "#gymroot",
      position: "before"
    },
    stocksBar: {
      waitFor: ["#stockmarketroot"],
      target: "#stockmarketroot",
      position: "before"
    },
    disposalBar: {
      waitFor: ['[class*="disposal-root"]'],
      target: '[class*="disposal-root"]',
      position: "before"
    },
    contentTitle: {
      waitFor: [".content-title", ".body > .content-title", "div.content-title"],
      target: ".content-title",
      position: "after"
    }
  };
  function resolveMountConfig(mount2) {
    if (!mount2) {
      return MOUNT_PRESETS.contentTitle;
    }
    if (typeof mount2 === "string") {
      if (mount2 === "page") {
        return MOUNT_PRESETS.contentTitle;
      }
      return MOUNT_PRESETS[mount2] ?? MOUNT_PRESETS.contentTitle;
    }
    return mount2;
  }
  function findMountTarget(config) {
    const pageReady = config.waitFor.some((selector) => document.querySelector(selector));
    if (!pageReady) {
      return null;
    }
    return document.querySelector(config.target);
  }
  function insertAt(target, element, position) {
    switch (position) {
      case "before":
        target.insertAdjacentElement("beforebegin", element);
        break;
      case "after":
        target.insertAdjacentElement("afterend", element);
        break;
      case "prepend":
        target.prepend(element);
        break;
      case "append":
        target.append(element);
        break;
    }
  }
  function isMountedAt(element, target, position) {
    if (!element.isConnected) {
      return false;
    }
    switch (position) {
      case "before":
        return element.nextElementSibling === target;
      case "after":
        return element.previousElementSibling === target;
      case "prepend":
        return target.firstElementChild === element;
      case "append":
        return target.lastElementChild === element;
      default:
        return false;
    }
  }
  function mountElement(config, element) {
    const target = findMountTarget(config);
    if (!target) {
      return false;
    }
    if (isMountedAt(element, target, config.position)) {
      return true;
    }
    insertAt(target, element, config.position);
    return true;
  }
  function getMountWatchRoot(config) {
    const target = findMountTarget(config);
    if (!target) {
      return null;
    }
    if (config.position === "before" || config.position === "after") {
      return target.parentElement;
    }
    return target;
  }
  function isOwnPanelMutation(mutations) {
    return mutations.every((mutation) => {
      if (mutation.target instanceof HTMLElement && mutation.target.closest(".hf-torn")) {
        return true;
      }
      for (const node of mutation.addedNodes) {
        if (node instanceof HTMLElement && (node.classList.contains("hf-torn") || node.closest(".hf-torn"))) {
          continue;
        }
        if (node.nodeType === Node.TEXT_NODE && node.parentElement?.closest(".hf-torn")) {
          continue;
        }
        return false;
      }
      for (const node of mutation.removedNodes) {
        if (node instanceof HTMLElement && (node.classList.contains("hf-torn") || node.closest(".hf-torn"))) {
          continue;
        }
        if (node.nodeType === Node.TEXT_NODE && node.parentElement?.closest(".hf-torn")) {
          continue;
        }
        return false;
      }
      return true;
    });
  }
  function watchMount(config, getElement) {
    let stopped = false;
    const start = Date.now();
    const timeoutMs = 15e3;
    let observer = null;
    let debounceTimer = null;
    let watchedRoot = null;
    const disconnectObserver = () => {
      observer?.disconnect();
      observer = null;
      watchedRoot = null;
    };
    const tryMount = () => {
      if (stopped) {
        return;
      }
      const element = getElement();
      mountElement(config, element);
    };
    const scheduleTryMount = () => {
      if (debounceTimer !== null) {
        window.clearTimeout(debounceTimer);
      }
      debounceTimer = window.setTimeout(() => {
        debounceTimer = null;
        tryMount();
      }, 80);
    };
    const attachObserver = () => {
      const watchRoot = getMountWatchRoot(config);
      if (!watchRoot || watchRoot === watchedRoot) {
        return Boolean(watchRoot);
      }
      disconnectObserver();
      watchedRoot = watchRoot;
      observer = new MutationObserver((mutations) => {
        if (isOwnPanelMutation(mutations)) {
          return;
        }
        scheduleTryMount();
      });
      observer.observe(watchRoot, {
        childList: true,
        subtree: config.position === "prepend" || config.position === "append"
      });
      return true;
    };
    const poll = window.setInterval(() => {
      tryMount();
      attachObserver();
      if (findMountTarget(config) || Date.now() - start > timeoutMs) {
        window.clearInterval(poll);
      }
    }, 250);
    tryMount();
    attachObserver();
    return () => {
      stopped = true;
      window.clearInterval(poll);
      if (debounceTimer !== null) {
        window.clearTimeout(debounceTimer);
      }
      disconnectObserver();
    };
  }
  var mount = {
    MOUNT_PRESETS,
    resolveMountConfig,
    findMountTarget,
    insertAt,
    isMountedAt,
    mountElement,
    watchMount
  };

  // src/shared/api.js
  var API_BASE = "https://api.torn.com/v2";
  var SETTINGS_KEY = "smart.torn.settings";
  var LEGACY_SETTINGS_KEY = "hf.torn.settings";
  function loadSettings() {
    try {
      let raw = GM_getValue(SETTINGS_KEY, null);
      if (raw == null || raw === "" || raw === "{}") {
        const legacy = GM_getValue(LEGACY_SETTINGS_KEY, null);
        if (legacy != null && legacy !== "" && legacy !== "{}") {
          GM_setValue(SETTINGS_KEY, typeof legacy === "string" ? legacy : JSON.stringify(legacy));
          raw = legacy;
        }
      }
      const parsed = typeof raw === "string" ? JSON.parse(raw || "{}") : raw ?? {};
      return {
        apiKey: String(parsed?.apiKey ?? "").trim()
      };
    } catch {
      return { apiKey: "" };
    }
  }
  function saveSettings(patch) {
    const current = loadSettings();
    const next = JSON.stringify({ ...current, ...patch });
    GM_setValue(SETTINGS_KEY, next);
  }
  function parseTornError(json) {
    if (!json || typeof json !== "object" || !("error" in json)) {
      return null;
    }
    const err = (
      /** @type {{ error?: unknown }} */
      json.error
    );
    if (typeof err === "string") {
      return new Error(err);
    }
    if (err && typeof err === "object") {
      const detail = (
        /** @type {{ error?: string, message?: string, code?: number }} */
        err
      );
      return new Error(detail.error || detail.message || `Torn API error (${detail.code ?? "unknown"})`);
    }
    return new Error("Torn API error");
  }
  function tornGet(path, params, apiKey) {
    const url = new URL(`${API_BASE}/${path.replace(/^\//, "")}`);
    url.searchParams.set("key", apiKey);
    for (const [key, value] of Object.entries(params ?? {})) {
      if (value !== void 0 && value !== "") {
        url.searchParams.set(key, String(value));
      }
    }
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: url.toString(),
        timeout: 2e4,
        onload: (response) => {
          try {
            const json = JSON.parse(response.responseText || "{}");
            const apiError = parseTornError(json);
            if (apiError) {
              reject(apiError);
              return;
            }
            resolve(json);
          } catch (error) {
            reject(error instanceof Error ? error : new Error("Invalid API response"));
          }
        },
        onerror: () => reject(new Error("Network error")),
        ontimeout: () => reject(new Error("API request timed out"))
      });
    });
  }
  var api = {
    loadSettings,
    saveSettings,
    parseTornError,
    tornGet,
    SETTINGS_KEY,
    LEGACY_SETTINGS_KEY
  };

  // src/shared/ui.js
  function panelCollapsedKey(panelId) {
    return `panel.${panelId}.collapsed`;
  }
  function readPanelCollapsed(panelId, legacyKeys = []) {
    const stored = getLocal(panelCollapsedKey(panelId), void 0);
    if (stored === true || stored === "true") {
      return true;
    }
    if (stored === false || stored === "false") {
      return false;
    }
    for (const key of legacyKeys) {
      const raw = localStorage.getItem(key);
      if (raw === "true") {
        setLocal(panelCollapsedKey(panelId), true);
        return true;
      }
      if (raw === "false") {
        setLocal(panelCollapsedKey(panelId), false);
        return false;
      }
    }
    return false;
  }
  var PANELS = /* @__PURE__ */ new Map();
  function createPanel(options) {
    const existing = PANELS.get(options.id);
    if (existing?.element.isConnected) {
      return existing;
    }
    if (existing) {
      PANELS.delete(options.id);
    }
    injectStyles();
    const mountOption = options.mount ?? "racing";
    const isFixed = mountOption === "fixed";
    const mountConfig = isFixed ? null : resolveMountConfig(mountOption);
    const pageBody = options.pageBody ?? "cap";
    const growPageBody = !isFixed && pageBody === "grow";
    const root = document.createElement("section");
    root.id = options.id;
    root.className = isFixed ? "hf-torn hf-panel hf-panel--fixed" : `hf-torn hf-panel hf-panel--page${growPageBody ? " hf-panel--grow" : ""}`;
    applyPanelLayout(root, isFixed, options.position ?? "bottom-right");
    const header = document.createElement("div");
    header.className = "hf-panel-header";
    header.style.cssText = `
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 9px 13px;
    cursor: pointer;
    user-select: none;
  `;
    const titleWrap = document.createElement("div");
    titleWrap.style.display = "flex";
    titleWrap.style.alignItems = "center";
    titleWrap.style.gap = "10px";
    titleWrap.style.flex = "1";
    titleWrap.style.minWidth = "0";
    if (options.badge) {
      const badge = document.createElement("span");
      badge.className = "hf-badge";
      badge.textContent = options.badge;
      titleWrap.appendChild(badge);
    } else if (options.title) {
      const title = document.createElement("div");
      title.textContent = options.title;
      title.style.fontWeight = "700";
      title.style.fontSize = "13px";
      title.style.color = "var(--hf-accent-text)";
      titleWrap.appendChild(title);
    }
    const right = document.createElement("div");
    right.className = "hf-panel-right";
    right.style.cssText = `
    display: flex;
    align-items: center;
    gap: 8px;
    margin-left: auto;
    flex-shrink: 0;
  `;
    const subtitle = document.createElement("div");
    subtitle.className = "hf-muted hf-panel-subtitle";
    subtitle.style.cssText = `
    font-size: 11px;
    max-width: min(280px, 40vw);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    pointer-events: none;
  `;
    const gear = document.createElement("button");
    gear.type = "button";
    gear.className = "hf-panel-gear";
    gear.title = "Settings";
    gear.innerHTML = "&#9881;";
    gear.style.cssText = `
    font-size: 15px;
    color: var(--hf-text-muted);
    background: none;
    border: none;
    cursor: pointer;
    padding: 0 4px;
    line-height: 1;
    flex-shrink: 0;
    position: relative;
    z-index: 2;
  `;
    const openSettings = (event) => {
      event.preventDefault();
      event.stopPropagation();
      options.onSettings?.();
    };
    gear.addEventListener("mousedown", openSettings);
    gear.addEventListener("click", openSettings);
    const toggle = document.createElement("span");
    toggle.className = "hf-panel-chev";
    toggle.textContent = "\u25BC";
    toggle.style.cssText = "font-size:12px;color:var(--hf-text-muted);transition:transform .2s;flex-shrink:0;";
    right.appendChild(subtitle);
    if (options.onSettings) {
      right.appendChild(gear);
    }
    right.appendChild(toggle);
    header.appendChild(titleWrap);
    header.appendChild(right);
    const tabBar = document.createElement("div");
    tabBar.className = "hf-panel-tabs";
    tabBar.style.cssText = `
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    padding: 6px 8px;
  `;
    const body = document.createElement("div");
    body.className = "hf-panel-body";
    if (growPageBody) {
      body.style.maxHeight = "none";
      body.style.overflow = "visible";
    }
    root.appendChild(header);
    if (options.tabs.length > 1) {
      root.appendChild(tabBar);
    }
    root.appendChild(body);
    let collapsed = readPanelCollapsed(options.id, options.collapsedLegacyKeys);
    let activeTabId = options.tabs[0]?.id ?? "";
    const tabButtons = /* @__PURE__ */ new Map();
    const setCollapsed = (value) => {
      collapsed = value;
      const hideBody = collapsed;
      if (options.tabs.length > 1) {
        tabBar.style.display = collapsed ? "none" : "flex";
      }
      body.style.display = hideBody ? "none" : "block";
      toggle.style.transform = collapsed ? "rotate(-90deg)" : "";
      root.classList.toggle("is-collapsed", collapsed);
      setLocal(panelCollapsedKey(options.id), value);
    };
    let renderGeneration = 0;
    const renderActiveTab = async () => {
      const generation = ++renderGeneration;
      const tab = options.tabs.find((item) => item.id === activeTabId);
      body.replaceChildren();
      if (!tab) {
        body.textContent = "No tab selected.";
        return;
      }
      const content = await tab.render();
      if (generation !== renderGeneration) {
        return;
      }
      body.replaceChildren();
      body.appendChild(content);
    };
    const setTab = (id) => {
      activeTabId = id;
      tabButtons.forEach((btn, tabId) => {
        btn.classList.toggle("is-active", tabId === id);
      });
      renderActiveTab();
    };
    if (options.tabs.length > 1) {
      for (const tab of options.tabs) {
        const btn = document.createElement("button");
        btn.type = "button";
        btn.className = "hf-btn";
        btn.textContent = tab.label;
        btn.addEventListener("click", (event) => {
          event.stopPropagation();
          setTab(tab.id);
        });
        tabButtons.set(tab.id, btn);
        tabBar.appendChild(btn);
      }
    }
    header.addEventListener("click", (event) => {
      if (
        /** @type {Element} */
        event.target.closest(".hf-panel-gear")
      ) {
        return;
      }
      if (
        /** @type {Element} */
        event.target.closest("button")
      ) {
        return;
      }
      setCollapsed(!collapsed);
    });
    gear.addEventListener("mouseenter", () => {
      gear.style.color = "#fff";
    });
    gear.addEventListener("mouseleave", () => {
      gear.style.color = "var(--hf-text-muted)";
    });
    setTab(activeTabId);
    setCollapsed(collapsed);
    let stopWatching = null;
    const ensureMounted = () => {
      if (isFixed) {
        if (root.parentElement !== document.body) {
          document.body.appendChild(root);
        }
        return;
      }
      if (mountConfig) {
        mountElement(mountConfig, root);
      }
    };
    if (isFixed) {
      ensureMounted();
    } else if (mountConfig) {
      stopWatching = watchMount(mountConfig, () => root);
    }
    const panel2 = {
      element: root,
      setTab,
      getActiveTabId: () => activeTabId,
      refresh: () => {
        if (options.getSubtitle) {
          subtitle.textContent = options.getSubtitle() ?? "";
        }
        renderActiveTab();
      },
      setSubtitle: (text) => {
        subtitle.textContent = text;
      },
      ensureMounted,
      destroy: () => {
        stopWatching?.();
        root.remove();
        PANELS.delete(options.id);
      }
    };
    if (options.getSubtitle) {
      subtitle.textContent = options.getSubtitle() ?? "";
    }
    PANELS.set(options.id, panel2);
    return panel2;
  }
  function applyPanelLayout(el, isFixed, position) {
    if (!isFixed) {
      el.style.cssText = `
      position: static;
      width: 100%;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    `;
      return;
    }
    el.style.cssText = `
    position: fixed;
    z-index: 9999;
    width: min(420px, calc(100vw - 24px));
    max-height: min(70vh, 560px);
    display: flex;
    flex-direction: column;
    overflow: hidden;
  `;
    applyFixedPosition(el, position);
  }
  function applyFixedPosition(el, position) {
    el.style.top = "";
    el.style.right = "";
    el.style.bottom = "";
    el.style.left = "";
    switch (position) {
      case "top-right":
        el.style.top = "12px";
        el.style.right = "12px";
        break;
      case "bottom-left":
        el.style.bottom = "12px";
        el.style.left = "12px";
        break;
      case "bottom-right":
      default:
        el.style.bottom = "12px";
        el.style.right = "12px";
        break;
    }
  }
  function emptyState(message) {
    const div = document.createElement("div");
    div.className = "hf-muted";
    div.textContent = message;
    return div;
  }
  var ui = {
    createPanel,
    emptyState
  };

  // src/shared/percent.js
  function parsePercent(raw, fallback = 0) {
    if (raw === null || raw === void 0 || raw === "") {
      return fallback;
    }
    if (typeof raw === "number") {
      return Number.isFinite(raw) ? raw : fallback;
    }
    const cleaned = String(raw).trim().replace("%", "").replace(",", ".");
    const value = parseFloat(cleaned);
    return Number.isFinite(value) ? value : fallback;
  }
  function weightToPercent(weight) {
    return Math.round(weight * 1e3) / 10;
  }
  function weightSharePercent(weight, weights) {
    const sum = Object.values(weights).reduce((total, w) => total + (w > 0 ? w : 0), 0);
    if (sum <= 0 || weight <= 0) {
      return 0;
    }
    return weight / sum * 100;
  }
  function percentToWeight(raw, fallback = 0) {
    const pct = parsePercent(raw, fallback * 100);
    return pct / 100;
  }
  function weightsToShares(weights) {
    const sum = Object.values(weights).reduce((total, w) => total + (w > 0 ? w : 0), 0);
    if (sum <= 0) {
      return {};
    }
    const out = {};
    for (const [stat, w] of Object.entries(weights)) {
      out[stat] = w > 0 ? w / sum * 100 : 0;
    }
    return out;
  }
  function normalizeWeightsToShares(weights) {
    const sum = Object.values(weights).reduce((total, w) => total + (w > 0 ? w : 0), 0);
    if (sum <= 0) {
      return weights;
    }
    const out = {};
    for (const [stat, w] of Object.entries(weights)) {
      out[stat] = w > 0 ? w / sum * 100 : 0;
    }
    return out;
  }
  function parseShareInputs(inputsByStat) {
    const raw = {};
    let sum = 0;
    for (const [stat, value] of Object.entries(inputsByStat)) {
      const pct = Math.max(0, parsePercent(value, 0));
      raw[stat] = pct;
      sum += pct;
    }
    if (sum <= 0) {
      return raw;
    }
    const out = {};
    for (const [stat, pct] of Object.entries(raw)) {
      out[stat] = pct > 0 ? pct / sum * 100 : 0;
    }
    return out;
  }
  function bonusesToMultiplier(bonuses) {
    const f = parsePercent(bonuses?.faction, 0);
    const e = parsePercent(bonuses?.education, 0);
    const p = parsePercent(bonuses?.property, 0);
    return (1 + f / 100) * (1 + e / 100) * (1 + p / 100);
  }
  function multiplierToTotalPercent(multiplier) {
    if (!Number.isFinite(multiplier) || multiplier <= 1) {
      return 0;
    }
    return Math.round((multiplier - 1) * 1e3) / 10;
  }
  function formatPercent(pct, digits = 1) {
    const rounded = Math.round(pct * Math.pow(10, digits)) / Math.pow(10, digits);
    const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(digits);
    return `${text}%`;
  }
  function formatWeightSummary(weights, shortLabels) {
    const parts = Object.entries(weights).filter(([, w]) => w > 0).sort((a, b) => b[1] - a[1]).map(([stat, w]) => `${shortLabels[stat] ?? stat} ${formatPercent(weightSharePercent(w, weights), 0)}`);
    return parts.join(" \xB7 ");
  }
  var percent = {
    parsePercent,
    weightToPercent,
    weightSharePercent,
    percentToWeight,
    weightsToShares,
    normalizeWeightsToShares,
    parseShareInputs,
    bonusesToMultiplier,
    multiplierToTotalPercent,
    formatPercent,
    formatWeightSummary
  };

  // src/shared/bars.js
  var FILL_SELECTORS = [
    '[class*="bar-fill"]',
    '[class*="barFill"]',
    '[class*="progress-fill"]',
    '[class*="progressFill"]',
    '[class*="progressBar"]',
    '[class*="progress-bar"]'
  ].join(", ");
  function applyBarFill(box, pct) {
    const clamped = Math.max(0, Math.min(100, pct));
    let updated = false;
    box.querySelectorAll(FILL_SELECTORS).forEach((fill) => {
      if (!(fill instanceof HTMLElement)) {
        return;
      }
      fill.style.width = `${clamped}%`;
      fill.style.transform = "";
      updated = true;
    });
    const progressbar = box.matches('[role="progressbar"]') ? box : box.querySelector('[role="progressbar"]');
    if (progressbar instanceof HTMLElement) {
      progressbar.setAttribute("aria-valuenow", String(Math.round(clamped)));
      const inner = progressbar.firstElementChild;
      if (inner instanceof HTMLElement) {
        inner.style.width = `${clamped}%`;
        inner.style.transform = `scaleX(${clamped / 100})`;
        inner.style.transformOrigin = "left center";
        updated = true;
      }
    }
    if (!updated) {
      for (const child of box.children) {
        if (!(child instanceof HTMLElement) || child.tagName === "P") {
          continue;
        }
        if (child.querySelector(FILL_SELECTORS)) {
          continue;
        }
        child.style.width = `${clamped}%`;
        child.style.transform = `scaleX(${clamped / 100})`;
        child.style.transformOrigin = "left center";
        updated = true;
      }
    }
  }
  function applyBarVisual(box, bar) {
    if (!box || !bar?.max) {
      return;
    }
    const val = box.querySelector('p[class*="bar-value"], [class*="bar-value"]');
    if (val) {
      val.textContent = `${bar.current}/${bar.max}`;
    }
    const pct = bar.max > 0 ? bar.current / bar.max * 100 : 0;
    applyBarFill(box, pct);
    box.querySelectorAll("[aria-valuemax]").forEach((el) => {
      el.setAttribute("aria-valuenow", String(bar.current));
      el.setAttribute("aria-valuemax", String(bar.max));
    });
  }
  function findBarBox(label) {
    let labels = document.querySelectorAll('p[class*="bar-name"]');
    if (!labels.length) {
      labels = document.querySelectorAll(".wai");
    }
    for (const el of labels) {
      if (el.textContent.trim().replace(":", "") === label) {
        return el.closest('[class*="bar-stats"]') || el.parentElement;
      }
    }
    const slug = label.toLowerCase();
    const link = document.querySelector(
      `a.bar-link[class*="${slug}"], a[class*="bar-link"][class*="${slug}"]`
    );
    return link;
  }
  function syncBarVisuals(bars2) {
    if (bars2.energy) {
      applyBarVisual(findBarBox("Energy"), bars2.energy);
    }
    if (bars2.happy) {
      applyBarVisual(findBarBox("Happy"), bars2.happy);
    }
    if (bars2.nerve) {
      applyBarVisual(findBarBox("Nerve"), bars2.nerve);
    }
    if (bars2.life) {
      applyBarVisual(findBarBox("Life"), bars2.life);
    }
  }
  var bars = {
    applyBarVisual,
    findBarBox,
    syncBarVisuals
  };

  // src/shared/index.js
  var HF = {
    version: "1.0.0",
    theme,
    storage,
    dom,
    mount,
    api,
    ui,
    percent,
    bars
  };

  // src/racing/race-log.js
  var LOG_FILTER_KEY = "racing.logFilter";
  var LOG_FILTER_LEGACY_KEY = "racing.logFilterCurrent";
  function buildCarKey(car) {
    return `${car.model}|${car.note ?? ""}`.toLowerCase();
  }
  function createRaceEntry(entry) {
    const car = entry.car ?? { model: "Unknown", note: "" };
    const id = entry.id ?? `race-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    return {
      id,
      timestamp: entry.timestamp ?? Date.now(),
      track: entry.track ?? "Unknown",
      type: entry.type ?? "unknown",
      car,
      carKey: buildCarKey(car),
      result: entry.result ?? {},
      source: entry.source ?? "api"
    };
  }
  function getLogFilter() {
    const stored = storage.getLocal(LOG_FILTER_KEY, null);
    if (stored === "track" || stored === "track-car") {
      return stored;
    }
    const legacy = storage.getLocal(LOG_FILTER_LEGACY_KEY, false);
    if (legacy === true || legacy === "true") {
      storage.setLocal(LOG_FILTER_KEY, "track-car");
      storage.removeLocal(LOG_FILTER_LEGACY_KEY);
      return "track-car";
    }
    return "all";
  }
  function setLogFilter(mode) {
    storage.setLocal(LOG_FILTER_KEY, mode);
  }
  function matchesTrack(race, track) {
    return race.track.toLowerCase() === track.toLowerCase();
  }
  function matchesCurrentContext(race, context) {
    if (!context.track) {
      return false;
    }
    if (!matchesTrack(race, context.track)) {
      return false;
    }
    const car = context.car;
    if (!car?.model || car.model === "Your car") {
      return true;
    }
    if (race.car.model.toLowerCase() !== car.model.toLowerCase()) {
      return false;
    }
    if (car.note) {
      return buildCarKey(race.car) === buildCarKey(car);
    }
    return true;
  }
  function formatTrackCarFilterLabel(context) {
    const car = context.car;
    if (!car?.model || car.model === "Your car") {
      return context.track ?? "This track & car";
    }
    const carLabel = car.note ? `${car.model} (${car.note})` : car.model;
    return `${context.track} \xB7 ${carLabel}`;
  }
  function applyLogFilter(races, filter, context) {
    if (filter === "all" || !context.track) {
      return races;
    }
    if (filter === "track") {
      return races.filter((race) => matchesTrack(race, context.track));
    }
    return races.filter((race) => matchesCurrentContext(race, context));
  }
  function emptyLogMessage(filter) {
    if (filter === "track") {
      return "No races logged for this track yet.";
    }
    if (filter === "track-car") {
      return "No races logged for this track and car yet.";
    }
    return "No races logged yet.";
  }
  function filterHintText(filter) {
    if (filter === "track") {
      return "All cars on this track";
    }
    if (filter === "track-car") {
      return "This track and car only";
    }
    return "";
  }
  function createCell(text, className) {
    const td = document.createElement("td");
    td.textContent = text;
    td.style.color = "var(--hf-text)";
    if (className) {
      td.className = className;
    }
    return td;
  }
  function createHeaderCell(text) {
    const th = document.createElement("th");
    th.textContent = text;
    th.style.color = "var(--hf-accent-text)";
    return th;
  }
  function renderRaceLogTable(races, options = {}) {
    const wrap = document.createElement("div");
    wrap.className = "hf-racing-log";
    const context = options.context ?? {};
    const filter = options.filter ?? "all";
    const hasContext = Boolean(context.track);
    if (hasContext) {
      const toolbar = document.createElement("div");
      toolbar.className = "hf-racing-log-toolbar";
      const trackBtn = document.createElement("button");
      trackBtn.type = "button";
      trackBtn.className = `hf-btn${filter === "track" ? " is-active" : ""}`;
      trackBtn.textContent = filter === "track" ? context.track : "This track";
      trackBtn.addEventListener("click", (event) => {
        event.stopPropagation();
        options.onFilterChange?.(filter === "track" ? "all" : "track");
      });
      toolbar.appendChild(trackBtn);
      const trackCarBtn = document.createElement("button");
      trackCarBtn.type = "button";
      trackCarBtn.className = `hf-btn${filter === "track-car" ? " is-active" : ""}`;
      trackCarBtn.textContent = filter === "track-car" ? formatTrackCarFilterLabel(context) : "This track & car";
      trackCarBtn.addEventListener("click", (event) => {
        event.stopPropagation();
        options.onFilterChange?.(filter === "track-car" ? "all" : "track-car");
      });
      toolbar.appendChild(trackCarBtn);
      const hintText = filterHintText(filter);
      if (hintText) {
        const hint = document.createElement("span");
        hint.className = "hf-racing-log-hint";
        hint.textContent = hintText;
        toolbar.appendChild(hint);
      }
      wrap.appendChild(toolbar);
    }
    const visible = applyLogFilter(races, filter, context);
    if (visible.length === 0) {
      const empty = document.createElement("div");
      empty.className = "hf-muted";
      empty.textContent = emptyLogMessage(filter);
      wrap.appendChild(empty);
      return wrap;
    }
    const table = document.createElement("table");
    table.className = "hf-table";
    const thead = document.createElement("thead");
    const headerRow = document.createElement("tr");
    for (const label of ["When", "Track", "Car", "Pos"]) {
      headerRow.appendChild(createHeaderCell(label));
    }
    thead.appendChild(headerRow);
    table.appendChild(thead);
    const tbody = document.createElement("tbody");
    for (const race of visible.slice(0, 50)) {
      const tr = document.createElement("tr");
      const when = new Date(race.timestamp).toLocaleString();
      const carLabel = race.car.note ? `${race.car.model} (${race.car.note})` : race.car.model;
      const pos = race.result.position != null ? String(race.result.position) : "\u2014";
      const posClass = race.result.position != null && race.result.position <= 3 ? `hf-pos--${race.result.position}` : "";
      tr.appendChild(createCell(when));
      tr.appendChild(createCell(race.track));
      tr.appendChild(createCell(carLabel));
      tr.appendChild(createCell(pos, posClass));
      tbody.appendChild(tr);
    }
    table.appendChild(tbody);
    wrap.appendChild(table);
    return wrap;
  }
  var pendingRace = null;
  function getPendingContext() {
    return {
      track: pendingRace?.track,
      car: pendingRace?.car
    };
  }
  function setPendingContext(context) {
    pendingRace = {
      ...pendingRace,
      track: context.track ?? pendingRace?.track,
      type: context.type ?? pendingRace?.type
    };
  }
  function setPendingCar(car) {
    pendingRace = {
      ...pendingRace,
      car
    };
  }
  function watchRaceFlows(onTrack) {
    document.body.addEventListener("click", (event) => {
      const target = (
        /** @type {Element} */
        event.target
      );
      const joinBtn = target.closest(".join-wrap a.link.btn-action-tab, .join-wrap a.btn-action-form");
      if (joinBtn) {
        const row = joinBtn.closest(".active-row");
        const trackEl = row?.querySelector(".track");
        if (trackEl) {
          const track = dom.normalizeText(trackEl.textContent.split("(")[0]);
          onTrack(track, "custom");
        }
      }
      const officialJoin = target.closest('a[href*="section=changeRacingCar"][href*="step=getInRace"]');
      if (officialJoin) {
        setTimeout(() => {
          const enlisted = document.querySelector(".enlisted-btn-wrap");
          if (enlisted) {
            const track = dom.normalizeText(enlisted.textContent.split(" - Official race")[0]);
            onTrack(track, "official");
          }
        }, 150);
      }
    });
    const createBtn = () => document.querySelector('[name="createCustomRace"]');
    const attachCreate = () => {
      const btn = createBtn();
      if (!btn || btn.dataset.hfBound) {
        return;
      }
      btn.dataset.hfBound = "1";
      btn.addEventListener("click", () => {
        setTimeout(() => {
          const selectMenu = document.querySelector(".ui-selectmenu");
          const mobileSelect = document.querySelector("#select-racing-track");
          let track = "";
          if (selectMenu) {
            track = dom.normalizeText(selectMenu.textContent);
          } else if (mobileSelect instanceof HTMLSelectElement) {
            track = dom.normalizeText(mobileSelect.options[mobileSelect.selectedIndex]?.text ?? "");
          }
          if (track) {
            onTrack(track, "custom");
          }
        }, 150);
      });
    };
    attachCreate();
    return dom.onAnchorClick(attachCreate);
  }
  var raceLog = {
    buildCarKey,
    createRaceEntry,
    getLogFilter,
    setLogFilter,
    getPendingContext,
    renderRaceLogTable,
    setPendingContext,
    setPendingCar,
    watchRaceFlows
  };

  // src/racing/templates.js
  var STORAGE_KEY = "racing.templates";
  var DEFAULT_GUIDE_TEMPLATES = [
    { name: "Mudpit: Colina Tanprice DS3", noteRecogniser: "DS3", source: "guide" },
    { name: "Two Islands: Edomondo NSX DL2", noteRecogniser: "DL2", source: "guide" },
    { name: "Parkland: Edomondo NSX DS3", noteRecogniser: "DS3", source: "guide" },
    { name: "Hammerhead: Edomondo NSX DS2", noteRecogniser: "DS2", source: "guide" },
    { name: "Stone Park: Echo R8 DS3", noteRecogniser: "DS3", source: "guide" },
    { name: "Withdrawal: Veloria LFA TL3", noteRecogniser: "TL3", source: "guide" },
    { name: "Speedway: Veloria LFA TL3", noteRecogniser: "TL3", source: "guide" },
    { name: "Uptown: Veloria LFA TL3", noteRecogniser: "TL3", source: "guide" },
    { name: "Underdog: Edomondo NSX TS2", noteRecogniser: "TS2", source: "guide" },
    { name: "Commerce: Edomondo NSX TS2", noteRecogniser: "TS2", source: "guide" },
    { name: "Sewage: Edomondo NSX TS2", noteRecogniser: "TS2", source: "guide" },
    { name: "Industrial: Edomondo NSX TS3", noteRecogniser: "TS3", source: "guide" },
    { name: "Vector: Edomondo NSX TS3", noteRecogniser: "TS3", source: "guide" },
    { name: "Meltdown: Edomondo NSX TS3", noteRecogniser: "TS3", source: "guide" },
    { name: "Docks: Volt GT TS3", noteRecogniser: "TS3", source: "guide" },
    { name: "Convict: Mercia SLR TL3", noteRecogniser: "TL3", source: "guide" }
  ];
  function migrateLegacyTemplates() {
    try {
      const legacy = localStorage.getItem("carTemplates");
      if (!legacy) {
        return null;
      }
      const parsed = JSON.parse(legacy);
      if (!Array.isArray(parsed)) {
        return null;
      }
      return parsed.map((item) => ({
        name: String(item.name),
        noteRecogniser: item.noteRecogniser ? String(item.noteRecogniser) : "",
        source: (
          /** @type {'guide'} */
          "guide"
        )
      }));
    } catch {
      return null;
    }
  }
  function loadTemplates() {
    const stored = storage.getLocal(STORAGE_KEY, null);
    if (Array.isArray(stored) && stored.length > 0) {
      return stored;
    }
    const migrated = migrateLegacyTemplates();
    const initial = migrated ?? DEFAULT_GUIDE_TEMPLATES;
    storage.setLocal(STORAGE_KEY, initial);
    return initial;
  }
  function saveTemplates(templates2) {
    storage.setLocal(STORAGE_KEY, templates2);
  }
  function getTemplatesForTrack(track) {
    return loadTemplates().filter((template) => {
      const trackName = template.name.split(":")[0]?.trim();
      return trackName?.toLowerCase() === track.toLowerCase();
    });
  }
  function getOwnedCarsFromDom() {
    const owned = [];
    document.querySelectorAll("li").forEach((li) => {
      const modelEl = li.querySelector('[class^="model-car-name-"]');
      if (!modelEl) {
        return;
      }
      const model = modelEl.textContent.trim();
      const fullText = li.textContent.replace(/\s+/g, " ").trim();
      const note = fullText.includes(model) ? fullText.replace(model, "").trim() : "";
      owned.push({ model, note, element: li });
    });
    return owned;
  }
  function resolveTemplatesForTrack(track) {
    const templates2 = getTemplatesForTrack(track);
    const ownedCars = getOwnedCarsFromDom();
    const owned = [];
    const reference = [];
    for (const template of templates2) {
      const carPart = template.name.split(":").slice(1).join(":").trim();
      const modelGuess = carPart.split(" ").slice(0, -1).join(" ").trim();
      const note = template.noteRecogniser ?? "";
      const match = ownedCars.find((car) => {
        const modelMatch = car.model.includes(modelGuess) || modelGuess.includes(car.model);
        const noteMatch = !note || car.note.includes(note) || car.element.textContent.includes(note);
        return modelMatch && noteMatch;
      });
      if (match) {
        owned.push({ ...template, match });
      } else {
        reference.push(template);
      }
    }
    return { owned, reference };
  }
  function formatGuideLabel(template) {
    const carPart = template.name.split(":").slice(1).join(":").trim();
    return carPart || template.name;
  }
  function renderTemplateSummary(track) {
    const wrap = document.createElement("div");
    if (!track) {
      wrap.className = "hf-muted";
      wrap.textContent = "Join or create a race to see track-specific suggestions.";
      return wrap;
    }
    const { owned, reference } = resolveTemplatesForTrack(track);
    if (owned.length === 0 && reference.length === 0) {
      const empty = document.createElement("div");
      empty.className = "hf-muted";
      empty.textContent = "No templates saved for this track.";
      wrap.appendChild(empty);
      return wrap;
    }
    const appendRow = (item, kind) => {
      const row = document.createElement("div");
      row.className = "hf-guide-row";
      const badge = document.createElement("span");
      badge.className = "hf-badge";
      badge.textContent = kind === "owned" ? "yours" : "guide";
      row.appendChild(badge);
      row.appendChild(document.createTextNode(` ${formatGuideLabel(item)}`));
      wrap.appendChild(row);
    };
    for (const item of owned) {
      appendRow(item, "owned");
    }
    for (const item of reference) {
      appendRow(item, "guide");
    }
    return wrap;
  }
  var templates = {
    loadTemplates,
    saveTemplates,
    getTemplatesForTrack,
    formatGuideLabel,
    getOwnedCarsFromDom,
    resolveTemplatesForTrack,
    renderTemplateSummary
  };

  // src/racing/suggest.js
  function bucketPlacement(position) {
    if (position === 1) {
      return "first";
    }
    if (position === 2) {
      return "second";
    }
    if (position === 3) {
      return "third";
    }
    return "out";
  }
  var SVG_NS = "http://www.w3.org/2000/svg";
  var ICON_SIZE = 14;
  var TONE_COLORS = {
    gold: "#e8c547",
    silver: "#c4c9d4",
    bronze: "#c98a5a",
    out: "#8a8d96"
  };
  var PLACEMENT_CHIPS = [
    { key: "first", label: "1st", tone: "gold" },
    { key: "second", label: "2nd", tone: "silver" },
    { key: "third", label: "3rd", tone: "bronze" },
    { key: "out", label: "out", tone: "out" }
  ];
  function createPlacementIcon(tone) {
    const color = TONE_COLORS[tone] ?? TONE_COLORS.out;
    const svg = document.createElementNS(SVG_NS, "svg");
    svg.setAttribute("viewBox", "0 0 16 16");
    svg.setAttribute("width", String(ICON_SIZE));
    svg.setAttribute("height", String(ICON_SIZE));
    svg.setAttribute("focusable", "false");
    svg.setAttribute("aria-hidden", "true");
    svg.classList.add("hf-placement-icon");
    if (tone === "out") {
      const ring = document.createElementNS(SVG_NS, "circle");
      ring.setAttribute("cx", "8");
      ring.setAttribute("cy", "8");
      ring.setAttribute("r", "5.5");
      ring.setAttribute("fill", "none");
      ring.setAttribute("stroke", color);
      ring.setAttribute("stroke-width", "1.4");
      svg.appendChild(ring);
      const slash = document.createElementNS(SVG_NS, "path");
      slash.setAttribute("d", "M5.2 5.2l5.6 5.6M10.8 5.2l-5.6 5.6");
      slash.setAttribute("fill", "none");
      slash.setAttribute("stroke", color);
      slash.setAttribute("stroke-width", "1.4");
      slash.setAttribute("stroke-linecap", "round");
      svg.appendChild(slash);
      return svg;
    }
    const cup = document.createElementNS(SVG_NS, "path");
    cup.setAttribute(
      "d",
      "M4 2.5h8v1.8c0 1.35-.85 2.55-2.1 3.15L11.2 9.8H12v1.2H4V9.8h.8l1.3-2.35C4.85 6.85 4 5.65 4 4.3V2.5z"
    );
    cup.setAttribute("fill", color);
    svg.appendChild(cup);
    const base = document.createElementNS(SVG_NS, "path");
    base.setAttribute("d", "M6.2 12.2h3.6v1.3H6.2v-1.3z");
    base.setAttribute("fill", color);
    svg.appendChild(base);
    return svg;
  }
  function renderPlacementRecord(placements) {
    const row = document.createElement("div");
    row.className = "hf-placement-row";
    for (const chip of PLACEMENT_CHIPS) {
      const count = placements[chip.key];
      const el = document.createElement("span");
      el.className = `hf-placement-chip hf-placement-chip--${chip.tone}${count === 0 ? " is-zero" : ""}`;
      el.title = `${count}\xD7 ${chip.label}`;
      el.appendChild(createPlacementIcon(chip.tone));
      const countEl = document.createElement("span");
      countEl.className = "hf-placement-count";
      countEl.textContent = String(count);
      el.appendChild(countEl);
      row.appendChild(el);
    }
    return row;
  }
  function aggregateTrackStats(races) {
    const map = /* @__PURE__ */ new Map();
    for (const race of races) {
      const key = buildCarKey(race.car);
      const current = map.get(key) ?? {
        model: race.car.model,
        note: race.car.note ?? "",
        races: 0,
        placements: { first: 0, second: 0, third: 0, out: 0 },
        positionSum: 0,
        countedPositions: 0,
        lastUsed: 0
      };
      current.races += 1;
      current.lastUsed = Math.max(current.lastUsed, race.timestamp);
      current.placements[bucketPlacement(race.result.position)] += 1;
      if (typeof race.result.position === "number") {
        current.positionSum += race.result.position;
        current.countedPositions += 1;
      }
      map.set(key, current);
    }
    return [...map.values()].map((item) => ({
      carKey: buildCarKey({ model: item.model, note: item.note }),
      model: item.model,
      note: item.note,
      races: item.races,
      placements: item.placements,
      avgPosition: item.countedPositions ? item.positionSum / item.countedPositions : 0,
      lastUsed: item.lastUsed
    })).sort((a, b) => {
      if (b.placements.first !== a.placements.first) {
        return b.placements.first - a.placements.first;
      }
      if (b.placements.second !== a.placements.second) {
        return b.placements.second - a.placements.second;
      }
      if (b.placements.third !== a.placements.third) {
        return b.placements.third - a.placements.third;
      }
      if (a.placements.out !== b.placements.out) {
        return a.placements.out - b.placements.out;
      }
      if (a.avgPosition && b.avgPosition) {
        return a.avgPosition - b.avgPosition;
      }
      return b.lastUsed - a.lastUsed;
    });
  }
  function renderTrackSuggestions(races, limit = 3) {
    const wrap = document.createElement("div");
    const stats = aggregateTrackStats(races).slice(0, limit);
    if (stats.length === 0) {
      wrap.className = "hf-muted";
      wrap.textContent = "No personal history for this track yet.";
      return wrap;
    }
    const list = document.createElement("div");
    for (const item of stats) {
      const row = document.createElement("div");
      row.className = "hf-racing-car-row";
      const label = item.note ? `${item.model} (${item.note})` : item.model;
      const avg = item.avgPosition ? item.avgPosition.toFixed(1) : "\u2014";
      const last = new Date(item.lastUsed).toLocaleDateString();
      const name = document.createElement("div");
      const strong = document.createElement("strong");
      strong.textContent = label;
      name.appendChild(strong);
      row.appendChild(name);
      row.appendChild(renderPlacementRecord(item.placements));
      const meta = document.createElement("div");
      meta.className = "hf-dim";
      meta.textContent = `${item.races} races \xB7 avg ${avg} \xB7 last ${last}`;
      row.appendChild(meta);
      list.appendChild(row);
    }
    wrap.appendChild(list);
    return wrap;
  }
  var suggest = {
    aggregateTrackStats,
    renderPlacementRecord,
    renderTrackSuggestions
  };

  // src/racing/tracks.js
  var TRACK_ID_FALLBACK = {
    6: "Uptown",
    7: "Withdrawal",
    8: "Underdog",
    9: "Parkland",
    10: "Docks",
    11: "Commerce",
    12: "Two Islands",
    15: "Industrial",
    16: "Vector",
    17: "Mudpit",
    18: "Hammerhead",
    19: "Sewage",
    20: "Meltdown",
    21: "Speedway",
    22: "Stone Park",
    23: "Convict"
  };
  var TRACK_NAMES = [...new Set(Object.values(TRACK_ID_FALLBACK))].sort(
    (a, b) => b.length - a.length
  );
  function parseTrackFromTitle(text) {
    const normalized = text.replace(/\s+/g, " ").trim();
    if (!normalized) {
      return null;
    }
    for (const track of TRACK_NAMES) {
      if (normalized.startsWith(track) || normalized.includes(`${track} -`)) {
        return track;
      }
    }
    const segment = normalized.split(" - ")[0]?.trim();
    return segment || null;
  }

  // src/racing/page-context.js
  function getRaceIdFromLeaderboard() {
    const firstDriver = document.querySelector("#leaderBoard > li[data-id]");
    if (!firstDriver?.dataset.id) {
      return null;
    }
    const raceId = firstDriver.dataset.id.split("-")[0];
    return /^\d+$/.test(raceId) ? Number(raceId) : null;
  }
  function detectCurrentCarFromDom() {
    const modelEl = document.querySelector(
      '#racingupdates .car-selected [class^="model-car-name-"], .car-selected-wrap [class^="model-car-name-"], [class^="model-car-name-"]'
    );
    if (modelEl) {
      const model = modelEl.textContent.trim();
      const wrap = modelEl.closest(".car-selected, .car-selected-wrap, li, .msg");
      const fullText = wrap?.textContent.replace(/\s+/g, " ").trim() ?? "";
      const note = fullText.replace(model, "").trim();
      return { model, note };
    }
    const carName = document.querySelector(".msg.right-round b");
    if (carName) {
      return { model: carName.textContent.trim(), note: "" };
    }
    return { model: "Your car", note: "" };
  }
  function detectLiveStatsFromDom() {
    const scopes = [
      document.querySelector(".racing-stats"),
      document.querySelector(".race-stats"),
      document.querySelector(".drivers-list .statistics"),
      document.querySelector("#racingupdates")
    ].filter(Boolean);
    for (const scope of scopes) {
      const text = scope.textContent ?? "";
      const positionMatch = text.match(/Position[:\s]*(\d+)\s*\/\s*(\d+)/i);
      if (positionMatch) {
        return { position: Number(positionMatch[1]) };
      }
    }
    return {};
  }
  function detectActiveRaceFromDom() {
    const raceRoot = document.querySelector("#racingupdates");
    if (!raceRoot) {
      return null;
    }
    const trackInfo = document.querySelector("div.track-info");
    const trackHeader = document.querySelector(
      '.drivers-list .title-black, .drivers-list div[class^="title"]'
    );
    let trackName = trackInfo?.getAttribute("title")?.trim() ?? "";
    const headerText = trackHeader?.textContent?.trim() ?? "";
    if (!trackName && headerText) {
      trackName = parseTrackFromTitle(headerText) ?? "";
    }
    if (!trackName) {
      return null;
    }
    const track = parseTrackFromTitle(headerText) ?? parseTrackFromTitle(trackName) ?? trackName;
    const raceId = getRaceIdFromLeaderboard() ?? 0;
    return {
      raceId,
      track,
      title: headerText || trackName,
      type: "unknown",
      car: detectCurrentCarFromDom(),
      live: detectLiveStatsFromDom(),
      source: "dom"
    };
  }
  function detectJoinFlowTrack() {
    const trackEl = document.querySelector(".active-row .track");
    if (trackEl) {
      const raw = trackEl.textContent.split("(")[0]?.trim() ?? "";
      const parsed = parseTrackFromTitle(raw) ?? raw;
      return parsed || null;
    }
    const enlisted = document.querySelector(".enlisted-btn-wrap");
    if (enlisted) {
      const raw = enlisted.textContent.split(" - Official race")[0]?.trim() ?? "";
      const parsed = parseTrackFromTitle(raw) ?? raw;
      return parsed || null;
    }
    return null;
  }
  function getRaceContextFingerprint() {
    const live = detectActiveRaceFromDom();
    if (live) {
      return `live:${live.raceId}|${live.track}`;
    }
    const joinTrack = detectJoinFlowTrack();
    if (joinTrack) {
      return `join:${joinTrack}`;
    }
    return "";
  }
  function mutationsOnlyInsidePanel(mutations, panelId) {
    const panelRoot = document.getElementById(panelId);
    if (!panelRoot) {
      return false;
    }
    return mutations.every((mutation) => {
      const target = mutation.target;
      return target instanceof Node && panelRoot.contains(target);
    });
  }
  function watchRaceContext(onChange, options = {}) {
    const panelId = options.panelId ?? "smart-racing-panel";
    let lastFingerprint = getRaceContextFingerprint();
    let timer = null;
    const check = () => {
      const next = getRaceContextFingerprint();
      if (next === lastFingerprint) {
        return;
      }
      lastFingerprint = next;
      onChange(next);
    };
    const scheduleCheck = () => {
      if (timer) {
        clearTimeout(timer);
      }
      timer = setTimeout(check, 150);
    };
    const observer = new MutationObserver((mutations) => {
      if (mutationsOnlyInsidePanel(mutations, panelId)) {
        return;
      }
      scheduleCheck();
    });
    observer.observe(document.body, { childList: true, subtree: true });
    scheduleCheck();
    const onNavClick = () => {
      scheduleCheck();
    };
    document.body.addEventListener("click", onNavClick);
    return () => {
      if (timer) {
        clearTimeout(timer);
      }
      observer.disconnect();
      document.body.removeEventListener("click", onNavClick);
    };
  }

  // src/racing/api.js
  var RACE_CACHE_MS = 60 * 1e3;
  var REFERENCE_CACHE_MS = 12 * 60 * 60 * 1e3;
  function toTimestampMs(value) {
    const n = Number(value);
    if (!Number.isFinite(n) || n <= 0) {
      return Date.now();
    }
    return n > 1e12 ? n : n * 1e3;
  }
  var referenceCache = null;
  var lastEnrichedRaceId = null;
  var refreshInFlight = null;
  var raceSnapshot = {
    status: "idle",
    fetchedAt: 0,
    races: [],
    active: null,
    error: ""
  };
  function isActiveRaceStatus(status) {
    const normalized = String(status ?? "").toLowerCase().replace(/\s+/g, "_");
    return normalized === "in_progress" || normalized === "open" || normalized === "running" || normalized === "started";
  }
  function userIsInRace(race, userId) {
    if (!userId) {
      return false;
    }
    const results = (
      /** @type {Array<Record<string, unknown>>} */
      race.results ?? []
    );
    if (results.some((row) => Number(row.driver_id) === userId)) {
      return true;
    }
    const participants = race.participants;
    if (participants && typeof participants === "object") {
      const values = Object.values(participants);
      if (values.some((value) => Number(value) === userId)) {
        return true;
      }
    }
    return false;
  }
  function getTornUserId() {
    const el = document.getElementById("torn-user");
    if (!el?.value) {
      return null;
    }
    try {
      const parsed = JSON.parse(el.value);
      return Number(parsed?.id) || null;
    } catch {
      return null;
    }
  }
  function referenceCacheFresh(force = false) {
    if (force || !referenceCache) {
      return false;
    }
    return Date.now() - referenceCache.fetchedAt < REFERENCE_CACHE_MS;
  }
  async function fetchTrackMap(apiKey, force = false) {
    if (referenceCacheFresh(force)) {
      return referenceCache.tracks;
    }
    const map = new Map(Object.entries(TRACK_ID_FALLBACK).map(([id, name]) => [Number(id), name]));
    try {
      const data = (
        /** @type {{ tracks?: Array<{ id?: number, title?: string, name?: string }> }} */
        await tornGet("racing/tracks", {}, apiKey)
      );
      for (const track of data.tracks ?? []) {
        const id = Number(track.id);
        const name = track.title ?? track.name;
        if (id && name) {
          map.set(id, name);
        }
      }
    } catch {
    }
    return map;
  }
  async function fetchCarMap(apiKey, force = false) {
    if (referenceCacheFresh(force)) {
      return referenceCache.cars;
    }
    const map = /* @__PURE__ */ new Map();
    try {
      const data = (
        /** @type {{ cars?: Array<Record<string, unknown>> }} */
        await tornGet("racing/cars", {}, apiKey)
      );
      for (const car of data.cars ?? []) {
        const id = Number(car.id);
        if (!id) {
          continue;
        }
        map.set(id, {
          model: String(car.car_item_name ?? car.name ?? "Unknown car"),
          note: String(car.name ?? "").trim()
        });
      }
    } catch {
    }
    return map;
  }
  function resolveTrackName(trackId, title, trackMap) {
    if (trackId && trackMap.has(trackId)) {
      return trackMap.get(trackId);
    }
    if (title) {
      return parseTrackFromTitle(title) ?? title.split(" - ")[0]?.trim() ?? "Unknown";
    }
    return "Unknown";
  }
  function mapRaceToEntry(race, userId, trackMap, carMap) {
    const results = (
      /** @type {Array<Record<string, unknown>>} */
      race.results ?? []
    );
    const userResult = userId ? results.find((row) => Number(row.driver_id) === userId) : results[0];
    if (!userResult && race.status === "finished") {
      return null;
    }
    const carId = Number(userResult?.car_id);
    const carFromGarage = carId ? carMap.get(carId) : null;
    const track = resolveTrackName(Number(race.track_id), String(race.title ?? ""), trackMap);
    const schedule = (
      /** @type {{ start?: number, end?: number } | undefined} */
      race.schedule
    );
    const tsMs = toTimestampMs(
      userResult?.time_ended ?? schedule?.end ?? schedule?.start ?? Date.now() / 1e3
    );
    return createRaceEntry({
      id: `api-race-${race.id}`,
      timestamp: tsMs,
      track,
      type: race.is_official ? "official" : "custom",
      car: {
        model: String(userResult?.car_item_name ?? carFromGarage?.model ?? "Unknown"),
        note: carFromGarage?.note ?? ""
      },
      result: {
        position: userResult?.position != null ? Number(userResult.position) : void 0,
        time: userResult?.race_time != null ? Number(userResult.race_time) : void 0
      },
      source: "api"
    });
  }
  function mapActiveRace(race, userId, trackMap, carMap) {
    if (!isActiveRaceStatus(race.status)) {
      return null;
    }
    if (!userIsInRace(race, userId)) {
      return null;
    }
    const track = resolveTrackName(Number(race.track_id), String(race.title ?? ""), trackMap);
    const results = (
      /** @type {Array<Record<string, unknown>>} */
      race.results ?? []
    );
    const userResult = userId ? results.find((row) => Number(row.driver_id) === userId) : null;
    const carId = Number(userResult?.car_id);
    const carFromGarage = carId ? carMap.get(carId) : null;
    return {
      raceId: Number(race.id),
      track,
      title: String(race.title ?? track),
      type: race.is_official ? "official" : "custom",
      car: {
        model: String(userResult?.car_item_name ?? carFromGarage?.model ?? "Your car"),
        note: carFromGarage?.note ?? "",
        carId: carId || void 0
      },
      live: {
        position: userResult?.position != null ? Number(userResult.position) : void 0
      },
      source: "api"
    };
  }
  async function fetchActiveRaceFromRacingList(apiKey, userId, trackMap, carMap) {
    try {
      const data = (
        /** @type {{ races?: Array<Record<string, unknown>> }} */
        await tornGet("racing/races", { limit: 100, sort: "desc" }, apiKey)
      );
      for (const race of data.races ?? []) {
        if (!isActiveRaceStatus(race.status)) {
          continue;
        }
        const mapped = mapActiveRace(race, userId, trackMap, carMap);
        if (mapped) {
          return mapped;
        }
      }
    } catch {
    }
    return null;
  }
  async function enrichActiveRaceFromApi(apiKey, raceId, userId, trackMap, carMap, domFallback) {
    if (!raceId) {
      return domFallback;
    }
    try {
      const data = (
        /** @type {{ race?: Record<string, unknown> } & Record<string, unknown>} */
        await tornGet(`racing/${raceId}/race`, {}, apiKey)
      );
      const race = (
        /** @type {Record<string, unknown>} */
        data.race ?? data
      );
      const mapped = mapActiveRace(race, userId, trackMap, carMap) ?? mapActiveRace({ ...race, status: "in_progress" }, userId, trackMap, carMap);
      if (mapped) {
        return {
          ...mapped,
          car: domFallback.car.note || !mapped.car.model.includes("Your") ? domFallback.car : mapped.car,
          live: {
            ...mapped.live,
            position: domFallback.live.position ?? mapped.live.position
          }
        };
      }
    } catch {
    }
    return domFallback;
  }
  function findActiveInUserRaces(rawRaces, userId, trackMap, carMap) {
    for (const race of rawRaces) {
      if (!isActiveRaceStatus(race.status)) {
        continue;
      }
      const mapped = mapActiveRace(race, userId, trackMap, carMap);
      if (mapped) {
        return mapped;
      }
    }
    return null;
  }
  async function resolveActiveRace(apiKey, userId, trackMap, carMap, rawRaces) {
    let active = findActiveInUserRaces(rawRaces, userId, trackMap, carMap);
    if (!active) {
      active = await fetchActiveRaceFromRacingList(apiKey, userId, trackMap, carMap);
    }
    const domActive = detectActiveRaceFromDom();
    if (domActive) {
      if (apiKey && domActive.raceId && domActive.raceId !== lastEnrichedRaceId) {
        lastEnrichedRaceId = domActive.raceId;
        return enrichActiveRaceFromApi(apiKey, domActive.raceId, userId, trackMap, carMap, domActive);
      }
      return domActive;
    }
    return active;
  }
  function mergeDomLive(current, domActive) {
    return {
      ...current,
      track: domActive.track || current.track,
      title: domActive.title || current.title,
      car: domActive.car.note || domActive.car.model && domActive.car.model !== "Your car" ? domActive.car : current.car,
      live: {
        ...current.live,
        position: domActive.live.position ?? current.live.position
      },
      source: current.source ?? domActive.source
    };
  }
  async function refreshActiveRaceContext(apiKey, options = {}) {
    const forceEnrich = Boolean(options.forceEnrich);
    const domActive = detectActiveRaceFromDom();
    if (domActive) {
      const sameRace = raceSnapshot.active?.raceId === domActive.raceId && domActive.raceId;
      if (apiKey && domActive.raceId && (forceEnrich || domActive.raceId !== lastEnrichedRaceId)) {
        const userId = getTornUserId();
        const trackMap = await fetchTrackMap(apiKey);
        const carMap = await fetchCarMap(apiKey);
        if (!referenceCacheFresh(false)) {
          referenceCache = { tracks: trackMap, cars: carMap, fetchedAt: Date.now() };
        }
        lastEnrichedRaceId = domActive.raceId;
        raceSnapshot.active = await enrichActiveRaceFromApi(
          apiKey,
          domActive.raceId,
          userId,
          trackMap,
          carMap,
          domActive
        );
        return raceSnapshot.active;
      }
      if (sameRace && raceSnapshot.active) {
        raceSnapshot.active = mergeDomLive(raceSnapshot.active, domActive);
        return raceSnapshot.active;
      }
      raceSnapshot.active = domActive;
      return domActive;
    }
    lastEnrichedRaceId = null;
    if (apiKey && referenceCache) {
      const userId = getTornUserId();
      raceSnapshot.active = await fetchActiveRaceFromRacingList(
        apiKey,
        userId,
        referenceCache.tracks,
        referenceCache.cars
      );
      return raceSnapshot.active;
    }
    raceSnapshot.active = null;
    return null;
  }
  async function refreshRaceDataInner(apiKey, force = false) {
    if (!apiKey) {
      raceSnapshot = {
        status: "missing-key",
        fetchedAt: Date.now(),
        races: [],
        active: null,
        error: ""
      };
      await refreshActiveRaceContext("");
      return raceSnapshot;
    }
    if (!force && raceSnapshot.fetchedAt && Date.now() - raceSnapshot.fetchedAt < RACE_CACHE_MS) {
      await refreshActiveRaceContext(apiKey);
      return raceSnapshot;
    }
    try {
      const userId = getTornUserId();
      const [trackMap, carMap, raceData] = await Promise.all([
        fetchTrackMap(apiKey, force),
        fetchCarMap(apiKey, force),
        tornGet("user/races", { limit: 100, sort: "desc" }, apiKey)
      ]);
      if (!referenceCacheFresh(force)) {
        referenceCache = { tracks: trackMap, cars: carMap, fetchedAt: Date.now() };
      }
      const rawRaces = (
        /** @type {Array<Record<string, unknown>>} */
        /** @type {{ races?: unknown[] }} */
        raceData.races ?? []
      );
      const races = rawRaces.filter((race) => race.status === "finished").map((race) => mapRaceToEntry(race, userId, trackMap, carMap)).filter((entry) => entry != null);
      lastEnrichedRaceId = null;
      const active = await resolveActiveRace(apiKey, userId, trackMap, carMap, rawRaces);
      raceSnapshot = {
        status: "ok",
        fetchedAt: Date.now(),
        races,
        active,
        error: ""
      };
    } catch (error) {
      raceSnapshot = {
        status: "error",
        fetchedAt: Date.now(),
        races: raceSnapshot.races,
        active: raceSnapshot.active,
        error: error instanceof Error ? error.message : "API error"
      };
    }
    return raceSnapshot;
  }
  async function refreshRaceData(apiKey, force = false) {
    if (refreshInFlight) {
      return refreshInFlight;
    }
    refreshInFlight = refreshRaceDataInner(apiKey, force).finally(() => {
      refreshInFlight = null;
    });
    return refreshInFlight;
  }
  function getRaceSnapshot() {
    return raceSnapshot;
  }
  function getRacesForTrackFromApi(track) {
    const needle = track.toLowerCase();
    return raceSnapshot.races.filter((race) => race.track.toLowerCase() === needle);
  }

  // src/racing/settings-ui.js
  function renderSettingsPanel(options = {}) {
    const wrap = document.createElement("div");
    const settings = loadSettings();
    const snapshot = getRaceSnapshot();
    wrap.innerHTML = `
    <div style="display:flex;flex-direction:column;gap:8px;">
      <label style="display:flex;flex-direction:column;gap:4px;">
        <strong>Torn API key</strong>
        <input type="password" class="hf-api-key" name="torn-api-key" autocomplete="off" data-1p-ignore data-lpignore="true" data-bwignore data-form-type="other" placeholder="Paste key from Preferences \u2192 API" value="" style="padding:4px 6px;border-radius:4px;border:1px solid var(--hf-accent-muted);background:rgba(0,0,0,0.25);color:var(--hf-text);">
      </label>
      <span class="hf-muted">
        Create a key at <strong>Preferences \u2192 API</strong> with at least <strong>Minimal Access</strong>.
      </span>
      <div style="display:flex;gap:8px;flex-wrap:wrap;">
        <button type="button" class="hf-btn hf-save-key">Save key</button>
        <button type="button" class="hf-btn hf-refresh-api">Refresh data</button>
      </div>
      <div class="hf-api-status hf-muted"></div>
    </div>
  `;
    const input = (
      /** @type {HTMLInputElement} */
      wrap.querySelector(".hf-api-key")
    );
    input.value = settings.apiKey;
    const status = (
      /** @type {HTMLElement} */
      wrap.querySelector(".hf-api-status")
    );
    status.textContent = formatApiStatus(snapshot);
    wrap.querySelector(".hf-save-key")?.addEventListener("click", () => {
      saveSettings({ apiKey: input.value.trim() });
      status.textContent = "API key saved.";
      options.onSave?.();
    });
    wrap.querySelector(".hf-refresh-api")?.addEventListener("click", async () => {
      const btn = (
        /** @type {HTMLButtonElement | null} */
        wrap.querySelector(".hf-refresh-api")
      );
      if (!btn || btn.disabled) {
        return;
      }
      const label = btn.textContent || "Refresh data";
      btn.disabled = true;
      btn.textContent = "Refreshing...";
      try {
        await options.onRefresh?.();
      } finally {
        if (btn.isConnected) {
          btn.disabled = false;
          btn.textContent = label;
        }
      }
    });
    return wrap;
  }
  function formatApiStatus(snapshot) {
    if (snapshot.status === "missing-key") {
      return "";
    }
    if (snapshot.status === "error") {
      return `API error: ${snapshot.error}`;
    }
    if (snapshot.status === "ok") {
      const active = snapshot.active ? ` Active race: ${/** @type {{ track: string, source?: string }} */
      snapshot.active.track} (${snapshot.active.source ?? "api"}).` : "";
      return `Loaded ${snapshot.races?.length ?? 0} races from the API.${active}`;
    }
    return "";
  }
  var settingsUi = {
    renderSettingsPanel
  };

  // src/racing/main.js
  var activeTrack = "";
  var lastUiSig = "";
  var panel = null;
  async function init() {
    HF.theme.injectStyles();
    panel = ui.createPanel({
      id: "smart-racing-panel",
      badge: "Smart Racing",
      mount: "racing",
      collapsedLegacyKeys: [
        "smart.torn.panel.smart-racing-panel.collapsed",
        "hf.torn.panel.hf-racing-panel.collapsed",
        "hf.torn.panel.smart-racing-panel.collapsed"
      ],
      tabs: [
        {
          id: "quick",
          label: "Quick pick",
          render: async () => renderQuickPickTab()
        },
        {
          id: "log",
          label: "Race log",
          render: async () => renderRaceLogTab()
        },
        {
          id: "settings",
          label: "Settings",
          render: async () => settingsUi.renderSettingsPanel({
            onSave: () => {
              lastUiSig = "";
            },
            onRefresh: async () => {
              await refreshFromApi(true, { forceUi: true });
            }
          })
        }
      ]
    });
    raceLog.watchRaceFlows(async (track, type) => {
      activeTrack = track;
      raceLog.setPendingContext({ track, type });
      updateContextualUi();
    });
    HF.dom.onAnchorClick(async () => {
      panel?.ensureMounted();
      await refreshFromApi(false);
      updateContextualUi();
    });
    document.body.addEventListener("click", (event) => {
      const target = (
        /** @type {Element} */
        event.target
      );
      const li = target.closest("li");
      const modelEl = li?.querySelector('[class^="model-car-name-"]');
      if (!modelEl) {
        return;
      }
      raceLog.setPendingCar({
        model: modelEl.textContent.trim(),
        note: li.textContent.replace(modelEl.textContent, "").trim()
      });
    });
    await refreshFromApi(false, { forceUi: true });
    watchRaceContext(() => {
      void onRaceContextChanged();
    }, { panelId: "smart-racing-panel" });
    window.setInterval(async () => {
      await refreshFromApi(false);
    }, 60 * 1e3);
  }
  async function onRaceContextChanged() {
    const { apiKey } = loadSettings();
    await refreshActiveRaceContext(apiKey);
    applyActiveRaceContext();
    updateContextualUi();
  }
  function buildUiSig() {
    const snapshot = getRaceSnapshot();
    const active = snapshot.active;
    return [
      snapshot.status,
      snapshot.error,
      snapshot.races.length,
      active?.raceId ?? "",
      active?.track ?? "",
      active?.car?.model ?? "",
      active?.car?.note ?? "",
      activeTrack,
      raceLog.getLogFilter()
    ].join("|");
  }
  function shouldPreserveUi() {
    if (panel?.getActiveTabId() === "settings") {
      return true;
    }
    const active = document.activeElement;
    if (active && panel?.element.contains(active)) {
      const tag = active.tagName;
      if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || active.isContentEditable) {
        return true;
      }
    }
    const selection = window.getSelection();
    if (selection && !selection.isCollapsed && panel?.element.contains(selection.anchorNode)) {
      return true;
    }
    return false;
  }
  function patchLiveUi() {
    if (!panel || panel.getActiveTabId() !== "quick") {
      return false;
    }
    const liveEl = panel.element.querySelector(".hf-racing-live");
    const snapshot = getRaceSnapshot();
    if (!liveEl || !snapshot.active) {
      return false;
    }
    const carLabel = snapshot.active.car.note ? `${snapshot.active.car.model} (${snapshot.active.car.note})` : snapshot.active.car.model;
    const pos = snapshot.active.live.position ? `Position ${snapshot.active.live.position}` : "Race in progress";
    liveEl.textContent = `${carLabel} \xB7 ${pos}`;
    return true;
  }
  function refreshPanelIfNeeded(options = {}) {
    const force = Boolean(options.force);
    const sig = buildUiSig();
    if (!force && sig === lastUiSig) {
      patchLiveUi();
      return;
    }
    if (!force && shouldPreserveUi()) {
      patchLiveUi();
      return;
    }
    lastUiSig = sig;
    panel?.refresh();
  }
  async function refreshFromApi(force, options = {}) {
    const { apiKey } = loadSettings();
    await refreshRaceData(apiKey, force);
    applyActiveRaceContext();
    refreshPanelIfNeeded({ force: Boolean(options.forceUi) });
  }
  function applyActiveRaceContext() {
    const snapshot = getRaceSnapshot();
    if (snapshot.active?.track) {
      activeTrack = snapshot.active.track;
      return;
    }
    const joinTrack = detectJoinFlowTrack();
    if (joinTrack) {
      activeTrack = joinTrack;
      return;
    }
    activeTrack = "";
  }
  function updateContextualUi() {
    applyActiveRaceContext();
    refreshPanelIfNeeded();
  }
  async function renderQuickPickTab() {
    const wrap = document.createElement("div");
    const snapshot = getRaceSnapshot();
    const { apiKey } = loadSettings();
    if (!apiKey) {
      wrap.appendChild(ui.emptyState("Add your Torn API key in Settings."));
      return wrap;
    }
    if (snapshot.status === "error") {
      wrap.appendChild(ui.emptyState(`API error: ${snapshot.error}`));
      return wrap;
    }
    if (snapshot.status === "missing-key") {
      wrap.appendChild(ui.emptyState("Add your Torn API key in Settings."));
      return wrap;
    }
    const track = snapshot.active?.track || activeTrack;
    if (!track) {
      const onRacePage = document.querySelector("#racingupdates");
      const hint = onRacePage ? "On a race page but could not read the track name. Try Refresh data, or reload the page." : "No active race detected. Join a race, or open the live race view.";
      wrap.appendChild(ui.emptyState(hint));
      return wrap;
    }
    activeTrack = track;
    const heading = document.createElement("div");
    heading.style.cssText = "font-weight:bold;font-size:13px;margin-bottom:6px;";
    heading.textContent = track;
    wrap.appendChild(heading);
    if (snapshot.active) {
      const carLabel = snapshot.active.car.note ? `${snapshot.active.car.model} (${snapshot.active.car.note})` : snapshot.active.car.model;
      const pos = snapshot.active.live.position ? `Position ${snapshot.active.live.position}` : "Race in progress";
      const live = document.createElement("div");
      live.className = "hf-racing-live";
      live.textContent = `${carLabel} \xB7 ${pos}`;
      wrap.appendChild(live);
    }
    const races = getRacesForTrackFromApi(track);
    wrap.appendChild(suggest.renderTrackSuggestions(races));
    const guideSection = document.createElement("div");
    guideSection.className = "hf-section-break";
    guideSection.appendChild(templates.renderTemplateSummary(track));
    wrap.appendChild(guideSection);
    return wrap;
  }
  async function renderRaceLogTab() {
    const snapshot = getRaceSnapshot();
    const { apiKey } = loadSettings();
    if (!apiKey) {
      return ui.emptyState("Add your Torn API key in Settings.");
    }
    if (snapshot.status === "error") {
      return ui.emptyState(`API error: ${snapshot.error}`);
    }
    const pending = raceLog.getPendingContext();
    const context = {
      track: snapshot.active?.track || pending.track || activeTrack,
      car: snapshot.active?.car || pending.car
    };
    return raceLog.renderRaceLogTable(snapshot.races, {
      context,
      filter: raceLog.getLogFilter(),
      onFilterChange: (mode) => {
        raceLog.setLogFilter(mode);
        refreshPanelIfNeeded({ force: true });
      }
    });
  }
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();