Smart Racing

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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