Torn Bookie Predictor

Torn Bookie Predictor with smart sports match analysis, betting insights, and stake suggestions.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn Bookie Predictor
// @namespace    ebnoreza.torn.bookie.v4
// @version      26.7.20.1
// @description  Torn Bookie Predictor with smart sports match analysis, betting insights, and stake suggestions.
// @author       Vreebn [4149405]
// @match        https://www.torn.com/page.php?sid=bookie*
// @match        https://www.torn.com/bookie.php*
// @match        https://www.torn.com/*sid=bookie*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @connect      workers.dev
// @connect      torn-bookie-advisor-v4.ebnoreza.workers.dev
// @connect      *.ebnoreza.workers.dev
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const INSTANCE_KEY = "__torn_bookie_predictor_v41_running__";

  if (window[INSTANCE_KEY]) {
    console.warn("[TBP] Duplicate script instance blocked.");
    return;
  }

  window[INSTANCE_KEY] = true;

  window.addEventListener("beforeunload", () => {
    try {
      window[INSTANCE_KEY] = false;
    } catch (_) {}
  });

  // ---------------------------------------------------------------------------
  // Configuration and storage keys
  // ---------------------------------------------------------------------------

  const SCRIPT_VERSION = "26.7.20.1";
  const DEFAULT_WORKER_URL =
    "https://torn-bookie-advisor-v4.ebnoreza.workers.dev";
  const DEFAULT_BUDGET = 1000000;
  const LS_PREFIX = "tba_v41_";
  const INLINE_PANEL_ID = "tba-v41-inline-panel";
  const INLINE_STYLE_ID = "tba-v41-inline-style";
  let headerHintTimer = null;

  const TORN_API_KEY_CREATE_URL =
    "https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=Torn%20Bookie%20Predictor&user=basic";
  const API_KEY_STORE_KEY = "tornApiKey";
  const IDB_NAME = "torn_bookie_predictor_v41";
  const IDB_STORE = "settings";

  // ---------------------------------------------------------------------------
  // Persistent settings
  // ---------------------------------------------------------------------------

  function getStore(key, fallback) {
    try {
      if (typeof GM_getValue === "function")
        return GM_getValue(LS_PREFIX + key, fallback);
    } catch (_) {}

    try {
      const v = localStorage.getItem(LS_PREFIX + key);
      return v == null ? fallback : JSON.parse(v);
    } catch (_) {
      return fallback;
    }
  }

  function setStore(key, value) {
    try {
      if (typeof GM_setValue === "function")
        return GM_setValue(LS_PREFIX + key, value);
    } catch (_) {}

    try {
      localStorage.setItem(LS_PREFIX + key, JSON.stringify(value));
    } catch (_) {}
  }

    const CUSTOM_THEME_STORE_KEY = "customTheme";

  const DEFAULT_CUSTOM_THEME = {
    panelBg1: "#111827",
    panelBg2: "#080d19",
    cardBg: "#020617",
    cardBorder: "#24364f",
    title: "#86b7ff",
    text: "#e8eefc",
    muted: "#9fb0c8",
    accent: "#86b7ff",
    success: "#59d98e",
    warning: "#ffd166",
    danger: "#ff7f8f",
    buttonBg1: "#182236",
    buttonBg2: "#0c1222",
  };

  const CUSTOM_THEME_FIELDS = [
    ["panelBg1", "Panel background 1"],
    ["panelBg2", "Panel background 2"],
    ["cardBg", "Card background"],
    ["cardBorder", "Card border"],
    ["title", "Title / Accent text"],
    ["text", "Main text"],
    ["muted", "Muted text"],
    ["accent", "Accent"],
    ["success", "Success"],
    ["warning", "Warning"],
    ["danger", "Danger"],
    ["buttonBg1", "Button background 1"],
    ["buttonBg2", "Button background 2"],
  ];

  const CUSTOM_THEME_CSS_VARS = {
    panelBg1: "--tba-panel-bg-1",
    panelBg2: "--tba-panel-bg-2",
    cardBg: "--tba-card-bg",
    cardBorder: "--tba-card-border",
    title: "--tba-title-color",
    text: "--tba-text-color",
    muted: "--tba-muted-color",
    accent: "--tba-accent-color",
    success: "--tba-success-color",
    warning: "--tba-warning-color",
    danger: "--tba-danger-color",
    buttonBg1: "--tba-button-bg-1",
    buttonBg2: "--tba-button-bg-2",
  };

  function normalizeHexColor(value, fallback) {
    const s = String(value || "").trim();

    if (/^#[0-9a-f]{6}$/i.test(s)) return s.toLowerCase();

    if (/^#[0-9a-f]{3}$/i.test(s)) {
      return (
        "#" +
        s
          .slice(1)
          .split("")
          .map((x) => x + x)
          .join("")
      ).toLowerCase();
    }

    return fallback;
  }

  function normalizeCustomTheme(value) {
    const raw = value && typeof value === "object" ? value : {};
    const out = {};

    for (const key of Object.keys(DEFAULT_CUSTOM_THEME)) {
      out[key] = normalizeHexColor(raw[key], DEFAULT_CUSTOM_THEME[key]);
    }

    return out;
  }

  function getCustomThemeFromStore() {
    return normalizeCustomTheme(
      getStore(CUSTOM_THEME_STORE_KEY, DEFAULT_CUSTOM_THEME),
    );
  }

  function setCustomThemeToStore(theme) {
    const clean = normalizeCustomTheme(theme);
    setStore(CUSTOM_THEME_STORE_KEY, clean);
    return clean;
  }

  function applyCustomThemeToPanel(panel = document.getElementById(INLINE_PANEL_ID)) {
    if (!panel) return;

    const theme = normalizeCustomTheme(state.customTheme);

    for (const [key, cssVar] of Object.entries(CUSTOM_THEME_CSS_VARS)) {
      panel.style.setProperty(cssVar, theme[key]);
    }
  }

  function setCustomThemeColor(key, value) {
    if (!Object.prototype.hasOwnProperty.call(DEFAULT_CUSTOM_THEME, key)) return;

    state.customTheme = setCustomThemeToStore({
      ...state.customTheme,
      [key]: value,
    });

    applyCustomThemeToPanel();
  }

  function resetCustomThemeToDefault() {
    state.customTheme = setCustomThemeToStore(DEFAULT_CUSTOM_THEME);
    state.lastRenderedHtml = "";
    setStatus("Customization reset to default colors.", "ok");
    render();
  }

  const ENABLED_SPORTS_STORE_KEY = "enabledSports";

  const ADVISOR_SPORTS = [
    "football",
    "basketball",
    "baseball",
    "handball",
    "rugby",
  ];

  const DEFAULT_ENABLED_SPORTS = {
    football: true,
    basketball: true,
    baseball: true,
    handball: true,
    rugby: true,
  };

  function normalizeEnabledSports(value) {
    const raw = value && typeof value === "object" ? value : {};

    const out = {
      football: raw.football !== false,
      basketball: raw.basketball !== false,
      baseball: raw.baseball !== false,
      handball: raw.handball !== false,
      rugby: raw.rugby !== false,
    };

    // Safety: never allow all sports disabled.
    if (!ADVISOR_SPORTS.some((s) => !!out[s])) {
      return { ...DEFAULT_ENABLED_SPORTS };
    }

    return out;
  }

  function getEnabledSportsFromStore() {
    return normalizeEnabledSports(
      getStore(ENABLED_SPORTS_STORE_KEY, DEFAULT_ENABLED_SPORTS),
    );
  }

  function setEnabledSportsToStore(enabledSports) {
    setStore(ENABLED_SPORTS_STORE_KEY, normalizeEnabledSports(enabledSports));
  }

  const ODDS_FILTER_STORE_KEY = "oddsFilter";

  const DEFAULT_ODDS_FILTER = {
    enabled: false,
    minOdds: 1.2,
  };

  function cleanOddsFilterInputValue(value) {
    let s = String(value ?? "").replace(/[^0-9.]/g, "");

    const firstDot = s.indexOf(".");

    if (firstDot !== -1) {
      s = s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, "");
    }

    if (s.startsWith(".")) s = "0" + s;

    // Prevent silly huge input strings.
    if (s.length > 8) s = s.slice(0, 8);

    return s;
  }

  function normalizeOddsFilterMin(
    value,
    fallback = DEFAULT_ODDS_FILTER.minOdds,
  ) {
    const clean = cleanOddsFilterInputValue(value);
    const n = Number(clean);

    if (!Number.isFinite(n) || n <= 0) return fallback;

    return Math.min(n, 99.99);
  }

  function normalizeOddsFilterSettings(value) {
    const raw = value && typeof value === "object" ? value : {};

    return {
      enabled: raw.enabled === true,
      minOdds: normalizeOddsFilterMin(raw.minOdds, DEFAULT_ODDS_FILTER.minOdds),
    };
  }

  function getOddsFilterFromStore() {
    return normalizeOddsFilterSettings(
      getStore(ODDS_FILTER_STORE_KEY, DEFAULT_ODDS_FILTER),
    );
  }

  function setOddsFilterToStore(value) {
    const clean = normalizeOddsFilterSettings(value);
    setStore(ODDS_FILTER_STORE_KEY, clean);
    return clean;
  }

  function oddsFilterMinText(value = state.oddsFilter?.minOdds) {
    const n = normalizeOddsFilterMin(value, DEFAULT_ODDS_FILTER.minOdds);
    return String(Number(n.toFixed(4))).replace(/\.0+$/, "");
  }

  function isSportEnabled(sport) {
    const s = String(sport || "").toLowerCase();
    return !!state.enabledSports?.[s];
  }

  function enabledSportsCount(next = state.enabledSports) {
    return ADVISOR_SPORTS.filter((s) => !!next?.[s]).length;
  }

  function allAdvisorSportsEnabled() {
    return ADVISOR_SPORTS.every((s) => !!state.enabledSports?.[s]);
  }

  function setSportEnabled(sport, enabled) {
    sport = String(sport || "").toLowerCase();

    if (!ADVISOR_SPORTS.includes(sport)) return false;

    const currentEnabled = normalizeEnabledSports(state.enabledSports);

    // Do not allow disabling the last enabled sport.
    if (
      !enabled &&
      enabledSportsCount(currentEnabled) <= 1 &&
      currentEnabled[sport]
    ) {
      setStatus("At least one sport must stay enabled.", "warn");
      render();
      return false;
    }

    const next = normalizeEnabledSports({
      ...currentEnabled,
      [sport]: !!enabled,
    });

    state.enabledSports = next;
    setEnabledSportsToStore(next);

    const current = currentBookieSport();

    if (current === sport && !next[sport]) {
      state.lastDecision = null;
      state.lastParsed = null;
      state.lastFill = null;
      state.lastRenderedHtml = "";
      state.decisionReadyToShow = false;
      setStatus(
        `${currentBookieSportName()} interface disabled in Settings.`,
        "warn",
      );
    } else {
      setStatus("Sport visibility settings saved.", "ok");
      scheduleAutoResolve("sport_visibility_changed", 200);
    }

    render();
    return true;
  }

  // ---------------------------------------------------------------------------
  // Runtime state
  // ---------------------------------------------------------------------------

  const state = {
    workerUrl: normalizeWorkerUrl(getStore("workerUrl", DEFAULT_WORKER_URL)),
    budget: Number(getStore("budget", DEFAULT_BUDGET)) || DEFAULT_BUDGET,
    tornApiKey: normalizeTornApiKey(getStore(API_KEY_STORE_KEY, "") || ""),
    enabledSports: getEnabledSportsFromStore(),
    oddsFilter: getOddsFilterFromStore(),
    subscription: null,
    authChecked: false,
    showLogs: getStore("showLogs", false),
    showSettings: getStore("showSettings", false),
    showCustomization: getStore("showCustomization", false),
    customTheme: getCustomThemeFromStore(),
    lastParsed: null,
    lastDecision: null,
    lastFill: null,
    lastStatus: "Ready",
    bookieApiCache: null,
    statusKind: "info",
    busy: false,
    observerTimer: null,
    autoBusy: false,
    autoTimer: null,
    lastAutoSignature: "",
    lastAutoAt: 0,
    lastSeenMarketSignature: "",
    lastSeenMarketIdentity: "",
    lastExpandSignature: "",
    lastExpandAt: 0,
    internalClickUntil: 0,
    lastResolvedSignature: "",
    lastResolvedIdentity: "",
    lastResolvedAt: 0,
    panelStableUntil: 0,
    navToken: 0,
    autoQueued: false,
    resolveInFlightSignature: "",
    resolveInFlightPromise: null,
    lastRenderedHtml: "",
    initialAutoDone: false,
    decisionReadyToShow: false,
    stableEventKey: "",
    marketWatchKey: "",
    directBetFlow: null,
    singleOutcomeBetStates: Object.create(null),
  };

  // ---------------------------------------------------------------------------
  // Torn API key storage and hydration
  // ---------------------------------------------------------------------------

  function normalizeTornApiKey(value) {
    let s = String(value || "").trim();

    // If old storage accidentally saved the key as a JSON string like: "abcd..."
    try {
      const parsed = JSON.parse(s);
      if (typeof parsed === "string") {
        s = parsed.trim();
      }
    } catch (_) {}

    s = s
      .replace(/[“”]/g, '"')
      .replace(/[‘’]/g, "'")
      .replace(/^["'`]+|["'`]+$/g, "")
      .trim();

    // Torn API keys should not contain whitespace.
    s = s.replace(/\s+/g, "");

    return s;
  }

  function hasTornApiKey() {
    state.tornApiKey = normalizeTornApiKey(state.tornApiKey);
    return state.tornApiKey.length >= 8;
  }

  function maskApiKey(key = state.tornApiKey) {
    const s = normalizeTornApiKey(key);
    if (!s) return "";
    if (s.length <= 8) return "••••••••";
    return `${s.slice(0, 4)}••••${s.slice(-4)}`;
  }

  function openSettingsForApiKey() {
    state.showSettings = true;
    setStore("showSettings", true);
    setStatus("Enter your Torn API key to use Torn Bookie Predictor.", "warn");
    render();
  }

  function idbOpen() {
    return new Promise((resolve, reject) => {
      if (!window.indexedDB) {
        reject(new Error("IndexedDB is not available."));
        return;
      }

      const req = indexedDB.open(IDB_NAME, 1);

      req.onupgradeneeded = () => {
        const db = req.result;
        if (!db.objectStoreNames.contains(IDB_STORE)) {
          db.createObjectStore(IDB_STORE);
        }
      };

      req.onsuccess = () => resolve(req.result);
      req.onerror = () =>
        reject(req.error || new Error("IndexedDB open failed."));
    });
  }

  async function idbSet(key, value) {
    try {
      const db = await idbOpen();

      await new Promise((resolve, reject) => {
        const tx = db.transaction(IDB_STORE, "readwrite");
        tx.objectStore(IDB_STORE).put(value, key);
        tx.oncomplete = resolve;
        tx.onerror = () =>
          reject(tx.error || new Error("IndexedDB write failed."));
      });

      db.close();
    } catch (_) {}
  }

  async function idbGet(key, fallback = "") {
    try {
      const db = await idbOpen();

      const value = await new Promise((resolve, reject) => {
        const tx = db.transaction(IDB_STORE, "readonly");
        const req = tx.objectStore(IDB_STORE).get(key);
        req.onsuccess = () =>
          resolve(req.result == null ? fallback : req.result);
        req.onerror = () =>
          reject(req.error || new Error("IndexedDB read failed."));
      });

      db.close();
      return value;
    } catch (_) {
      return fallback;
    }
  }

  async function idbDelete(key) {
    try {
      const db = await idbOpen();

      await new Promise((resolve, reject) => {
        const tx = db.transaction(IDB_STORE, "readwrite");
        tx.objectStore(IDB_STORE).delete(key);
        tx.oncomplete = resolve;
        tx.onerror = () =>
          reject(tx.error || new Error("IndexedDB delete failed."));
      });

      db.close();
    } catch (_) {}
  }

  async function saveTornApiKey(key) {
    const clean = normalizeTornApiKey(key);

    state.tornApiKey = clean;
    state.subscription = null;
    state.authChecked = false;

    setStore(API_KEY_STORE_KEY, clean);
    await idbSet(API_KEY_STORE_KEY, clean);

    if (clean) {
      setStatus(
        `API key saved locally: ${maskApiKey(clean)}. Verifying subscription...`,
        "busy",
      );
      scheduleAutoResolve("api_key_saved", 200);
    } else {
      setStatus(
        "API key removed. Enter a Torn API key to use Torn Bookie Predictor.",
        "warn",
      );
    }

    render();
  }

  async function clearTornApiKey() {
    state.tornApiKey = "";
    state.subscription = null;
    state.authChecked = false;
    state.lastDecision = null;
    state.lastParsed = null;
    state.lastFill = null;
    state.lastRenderedHtml = "";
    state.decisionReadyToShow = false;

    setStore(API_KEY_STORE_KEY, "");
    await idbDelete(API_KEY_STORE_KEY);

    clearStakeInputs();
    setStatus("API key deleted from local storage and IndexedDB.", "warn");
    render();
  }

  async function hydrateApiKeyFromIndexedDb() {
    const fromIdb = normalizeTornApiKey(
      (await idbGet(API_KEY_STORE_KEY, "")) || "",
    );

    if (fromIdb && !state.tornApiKey) {
      state.tornApiKey = fromIdb;
      setStore(API_KEY_STORE_KEY, fromIdb);
      await idbSet(API_KEY_STORE_KEY, fromIdb);
      render();
      scheduleAutoResolve("api_key_loaded_from_idb", 250);
      return;
    }

    if (fromIdb && fromIdb !== state.tornApiKey) {
      state.tornApiKey = fromIdb;
      setStore(API_KEY_STORE_KEY, fromIdb);
      await idbSet(API_KEY_STORE_KEY, fromIdb);
      render();
    }
  }

  // ---------------------------------------------------------------------------
  // Subscription state and copy
  // ---------------------------------------------------------------------------

  function updateSubscriptionFromResponse(data) {
    const sub = data?.subscription || data?.subscriber || null;

    if (!sub) return;

    state.subscription = {
      active: !!sub.active,
      days_remaining: Number(sub.days_remaining ?? sub.daysRemaining ?? 0) || 0,
      user_id: sub.user_id ?? sub.userId ?? null,
      name: sub.name || sub.player_name || "",
      level: sub.level ?? null,
      gender: sub.gender || "",
      expires_at: sub.expires_at || sub.expiresAt || null,
      trial_granted: !!sub.trial_granted,
      renewal_count: Number(sub.renewal_count || 0),
      verification_source: sub.verification_source || "",
      verified_cache_seconds_left: Number(sub.verified_cache_seconds_left || 0),
    };

    state.authChecked = true;
  }

  function formatSubscriptionDate(value) {
    if (!value) return "";

    try {
      const d = new Date(value);
      if (Number.isNaN(d.getTime())) return String(value);

      return d.toUTCString().replace(" GMT", " TCT");
    } catch (_) {
      return String(value);
    }
  }

  function subscriptionPillClass(sub = state.subscription) {
    if (!sub) return "tba-sub-pill-warn";
    if (!sub.active) return "tba-sub-pill-bad";
    if (Number(sub.days_remaining || 0) <= 3) return "tba-sub-pill-warn";
    return "tba-sub-pill-good";
  }

  function renderSubscriptionBadgeHtml() {
    if (!hasTornApiKey()) {
      return `
      <div class="tba-subscription-line">
        <span class="tba-sub-pill tba-sub-pill-bad">No API key</span>
      </div>
    `;
    }

    const sub = state.subscription;

    if (!sub) {
      return `
      <div class="tba-subscription-line">
        <span class="tba-sub-pill tba-sub-pill-warn">Subscription not checked yet</span>
        <span class="tba-sub-detail">Open a match to verify.</span>
      </div>
    `;
    }

    const days = Number(sub.days_remaining || 0);
    const name = sub.name ? ` • ${sub.name}` : "";
    const expires = sub.expires_at
      ? ` • Expires: ${formatSubscriptionDate(sub.expires_at)}`
      : "";

    return `
    <div class="tba-subscription-line">
      <span class="tba-sub-pill ${subscriptionPillClass(sub)}">
        ${sub.active ? `Active: ${days} day${days === 1 ? "" : "s"} left` : "Expired"}
      </span>
      <span class="tba-sub-detail">${htmlEscape(name + expires)}</span>
    </div>
  `;
  }

  function renderBettingDisclaimerHtml() {
    return `
    <div class="tba-settings-note">
      <b>Reminder:</b> Predictions are not guaranteed results. Bet carefully, think before placing any bet, and never treat this tool as 100% accurate or guaranteed profit.
    </div>
  `;
  }

  function renderSubscriptionInstructionsHtml() {
    return `
    <div class="tba-subscription-help">
      <div class="tba-subscription-help-title">Subscription / Trial</div>

      <div class="tba-subscription-help-text">
        New users get a <b>7-day free trial</b>.
        After the trial, renewal costs <b>1 Xanax for every 10 days</b> of access.
      </div>

      <div class="tba-subscription-help-text">
        To renew, send <b>any amount of Xanax</b> to
        <a
          href="https://www.torn.com/profiles.php?XID=4149405"
          target="_blank"
          rel="noopener noreferrer"
        ><b>Vreebn [4149405]</b></a>
        with the message <b>Bookie</b>.
      </div>

      <div class="tba-subscription-help-text">
        Write only <b>Bookie</b> in the message. Do not add anything else.
        Uppercase/lowercase does not matter, so <b>Bookie</b>, <b>bookie</b>, or <b>BOOKIE</b> are all accepted.
      </div>

      <div class="tba-subscription-help-text">
         Any amount from <b>1 Xanax and above</b> works.
         Each Xanax adds <b>10 days</b> of access, with no renewal limit.
      </div>
      </div>
    `;
  }

  // ---------------------------------------------------------------------------
  // Decision policy and odds-filter safety
  // ---------------------------------------------------------------------------

  function decisionUserMessage(d) {
    if (!d) return "";

    return String(
      d.user_message ||
        d.message ||
        d.prediction_status?.user_message ||
        d.prediction_status?.quota?.user_message ||
        d.subscription?.message ||
        d.reason ||
        d.error ||
        "",
    ).trim();
  }

  function getOddsFilterBlockInfo(decision) {
    const filter = normalizeOddsFilterSettings(state.oddsFilter);

    if (
      !filter.enabled ||
      !decision ||
      String(decision.action || "").toUpperCase() !== "BET"
    ) {
      return null;
    }

    const selected = Array.isArray(decision.selected_outcomes)
      ? decision.selected_outcomes
      : [];

    if (!selected.length) return null;

    const minOdds = Number(filter.minOdds || DEFAULT_ODDS_FILTER.minOdds);
    const blocked = selected.filter((o) => Number(o.odds || 0) < minOdds);

    if (!blocked.length) return null;

    return {
      enabled: true,
      minOdds,
      blocked: blocked.map((o) => ({
        key: o.key || o.role || "",
        label: o.label || o.key || o.role || "Selected outcome",
        odds: Number(o.odds || 0),
      })),
    };
  }

  function assertOddsFilterAllowsBetting(decision = state.lastDecision) {
    const info = getOddsFilterBlockInfo(decision);

    if (!info) return true;

    const blockedText = (info.blocked || [])
      .map((o) => `${o.label} @ ${Number(o.odds || 0).toFixed(2)}`)
      .join(", ");

    throw new Error(
      `Odds Filters blocked this bet. Minimum odds: ${Number(info.minOdds || 0).toFixed(2)}${blockedText ? ` • ${blockedText}` : ""}`,
    );
  }

  function ensureHeaderHintEl() {
    const panel = document.getElementById(INLINE_PANEL_ID);
    if (!panel) return null;

    let tip = panel.querySelector(".tba-header-hint");

    if (!tip) {
      tip = document.createElement("div");
      tip.className = "tba-header-hint";
      panel.appendChild(tip);
    }

    return tip;
  }

  function showHeaderHint(text) {
    const tip = ensureHeaderHintEl();
    if (!tip) return;

    tip.textContent = String(text || "");
    tip.classList.add("show");

    clearTimeout(headerHintTimer);

    headerHintTimer = setTimeout(() => {
      tip.classList.remove("show");
    }, 3000);
  }

  // ---------------------------------------------------------------------------
  // Bookie route and sport detection
  // ---------------------------------------------------------------------------

  function currentBookieSport() {
    const hash = String(location.hash || "").toLowerCase();

    if (/^#\/basketball(?:\/|$|\?)/.test(hash)) return "basketball";
    if (/^#\/baseball(?:\/|$|\?)/.test(hash)) return "baseball";
    if (/^#\/rugby(?:\/|$|\?)/.test(hash)) return "rugby";
    if (/^#\/handball(?:\/|$|\?)/.test(hash)) return "handball";
    if (/^#\/football(?:\/|$|\?)/.test(hash)) return "football";

    // Important for #/your-bets:
    // Do not assume football. Read active match DOM.
    if (/^#\/(?:your-bets|popular)(?:\/|$|\?)/.test(hash)) {
      return detectBookieSportFromParsed(null) || "unknown";
    }

    return "unknown";
  }

  function currentBookieSportIcon() {
    const sport = currentBookieSport();

    if (sport === "basketball") return "🏀";
    if (sport === "baseball") return "⚾";
    if (sport === "rugby") return "🏉";
    if (sport === "handball") return "🤾";
    if (sport === "football") return "⚽";

    return "🎲";
  }

  function currentBookieSportName() {
    const sport = currentBookieSport();

    if (sport === "basketball") return "Basketball";
    if (sport === "baseball") return "Baseball";
    if (sport === "rugby") return "Rugby";
    if (sport === "handball") return "Handball";
    if (sport === "football") return "Football";

    return "Unsupported sport";
  }

  function attachSportToParsed(parsed) {
    if (!parsed || typeof parsed !== "object") return parsed;

    const domSport = detectBookieSportFromParsed(parsed);
    const urlSport = currentBookieSport();

    const sport =
      domSport || (urlSport !== "unknown" ? urlSport : "") || "unknown";

    return {
      ...parsed,
      sport,
      marketSport: sport,
      sportDetectedFrom: domSport ? "dom_game_title" : "url_hash",
    };
  }

  function isSupportedBookieView() {
    const href = String(location.href || "");
    const hash = String(location.hash || "").toLowerCase();

    const isBookie = /sid=bookie|bookie\.php/i.test(href);
    if (!isBookie) return false;

    return (
      /^#\/football(?:\/|$|\?)/.test(hash) ||
      /^#\/basketball(?:\/|$|\?)/.test(hash) ||
      /^#\/baseball(?:\/|$|\?)/.test(hash) ||
      /^#\/handball(?:\/|$|\?)/.test(hash) ||
      /^#\/rugby(?:\/|$|\?)/.test(hash) ||
      /^#\/your-bets(?:\/|$|\?)/.test(hash) ||
      /^#\/popular(?:\/|$|\?)/.test(hash)
    );
  }

  function isCurrentBookieSportEnabled() {
    const sport = currentBookieSport();

    // In #/your-bets, DOM may not be ready yet.
    if (!sport || sport === "unknown") return true;

    // Unsupported sports should not be controlled by Football/Handball/Rugby toggles.
    // Let auto resolver show UNSUPPORTED_SPORT / SKIP instead of silently hiding the panel.
    if (!isAdvisorSupportedSport(sport)) return true;

    return isSportEnabled(sport);
  }

  function normalizeWorkerUrl(url) {
    let u = String(url || "").trim();

    if (!u) u = DEFAULT_WORKER_URL;

    try {
      u = decodeURIComponent(u);
    } catch (_) {}

    u = u
      .trim()
      .replace(/[“”]/g, '"')
      .replace(/[‘’]/g, "'")
      .replace(/^["'`]+|["'`]+$/g, "")
      .trim();

    // Fix common bad pasted forms:
    // https//domain
    // http//domain
    u = u.replace(/^https\/\//i, "https://");
    u = u.replace(/^http\/\//i, "http://");

    // Remove accidental nested quotes/protocol junk.
    u = u.replace(/^https:\/\/["']+/i, "https://");
    u = u.replace(/^http:\/\/["']+/i, "http://");

    if (!/^https?:\/\//i.test(u)) {
      u = "https://" + u;
    }

    // Collapse accidental double protocol.
    u = u.replace(/^https?:\/\/https?:\/\//i, "https://");

    return u.replace(/\/+$/, "");
  }

  // ---------------------------------------------------------------------------
  // Shared parsing, formatting, and DOM utilities
  // ---------------------------------------------------------------------------

  function cleanText(s) {
    return String(s || "")
      .replace(/\u00a0/g, " ")
      .replace(/\s+/g, " ")
      .trim();
  }

  function normalizeLoose(s) {
    return String(s || "")
      .normalize("NFD")
      .replace(/[\u0300-\u036f]/g, "")
      .replace(/&/g, " and ")
      .replace(/[’'`]/g, "")
      .replace(/[^a-zA-Z0-9]+/g, " ")
      .toLowerCase()
      .replace(/\s+/g, " ")
      .trim();
  }

  function normalizeSportName(value) {
    const s = normalizeLoose(value);

    if (s.includes("football") || s.includes("soccer")) return "football";
    if (s.includes("basketball")) return "basketball";
    if (s.includes("baseball")) return "baseball";
    if (s.includes("handball")) return "handball";
    if (s.includes("rugby")) return "rugby";

    // Sports we do not support yet.
    if (s.includes("tennis")) return "tennis";
    if (s.includes("hockey")) return "hockey";
    if (s.includes("cricket")) return "cricket";
    if (s.includes("volleyball")) return "volleyball";
    if (s.includes("mma")) return "mma";
    if (s.includes("boxing")) return "boxing";

    return "";
  }

  function isAdvisorSupportedSport(sport) {
    return ADVISOR_SPORTS.includes(String(sport || "").toLowerCase());
  }

  function detectBookieSportFromElement(el) {
    if (!el || !(el instanceof Element)) return "";

    const container =
      el.closest('li[class*="c-pointer"]') ||
      el.closest('li[class*="active"]') ||
      el.closest("li") ||
      el.closest('div[class*="info-wrap"]') ||
      el.parentElement ||
      document.body;

    if (!container) return "";

    // Best source: <li class="game" title="Rugby">
    const gameTitleEl =
      container.querySelector('li[class*="game"][title]') ||
      container.querySelector('[class*="game"][title]');

    const byTitle = normalizeSportName(
      gameTitleEl?.getAttribute("title") || "",
    );
    if (byTitle) return byTitle;

    // Icon fallback: gm-rugby-icon / gm-football-icon / gm-handball-icon
    const iconEl = container.querySelector('[class*="gm-"][class*="-icon"]');

    if (iconEl) {
      const cls = String(iconEl.className || "");
      const byClass = normalizeSportName(cls.replace(/gm-|icon|-/g, " "));
      if (byClass) return byClass;
    }

    // Text fallback.
    const text = cleanText(container.innerText || container.textContent || "");
    const byText = normalizeSportName(text);
    if (byText) return byText;

    return "";
  }

  function detectBookieSportFromParsed(parsed) {
    if (parsed?.root && document.body.contains(parsed.root)) {
      const sport = detectBookieSportFromElement(parsed.root);
      if (sport) return sport;
    }

    const active =
      document.querySelector(
        '#your-bets li[class*="c-pointer"][class*="active"]',
      ) ||
      document.querySelector('#your-bets li[class*="active"]') ||
      document.querySelector(
        '#popular li[class*="c-pointer"][class*="active"]',
      ) ||
      document.querySelector('#popular li[class*="active"]') ||
      document.querySelector('li[class*="c-pointer"][class*="active"]') ||
      document.querySelector('li[class*="active"]');

    return detectBookieSportFromElement(active);
  }

  function formatIntegerInputValue(value) {
    const n = Number(String(value || "").replace(/[^0-9]/g, "")) || 0;
    return n ? n.toLocaleString() : "";
  }

  function parseIntegerInputValue(value, fallback = 0) {
    const n = Number(String(value || "").replace(/[^0-9]/g, ""));
    return Number.isFinite(n) && n > 0 ? n : fallback;
  }

  function money(n) {
    const num = Number(n || 0);
    return "$" + Math.round(num).toLocaleString();
  }

  function pct(n) {
    if (n == null || Number.isNaN(Number(n))) return "-";
    return (Number(n) * 100).toFixed(2) + "%";
  }

  function dec(n, d = 4) {
    if (n == null || Number.isNaN(Number(n))) return "-";
    return Number(n).toFixed(d);
  }

  function htmlEscape(s) {
    return String(s ?? "")
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#039;");
  }

  function sleep(ms) {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  function isVisible(el) {
    if (!el || !(el instanceof Element)) return false;

    const st = getComputedStyle(el);
    if (
      st.display === "none" ||
      st.visibility === "hidden" ||
      st.opacity === "0"
    )
      return false;

    const r = el.getBoundingClientRect();
    return (
      r.width > 0 && r.height > 0 && r.bottom > 0 && r.top < window.innerHeight
    );
  }

  function isDomVisible(el) {
    if (!el || !(el instanceof Element)) return false;

    const st = getComputedStyle(el);
    if (
      st.display === "none" ||
      st.visibility === "hidden" ||
      st.opacity === "0"
    )
      return false;

    const r = el.getBoundingClientRect();
    return r.width > 0 && r.height > 0;
  }

  function parseOddsFromText(text) {
    const s = cleanText(text);

    const mx = s.match(/x\s*([0-9]+(?:\.[0-9]+)?)/i);
    if (mx) {
      const n = Number(mx[1]);
      if (Number.isFinite(n) && n > 1.001 && n < 100) return n;
    }

    const matches = [...s.matchAll(/\b(\d{1,2}\.\d{1,4})\b/g)]
      .map((m) => Number(m[1]))
      .filter((n) => Number.isFinite(n) && n > 1.001 && n < 100);

    if (!matches.length) return null;
    return matches[matches.length - 1];
  }

  function parseMoneyInputValue(el) {
    return Number(String(el?.value || "").replace(/[^0-9.]/g, "")) || 0;
  }

  function setInputValue(input, value) {
    if (!input) return false;

    const keepX = window.scrollX;
    const keepY = window.scrollY;

    const str =
      Number(value || 0) > 0
        ? String(Math.max(0, Math.round(Number(value || 0))))
        : "";

    try {
      const proto = window.HTMLInputElement.prototype;
      const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;

      if (setter) setter.call(input, str);
      else input.value = str;
    } catch (_) {
      input.value = str;
    }

    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    input.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true }));
    input.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true }));
    input.dispatchEvent(new FocusEvent("blur", { bubbles: true }));

    try {
      window.scrollTo(keepX, keepY);
    } catch (_) {}

    return true;
  }

  function looksLikeMatchTitle(t) {
    const s = cleanText(t);
    if (s.length < 5 || s.length > 220) return false;
    return /\b(v|vs|versus)\.?\b/i.test(s);
  }

  function cleanMatchTitleText(text) {
    return cleanText(
      String(text || "").replace(
        /\s+(?:2-?Way(?:\s+Full\s+event)?|2Way(?:\s+Full\s+event)?|Full\s+event|3-?Way|Draw No Bet|Double Chance|Win to nil|Both Teams to Score|Total Goals|Total Points|Over\/Under|Under\/Over|Handicap|Ordinary time|due to start)\b.*$/i,
        "",
      ),
    );
  }

  function stripContext(s) {
    return cleanText(
      String(s || "")
        .replace(
          /\s+(?:2-?Way(?:\s+Full\s+event)?|2Way(?:\s+Full\s+event)?|Full\s+event|3-?Way|Draw No Bet|Double Chance|Win to nil|Both Teams to Score|Total Goals|Total Points|Over\/Under|Under\/Over|Handicap|Ordinary time|due to start)\b.*$/i,
          "",
        )
        .replace(/\s+-\s+.*$/, "")
        .replace(/\s+\(.+?\)\s*$/, "")
        .replace(/\bMatch Winner\b/gi, "")
        .replace(/\bWinner\b/gi, ""),
    );
  }

  function parseHomeAway(title, outcomes = []) {
    const t = cleanText(title);
    const parts = t.split(/\s+(?:v|vs|versus)\.?\s+/i);

    if (parts.length >= 2) {
      return {
        home: stripContext(parts[0]),
        away: stripContext(parts.slice(1).join(" v ")),
      };
    }

    const nonDraw = outcomes
      .map((o) => o.label)
      .filter((x) => !/\bdraw\b|\btie\b/i.test(x));

    return {
      home: nonDraw[0] || "",
      away: nonDraw[1] || "",
    };
  }

  function extractStartTextFromString(text) {
    const s = cleanText(text);

    const patterns = [
      /\b(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat),\s*(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2},\s+\d{1,2}:\d{2}\s*(?:AM|PM)\s*TCT\b/i,
      /\b\d{1,2}:\d{2}\s*(?:AM|PM)\s*(?:TCT|UTC|GMT)?\b/i,
      /\b\d{1,2}:\d{2}(?::\d{2})?\s*-\s*\d{1,2}\/\d{1,2}\/\d{4}\s*TCT\b/i,
      /\b\d{4}-\d{2}-\d{2}[ T]\d{1,2}:\d{2}(?::\d{2})?\b/,
      /\b\d{1,2}\/[0-3]?\d\/\d{2,4}\s+\d{1,2}:\d{2}\b/,
      /\b(?:Today|Tomorrow)\s+\d{1,2}:\d{2}\b/i,
      /\b\d{1,2}:\d{2}\s*(?:TCT|UTC|GMT)?\b/i,
    ];

    for (const p of patterns) {
      const m = s.match(p);
      if (m) return m[0];
    }

    return "";
  }

  function parseTornStartTimestamp(value) {
    if (!value) return null;

    if (typeof value === "number" && Number.isFinite(value)) {
      return value > 1e12 ? Math.floor(value / 1000) : Math.floor(value);
    }

    const s = cleanText(value);

    const exact = s.match(
      /(\d{1,2}):(\d{2})(?::(\d{2}))?\s*-\s*(\d{1,2})\/(\d{1,2})\/(\d{4})\s*TCT/i,
    );

    if (exact) {
      const hh = Number(exact[1]);
      const mm = Number(exact[2]);
      const ss = Number(exact[3] || 0);
      const dd = Number(exact[4]);
      const mo = Number(exact[5]);
      const yy = Number(exact[6]);

      return Math.floor(Date.UTC(yy, mo - 1, dd, hh, mm, ss) / 1000);
    }

    const human = s.match(
      /(?:[a-z]{3},?\s*)?([a-z]+)\s+(\d{1,2}),?\s+(\d{1,2})(?::(\d{2}))?\s*(AM|PM)\s*TCT/i,
    );

    if (human) {
      const monthName = human[1].toLowerCase();
      const dd = Number(human[2]);
      let hh = Number(human[3]);
      const mm = Number(human[4] || 0);
      const ampm = human[5].toUpperCase();

      const monthMap = {
        jan: 0,
        january: 0,
        feb: 1,
        february: 1,
        mar: 2,
        march: 2,
        apr: 3,
        april: 3,
        may: 4,
        jun: 5,
        june: 5,
        jul: 6,
        july: 6,
        aug: 7,
        august: 7,
        sep: 8,
        sept: 8,
        september: 8,
        oct: 9,
        october: 9,
        nov: 10,
        november: 10,
        dec: 11,
        december: 11,
      };

      const mo = monthMap[monthName];

      if (mo == null) return null;

      if (ampm === "PM" && hh < 12) hh += 12;
      if (ampm === "AM" && hh === 12) hh = 0;

      const now = new Date();
      let yy = now.getUTCFullYear();

      let ts = Math.floor(Date.UTC(yy, mo, dd, hh, mm, 0) / 1000);
      const nowTs = Math.floor(Date.now() / 1000);

      if (ts < nowTs - 180 * 86400) {
        yy += 1;
        ts = Math.floor(Date.UTC(yy, mo, dd, hh, mm, 0) / 1000);
      }

      if (ts > nowTs + 220 * 86400) {
        yy -= 1;
        ts = Math.floor(Date.UTC(yy, mo, dd, hh, mm, 0) / 1000);
      }

      return ts;
    }

    return null;
  }

  // ---------------------------------------------------------------------------
  // Torn internal Bookie API integration
  // ---------------------------------------------------------------------------

  function getTornRfcvToken() {
    return document.cookie.match(/(?:^|;\s*)rfc_v=([^;]+)/)?.[1] || "";
  }

  function getBookieEventIdFromUrl() {
    const hash = String(location.hash || "");

    const m = hash.match(
      /^#\/(?:football|basketball|baseball|handball|rugby|your-bets|popular)\/(\d+)/i,
    );

    if (m) return m[1];

    return "";
  }

  function getBookieApiCacheKey() {
    const sport = currentBookieSport();
    const eventId = getBookieEventIdFromUrl();

    if (!sport || sport === "unknown" || !eventId) return "";

    return `${sport}:${eventId}`;
  }

  function canUseTornBookieApi() {
    const sport = currentBookieSport();
    const eventId = getBookieEventIdFromUrl();
    const token = getTornRfcvToken();

    return !!(token && eventId && ADVISOR_SPORTS.includes(sport));
  }

  async function tornBookieApi(action, params = {}, opts = {}) {
    const rfcv = getTornRfcvToken();

    if (!rfcv) {
      throw new Error("Torn rfc_v token not found.");
    }

    const url = `https://www.torn.com/page.php?sid=bookieApi&rfcv=${encodeURIComponent(rfcv)}`;

    let body;

    if (opts.formData) {
      body = new FormData();
      body.append("sid", "bookieApi");
      body.append("rfcv", rfcv);
      body.append("action", action);

      for (const [k, v] of Object.entries(params || {})) {
        body.append(k, v);
      }
    } else {
      body = new URLSearchParams({
        sid: "bookieApi",
        rfcv,
        action,
        ...params,
      });
    }

    const res = await fetch(url, {
      method: "POST",
      credentials: "same-origin",
      headers: {
        "X-Requested-With": "XMLHttpRequest",
      },
      body,
    });

    const text = await res.text();

    let data;

    try {
      data = JSON.parse(text);
    } catch (_) {
      throw new Error(
        `Torn Bookie API returned non-JSON response: ${text.slice(0, 120)}`,
      );
    }

    if (!res.ok) {
      throw new Error(`Torn Bookie API failed: HTTP ${res.status}`);
    }

    return data;
  }

  function findPrimaryApiOutcomeType(outcomeTypes, sport) {
    const list = Array.isArray(outcomeTypes) ? outcomeTypes : [];

    const valid = list.filter(
      (t) =>
        t && t.isValid !== false && Array.isArray(t.odds) && t.odds.length >= 2,
    );

    if (!valid.length) return null;

    const isNoDrawSport = sport === "basketball" || sport === "baseball";

    const preferred = valid.find((t) => {
      const name = normalizeLoose(t.outcome || "");

      if (isNoDrawSport) {
        return (
          name.includes("2 way") ||
          name.includes("2way") ||
          name.includes("full event") ||
          name.includes("moneyline") ||
          name.includes("money line")
        );
      }

      return (
        name.includes("3 way") ||
        name.includes("3way") ||
        name.includes("match winner") ||
        name.includes("ordinary time")
      );
    });

    return preferred || valid[0];
  }

  function extractApiTeams(outcomeTypes, sport) {
    const primary = findPrimaryApiOutcomeType(outcomeTypes, sport);

    if (!primary) {
      return {
        home: "",
        away: "",
        primary: null,
      };
    }

    const labels = (primary.odds || [])
      .map((o) => cleanText(o.bettingoffer || ""))
      .filter(Boolean)
      .filter((x) => !/\bdraw\b|\btie\b/i.test(x));

    return {
      home: labels[0] || "",
      away: labels[labels.length - 1] || labels[1] || "",
      primary,
    };
  }

  function parseTornBookieApiData(data, meta = {}) {
    const sport = meta.sport || currentBookieSport();
    const eventId = String(meta.eventId || getBookieEventIdFromUrl() || "");

    const outcomeTypes = Array.isArray(data?.outcomeTypes)
      ? data.outcomeTypes
      : [];
    const teamInfo = extractApiTeams(outcomeTypes, sport);

    const home = teamInfo.home || "";
    const away = teamInfo.away || "";
    const title =
      home && away ? `${home} v ${away}` : `Torn Bookie Event ${eventId}`;

    const root =
      document.querySelector('ul[class*="bets-wrap"]') ||
      document.querySelector('div[class*="info-wrap"]') ||
      document.body;

    const outcomes = [];

    let idx = 0;

    for (const type of outcomeTypes) {
      if (!type || type.isValid === false) continue;

      const marketName = cleanText(type.outcome || "Unknown Market");
      const family = marketFamily(marketName);
      const lineRaw = Number(type.dparamAbs);
      const line = Number.isFinite(lineRaw) && lineRaw > 0 ? lineRaw : null;

      for (const odd of type.odds || []) {
        if (!odd) continue;
        if (odd.active && odd.active !== "yes") continue;
        if (odd.state && odd.state !== "open") continue;
        if (odd.betIsAllowed === false) continue;

        const label = cleanText(odd.bettingoffer || "");
        const odds = Number(odd.odds || 0);

        if (!label || !Number.isFinite(odds) || odds <= 1) continue;

        const item = {
          index: idx++,
          row: null,
          input: null,
          button: null,

          label,
          odds,
          key: classifyOutcomeKey(label, home, away, marketName),

          eventId: String(odd.eventId || eventId),
          outcomeId: String(odd.outcomeId || ""),
          bettingofferId: String(odd.bettingofferId || ""),
          handicap: Number(odd.handicap || 0),

          line,
          total_line: line,

          source_market_name: marketName,
          source_market_family: family,
          source_market_index: Number(type.outcomeTypeId || 0),
          outcomeTypeId: String(type.outcomeTypeId || ""),
          outcomeScopeId: String(type.outcomeScopeId || ""),
        };

        outcomes.push(item);
      }
    }

    const keyedOutcomes = outcomes.filter((o) => o.key);

    // If the match is already started/finished, Torn returns odds as suspended.
    // We still keep those odds for display/advisor resolution only.
    // They are NOT bettable because row/input/button are null and action will be forced to SKIP later.
    const closedAdvisorOutcomes = [];
    let closedIdx = 0;

    if (!keyedOutcomes.length) {
      for (const type of outcomeTypes) {
        if (!type || type.isValid === false) continue;

        const marketName = cleanText(type.outcome || "Unknown Market");
        const family = marketFamily(marketName);
        const lineRaw = Number(type.dparamAbs);
        const line = Number.isFinite(lineRaw) && lineRaw > 0 ? lineRaw : null;

        for (const odd of type.odds || []) {
          if (!odd) continue;

          const label = cleanText(odd.bettingoffer || "");
          const odds = Number(odd.odds || 0);

          if (!label || !Number.isFinite(odds) || odds <= 1) continue;

          const key = classifyOutcomeKey(label, home, away, marketName);

          if (!key) continue;

          closedAdvisorOutcomes.push({
            index: closedIdx++,
            row: null,
            input: null,
            button: null,

            label,
            odds,
            key,

            eventId: String(odd.eventId || eventId),
            outcomeId: String(odd.outcomeId || ""),
            bettingofferId: String(odd.bettingofferId || ""),
            handicap: Number(odd.handicap || 0),

            line,
            total_line: line,

            source_market_name: marketName,
            source_market_family: family,
            source_market_index: Number(type.outcomeTypeId || 0),
            outcomeTypeId: String(type.outcomeTypeId || ""),
            outcomeScopeId: String(type.outcomeScopeId || ""),

            torn_active: String(odd.active || ""),
            torn_state: String(odd.state || ""),
            torn_is_live: String(odd.is_live || ""),
            torn_status: String(odd.status || ""),
            torn_result: String(odd.result || ""),
            torn_bet_is_allowed: odd.betIsAllowed !== false,
          });
        }
      }
    }

    const useClosedAdvisorOutcomes =
      !keyedOutcomes.length && closedAdvisorOutcomes.length > 0;

    const finalKeyedOutcomes = useClosedAdvisorOutcomes
      ? closedAdvisorOutcomes
      : keyedOutcomes;

    const publicOutcomes = finalKeyedOutcomes.map((o) => ({
      ...toPublicOutcome(o),
      bettingofferId: o.bettingofferId,
      eventId: o.eventId,
      outcomeId: o.outcomeId,
    }));

    const oddsObj = {};

    for (const o of publicOutcomes) {
      if (o.key && !oddsObj[o.key]) oddsObj[o.key] = o.odds;
    }

    const marketType =
      sport === "football"
        ? inferMarketType("Primary Football Bundle", publicOutcomes)
        : inferMarketType(
            teamInfo.primary?.outcome || "Match Winner",
            publicOutcomes,
          );

    return {
      root,
      title,
      marketName: teamInfo.primary?.outcome || "Bookie API Market",
      home,
      away,
      leagueText: "",
      startText: findStartText(root),
      eventId,
      sport,
      marketSport: sport,
      marketType,
      outcomes: finalKeyedOutcomes,
      publicOutcomes,
      closedAdvisorOutcomesUsed: useClosedAdvisorOutcomes,
      marketClosedForAdvisorOnly: useClosedAdvisorOutcomes,
      oddsObj,
      parsedAt: new Date().toISOString(),
      parser: "torn_bookie_api",
      rawOutcomeTypesCount: outcomeTypes.length,
    };
  }

  function getCachedTornBookieApiParsed(maxAgeMs = 15000) {
    const cache = state.bookieApiCache;
    const key = getBookieApiCacheKey();

    if (!cache || !key || cache.key !== key) return null;
    if (Date.now() - Number(cache.at || 0) > maxAgeMs) return null;

    return cache.parsed || null;
  }

  async function refreshTornBookieApiMarketIfPossible(
    reason = "api",
    opts = {},
  ) {
    if (!canUseTornBookieApi()) return null;

    const key = getBookieApiCacheKey();

    if (!opts.force) {
      const cached = getCachedTornBookieApiParsed();
      if (cached) return cached;
    }

    const sport = currentBookieSport();
    const eventId = getBookieEventIdFromUrl();

    const data = await tornBookieApi("getEventOutcomeTypes", {
      gamebox: sport,
      eventId,
    });

    const parsed = parseTornBookieApiData(data, {
      sport,
      eventId,
      reason,
    });

    state.bookieApiCache = {
      key,
      at: Date.now(),
      parsed,
      raw: data,
    };

    return parsed;
  }

  // ---------------------------------------------------------------------------
  // Torn market DOM parsers
  // ---------------------------------------------------------------------------

  function findStartText(root) {
    if (!root) return "";
    return extractStartTextFromString(root.innerText || root.textContent || "");
  }

  function findLeagueText(root) {
    if (!root) return "";

    const candidates = [
      ...root.querySelectorAll(
        '[class*="league" i], [class*="competition" i], [class*="country" i], [class*="breadcrumb" i]',
      ),
    ]
      .filter(isVisible)
      .map((el) => cleanText(el.innerText || el.textContent || ""))
      .filter((t) => t.length >= 3 && t.length <= 180);

    return candidates[0] || "";
  }

  function findTitle(root) {
    if (!root) return "";

    const selectors = [
      'li[class*="matchName"]',
      'div[class*="matchName"]',
      '[class*="eventName"]',
      '[class*="match-title"]',
      '[class*="matchTitle"]',
      "h1",
      "h2",
      "h3",
      '[class*="title" i]',
    ];

    for (const sel of selectors) {
      const nodes = [...root.querySelectorAll(sel)].filter(isVisible);
      for (const n of nodes) {
        const t = cleanText(n.innerText || n.textContent || "");
        if (looksLikeMatchTitle(t)) return t;
      }
    }

    const text = cleanText(root.innerText || root.textContent || "");
    const m = text.match(
      /([A-Za-zÀ-ž0-9 .'-]+)\s+(?:v|vs|versus)\.?\s+([A-Za-zÀ-ž0-9 .'-]+)/i,
    );
    if (m) return cleanMatchTitleText(m[0]);

    return cleanText(document.title.replace(/Torn/i, "").replace(/[-|]/g, " "));
  }

  function findMatchTitleAround(el) {
    const infoWrap =
      el.closest('div[class*="info-wrap"]') ||
      el.closest("li") ||
      document.body;

    let node = el;
    for (let i = 0; i < 7 && node && node !== document.body; i++) {
      const t = findTitle(node);
      if (looksLikeMatchTitle(t)) return t;
      node = node.parentElement;
    }

    const t = findTitle(infoWrap);
    if (looksLikeMatchTitle(t)) return cleanMatchTitleText(t);

    return findTitle(document.body);
  }

  function getRowName(row) {
    const desc = row.querySelector('div[class*="description"] span');

    if (desc && cleanText(desc.textContent)) {
      return cleanText(desc.textContent);
    }

    const desc2 = row.querySelector('[class*="description"]');

    if (desc2 && cleanText(desc2.textContent)) {
      const t = cleanText(desc2.textContent);
      if (t && !/^odds/i.test(t) && !/^x\s*\d/i.test(t)) return t;
    }

    const text = row.textContent || "";
    const lines = text
      .split("\n")
      .map((x) => cleanText(x))
      .filter(Boolean);

    const filtered = lines.filter(
      (x) =>
        !/^odds/i.test(x) &&
        !/^multiplier/i.test(x) &&
        !/^x\s*\d/i.test(x) &&
        !/^\d+\s*\/\s*\d+$/.test(x) &&
        x !== "$" &&
        !/^BET$/i.test(x) &&
        !/^Yes$/i.test(x) &&
        !/^No$/i.test(x) &&
        !/\b\d{1,2}\.\d{1,4}\b/.test(x),
    );

    return filtered[filtered.length - 1] || "Unknown";
  }

  function getAllAmountInputs(row) {
    return [
      ...row.querySelectorAll(
        'input.amount.input-money, input[class*="amount"][class*="input-money"], input[class*="input-money"], input[type="text"], input[type="tel"], input[type="number"]',
      ),
    ].filter((input) => {
      const type = String(input.type || "").toLowerCase();
      return ["text", "number", "tel", ""].includes(type);
    });
  }

  function getVisibleTextInput(row) {
    const inputs = getAllAmountInputs(row);
    return inputs.find(isVisible) || inputs[0] || null;
  }

  function getBetButton(row) {
    return (
      row.querySelector('button[class*="betButton"]') ||
      row.querySelector('button[title="BET"]') ||
      row.querySelector("button.input-btn") ||
      [
        ...row.querySelectorAll(
          'button, input[type="button"], input[type="submit"], a',
        ),
      ].find((el) =>
        /\bBET\b/i.test(
          cleanText(el.innerText || el.value || el.textContent || ""),
        ),
      ) ||
      null
    );
  }

  function looksLikeMarketHeadingText(text) {
    const t = cleanText(text);
    const n = normalizeLoose(t);

    if (!t || t.length < 3 || t.length > 220) return false;
    if (n === "bet" || n === "yes" || n === "no") return false;
    if (/\bx\s*\d+(?:\.\d+)?/i.test(t)) return false;

    return (
      n.includes("2 way") ||
      n.includes("2way") ||
      n.includes("full event") ||
      n.includes("3 way") ||
      n.includes("3way") ||
      n.includes("match winner") ||
      n === "winner" ||
      n.includes("winner ordinary time") ||
      n.includes("ordinary time winner") ||
      n.includes("moneyline") ||
      n.includes("money line") ||
      n.includes("double chance") ||
      n.includes("draw no bet") ||
      n.includes("win to nil") ||
      n.includes("both teams to score") ||
      n.includes("total goals") ||
      n.includes("over under") ||
      n.includes("under over") ||
      n.includes("handicap") ||
      n.includes("ordinary time")
    );
  }

  function findMarketTitleForBetsWrap(betsWrap) {
    const wrap =
      betsWrap.closest('div[class*="info-wrap"]') ||
      betsWrap.closest("li") ||
      betsWrap.parentElement ||
      document.body;

    const betsRect = betsWrap.getBoundingClientRect();

    const headingCandidates = [
      ...wrap.querySelectorAll("li, div, h2, h3, h4, span"),
    ]
      .filter(isVisible)
      .filter((el) => !el.closest('ul[class*="bets-wrap"]'))
      .map((el) => {
        const text = cleanText(el.textContent || el.innerText || "");
        const rect = el.getBoundingClientRect();

        return {
          el,
          text,
          rect,
          distance: Math.abs(betsRect.top - rect.bottom),
        };
      })
      .filter((x) => looksLikeMarketHeadingText(x.text))
      .filter((x) => x.rect.bottom <= betsRect.top + 14)
      .filter((x) => betsRect.top - x.rect.bottom < 180)
      .sort((a, b) => a.distance - b.distance);

    if (headingCandidates.length) return headingCandidates[0].text;

    let node = betsWrap.previousElementSibling;

    while (node) {
      const txt = cleanText(node.textContent || node.innerText || "");
      if (looksLikeMarketHeadingText(txt)) return txt;
      node = node.previousElementSibling;
    }

    return "Unknown Market";
  }

  function getDirectBetRows(betsWrap) {
    let rows = [...betsWrap.children].filter(
      (el) => el.matches && el.matches('li[class*="bets"]'),
    );

    if (!rows.length)
      rows = [...betsWrap.querySelectorAll('li[class*="bets"]')];

    return rows.filter(isDomVisible);
  }

  function parseRowsFromBetsWrap(betsWrap) {
    const betRows = getDirectBetRows(betsWrap);

    return betRows
      .map((row, index) => {
        const oddsEl = row.querySelector('div[class*="multiplier"]');
        const odds = parseOddsFromText(
          oddsEl ? oddsEl.textContent : row.textContent,
        );
        const label = getRowName(row);
        const input = getVisibleTextInput(row);
        const button = getBetButton(row);

        return {
          index,
          row,
          label,
          odds,
          input,
          button,
          key: "",
        };
      })
      .filter((x) => x.odds > 1 && x.label && x.label !== "Unknown");
  }

  function marketFamily(marketName) {
    const m = normalizeLoose(marketName);

    if (m.includes("draw no bet")) return "draw_no_bet";
    if (m.includes("double chance")) return "double_chance";

    if (
      m.includes("over under") ||
      m.includes("under over") ||
      m.includes("total points") ||
      m.includes("total score") ||
      m.includes("game total") ||
      m.includes("match total") ||
      m.includes("total goals") ||
      m === "totals" ||
      m === "total"
    ) {
      return "under_over";
    }

    if (
      m.includes("2 way") ||
      m.includes("2way") ||
      m.includes("full event") ||
      m.includes("3 way") ||
      m.includes("3way") ||
      m.includes("match winner") ||
      m === "winner" ||
      m.includes("winner ordinary time") ||
      m.includes("ordinary time winner") ||
      m.includes("moneyline") ||
      m.includes("money line")
    ) {
      return "match_winner";
    }

    return "other";
  }

  function isPrimaryFootballMarketName(marketName) {
    const family = marketFamily(marketName);
    return (
      family === "match_winner" ||
      family === "draw_no_bet" ||
      family === "double_chance"
    );
  }

  function classifyOutcomeKey(label, home, away, marketName) {
    const raw = cleanText(label);
    const rawCompact = raw.replace(/\s+/g, "").toUpperCase();

    const l = normalizeLoose(label);
    const h = normalizeLoose(home);
    const a = normalizeLoose(away);
    const m = normalizeLoose(marketName);

    if (rawCompact === "1") return "H";
    if (rawCompact === "X") return "D";
    if (rawCompact === "2") return "A";
    if (rawCompact === "1X" || rawCompact === "X1") return "HD";
    if (rawCompact === "X2" || rawCompact === "2X") return "AD";
    if (rawCompact === "12" || rawCompact === "21") return "HA";

    if (m.includes("draw no bet")) {
      if (h && (l === h || l.includes(h) || h.includes(l))) return "H_DNB";
      if (a && (l === a || l.includes(a) || a.includes(l))) return "A_DNB";
    }

    if (/\bdraw\b|\btie\b/.test(l) && !/(or|and|double|no bet)/.test(l))
      return "D";

    if (
      l.includes("home draw") ||
      l.includes("home or draw") ||
      l.includes("draw or home") ||
      l.includes("1x") ||
      (h && l.includes(h) && l.includes("draw"))
    ) {
      return "HD";
    }

    if (
      l.includes("away draw") ||
      l.includes("away or draw") ||
      l.includes("draw or away") ||
      l.includes("x2") ||
      (a && l.includes(a) && l.includes("draw"))
    ) {
      return "AD";
    }

    if (
      l.includes("home away") ||
      l.includes("home or away") ||
      l.includes("away or home") ||
      l.includes("12") ||
      (h && a && l.includes(h) && l.includes(a))
    ) {
      return "HA";
    }

    if (/\bover\b/.test(l)) return "OVER";
    if (/\bunder\b/.test(l)) return "UNDER";

    if (h && (l === h || l.includes(h) || h.includes(l))) return "H";
    if (a && (l === a || l.includes(a) || a.includes(l))) return "A";

    return "";
  }

  function assignKeysForMarketGroup(rows, marketName, home, away) {
    const family = marketFamily(marketName);

    const out = rows.map((o, idx) => ({
      ...o,
      key: classifyOutcomeKey(o.label, home, away, marketName),
      source_market_name: marketName,
      source_market_family: family,
      source_market_index: idx,
    }));

    if (family === "match_winner" && out.length === 3) {
      out[0].key = "H";
      out[1].key = "D";
      out[2].key = "A";
    }

    if (
      family === "match_winner" &&
      out.length === 2 &&
      ["basketball", "baseball"].includes(currentBookieSport())
    ) {
      out[0].key = "H";
      out[1].key = "A";
    }

    if (family === "draw_no_bet" && out.length === 2) {
      out[0].key = "H_DNB";
      out[1].key = "A_DNB";
    }

    if (family === "double_chance" && out.length === 3) {
      out[0].key = "HD";
      out[1].key = "HA";
      out[2].key = "AD";
    }

    return out;
  }

  function parseOverUnderLineFromText(...texts) {
    const s = cleanText(texts.filter(Boolean).join(" "));

    if (!s) return null;

    const contextual = s.match(
      /\b(?:over|under|o\/u|total(?:\s+points|\s+score|\s+goals)?|line)\D{0,24}(\d{1,3}(?:\.\d+)?)/i,
    );

    if (contextual) {
      const n = Number(contextual[1]);
      if (Number.isFinite(n) && n > 5 && n < 400) return n;
    }

    const nums = [...s.matchAll(/\b(\d{1,3}(?:\.\d+)?)\b/g)]
      .map((m) => Number(m[1]))
      .filter((n) => Number.isFinite(n) && n > 5 && n < 400);

    return nums.length ? nums[0] : null;
  }

  function decorateBasketballOverUnderOutcome(row, marketName) {
    const key = row.key || classifyOutcomeKey(row.label, "", "", marketName);

    if (key !== "OVER" && key !== "UNDER") {
      return row;
    }

    const line = parseOverUnderLineFromText(row.label, marketName);

    return {
      ...row,
      key,
      line,
      total_line: line,
      source_market_name: row.source_market_name || marketName,
      source_market_family: "under_over",
    };
  }

  function toPublicOutcome(o) {
    return {
      index: o.index,
      key: o.key,
      label: o.label,
      odds: o.odds,
      source_market_name: o.source_market_name,
      source_market_family: o.source_market_family,
      source_market_index: o.source_market_index,
      line: o.line ?? o.total_line ?? null,
      total_line: o.total_line ?? o.line ?? null,
    };
  }

  function hasBasketballMoneyline(parsed) {
    const keys = new Set((parsed?.publicOutcomes || []).map((o) => o.key));
    return keys.has("H") && keys.has("A");
  }

  function hasBasketballOverUnder(parsed) {
    const keys = new Set((parsed?.publicOutcomes || []).map((o) => o.key));
    return keys.has("OVER") && keys.has("UNDER");
  }

  function getBasketballOverUnderFromParsed(parsed) {
    const outcomes = parsed?.publicOutcomes || parsed?.outcomes || [];

    const over = outcomes.find((o) => o.key === "OVER") || null;
    const under = outcomes.find((o) => o.key === "UNDER") || null;

    if (!over && !under) return null;

    const line =
      parsed?.basketball_over_under?.line ??
      over?.line ??
      over?.total_line ??
      under?.line ??
      under?.total_line ??
      parseOverUnderLineFromText(
        over?.label,
        under?.label,
        over?.source_market_name,
        under?.source_market_name,
        parsed?.marketName,
      );

    return {
      line: line ?? null,
      over: over
        ? {
            key: "OVER",
            label: over.label || "Over",
            odds: Number(over.odds || 0),
            line: over.line ?? over.total_line ?? line ?? null,
          }
        : null,
      under: under
        ? {
            key: "UNDER",
            label: under.label || "Under",
            odds: Number(under.odds || 0),
            line: under.line ?? under.total_line ?? line ?? null,
          }
        : null,
    };
  }

  function parseBasketballBundle() {
    const betsWraps = [
      ...document.querySelectorAll('ul[class*="bets-wrap"]'),
    ].filter(isDomVisible);

    const groups = [];

    for (const betsWrap of betsWraps) {
      const rows = parseRowsFromBetsWrap(betsWrap);

      if (rows.length < 2 || rows.length > 4) continue;

      const title = findMatchTitleAround(betsWrap);
      if (!looksLikeMatchTitle(title)) continue;

      const marketName = findMarketTitleForBetsWrap(betsWrap);
      const ha = parseHomeAway(title, rows);

      if (!ha.home || !ha.away) continue;

      let keyedRows = assignKeysForMarketGroup(
        rows,
        marketName,
        ha.home,
        ha.away,
      );

      const keys = new Set(keyedRows.map((o) => o.key).filter(Boolean));

      const isTotal =
        rows.length === 2 && keys.has("OVER") && keys.has("UNDER");

      const isMoneyline =
        rows.length === 2 && keys.has("H") && keys.has("A") && !isTotal;

      if (!isMoneyline && !isTotal) continue;

      if (isTotal) {
        keyedRows = keyedRows.map((o) =>
          decorateBasketballOverUnderOutcome(o, marketName),
        );
      }

      const rect = betsWrap.getBoundingClientRect();

      groups.push({
        root: betsWrap,
        title,
        titleNorm: normalizeLoose(title),
        marketName,
        family: isTotal ? "under_over" : "match_winner",
        rows,
        keyedRows,
        isMoneyline,
        isTotal,
        score:
          scoreBetsWrapAsActiveMarket(betsWrap, rows, marketName) +
          (isMoneyline ? 100000 : 1000),
        rectTop: Math.round(rect.top),
      });
    }

    if (!groups.length) return null;

    const moneylineGroups = groups
      .filter((g) => g.isMoneyline)
      .sort((a, b) => b.score - a.score);

    if (!moneylineGroups.length) return null;

    const main = moneylineGroups[0];
    const sameMatchGroups = groups.filter(
      (g) => g.titleNorm === main.titleNorm,
    );

    const moneyline = sameMatchGroups
      .filter((g) => g.isMoneyline)
      .sort((a, b) => b.score - a.score)[0];

    const totalGroup =
      sameMatchGroups
        .filter((g) => g.isTotal)
        .sort((a, b) => b.score - a.score)[0] || null;

    if (!moneyline) return null;

    const wantedOrder = ["H", "A", "OVER", "UNDER"];

    let outcomes = [];

    outcomes.push(
      ...moneyline.keyedRows.filter((o) => o.key === "H" || o.key === "A"),
    );

    if (totalGroup) {
      outcomes.push(
        ...totalGroup.keyedRows.filter(
          (o) => o.key === "OVER" || o.key === "UNDER",
        ),
      );
    }

    outcomes = outcomes
      .filter((o) => wantedOrder.includes(o.key))
      .sort((a, b) => wantedOrder.indexOf(a.key) - wantedOrder.indexOf(b.key));

    const seen = new Set();

    outcomes = outcomes.filter((o) => {
      const id = `${o.key}|${normalizeLoose(o.label)}|${Number(o.odds || 0).toFixed(4)}`;
      if (seen.has(id)) return false;
      seen.add(id);
      return true;
    });

    const oddsObj = {};

    for (const o of outcomes) {
      if (o.key && !oddsObj[o.key]) oddsObj[o.key] = o.odds;
    }

    const publicOutcomes = outcomes.map(toPublicOutcome);
    const basketballOverUnder = getBasketballOverUnderFromParsed({
      publicOutcomes,
    });

    const startText =
      sameMatchGroups
        .map((g) => extractStartTextFromString(g.marketName))
        .find(Boolean) ||
      sameMatchGroups.map((g) => findStartText(g.root)).find(Boolean) ||
      extractStartTextFromString(document.body.innerText || "");

    const ha = parseHomeAway(main.title, moneyline.rows);

    return {
      root: moneyline.root,
      title: main.title,
      marketName: totalGroup
        ? "Basketball Bundle: Moneyline + Over/Under"
        : "Basketball Moneyline",
      home: ha.home,
      away: ha.away,
      leagueText: findLeagueText(moneyline.root),
      startText,
      marketType: "match_winner",
      outcomes,
      publicOutcomes,
      oddsObj,
      basketball_over_under: basketballOverUnder,
      parsedAt: new Date().toISOString(),
      parser: totalGroup
        ? "basketball_bundle_moneyline_plus_over_under"
        : "basketball_moneyline_only",
      groups: sameMatchGroups.map((g) => ({
        marketName: g.marketName,
        family: g.family,
        rows: g.rows.length,
      })),
    };
  }

  function parseBaseballMoneyline() {
    const betsWraps = [
      ...document.querySelectorAll('ul[class*="bets-wrap"]'),
    ].filter(isDomVisible);

    const candidates = [];

    for (const betsWrap of betsWraps) {
      const rows = parseRowsFromBetsWrap(betsWrap);

      if (rows.length !== 2) continue;

      const title = findMatchTitleAround(betsWrap);
      if (!looksLikeMatchTitle(title)) continue;

      const marketName = findMarketTitleForBetsWrap(betsWrap);
      const ha = parseHomeAway(title, rows);

      if (!ha.home || !ha.away) continue;

      const keyedRows = assignKeysForMarketGroup(
        rows,
        marketName,
        ha.home,
        ha.away,
      );
      const keys = new Set(keyedRows.map((o) => o.key).filter(Boolean));

      const isMoneyline =
        keys.has("H") &&
        keys.has("A") &&
        !keys.has("OVER") &&
        !keys.has("UNDER");

      if (!isMoneyline) continue;

      const rect = betsWrap.getBoundingClientRect();

      candidates.push({
        root: betsWrap,
        title,
        marketName,
        rows,
        keyedRows,
        score: scoreBetsWrapAsActiveMarket(betsWrap, rows, marketName) + 100000,
        rectTop: Math.round(rect.top),
      });
    }

    if (!candidates.length) return null;

    candidates.sort((a, b) => b.score - a.score);

    const best = candidates[0];
    const ha = parseHomeAway(best.title, best.rows);

    let outcomes = best.keyedRows
      .filter((o) => o.key === "H" || o.key === "A")
      .sort((a, b) => ["H", "A"].indexOf(a.key) - ["H", "A"].indexOf(b.key));

    const seen = new Set();

    outcomes = outcomes.filter((o) => {
      const id = `${o.key}|${normalizeLoose(o.label)}|${Number(o.odds || 0).toFixed(4)}`;
      if (seen.has(id)) return false;
      seen.add(id);
      return true;
    });

    const oddsObj = {};

    for (const o of outcomes) {
      if (o.key && !oddsObj[o.key]) oddsObj[o.key] = o.odds;
    }

    const publicOutcomes = outcomes.map(toPublicOutcome);

    const startText =
      extractStartTextFromString(best.marketName) ||
      findStartText(best.root) ||
      extractStartTextFromString(document.body.innerText || "");

    return {
      root: best.root,
      title: best.title,
      marketName: "Baseball Moneyline",
      home: ha.home,
      away: ha.away,
      leagueText: findLeagueText(best.root),
      startText,
      marketType: "match_winner",
      outcomes,
      publicOutcomes,
      oddsObj,
      parsedAt: new Date().toISOString(),
      parser: "baseball_moneyline",
      groups: [
        {
          marketName: best.marketName,
          family: "match_winner",
          rows: best.rows.length,
        },
      ],
    };
  }

  function inferMarketType(marketName, outcomes) {
    const m = normalizeLoose(marketName);

    if (m.includes("primary football bundle")) return "football_bundle";
    if (m.includes("draw no bet")) return "draw_no_bet";
    if (m.includes("double chance")) return "double_chance";
    if (m.includes("over") || m.includes("under") || m.includes("total"))
      return "under_over";

    if (
      outcomes.some((o) => ["H_DNB", "A_DNB", "HD", "AD", "HA"].includes(o.key))
    ) {
      return "football_bundle";
    }

    return "match_winner";
  }

  function sideFromLabel(label, home, away) {
    const l = normalizeLoose(label);
    const h = normalizeLoose(home);
    const a = normalizeLoose(away);

    if (/\bdraw\b|\btie\b/.test(l) && !/\bor\b/.test(l)) return "D";

    if (
      (h && l.includes(h) && l.includes("draw")) ||
      l.includes("home or draw") ||
      l.includes("draw or home") ||
      l.includes("1x")
    ) {
      return "HD";
    }

    if (
      (a && l.includes(a) && l.includes("draw")) ||
      l.includes("away or draw") ||
      l.includes("draw or away") ||
      l.includes("x2")
    ) {
      return "AD";
    }

    if (
      (h && a && l.includes(h) && l.includes(a)) ||
      l.includes("home or away") ||
      l.includes("away or home") ||
      l.includes("12")
    ) {
      return "HA";
    }

    if (h && (l === h || l.includes(h) || h.includes(l))) return "H";
    if (a && (l === a || l.includes(a) || a.includes(l))) return "A";

    return "";
  }

  function findSequence(
    rows,
    pattern,
    home,
    away,
    startAt = 0,
    endBefore = Infinity,
  ) {
    for (
      let i = startAt;
      i <= rows.length - pattern.length && i < endBefore;
      i++
    ) {
      let ok = true;

      for (let j = 0; j < pattern.length; j++) {
        const side = sideFromLabel(rows[i + j].label, home, away);
        if (side !== pattern[j]) {
          ok = false;
          break;
        }
      }

      if (ok) return i;
    }

    return -1;
  }

  function makeBundleOutcome(row, key, family, sourceMarketName, sourceIndex) {
    return {
      ...row,
      key,
      source_market_name: sourceMarketName,
      source_market_family: family,
      source_market_index: sourceIndex,
    };
  }

  function buildFlatFootballBundleFromRows(root, rows, title) {
    if (!rows || rows.length < 6) return null;

    const ha = parseHomeAway(title, rows);
    if (!ha.home || !ha.away) return null;

    // پیدا کردن 3-Way: Home / Draw / Away
    const mwStart = findSequence(rows, ["H", "D", "A"], ha.home, ha.away, 0);

    // پیدا کردن Double Chance: HD / HA / AD
    const dcStart = findSequence(
      rows,
      ["HD", "HA", "AD"],
      ha.home,
      ha.away,
      mwStart >= 0 ? mwStart + 3 : 0,
    );

    if (mwStart < 0 || dcStart < 0) return null;

    // پیدا کردن Draw No Bet: اولین H/A pair بین 3-Way و Double Chance
    const dnbStart = findSequence(
      rows,
      ["H", "A"],
      ha.home,
      ha.away,
      mwStart + 3,
      dcStart,
    );

    const outcomes = [];

    outcomes.push(
      makeBundleOutcome(
        rows[mwStart],
        "H",
        "match_winner",
        "3-Way Ordinary time",
        0,
      ),
    );
    outcomes.push(
      makeBundleOutcome(
        rows[mwStart + 1],
        "D",
        "match_winner",
        "3-Way Ordinary time",
        1,
      ),
    );
    outcomes.push(
      makeBundleOutcome(
        rows[mwStart + 2],
        "A",
        "match_winner",
        "3-Way Ordinary time",
        2,
      ),
    );

    if (dnbStart >= 0) {
      outcomes.push(
        makeBundleOutcome(
          rows[dnbStart],
          "H_DNB",
          "draw_no_bet",
          "Draw No Bet Ordinary time",
          0,
        ),
      );
      outcomes.push(
        makeBundleOutcome(
          rows[dnbStart + 1],
          "A_DNB",
          "draw_no_bet",
          "Draw No Bet Ordinary time",
          1,
        ),
      );
    }

    outcomes.push(
      makeBundleOutcome(
        rows[dcStart],
        "HD",
        "double_chance",
        "Double Chance Ordinary time",
        0,
      ),
    );
    outcomes.push(
      makeBundleOutcome(
        rows[dcStart + 1],
        "HA",
        "double_chance",
        "Double Chance Ordinary time",
        1,
      ),
    );
    outcomes.push(
      makeBundleOutcome(
        rows[dcStart + 2],
        "AD",
        "double_chance",
        "Double Chance Ordinary time",
        2,
      ),
    );

    const wantedOrder = ["H", "D", "A", "H_DNB", "A_DNB", "HD", "HA", "AD"];

    outcomes.sort(
      (a, b) => wantedOrder.indexOf(a.key) - wantedOrder.indexOf(b.key),
    );

    const oddsObj = {};
    for (const o of outcomes) {
      oddsObj[o.key] = o.odds;
    }

    const fullText = cleanText(
      root?.innerText || root?.textContent || document.body.innerText || "",
    );

    return {
      root,
      title,
      marketName:
        "Primary Football Bundle: 3-Way + Draw No Bet + Double Chance",
      home: ha.home,
      away: ha.away,
      leagueText: findLeagueText(root),
      startText: extractStartTextFromString(fullText),
      marketType: "football_bundle",
      outcomes,
      publicOutcomes: outcomes.map((o) => ({
        index: o.index,
        key: o.key,
        label: o.label,
        odds: o.odds,
        source_market_name: o.source_market_name,
        source_market_family: o.source_market_family,
        source_market_index: o.source_market_index,
      })),
      oddsObj,
      parsedAt: new Date().toISOString(),
      parser: "primary_football_bundle_flat_rows",
      groups: [
        { marketName: "3-Way Ordinary time", family: "match_winner", rows: 3 },
        ...(dnbStart >= 0
          ? [
              {
                marketName: "Draw No Bet Ordinary time",
                family: "draw_no_bet",
                rows: 2,
              },
            ]
          : []),
        {
          marketName: "Double Chance Ordinary time",
          family: "double_chance",
          rows: 3,
        },
      ],
    };
  }

  function parsePrimaryFootballBundle() {
    const betsWraps = [
      ...document.querySelectorAll('ul[class*="bets-wrap"]'),
    ].filter(isDomVisible);

    // حالت اول: Torn همه marketهای اصلی را داخل یک bets-wrap بزرگ گذاشته.
    // این دقیقاً همان حالتی است که الان برای تو رخ داده.
    const flatCandidates = [];

    for (const betsWrap of betsWraps) {
      const rows = parseRowsFromBetsWrap(betsWrap);
      if (rows.length < 6) continue;

      const title = findMatchTitleAround(betsWrap);
      if (!looksLikeMatchTitle(title)) continue;

      const bundle = buildFlatFootballBundleFromRows(betsWrap, rows, title);

      if (bundle) {
        const rect = betsWrap.getBoundingClientRect();
        flatCandidates.push({
          bundle,
          score:
            rows.length * 10 -
            Math.abs(rect.top + rect.height / 2 - window.innerHeight * 0.5),
        });
      }
    }

    if (flatCandidates.length) {
      flatCandidates.sort((a, b) => b.score - a.score);
      return flatCandidates[0].bundle;
    }

    // حالت دوم: هر market یک bets-wrap جدا دارد.
    const groups = [];

    for (const betsWrap of betsWraps) {
      const marketName = findMarketTitleForBetsWrap(betsWrap);
      const family = marketFamily(marketName);

      if (!isPrimaryFootballMarketName(marketName)) continue;

      const rows = parseRowsFromBetsWrap(betsWrap);

      if (rows.length < 2 || rows.length > 4) continue;

      const title = findMatchTitleAround(betsWrap);
      if (!looksLikeMatchTitle(title)) continue;

      const rect = betsWrap.getBoundingClientRect();

      groups.push({
        root: betsWrap,
        title,
        marketName,
        family,
        rows,
        rectTop: Math.round(rect.top),
        centerDistance: Math.abs(
          rect.top + rect.height / 2 - window.innerHeight * 0.5,
        ),
      });
    }

    if (!groups.length) return null;

    const scored = groups
      .map((g) => ({
        ...g,
        score:
          -g.centerDistance +
          g.rows.length * 10 +
          (g.family === "match_winner" ? 8 : 0),
      }))
      .sort((a, b) => b.score - a.score);

    const mainTitle = scored[0].title;
    const mainNorm = normalizeLoose(mainTitle);

    const sameMatchGroups = groups.filter(
      (g) => normalizeLoose(g.title) === mainNorm,
    );

    if (!sameMatchGroups.length) return null;

    const title = mainTitle;
    const ha = parseHomeAway(title, []);

    const preferredOrder = {
      match_winner: 1,
      draw_no_bet: 2,
      double_chance: 3,
    };

    sameMatchGroups.sort((a, b) => {
      const oa = preferredOrder[a.family] || 99;
      const ob = preferredOrder[b.family] || 99;
      if (oa !== ob) return oa - ob;
      return a.rectTop - b.rectTop;
    });

    let outcomes = [];

    for (const group of sameMatchGroups) {
      const keyed = assignKeysForMarketGroup(
        group.rows,
        group.marketName,
        ha.home,
        ha.away,
      );
      outcomes.push(...keyed);
    }

    const wantedOrder = ["H", "D", "A", "H_DNB", "A_DNB", "HD", "HA", "AD"];

    outcomes = outcomes
      .filter((o) => wantedOrder.includes(o.key))
      .sort((a, b) => wantedOrder.indexOf(a.key) - wantedOrder.indexOf(b.key));

    const seen = new Set();

    outcomes = outcomes.filter((o) => {
      if (seen.has(o.key)) return false;
      seen.add(o.key);
      return true;
    });

    if (outcomes.length < 3) return null;

    const oddsObj = {};

    for (const o of outcomes) {
      oddsObj[o.key] = o.odds;
    }

    const startText =
      sameMatchGroups
        .map((g) => extractStartTextFromString(g.marketName))
        .find(Boolean) ||
      sameMatchGroups.map((g) => findStartText(g.root)).find(Boolean) ||
      extractStartTextFromString(document.body.innerText || "");

    return {
      root: sameMatchGroups[0]?.root || scored[0].root,
      title,
      marketName:
        "Primary Football Bundle: 3-Way + Draw No Bet + Double Chance",
      home: ha.home,
      away: ha.away,
      leagueText: findLeagueText(sameMatchGroups[0]?.root || scored[0].root),
      startText,
      marketType: "football_bundle",
      outcomes,
      publicOutcomes: outcomes.map((o) => ({
        index: o.index,
        key: o.key,
        label: o.label,
        odds: o.odds,
        source_market_name: o.source_market_name,
        source_market_family: o.source_market_family,
        source_market_index: o.source_market_index,
      })),
      oddsObj,
      parsedAt: new Date().toISOString(),
      parser: "primary_football_bundle",
      groups: sameMatchGroups.map((g) => ({
        marketName: g.marketName,
        family: g.family,
        rows: g.rows.length,
      })),
    };
  }

  function scoreBetsWrapAsActiveMarket(betsWrap, rows, marketName) {
    const r = betsWrap.getBoundingClientRect();

    const visiblePart = Math.max(
      0,
      Math.min(r.bottom, window.innerHeight) - Math.max(r.top, 0),
    );

    const center = r.top + r.height / 2;
    const targetCenter = window.innerHeight * 0.52;
    const distance = Math.abs(center - targetCenter);

    let score = visiblePart * 2 - distance + rows.length * 20;

    if (rows.length > 12) score -= 500;
    if (/Double Chance/i.test(marketName)) score += 25;
    if (/3-?Way/i.test(marketName)) score += 20;
    if (/Total Goals|Over\/Under|Under\/Over/i.test(marketName)) score -= 10;

    return score;
  }

  function exactTornParseActiveBookieMarket() {
    const sportHint = currentBookieSport();

    const betsWraps = [
      ...document.querySelectorAll('ul[class*="bets-wrap"]'),
    ].filter(isDomVisible);

    const candidates = [];

    for (const betsWrap of betsWraps) {
      const rows = parseRowsFromBetsWrap(betsWrap);

      if (rows.length < 2) continue;
      if (rows.length > 12) continue;

      const market = findMarketTitleForBetsWrap(betsWrap);
      const title = findMatchTitleAround(betsWrap);
      const ha = parseHomeAway(title, rows);

      const keyedRows = assignKeysForMarketGroup(
        rows,
        market,
        ha.home,
        ha.away,
      );
      const keys = new Set(keyedRows.map((o) => o.key).filter(Boolean));

      let score = scoreBetsWrapAsActiveMarket(betsWrap, rows, market);

      if (sportHint === "basketball" || sportHint === "baseball") {
        const isTwoWayMoneyline =
          rows.length === 2 &&
          keys.has("H") &&
          keys.has("A") &&
          !keys.has("OVER") &&
          !keys.has("UNDER");

        const isTotal =
          rows.length === 2 && keys.has("OVER") && keys.has("UNDER");

        if (isTwoWayMoneyline) {
          score += 100000;
        }

        if (isTotal) {
          score -= 100000;
        }
      }

      candidates.push({
        root: betsWrap,
        rows,
        keyedRows,
        title,
        market,
        score,
      });
    }

    if (!candidates.length) return null;

    candidates.sort((a, b) => b.score - a.score);
    return candidates[0];
  }

  function parseBookieMarket() {
    const sportHint = detectBookieSportFromParsed(null) || currentBookieSport();

    const baseballMoneyline =
      sportHint === "baseball" ? parseBaseballMoneyline() : null;

    if (baseballMoneyline) return baseballMoneyline;

    const basketballBundle =
      sportHint === "basketball" ? parseBasketballBundle() : null;

    if (basketballBundle) return basketballBundle;

    const bundle =
      sportHint === "football" ? parsePrimaryFootballBundle() : null;

    if (bundle) return bundle;

    const exact = exactTornParseActiveBookieMarket();

    if (exact) {
      const title = exact.title || findTitle(exact.root);
      const marketName = exact.market || "Unknown Market";
      const ha = parseHomeAway(title, exact.rows);
      const outcomes =
        exact.keyedRows ||
        assignKeysForMarketGroup(exact.rows, marketName, ha.home, ha.away);

      const publicOutcomes = outcomes.map((o) => ({
        index: o.index,
        key: o.key,
        label: o.label,
        odds: o.odds,
        source_market_name: o.source_market_name,
        source_market_family: o.source_market_family,
        source_market_index: o.source_market_index,
      }));

      const oddsObj = {};
      for (const o of publicOutcomes) {
        if (o.key && !oddsObj[o.key]) oddsObj[o.key] = o.odds;
      }

      return {
        root: exact.root,
        title,
        marketName,
        home: ha.home,
        away: ha.away,
        leagueText: findLeagueText(exact.root),
        startText:
          extractStartTextFromString(marketName) || findStartText(exact.root),
        marketType: inferMarketType(marketName, publicOutcomes),
        outcomes,
        publicOutcomes,
        oddsObj,
        parsedAt: new Date().toISOString(),
        parser: "exact_torn_single_market",
      };
    }

    return {
      root: document.body,
      title: "",
      marketName: "No market detected",
      home: "",
      away: "",
      leagueText: "",
      startText: "",
      marketType: "unknown",
      outcomes: [],
      publicOutcomes: [],
      oddsObj: {},
      parsedAt: new Date().toISOString(),
      parser: "none",
    };
  }

  const __tbpOriginalParseBookieMarket = parseBookieMarket;

  function parseBookieMarketFromDom() {
    return attachSportToParsed(__tbpOriginalParseBookieMarket());
  }

  parseBookieMarket = function () {
    const apiParsed = getCachedTornBookieApiParsed();

    if (apiParsed) {
      return attachSportToParsed(apiParsed);
    }

    return parseBookieMarketFromDom();
  };

  // ---------------------------------------------------------------------------
  // Worker request contract
  // ---------------------------------------------------------------------------

  function buildResolvePayload(parsed) {
    const cleanApiKey = normalizeTornApiKey(state.tornApiKey);
    state.tornApiKey = cleanApiKey;

    const sport = parsed?.sport || currentBookieSport();

    const basketballOverUnder =
      sport === "basketball" ? getBasketballOverUnderFromParsed(parsed) : null;

    if (!isAdvisorSupportedSport(sport)) {
      throw new Error(
        `Unsupported sport detected: ${sport || "unknown"}. This advisor currently supports Football, Basketball, Baseball, Handball, and Rugby only.`,
      );
    }

    return {
      sport,
      torn_sport: sport,
      market_sport: sport,

      basketball_over_under: basketballOverUnder,
      torn_basketball_over_under: basketballOverUnder,

      torn_api_key: cleanApiKey,
      auth: {
        torn_api_key: cleanApiKey,
      },

      torn_match_title: parsed.title,
      torn_market_name: parsed.marketName,
      torn_home_name: parsed.home,
      torn_away_name: parsed.away,
      torn_league_text: parsed.leagueText,
      torn_start_text: parsed.startText,
      torn_start_ts: parseTornStartTimestamp(parsed.startText),
      torn_odds: parsed.oddsObj,
      market_type: parsed.marketType,
      outcomes: parsed.publicOutcomes,
      budget: state.budget,
      client_time: new Date().toISOString(),

      client: {
        script: "Torn Bookie Predictor",
        version: SCRIPT_VERSION,
        url: location.href,
        sport,
      },
    };
  }

  function isRetryableWorkerError(err) {
    const msg = String(err?.message || err || "");

    return (
      /Failed to fetch/i.test(msg) ||
      /status["']?\s*:\s*0/i.test(msg) ||
      /Worker request timed out/i.test(msg) ||
      /NETWORK_ERROR/i.test(msg) ||
      /HTTP\s+(429|500|502|503|504)/i.test(msg)
    );
  }

  function httpJson(method, path, body, options = {}) {
    const base = normalizeWorkerUrl(state.workerUrl || DEFAULT_WORKER_URL);
    const url = base + path;
    const cleanApiKey = normalizeTornApiKey(state.tornApiKey);
    state.tornApiKey = cleanApiKey;
    const bodyWithAuth =
      body && typeof body === "object"
        ? {
            ...body,
            torn_api_key: cleanApiKey,
            auth: {
              ...(body.auth || {}),
              torn_api_key: cleanApiKey,
            },
          }
        : body;

    const payload = bodyWithAuth == null ? null : JSON.stringify(bodyWithAuth);
    const retries = Number(options.retries ?? 2);

    const requestOnce = () =>
      new Promise((resolve, reject) => {
        const parseResponse = (status, text) => {
          let data = null;

          try {
            data = text ? JSON.parse(text) : null;
          } catch (_) {
            const err = new Error(
              `Invalid JSON from Worker. HTTP ${status}: ${String(text || "").slice(0, 500)}`,
            );
            err.retryable = status >= 500 || status === 429;
            throw err;
          }

          if (status >= 200 && status < 300) return data;

          const err = new Error(
            `HTTP ${status}: ${text ? text.slice(0, 800) : "empty response"}`,
          );
          err.retryable = status === 429 || status >= 500;
          throw err;
        };

        if (typeof GM_xmlhttpRequest !== "function") {
          reject(
            new Error(
              "GM_xmlhttpRequest is not available. Do not use fetch on Torn; CSP blocks external Worker calls.",
            ),
          );
          return;
        }

        try {
          GM_xmlhttpRequest({
            method,
            url,
            data: payload,
            headers: payload
              ? {
                  "Content-Type": "application/json",
                  Accept: "application/json",
                  "Cache-Control": "no-cache",
                }
              : {
                  Accept: "application/json",
                  "Cache-Control": "no-cache",
                },
            timeout: 45000,

            onload: (r) => {
              try {
                const data = parseResponse(
                  Number(r.status || 0),
                  r.responseText || "",
                );
                updateSubscriptionFromResponse(data);
                resolve(data);
              } catch (err) {
                reject(err);
              }
            },

            onerror: (e) => {
              const status = Number(e?.status || 0);
              const statusText = String(e?.statusText || "Failed to fetch");

              const err = new Error(
                `NETWORK_ERROR: GM_xmlhttpRequest failed. URL: ${url}. Status: ${status}. ${statusText}`,
              );

              err.retryable = true;
              reject(err);
            },

            ontimeout: () => {
              const err = new Error(`Worker request timed out. URL: ${url}`);
              err.retryable = true;
              reject(err);
            },
          });
        } catch (err) {
          const wrapped = new Error(
            `GM_xmlhttpRequest crashed: ${String(err?.message || err)}`,
          );
          wrapped.retryable = true;
          reject(wrapped);
        }
      });

    return (async () => {
      let lastErr = null;

      for (let attempt = 0; attempt <= retries; attempt++) {
        try {
          return await requestOnce();
        } catch (err) {
          lastErr = err;

          const canRetry =
            attempt < retries &&
            (err?.retryable || isRetryableWorkerError(err));

          if (!canRetry) {
            throw err;
          }

          const delay =
            500 * Math.pow(2, attempt) + Math.floor(Math.random() * 250);
          await sleep(delay);
        }
      }

      throw lastErr;
    })();
  }

  async function resolveMarketViaWorker(parsed, payload, options = {}) {
    const sig =
      options.signature ||
      marketAutoSignature(parsed) ||
      normalizeLoose(JSON.stringify(payload || {}).slice(0, 500));

    if (
      !options.force &&
      state.resolveInFlightPromise &&
      state.resolveInFlightSignature === sig
    ) {
      return state.resolveInFlightPromise;
    }

    const p = httpJson("POST", "/api/resolve-market", payload).finally(() => {
      if (state.resolveInFlightPromise === p) {
        state.resolveInFlightPromise = null;
        state.resolveInFlightSignature = "";
      }
    });

    state.resolveInFlightSignature = sig;
    state.resolveInFlightPromise = p;

    return p;
  }

  // ---------------------------------------------------------------------------
  // Stake mapping, Fill actions, and safe DOM activation
  // ---------------------------------------------------------------------------

  function selectedLocalOutcomes(decision, parsed) {
    if (!decision || decision.action !== "BET") return [];

    const selected = Array.isArray(decision.selected_outcomes)
      ? decision.selected_outcomes
      : [];

    return selected
      .map((sel) => {
        const key = sel.key || sel.role;
        const odds = Number(sel.odds || 0);
        const normLabel = normalizeLoose(sel.label || "");

        let local =
          parsed.outcomes.find(
            (o) => o.key === key && Math.abs(o.odds - odds) < 0.001,
          ) ||
          parsed.outcomes.find(
            (o) =>
              normalizeLoose(o.label) === normLabel &&
              Math.abs(o.odds - odds) < 0.001,
          ) ||
          parsed.outcomes.find((o) => o.key === key) ||
          parsed.outcomes.find((o) => normalizeLoose(o.label) === normLabel);

        return local
          ? { local, selected: sel, stake: Number(sel.stake || 0) }
          : null;
      })
      .filter(Boolean);
  }

  function dedupeSelectedPairs(pairs) {
    const seen = new Set();

    return (pairs || []).filter((pair) => {
      const key = [
        pair.local.source_market_name || "",
        pair.local.source_market_family || "",
        pair.local.key || "",
        normalizeLoose(pair.local.label || ""),
        Number(pair.local.odds || 0).toFixed(4),
      ].join("|");

      if (seen.has(key)) return false;

      seen.add(key);
      return true;
    });
  }

  async function fillStakes({ silent = false } = {}) {
    let parsed = parseBookieMarketFromDom();
    const decision = state.lastDecision;

    if (!decision || decision.action !== "BET") {
      throw new Error("No BET decision to fill. Resolve first.");
    }

    assertOddsFilterAllowsBetting(decision);

    let pairs = selectedLocalOutcomes(decision, parsed);

    if (!pairs.length) {
      await expandAdditionalBettingOptionsIfNeeded();
      parsed = parseBookieMarketFromDom();
      pairs = selectedLocalOutcomes(decision, parsed);
    }

    if (!pairs.length) {
      throw new Error(
        "Could not map Worker selected outcomes to visible Torn rows.",
      );
    }

    for (const pair of pairs) {
      if (!pair.local.input) {
        throw new Error(`No stake input found for ${pair.local.label}`);
      }

      setInputValue(pair.local.input, pair.stake);
    }

    state.lastParsed = parsed;
    state.lastFill = {
      at: new Date().toISOString(),
      pairs: pairs.map((p) => ({
        key: p.local.key,
        label: p.local.label,
        odds: p.local.odds,
        stake: p.stake,
      })),
    };

    if (!silent) {
      setStatus(
        `Filled ${pairs.length} stake input(s). Review before BET + YES.`,
        "ok",
      );
      render();
    }

    return pairs;
  }

  function clearStakeInputs(parsed = null) {
    const p = parsed || parseBookieMarketFromDom();

    for (const o of p.outcomes || []) {
      if (o.input && parseMoneyInputValue(o.input) > 0) {
        setInputValue(o.input, 0);
      }
    }
  }

  function getSelectedPairsForCurrentMarket() {
    const parsed = parseBookieMarketFromDom();
    const decision = state.lastDecision;

    if (!decision || decision.action !== "BET") {
      return { parsed, pairs: [] };
    }

    return {
      parsed,
      pairs: selectedLocalOutcomes(decision, parsed),
    };
  }

  function selectedBetFieldsHaveValue() {
    try {
      const { pairs } = getSelectedPairsForCurrentMarket();
      return pairs.some(
        (p) => p.local.input && parseMoneyInputValue(p.local.input) > 0,
      );
    } catch (_) {
      return false;
    }
  }

  async function toggleFillClear() {
    const decision = state.lastDecision;
    if (!hasTornApiKey()) {
      openSettingsForApiKey();
      throw new Error("Torn API key is required before using Fill.");
    }

    if (!decision || decision.action !== "BET") {
      throw new Error("No BET decision. Press Parse first.");
    }

    const { parsed, pairs } = getSelectedPairsForCurrentMarket();

    if (!pairs.length) {
      throw new Error(
        "Could not map Worker selected outcomes to visible Torn rows.",
      );
    }

    const hasValue = pairs.some(
      (p) => p.local.input && parseMoneyInputValue(p.local.input) > 0,
    );

    if (hasValue) {
      for (const pair of pairs) {
        if (pair.local.input) setInputValue(pair.local.input, 0);
      }

      state.lastParsed = parsed;
      state.lastFill = null;
      setStatus(`Cleared ${pairs.length} selected stake input(s).`, "ok");
      render();
      return;
    }

    assertOddsFilterAllowsBetting(decision);

    await fillStakes();
  }

  function getActivationTarget(el) {
    if (!el) return null;

    const text = cleanText(el.textContent || el.innerText || el.value || "");

    // YES must be clicked directly. Do not climb to the bet row.
    if (
      el.matches?.('span[class*="confirmYes"], [class*="confirmYes"]') ||
      /^yes$/i.test(text)
    ) {
      return el;
    }

    return (
      el.closest(
        'button, a, input[type="button"], input[type="submit"], [role="button"], li[class*="confirm"]',
      ) || el
    );
  }

  function markInternalClick(ms = 1800) {
    state.internalClickUntil = Date.now() + ms;
  }

  function isInternalScriptClick() {
    return Date.now() < Number(state.internalClickUntil || 0);
  }

  function dispatchActivation(el) {
    const target = getActivationTarget(el);
    if (!target) return false;

    // Prevent our own programmatic clicks from triggering bookie_click auto-resolve.
    markInternalClick();

    try {
      if (typeof target.click === "function") {
        target.click();
        return true;
      }
    } catch (_) {}

    try {
      target.dispatchEvent(
        new MouseEvent("click", {
          bubbles: true,
          cancelable: true,
          view: window,
        }),
      );
      return true;
    } catch (_) {
      return false;
    }
  }

  function findCurrentBookieContainerByUrl() {
    const sport = currentBookieSport();
    const eventId = getBookieEventIdFromUrl();

    if (!sport || sport === "unknown" || !eventId) return null;

    const selectors = [
      `a[href*="#/${sport}/${eventId}"]`,
      `a[href*="#/${sport}/"][href*="${eventId}"]`,
      `a[href*="${eventId}"]`,
    ];

    for (const sel of selectors) {
      const link = document.querySelector(sel);

      if (!link) continue;

      const container =
        link.closest('li[class*="c-pointer"]') ||
        link.closest('li[class*="active"]') ||
        link.closest("li") ||
        link.closest('[class*="bookie-date-box"]') ||
        link.parentElement;

      if (container && document.body.contains(container)) {
        return container;
      }
    }

    const active =
      document.querySelector('li[class*="c-pointer"][class*="active"]') ||
      document.querySelector('li[class*="active"]');

    if (active && document.body.contains(active)) {
      return active;
    }

    return null;
  }

  function findCurrentBookieContainer(parsed = state.lastParsed) {
    const root = parsed?.root;

    if (root && document.body.contains(root)) {
      const fromRoot =
        root.closest('li[class*="active"]') ||
        root.closest('li[class*="c-pointer"]') ||
        root.closest("li") ||
        root.closest('[class*="bookie-date-box"]') ||
        root.parentElement;

      if (fromRoot && document.body.contains(fromRoot)) {
        return fromRoot;
      }
    }

    return findCurrentBookieContainerByUrl();
  }

  function getClickableMarketOpener(container) {
    if (!container) return null;

    const sport = currentBookieSport();

    return (
      container.querySelector(`a[href*="#/${sport}"]`) ||
      container.querySelector('a[href*="#/basketball"]') ||
      container.querySelector('a[href*="#/baseball"]') ||
      container.querySelector('a[href*="#/rugby"]') ||
      container.querySelector('a[href*="#/handball"]') ||
      container.querySelector('a[href*="#/football"]') ||
      container.querySelector('a[href*="#/"]') ||
      container.querySelector('li[class*="title"]') ||
      container.querySelector('[class*="matchName"]') ||
      container.querySelector('[class*="eventName"]') ||
      container.querySelector('[class*="title"]') ||
      container.querySelector("a") ||
      container
    );
  }

  async function openNextBookieMarketAfterCurrent(previousParsed) {
    let current = findCurrentBookieContainer(previousParsed);

    if (!current) {
      current = findCurrentBookieContainerByUrl();
    }

    if (!current) {
      setStatus(
        "BET done, but could not find current market container for auto-next.",
        "warn",
      );
      render();
      return false;
    }

    let node = current.nextElementSibling;
    let checked = 0;

    while (node && checked < 35) {
      checked++;

      if (
        node instanceof Element &&
        !node.querySelector(`#${INLINE_PANEL_ID}`) &&
        isDomVisible(node)
      ) {
        const text = cleanText(node.innerText || node.textContent || "");

        if (
          text &&
          /v|vs|3-?Way|Double Chance|Draw No Bet|x\s*\d/i.test(text)
        ) {
          const opener = getClickableMarketOpener(node);

          if (opener) {
            resetAdvisorStateForNavigation("Opening next Bookie market...");

            try {
              opener.scrollIntoView({ behavior: "smooth", block: "center" });
            } catch (_) {}

            await sleep(100);
            dispatchActivation(opener);

            setTimeout(() => {
              scheduleAutoResolve("after_bet_next_market", 1600);
            }, 600);

            return true;
          }
        }
      }

      node = node.nextElementSibling;
    }

    setStatus("BET done. No next Bookie market found automatically.", "ok");
    render();
    return false;
  }

  function directBetButtonLabel() {
    const flow = state.directBetFlow;

    if (flow?.phase === "next" && flow?.eventKey === currentBookieEventKey()) {
      return "Next Match";
    }

    if (flow?.phase === "bet" && flow?.eventKey === currentBookieEventKey()) {
      const total = Number(flow.pairs?.length || 0);
      const current = Number(flow.index || 0) + 1;

      return total > 1 ? `Direct Bet ${current}/${total}` : "Direct Bet";
    }

    return "Direct Bet";
  }

  // ---------------------------------------------------------------------------
  // Torn React/Redux synchronization
  // ---------------------------------------------------------------------------

  let tornReduxStoreCache = null;

  function getReactRootFiber() {
    const root = document.getElementById("react-root");

    if (!root) {
      throw new Error("react-root not found");
    }

    const key = Object.keys(root).find(
      (k) => k.startsWith("__reactContainer$") || k.startsWith("__reactFiber$"),
    );

    if (!key) {
      throw new Error("React fiber key not found on #react-root");
    }

    const fiber = root[key];

    return fiber._internalRoot?.current || fiber;
  }

  function walkFiberSafe(rootFiber, predicate) {
    const stack = [rootFiber.child || rootFiber];
    const seen = new WeakSet();
    let inspected = 0;
    const MAX = 100000;

    while (stack.length) {
      const fiber = stack.pop();

      if (!fiber || seen.has(fiber)) continue;

      seen.add(fiber);
      inspected++;

      if (inspected > MAX) {
        throw new Error("Fiber walk exceeded safety limit");
      }

      if (predicate(fiber)) {
        console.log("[TBP Fiber] Found after inspecting:", inspected);
        return fiber;
      }

      if (fiber.sibling) stack.push(fiber.sibling);
      if (fiber.child) stack.push(fiber.child);
    }

    console.warn("[TBP Fiber] Redux store not found. Inspected:", inspected);
    return null;
  }

  function getFiberProps(fiber) {
    return fiber?.memoizedProps || fiber?.pendingProps || {};
  }

  function isReduxStore(x) {
    return !!(
      x &&
      typeof x.dispatch === "function" &&
      typeof x.getState === "function"
    );
  }

  function findTornReduxStore(force = false) {
    if (!force && tornReduxStoreCache && isReduxStore(tornReduxStoreCache)) {
      return tornReduxStoreCache;
    }

    const rootFiber = getReactRootFiber();

    const fiber = walkFiberSafe(rootFiber, (f) => {
      const p = getFiberProps(f);

      return (
        isReduxStore(p.store) ||
        isReduxStore(p.value?.store) ||
        isReduxStore(f.stateNode?.store)
      );
    });

    if (!fiber) {
      throw new Error("Torn Redux store not found");
    }

    const p = getFiberProps(fiber);

    const store = p.store || p.value?.store || fiber.stateNode?.store;

    if (!isReduxStore(store)) {
      throw new Error("Invalid Torn Redux store");
    }

    tornReduxStoreCache = store;

    return store;
  }

  function captureTbpUiState() {
    const active = document.activeElement;
    const isInput =
      active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA");

    return {
      scrollX: window.scrollX,
      scrollY: window.scrollY,
      active,
      activeValue: isInput ? active.value : null,
      selectionStart: isInput ? active.selectionStart : null,
      selectionEnd: isInput ? active.selectionEnd : null,
    };
  }

  function restoreTbpUiState(saved) {
    if (!saved) return;

    requestAnimationFrame(() => {
      try {
        window.scrollTo(saved.scrollX, saved.scrollY);
      } catch (_) {}

      const el = saved.active;

      if (!el || !document.body.contains(el)) return;

      try {
        el.focus();

        if (saved.activeValue !== null && "value" in el) {
          el.value = saved.activeValue;
          el.dispatchEvent(new Event("input", { bubbles: true }));
        }

        if (
          saved.selectionStart !== null &&
          saved.selectionEnd !== null &&
          typeof el.setSelectionRange === "function"
        ) {
          el.setSelectionRange(saved.selectionStart, saved.selectionEnd);
        }
      } catch (_) {}
    });
  }

  async function refreshTornReactBookieMarketFromBookieApi(
    reason = "react_sync",
  ) {
    const gamebox = currentBookieSport();
    const eventId = getBookieEventIdFromUrl();

    if (!gamebox || gamebox === "unknown" || !eventId) {
      throw new Error(
        "Cannot refresh Torn React Bookie market: missing gamebox/eventId.",
      );
    }

    const store = findTornReduxStore();
    const savedUi = captureTbpUiState();

    const json = await tornBookieApi("getEventOutcomeTypes", {
      gamebox,
      eventId,
    });

    if (!json || !Array.isArray(json.outcomeTypes)) {
      console.warn(
        "[TBP] Unexpected Torn Bookie API response for React sync:",
        json,
      );
      throw new Error("Unexpected Torn Bookie API response during React sync.");
    }

    store.dispatch({
      type: "game markets loaded",
      payload: {
        json,
        gameBox: gamebox,
        gamebox,
        eventId,
        meta: "tbp_ajax",
        reason,
      },
    });

    restoreTbpUiState(savedUi);

    console.log("[TBP] Torn React Bookie market synced:", {
      reason,
      gamebox,
      eventId,
      outcomeTypes: json.outcomeTypes.length,
      bets: Array.isArray(json.bets) ? json.bets.length : 0,
    });

    return json;
  }

  // ---------------------------------------------------------------------------
  // Direct Bet flow
  // ---------------------------------------------------------------------------

  async function recheckBeforeDirectBet() {
    const parsedBefore = await refreshTornBookieApiMarketIfPossible(
      "direct_bet_before",
      { force: true },
    );

    if (!parsedBefore?.publicOutcomes?.length) {
      throw new Error("Direct Bet requires Torn Bookie API market data.");
    }

    const sigBefore = marketAutoSignature(parsedBefore);
    const payload = buildResolvePayload(parsedBefore);

    const previousDisplayCard = state.lastDecision?.display_card || null;

    const fresh = await resolveMarketViaWorker(parsedBefore, payload, {
      signature: sigBefore,
      force: true,
    });

    if (fresh.action !== "BET") {
      const msg =
        decisionUserMessage(fresh) ||
        `Worker now says ${fresh.action || "SKIP"}: ${fresh.reason || "UNKNOWN"}`;

      throw new Error(msg);
    }

    const parsedAfter = await refreshTornBookieApiMarketIfPossible(
      "direct_bet_after",
      { force: true },
    );
    const sigAfter = marketAutoSignature(parsedAfter);

    if (sigBefore && sigAfter && sigBefore !== sigAfter) {
      throw new Error("Market changed while re-checking. Direct Bet stopped.");
    }

    state.lastDecision = {
      ...fresh,
      display_card: fresh.display_card || previousDisplayCard,
    };

    state.lastParsed = parsedAfter || parsedBefore;

    return {
      decision: state.lastDecision,
      parsed: state.lastParsed,
    };
  }

  async function addDirectTornBookieBet(localOutcome, amount) {
    const sport = localOutcome?.sport || currentBookieSport();
    const eventId = String(
      localOutcome?.eventId || getBookieEventIdFromUrl() || "",
    );
    const bettingofferId = String(localOutcome?.bettingofferId || "");
    const stake = Math.max(1, Math.round(Number(amount || 0)));

    if (!ADVISOR_SPORTS.includes(sport)) {
      throw new Error(`Direct Bet unsupported sport: ${sport}`);
    }

    if (!eventId || !bettingofferId) {
      throw new Error(
        `Direct Bet missing eventId/bettingofferId for ${localOutcome?.label || "selected outcome"}.`,
      );
    }

    const payload = {
      bettingofferId,
      amount: stake,
      odds: Number(localOutcome.odds || 0),
      handicap: Number(localOutcome.handicap || 0),
    };

    return tornBookieApi(
      "addBet",
      {
        gamebox: sport,
        eventId,
        data: JSON.stringify(payload),
      },
      {
        formData: true,
      },
    );
  }

  function makeSingleOutcomeBetTarget(outcome) {
    const source =
      outcome && typeof outcome === "object"
        ? outcome
        : {};

    return {
      bettingofferId: String(
        source.bettingofferId || "",
      ),

      outcomeId: String(
        source.outcomeId || "",
      ),

      key: String(
        source.key ||
        source.role ||
        "",
      ).toUpperCase(),

      label: cleanText(
        source.label || "",
      ),

      source_market_name: cleanText(
        source.source_market_name ||
        source.market ||
        "",
      ),

      source_market_family: cleanText(
        source.source_market_family || "",
      ),

      odds: Number(
        source.odds || 0,
      ),

      stake: Math.max(
        0,
        Math.round(
          Number(source.stake || 0),
        ),
      ),
    };
  }

  function singleOutcomeBetGuardKey(outcome) {
    const source =
      outcome && typeof outcome === "object"
        ? outcome
        : {};

    const eventKey =
      currentBookieEventKey() ||
      `${currentBookieSport()}:${getBookieEventIdFromUrl()}`;

    const key = String(
      source.key ||
      source.role ||
      "",
    ).toUpperCase();

    const label = normalizeLoose(
      source.label || "",
    );

    const market = normalizeLoose(
      source.source_market_name ||
      source.market ||
      "",
    );

    const line = String(
      source.handicap_line ??
      source.total_line ??
      source.line ??
      "",
    );

    return [
      eventKey,
      key,
      label,
      market,
      line,
    ].join("::");
  }

  function singleOutcomeMatchesTarget(
    outcome,
    target,
  ) {
    if (!outcome || !target) {
      return false;
    }

    const outcomeOfferId = String(
      outcome.bettingofferId || "",
    );

    const targetOfferId = String(
      target.bettingofferId || "",
    );

    if (
      outcomeOfferId &&
      targetOfferId &&
      outcomeOfferId === targetOfferId
    ) {
      return true;
    }

    const outcomeLabel = normalizeLoose(
      outcome.label || "",
    );

    const targetLabel = normalizeLoose(
      target.label || "",
    );

    if (
      !outcomeLabel ||
      !targetLabel ||
      outcomeLabel !== targetLabel
    ) {
      return false;
    }

    const outcomeKey = String(
      outcome.key ||
      outcome.role ||
      "",
    ).toUpperCase();

    if (
      target.key &&
      outcomeKey &&
      target.key !== outcomeKey
    ) {
      return false;
    }

    const outcomeMarket = normalizeLoose(
      outcome.source_market_name ||
      outcome.market ||
      "",
    );

    const targetMarket = normalizeLoose(
      target.source_market_name || "",
    );

    if (
      outcomeMarket &&
      targetMarket &&
      outcomeMarket !== targetMarket
    ) {
      return false;
    }

    const outcomeFamily = normalizeLoose(
      outcome.source_market_family || "",
    );

    const targetFamily = normalizeLoose(
      target.source_market_family || "",
    );

    if (
      outcomeFamily &&
      targetFamily &&
      outcomeFamily !== targetFamily
    ) {
      return false;
    }

    return true;
  }

  function findLocalOutcomeForSingleBet(
    selectedOutcome,
    parsed,
  ) {
    const outcomes = Array.isArray(
      parsed?.outcomes,
    )
      ? parsed.outcomes
      : [];

    if (!outcomes.length) {
      return null;
    }

    const bettingofferId = String(
      selectedOutcome?.bettingofferId || "",
    );

    if (bettingofferId) {
      const exactOffer = outcomes.find(
        (outcome) =>
          String(
            outcome?.bettingofferId || "",
          ) === bettingofferId,
      );

      if (exactOffer) {
        return exactOffer;
      }
    }

    const label = normalizeLoose(
      selectedOutcome?.label || "",
    );

    const key = String(
      selectedOutcome?.key ||
      selectedOutcome?.role ||
      "",
    ).toUpperCase();

    const marketName = normalizeLoose(
      selectedOutcome?.source_market_name ||
      selectedOutcome?.market ||
      "",
    );

    const marketFamily = normalizeLoose(
      selectedOutcome?.source_market_family ||
      "",
    );

    const odds = Number(
      selectedOutcome?.odds || 0,
    );

    return (
      outcomes.find((outcome) => {
        if (
          normalizeLoose(
            outcome?.label || "",
          ) !== label
        ) {
          return false;
        }

        const localMarketName = normalizeLoose(
          outcome?.source_market_name || "",
        );

        const localMarketFamily = normalizeLoose(
          outcome?.source_market_family || "",
        );

        if (
          marketName &&
          localMarketName &&
          marketName !== localMarketName
        ) {
          return false;
        }

        if (
          marketFamily &&
          localMarketFamily &&
          marketFamily !== localMarketFamily
        ) {
          return false;
        }

        return true;
      }) ||

      outcomes.find((outcome) => {
        const localKey = String(
          outcome?.key || "",
        ).toUpperCase();

        return (
          normalizeLoose(
            outcome?.label || "",
          ) === label &&
          (!key || localKey === key) &&
          (
            !odds ||
            Math.abs(
              Number(outcome?.odds || 0) -
              odds,
            ) < 0.001
          )
        );
      }) ||

      null
    );
  }

  function assertOddsFilterAllowsSingleOutcome(
    outcome,
  ) {
    const filter =
      normalizeOddsFilterSettings(
        state.oddsFilter,
      );

    if (!filter.enabled) {
      return true;
    }

    const odds = Number(
      outcome?.odds || 0,
    );

    const minOdds = Number(
      filter.minOdds ||
      DEFAULT_ODDS_FILTER.minOdds,
    );

    if (odds < minOdds) {
      throw new Error(
        `Odds Filters blocked this outcome. Minimum odds: ${minOdds.toFixed(2)} • ${outcome?.label || "Selected outcome"} @ ${odds.toFixed(2)}`,
      );
    }

    return true;
  }

  async function directBetSingleOutcome(
    targetOutcome,
  ) {
    if (!hasTornApiKey()) {
      openSettingsForApiKey();

      throw new Error(
        "Torn API key is required before using Bet.",
      );
    }

    if (state.autoBusy) {
      throw new Error(
        "Predictor is still resolving this market. Please wait.",
      );
    }

    if (
      state.directBetFlow?.phase === "bet" &&
      state.directBetFlow?.eventKey ===
        currentBookieEventKey()
    ) {
      throw new Error(
        "Complete the current multi-outcome Direct Bet sequence first.",
      );
    }

    const target =
      makeSingleOutcomeBetTarget(
        targetOutcome,
      );

    if (!target.label) {
      throw new Error(
        "Could not identify the selected outcome.",
      );
    }

    if (target.stake <= 0) {
      throw new Error(
        `No valid stake is available for ${target.label}.`,
      );
    }

    const currentParsed =
      state.lastParsed ||
      parseBookieMarket();

    if (
      isMarketStartedOrFinished(
        currentParsed,
      )
    ) {
      throw new Error(
        "This market has started, finished, or closed. Bet stopped.",
      );
    }

    /*
    * Refresh BookieAPI, re-run the Worker and ensure
    * the market remains safe before placing anything.
    */
    const {
      decision,
      parsed,
    } = await recheckBeforeDirectBet();

    if (
      isMarketStartedOrFinished(parsed)
    ) {
      throw new Error(
        "This market is no longer open. Bet stopped.",
      );
    }

    const freshSelected = Array.isArray(
      decision?.selected_outcomes,
    )
      ? decision.selected_outcomes
      : [];

    const selectedOutcome =
      freshSelected.find((outcome) =>
        singleOutcomeMatchesTarget(
          outcome,
          target,
        ),
      );

    if (!selectedOutcome) {
      throw new Error(
        `${target.label} is no longer included in the current prediction. Bet stopped.`,
      );
    }

    const localOutcome =
      findLocalOutcomeForSingleBet(
        selectedOutcome,
        parsed,
      );

    if (
      !localOutcome?.bettingofferId
    ) {
      throw new Error(
        `Could not map ${target.label} to the current Torn betting offer.`,
      );
    }

    const freshStake = Math.max(
      1,
      Math.round(
        Number(
          selectedOutcome.stake ||
          target.stake ||
          0,
        ),
      ),
    );

    assertOddsFilterAllowsSingleOutcome(
      localOutcome,
    );

    setStatus(
      `Direct Bet: placing only ${localOutcome.label} for ${money(freshStake)}...`,
      "warn",
    );

    render();

    const result =
      await addDirectTornBookieBet(
        localOutcome,
        freshStake,
      );

    console.log(
      "[TBP] Single Outcome Bet result:",
      {
        label: localOutcome.label,
        stake: freshStake,
        result,
      },
    );

    let reactSyncOk = true;

    try {
      await refreshTornReactBookieMarketFromBookieApi(
        "after_single_outcome_bet",
      );
    } catch (err) {
      reactSyncOk = false;

      console.warn(
        "[TBP] Torn React sync after Single Outcome Bet failed:",
        err,
      );
    }

    await refreshTornBookieApiMarketIfPossible(
      "after_single_outcome_bet_confirm",
      {
        force: true,
      },
    ).catch(() => null);

    state.lastFill = null;
    state.directBetFlow = null;

    lockPanelToCurrentEvent();

    if (reactSyncOk) {
      setStatus(
        `Bet placed: ${localOutcome.label} • ${money(freshStake)}.`,
        "ok",
      );
    } else {
      setStatus(
        `Bet sent: ${localOutcome.label} • ${money(freshStake)}, but Torn UI sync failed.`,
        "warn",
      );
    }

    render();
  }

  async function placeCurrentDirectBetFlowOutcome() {
    const flow = state.directBetFlow;

    if (
      !flow ||
      flow.phase !== "bet" ||
      flow.eventKey !== currentBookieEventKey()
    ) {
      state.directBetFlow = null;
      throw new Error("Direct Bet flow is invalid. Resolve this market again.");
    }

    const total = Number(flow.pairs?.length || 0);
    const index = Number(flow.index || 0);
    const pair = flow.pairs?.[index];

    if (!pair?.local || !Number(pair.stake || 0)) {
      state.directBetFlow = null;
      throw new Error("Direct Bet could not find a valid selected bet.");
    }

    if (normalizeOddsFilterSettings(state.oddsFilter).enabled) {
      const minOdds = Number(
        state.oddsFilter?.minOdds || DEFAULT_ODDS_FILTER.minOdds,
      );

      if (Number(pair.local?.odds || 0) < minOdds) {
        state.directBetFlow = null;
        throw new Error(
          `Odds Filters blocked this bet. Minimum odds: ${minOdds.toFixed(2)} • ${pair.local.label} @ ${Number(pair.local.odds || 0).toFixed(2)}`,
        );
      }
    }

    const current = index + 1;

    setStatus(
      `Direct Bet: placing ${current}/${total} - ${pair.local.label}...`,
      "warn",
    );
    render();

    const result = await addDirectTornBookieBet(pair.local, pair.stake);

    console.log("[TBP] Direct Bet result:", {
      label: pair.local.label,
      stake: pair.stake,
      result,
    });

    let reactSyncOk = true;

    try {
      await refreshTornReactBookieMarketFromBookieApi("after_direct_bet");
    } catch (e) {
      reactSyncOk = false;
      console.warn("[TBP] Torn React sync after Direct Bet failed:", e);
    }

    await refreshTornBookieApiMarketIfPossible("after_direct_bet_confirm", {
      force: true,
    }).catch(() => null);

    flow.index = index + 1;

    state.lastFill = null;

    if (flow.index >= total) {
      state.directBetFlow = {
        phase: "next",
        parsed: flow.parsed || state.lastParsed || null,
        eventKey: currentBookieEventKey(),
      };

      lockPanelToCurrentEvent();

      if (reactSyncOk) {
        setStatus(
          `Direct Bet completed: ${total}/${total} bet(s) sent. Click Next Match.`,
          "ok",
        );
      } else {
        setStatus(
          "Direct Bet completed, but Torn UI sync failed. Click Next Match.",
          "warn",
        );
      }

      render();
      return;
    }

    state.directBetFlow = flow;

    lockPanelToCurrentEvent();

    if (reactSyncOk) {
      setStatus(
        `Direct Bet completed: ${current}/${total}. Click Direct Bet ${flow.index + 1}/${total}.`,
        "ok",
      );
    } else {
      setStatus(
        `Direct Bet completed: ${current}/${total}, but Torn UI sync failed. Click Direct Bet ${flow.index + 1}/${total}.`,
        "warn",
      );
    }

    render();
  }

  async function directBetClick() {
    if (!hasTornApiKey()) {
      openSettingsForApiKey();
      return;
    }

    if (
      state.directBetFlow?.phase === "next" &&
      state.directBetFlow?.eventKey === currentBookieEventKey()
    ) {
      const beforeParsed =
        state.directBetFlow.parsed || state.lastParsed || parseBookieMarket();

      state.directBetFlow = null;

      await withBusy("Opening next Bookie market.", async () => {
        await openNextBookieMarketAfterCurrent(beforeParsed);
      });

      return;
    }

    if (
      state.directBetFlow?.phase === "bet" &&
      state.directBetFlow?.eventKey === currentBookieEventKey()
    ) {
      if (state.autoBusy) {
        setStatus(
          "Advisor is still resolving this market. Please wait.",
          "warn",
        );
        render();
        return;
      }

      await withBusy("Direct Bet: placing selected outcome...", async () => {
        await placeCurrentDirectBetFlowOutcome();
      });

      return;
    }

    if (state.autoBusy) {
      setStatus("Advisor is still resolving this market. Please wait.", "warn");
      render();
      return;
    }

    await withBusy("Direct Bet: placing selected outcome...", async () => {
      const decision = state.lastDecision;
      const parsed = state.lastParsed || parseBookieMarket();

      assertOddsFilterAllowsBetting(decision);

      const rawPairs = selectedLocalOutcomes(decision, parsed);
      const pairs = dedupeSelectedPairs(rawPairs).filter(
        (p) => p.local?.bettingofferId && Number(p.stake || 0) > 0,
      );

      if (!pairs.length) {
        throw new Error(
          "Direct Bet could not map selected outcomes to Torn bettingofferId.",
        );
      }

      state.directBetFlow = {
        phase: "bet",
        index: 0,
        pairs,
        parsed: parsed || state.lastParsed || null,
        eventKey: currentBookieEventKey(),
      };

      await placeCurrentDirectBetFlowOutcome();
    });
  }

  // ---------------------------------------------------------------------------
  // Shared UI state and settings
  // ---------------------------------------------------------------------------

  async function withBusy(label, fn) {
    if (state.busy) return;

    state.busy = true;
    setStatus(label, "busy");
    render();

    try {
      await fn();
    } catch (err) {
      setStatus(String(err?.message || err), "err");
      render();
    } finally {
      state.busy = false;
      render();
    }
  }

  function setStatus(text, kind = "info") {
    state.lastStatus = text;
    state.statusKind = kind;
  }

  function saveSettingsFromUi() {
    const budgetInput = document.querySelector("#tba-budget");

    if (budgetInput) {
      const n = parseIntegerInputValue(budgetInput.value, DEFAULT_BUDGET);
      state.budget = n;
      setStore("budget", n);
    }

    const oddsEnabledInput = document.querySelector("#tba-odds-filter-enabled");
    const oddsMinInput = document.querySelector("#tba-odds-filter-min");

    if (oddsEnabledInput || oddsMinInput) {
      const next = setOddsFilterToStore({
        enabled: !!oddsEnabledInput?.checked,
        minOdds:
          oddsMinInput?.value ||
          state.oddsFilter?.minOdds ||
          DEFAULT_ODDS_FILTER.minOdds,
      });

      state.oddsFilter = next;

      if (oddsMinInput) {
        oddsMinInput.value = oddsFilterMinText(next.minOdds);
        oddsMinInput.disabled = !next.enabled;
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Styles
  // ---------------------------------------------------------------------------

  function injectStyle() {
    let style = document.getElementById(INLINE_STYLE_ID);

    if (!style) {
      style = document.createElement("style");
      style.id = INLINE_STYLE_ID;
      document.head.appendChild(style);
    }

    style.textContent = `
#${INLINE_PANEL_ID}, #${INLINE_PANEL_ID} * {
  box-sizing: border-box;
  font-family: Arial, Helvetica, sans-serif;
}

#${INLINE_PANEL_ID} {
  position: relative;
  margin: 7px 0 9px;
  padding: 9px 11px;
  border: 1px solid rgba(134,183,255,.24);
  border-top: 1px solid rgba(134,183,255,.48);
  background:
    radial-gradient(circle at top right, rgba(134,183,255,.10), transparent 34%),
    linear-gradient(135deg, rgba(17,24,39,.96), rgba(8,13,25,.98));
  color: #e8eefc !important;
  border-radius: 8px;
  box-shadow:
    0 8px 20px rgba(0,0,0,.24),
    inset 0 1px 0 rgba(255,255,255,.035);
  overflow: hidden;
  clear: both;
}

#${INLINE_PANEL_ID} .tba-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 10px;
  margin-bottom: 8px;
}

#${INLINE_PANEL_ID} .tba-title {
  font-size: 16px;
  font-weight: 900;
  color: #86b7ff;
  text-transform: uppercase;
  letter-spacing: 1px;
  line-height: 1.05;
  text-shadow: 0 0 12px rgba(134,183,255,.18);
}

#${INLINE_PANEL_ID} .tba-sub {
  font-size: 11px;
  color: #aebbd2;
  margin-top: 3px;
  opacity: .85;
}

#${INLINE_PANEL_ID} .tba-budget-wrap {
  display: flex;
  align-items: center;
  gap: 6px;
}

#${INLINE_PANEL_ID} .tba-budget-wrap label {
  font-size: 10px;
  color: #9fb0c8;
  text-transform: uppercase;
  font-weight: 800;
  letter-spacing: .5px;
}

#${INLINE_PANEL_ID} #tba-budget {
  width: 105px;
  border: 1px solid rgba(134,183,255,.25);
  border-radius: 7px;
  background: rgba(2,6,23,.72);
  color: #fff !important;
  padding: 5px 7px;
  font-size: 12px;
  outline: none;
  text-align: right;
}

#${INLINE_PANEL_ID} button {
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 8px;
  color: #fff !important;
  cursor: pointer;
  font-size: 11px;
  font-weight: 900;
  padding: 6px 10px;
  line-height: 1;
}

#${INLINE_PANEL_ID} .tba-fill-btn {
  background: linear-gradient(135deg, rgba(185, 55, 65, .96), rgba(135, 34, 42, .96));
}

#${INLINE_PANEL_ID} .tba-bet-btn {
  background: linear-gradient(135deg, rgba(34, 150, 82, .95), rgba(22, 115, 64, .95));
}

#${INLINE_PANEL_ID} .tba-skip-action-row {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  margin-top: 12px;
}

#${INLINE_PANEL_ID} .tba-skip-btn {
  background: linear-gradient(135deg, rgba(185, 55, 65, .96), rgba(135, 34, 42, .96));
  border: 1px solid rgba(255,127,143,.35) !important;
  color: #fff !important;
  min-width: 92px;
  box-shadow: 0 8px 18px rgba(0,0,0,.18);
}

#${INLINE_PANEL_ID} .tba-skip-btn:hover {
  filter: brightness(1.08);
}

#${INLINE_PANEL_ID} .tba-status {
  margin: 8px 0;
  padding: 7px 9px;
  border-radius: 7px;
  font-size: 12px;
  line-height: 1.35;
  color: #e8eefc !important;
  border: 1px solid rgba(134,183,255,.14);
  background: rgba(255,255,255,.045);
  overflow-wrap: anywhere;
}

#${INLINE_PANEL_ID} .tba-status.ok {
  border-color: rgba(89,217,142,.36);
}

#${INLINE_PANEL_ID} .tba-status.warn {
  border-color: rgba(255,209,102,.40);
}

#${INLINE_PANEL_ID} .tba-status.err {
  border-color: rgba(255,127,143,.45);
}

#${INLINE_PANEL_ID} .tba-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

#${INLINE_PANEL_ID} .tba-grid + .tba-card {
  margin-top: 6px;
}

#${INLINE_PANEL_ID} .tba-card {
  border: 1px solid rgba(255,255,255,.09);
  background: rgba(2,6,23,.42);
  border-radius: 8px;
  padding: 8px;
  color: #e8eefc !important;
}

#${INLINE_PANEL_ID} .tba-card h4 {
  margin: 0 0 7px;
  font-size: 13px;
  color: #fff !important;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-kv {
  display: grid;
  grid-template-columns: 95px 1fr;
  gap: 4px 7px;
  font-size: 11px;
  line-height: 1.35;
}

#${INLINE_PANEL_ID} .tba-kv b {
  color: #9fb0c8 !important;
  font-weight: 800;
}

#${INLINE_PANEL_ID} .tba-kv span {
  color: #e8eefc !important;
  overflow-wrap: anywhere;
}

#${INLINE_PANEL_ID} .tba-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  padding: 3px 8px;
  font-size: 10px;
  font-weight: 900;
  border: 1px solid rgba(255,255,255,.12);
  background: rgba(255,255,255,.06);
  color: #fff !important;
  margin-left: 5px;
}

#${INLINE_PANEL_ID} .tba-pill-good {
  color: #59d98e !important;
  border-color: rgba(89,217,142,.45);
  background: rgba(89,217,142,.13);
}

#${INLINE_PANEL_ID} .tba-pill-warn {
  color: #ffd166 !important;
  border-color: rgba(255,209,102,.45);
  background: rgba(255,209,102,.13);
}

#${INLINE_PANEL_ID} .tba-pill-bad {
  color: #ff7f8f !important;
  border-color: rgba(255,127,143,.45);
  background: rgba(255,127,143,.13);
}

#${INLINE_PANEL_ID} .tba-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  color: #e8eefc !important;
}

#${INLINE_PANEL_ID} .tba-table th,
#${INLINE_PANEL_ID} .tba-table td {
  color: #e8eefc !important;
  padding: 5px 4px;
  border-top: 1px solid rgba(255,255,255,.07);
  text-align: left;
  vertical-align: middle;
}

#${INLINE_PANEL_ID} .tba-table th {
  color: #9fb0c8 !important;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: .5px;
}

#${INLINE_PANEL_ID} .tba-key {
  display: inline-block;
  color: #fff !important;
  background: rgba(70,120,255,.35);
  border: 1px solid rgba(120,160,255,.28);
  border-radius: 999px;
  padding: 2px 7px;
  margin-right: 6px;
  font-weight: 900;
  min-width: 34px;
  text-align: center;
}

#${INLINE_PANEL_ID} .tba-muted {
  color: #9fb0c8 !important;
  opacity: 1;
}

#${INLINE_PANEL_ID} details {
  margin-top: 8px;
}

#${INLINE_PANEL_ID} summary {
  cursor: pointer;
  color: #86b7ff !important;
  font-size: 11px;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-fixture-card {
  position: relative;
  margin: 8px 0;
  padding: 11px;
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid rgba(134,183,255,.18);
  background:
    radial-gradient(circle at 20% 0%, rgba(89,217,142,.13), transparent 28%),
    radial-gradient(circle at 85% 8%, rgba(134,183,255,.16), transparent 32%),
    linear-gradient(145deg, rgba(3,7,18,.72), rgba(15,23,42,.92));
  box-shadow:
    0 10px 24px rgba(0,0,0,.22),
    inset 0 1px 0 rgba(255,255,255,.04);
}

#${INLINE_PANEL_ID} .tba-fixture-glow {
  position: absolute;
  inset: -80px -80px auto auto;
  width: 180px;
  height: 180px;
  border-radius: 999px;
  background: rgba(134,183,255,.10);
  filter: blur(8px);
  pointer-events: none;
}

#${INLINE_PANEL_ID} .tba-fixture-top {
  position: relative;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 8px;
  margin-bottom: 10px;
}

#${INLINE_PANEL_ID} .tba-fixture-badge {
  flex-shrink: 0;
  border-radius: 999px;
  padding: 5px 9px;
  font-size: 10px;
  font-weight: 900;
  color: #fff !important;
  border: 1px solid rgba(255,255,255,.13);
  background: rgba(255,255,255,.07);
}

#${INLINE_PANEL_ID} .tba-fixture-badge.is-bet {
  color: #59d98e !important;
  border-color: rgba(89,217,142,.45);
  background: rgba(89,217,142,.12);
}

#${INLINE_PANEL_ID} .tba-fixture-badge.is-review {
  color: #ffd166 !important;
  border-color: rgba(255,209,102,.45);
  background: rgba(255,209,102,.12);
}

#${INLINE_PANEL_ID} .tba-fixture-badge.is-skip {
  color: #ff7f8f !important;
  border-color: rgba(255,127,143,.45);
  background: rgba(255,127,143,.12);
}

#${INLINE_PANEL_ID} .tba-versus-box {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100%;
  height: 100%;
}

#${INLINE_PANEL_ID} .tba-matchup {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) 76px minmax(0, 1fr);
  gap: 8px;
  align-items: stretch;
}

#${INLINE_PANEL_ID} .tba-team-box {
  border: 1px solid rgba(255,255,255,.08);
  background: rgba(2,6,23,.38);
  border-radius: 9px;
  padding: 9px 7px;
  text-align: center;
  min-width: 0;
}

#${INLINE_PANEL_ID} .tba-team-name {
  color: #fff !important;
  font-size: 13px;
  font-weight: 900;
  line-height: 1.15;
  min-height: 30px;
  overflow-wrap: anywhere;
}

#${INLINE_PANEL_ID} .tba-vs {
  width: auto;
  height: auto;
  border-radius: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: #ffffff !important;
  font-weight: 900;
  font-size: 24px;
  line-height: 1;
  letter-spacing: 1px;
  text-shadow: 0 0 12px rgba(255,255,255,.12);
}

#${INLINE_PANEL_ID} .tba-fixture-label {
  font-size: 10px;
  font-weight: 900;
  color: #86b7ff !important;
  text-transform: uppercase;
  letter-spacing: .8px;
}

#${INLINE_PANEL_ID} .tba-fixture-league {
  margin-top: 2px;
  font-size: 11px;
  color: #aebbd2 !important;
}

#${INLINE_PANEL_ID} .tba-kickoff {
  font-size: 9px;
  color: #9fb0c8 !important;
  line-height: 1.25;
  overflow-wrap: anywhere;
}

#${INLINE_PANEL_ID} .tba-best-pick {
  border-radius: 8px;
  padding: 5px;
  background: rgba(255,255,255,.045);
  border: 1px solid rgba(255,255,255,.07);
  width: 100%;
}

#${INLINE_PANEL_ID} .tba-best-pick span {
  display: block;
  color: #9fb0c8 !important;
  font-size: 9px;
  text-transform: uppercase;
  font-weight: 800;
}

#${INLINE_PANEL_ID} .tba-best-pick b {
  display: block;
  color: #59d98e !important;
  font-size: 10px;
  margin-top: 2px;
  overflow-wrap: anywhere;
}

#${INLINE_PANEL_ID} .tba-team-insights {
  margin-top: 8px;
  text-align: left;
}

#${INLINE_PANEL_ID} .tba-insight-title {
  color: #9fb0c8 !important;
  font-size: 9px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .4px;
  margin-bottom: 4px;
}

#${INLINE_PANEL_ID} .tba-insight-list {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

#${INLINE_PANEL_ID} .tba-insight-pill {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  border-radius: 999px;
  padding: 3px 6px;
  background: rgba(89,217,142,.10);
  border: 1px solid rgba(89,217,142,.18);
  color: #dff8e9 !important;
  font-size: 9px;
  font-weight: 800;
}

#${INLINE_PANEL_ID} .tba-insight-pill span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-insight-pill b {
  color: #59d98e !important;
  font-style: normal;
}

#${INLINE_PANEL_ID} .tba-insight-empty {
  color: #6f7f98 !important;
  font-size: 9px;
  font-style: italic;
}

#${INLINE_PANEL_ID} .tba-prediction-main {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  margin-top: 9px;
}

#${INLINE_PANEL_ID} .tba-advice-box,
#${INLINE_PANEL_ID} .tba-prob-box {
  border: 1px solid rgba(255,255,255,.08);
  background: rgba(2,6,23,.32);
  border-radius: 9px;
  padding: 8px;
}

#${INLINE_PANEL_ID} .tba-advice-title {
  color: #86b7ff !important;
  font-size: 10px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .6px;
}

#${INLINE_PANEL_ID} .tba-advice-text {
  color: #fff !important;
  font-size: 13px;
  font-weight: 900;
  line-height: 1.25;
  margin-top: 4px;
  overflow-wrap: anywhere;
  white-space: pre-line;
}

#${INLINE_PANEL_ID} .tba-advice-sub {
  color: #9fb0c8 !important;
  font-size: 10px;
  margin-top: 5px;
}

#${INLINE_PANEL_ID} .tba-prob-row + .tba-prob-row {
  margin-top: 7px;
}

#${INLINE_PANEL_ID} .tba-prob-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: 10px;
  margin-bottom: 3px;
}

#${INLINE_PANEL_ID} .tba-prob-top span {
  color: #cbd5e1 !important;
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-prob-top b {
  color: #fff !important;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-prob-track {
  height: 7px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.06);
}

#${INLINE_PANEL_ID} .tba-prob-fill {
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, rgba(134,183,255,.55), rgba(89,217,142,.80));
  box-shadow: 0 0 14px rgba(89,217,142,.16);
}

#${INLINE_PANEL_ID} .tba-prob-row.draw .tba-prob-fill {
  background: linear-gradient(90deg, rgba(255,209,102,.45), rgba(255,209,102,.75));
}

#${INLINE_PANEL_ID} .tba-mini-stats {
  display: grid;
  grid-template-columns: 95px 85px 1fr;
  gap: 6px;
  margin-top: 8px;
}

#${INLINE_PANEL_ID} .tba-mini-stats > div {
  border: 1px solid rgba(255,255,255,.07);
  background: rgba(255,255,255,.04);
  border-radius: 8px;
  padding: 6px;
  min-width: 0;
}

#${INLINE_PANEL_ID} .tba-mini-stats span {
  display: block;
  color: #9fb0c8 !important;
  font-size: 9px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .4px;
}

#${INLINE_PANEL_ID} .tba-mini-stats b {
  display: block;
  color: #fff !important;
  font-size: 11px;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-card-picks {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 8px;
}

#${INLINE_PANEL_ID} .tba-card-pick {
  display: inline-grid;
  grid-template-columns: auto 1fr auto auto;
  gap: 5px;
  align-items: center;
  max-width: 100%;
  border: 1px solid rgba(89,217,142,.20);
  background: rgba(89,217,142,.08);
  border-radius: 999px;
  padding: 4px 7px;
  font-size: 10px;
}

#${INLINE_PANEL_ID} .tba-card-pick-key {
  color: #fff !important;
  background: rgba(70,120,255,.35);
  border-radius: 999px;
  padding: 2px 5px;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-card-pick-label {
  color: #dff8e9 !important;
  font-weight: 800;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-card-pick b {
  color: #59d98e !important;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-card-pick em {
  color: #fff !important;
  font-style: normal;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-kickoff-strong {
  color: #ffffff !important;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-stake-inline {
  color: #aebbd2 !important;
  font-weight: 700;
}

#${INLINE_PANEL_ID} .tba-kickoff-inline {
  color: #d7e2f4 !important;
  font-weight: 800;
  margin-left: 5px;
}

#${INLINE_PANEL_ID} .tba-fixture-actions {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-shrink: 0;
  flex-wrap: wrap;
  justify-content: flex-end;
}

#${INLINE_PANEL_ID} .tba-prob-row.is-highest .tba-prob-fill {
  background: linear-gradient(90deg, rgba(134,183,255,.55), rgba(89,217,142,.85));
  box-shadow: 0 0 14px rgba(89,217,142,.18);
}

#${INLINE_PANEL_ID} .tba-prob-row.is-tie-top .tba-prob-fill,
#${INLINE_PANEL_ID} .tba-prob-row.is-middle .tba-prob-fill {
  background: linear-gradient(90deg, rgba(255,209,102,.45), rgba(255,209,102,.78));
  box-shadow: 0 0 14px rgba(255,209,102,.13);
}

#${INLINE_PANEL_ID} .tba-prob-row.is-lowest .tba-prob-fill {
  background: linear-gradient(90deg, rgba(255,127,143,.45), rgba(255,80,105,.78));
  box-shadow: 0 0 14px rgba(255,80,105,.13);
}

#${INLINE_PANEL_ID} .tba-outcome-section {
  margin-top: 9px;
}

#${INLINE_PANEL_ID} .tba-outcome-section-title {
  color: #86b7ff !important;
  font-size: 10px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .7px;
  margin: 0 0 6px;
}

#${INLINE_PANEL_ID} .tba-outcome-cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
}

#${INLINE_PANEL_ID} .tba-outcome-card {
  border: 1px solid rgba(89,217,142,.20);
  background:
    radial-gradient(circle at top right, rgba(89,217,142,.10), transparent 34%),
    rgba(2,6,23,.38);
  border-radius: 9px;
  padding: 8px;
  min-width: 0;
}

#${INLINE_PANEL_ID} .tba-outcome-main {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: 7px;
  align-items: center;
  margin-bottom: 7px;
}

#${INLINE_PANEL_ID} .tba-outcome-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 34px;
  height: 28px;
  border-radius: 999px;
  color: #fff !important;
  background: rgba(70,120,255,.35);
  border: 1px solid rgba(120,160,255,.28);
  font-weight: 900;
  font-size: 11px;
}

#${INLINE_PANEL_ID} .tba-outcome-label {
  color: #fff !important;
  font-size: 12px;
  font-weight: 900;
  line-height: 1.15;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-outcome-market {
  color: #9fb0c8 !important;
  font-size: 9px;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-outcome-copy {
  min-width: 0;
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn {
  min-width: 46px;
  height: 28px;
  padding: 0 9px !important;
  border-radius: 7px;
  border: 1px solid rgba(89,217,142,.38) !important;
  background:
    linear-gradient(
      135deg,
      rgba(34,150,82,.96),
      rgba(22,115,64,.96)
    );
  color: #fff !important;
  font-size: 10px !important;
  font-weight: 900 !important;
  line-height: 1;
  cursor: pointer;
  box-shadow: 0 5px 12px rgba(0,0,0,.18);
  transition:
    filter .15s ease,
    opacity .15s ease,
    transform .15s ease;
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn:hover:not(:disabled) {
  filter: brightness(1.10);
  transform: translateY(-1px);
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn:active:not(:disabled) {
  transform: translateY(0);
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn:disabled {
  opacity: .42;
  cursor: not-allowed;
  box-shadow: none;
  filter: grayscale(.25);
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn.is-pending:disabled,
#${INLINE_PANEL_ID} .tba-outcome-bet-btn.is-placed:disabled {
  min-width: 74px;
  opacity: 1;
  cursor: not-allowed;
  filter: none;
  transform: none;
  box-shadow: none;
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn.is-pending:disabled {
  background:
    linear-gradient(
      135deg,
      rgba(70, 78, 94, .96),
      rgba(43, 49, 61, .96)
    );
  border-color:
    rgba(170, 180, 200, .24) !important;
  color: #d8deea !important;
}

#${INLINE_PANEL_ID} .tba-outcome-bet-btn.is-placed:disabled {
  background:
    linear-gradient(
      135deg,
      rgba(34, 67, 52, .98),
      rgba(22, 43, 35, .98)
    );
  border-color:
    rgba(89, 217, 142, .28) !important;
  color: #9fdbba !important;
}

#${INLINE_PANEL_ID} .tba-outcome-numbers {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 5px;
}

#${INLINE_PANEL_ID} .tba-outcome-numbers > div {
  border: 1px solid rgba(255,255,255,.07);
  background: rgba(255,255,255,.035);
  border-radius: 7px;
  padding: 5px;
  min-width: 0;
}

#${INLINE_PANEL_ID} .tba-outcome-numbers span {
  display: block;
  color: #9fb0c8 !important;
  font-size: 8px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .35px;
}

#${INLINE_PANEL_ID} .tba-outcome-numbers b {
  display: block;
  color: #fff !important;
  font-size: 10px;
  font-weight: 900;
  margin-top: 2px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn {
  width: 34px;
  height: 30px;
  padding: 0 !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 15px !important;
  border-radius: 8px;
  background: linear-gradient(135deg, rgba(24,34,54,.96), rgba(12,18,34,.96));
  border: 1px solid rgba(134,183,255,.20) !important;
  color: #fff !important;
}

#${INLINE_PANEL_ID} a.tba-header-icon-btn {
  text-decoration: none !important;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn.is-active {
  border-color: rgba(89,217,142,.45) !important;
  box-shadow: 0 0 0 1px rgba(89,217,142,.18) inset;
  background: linear-gradient(135deg, rgba(18,54,38,.96), rgba(12,30,23,.96));
}


#${INLINE_PANEL_ID} .tba-settings-card {
  margin: 8px 0;
  padding: 10px 11px;
  border-radius: 8px;
  border: 1px solid rgba(134,183,255,.18);
  background: rgba(255,255,255,.04);
}

#${INLINE_PANEL_ID} .tba-settings-title {
  color: #86b7ff !important;
  font-size: 11px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .6px;
  margin-bottom: 8px;
}

#${INLINE_PANEL_ID} .tba-settings-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}

#${INLINE_PANEL_ID} .tba-settings-row label {
  color: #9fb0c8 !important;
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
}

#${INLINE_PANEL_ID} .tba-settings-note {
  margin-top: 8px;
  color: #9fb0c8 !important;
  font-size: 10px;
}

#${INLINE_PANEL_ID} .tba-settings-budget {
  width: 140px;
  border: 1px solid rgba(134,183,255,.25);
  border-radius: 7px;
  background: rgba(2,6,23,.72);
  color: #fff !important;
  padding: 6px 8px;
  font-size: 12px;
  outline: none;
  text-align: right;
}

#${INLINE_PANEL_ID} .tba-settings-divider {
  height: 1px;
  margin: 10px 0;
  background: rgba(134,183,255,.16);
}

#${INLINE_PANEL_ID} .tba-odds-filter-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-top: 8px;
  flex-wrap: wrap;
}

#${INLINE_PANEL_ID} .tba-odds-filter-min-wrap {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  color: #cbd5e1;
  font-weight: 800;
}

#${INLINE_PANEL_ID} .tba-odds-filter-input {
  width: 74px;
  padding: 6px 7px;
  border-radius: 6px;
  border: 1px solid rgba(134,183,255,.28);
  background: rgba(10,16,30,.92);
  color: #e8eefc;
  font-size: 12px;
  font-weight: 800;
  outline: none;
}

#${INLINE_PANEL_ID} .tba-odds-filter-input:disabled {
  opacity: .45;
  cursor: not-allowed;
}

#${INLINE_PANEL_ID} .tba-filtered-pill {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 6px 9px;
  border-radius: 999px;
  background: rgba(255, 190, 80, .12);
  border: 1px solid rgba(255, 190, 80, .30);
  color: #ffd98a;
  font-size: 11px;
  font-weight: 900;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} #tba-budget.tba-settings-budget {
  width: 140px;
  border: 1px solid rgba(134,183,255,.25);
  border-radius: 7px;
  background: rgba(2,6,23,.72);
  color: #fff !important;
  padding: 6px 8px;
  font-size: 12px;
  outline: none;
  text-align: right;
}

#${INLINE_PANEL_ID} .tba-team-box {
  display: flex;
  flex-direction: column;
  align-items: center;
}

#${INLINE_PANEL_ID} .tba-team-logo {
  width: 50%;
  max-width: 140px;
  min-width: 96px;
  height: auto;
  display: block;
  margin: 8px auto 10px;
  object-fit: contain;
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
}

#${INLINE_PANEL_ID} .tba-team-logo-fallback {
  width: 50%;
  max-width: 140px;
  min-width: 96px;
  aspect-ratio: 1 / 1;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff !important;
  font-size: 20px;
  font-weight: 900;
  background: linear-gradient(135deg, rgba(70,120,255,.35), rgba(89,217,142,.18));
  border: none;
  border-radius: 10px;
  margin: 8px auto 10px;
}

#${INLINE_PANEL_ID} .tba-team-role {
  margin-top: 0;
  margin-bottom: 4px;
  color: #9fb0c8 !important;
  font-size: 10px;
  text-transform: uppercase;
  font-weight: 800;
  letter-spacing: .5px;
}

#${INLINE_PANEL_ID} .tba-mini-stats {
  display: grid;
  grid-template-columns: 110px 1fr 110px;
  gap: 6px;
  margin-top: 8px;
}

#${INLINE_PANEL_ID} .tba-mini-stats .tba-top-side-mini b {
  color: #59d98e !important;
  white-space: normal;
  line-height: 1.15;
}

#${INLINE_PANEL_ID} .tba-header-hint {
  position: absolute;
  top: 52px;
  right: 10px;
  z-index: 30;
  background: rgba(70,70,70,.96);
  color: #fff !important;
  padding: 8px 12px;
  border-radius: 8px;
  font-size: 12px;
  font-weight: 800;
  box-shadow: 0 8px 20px rgba(0,0,0,.35);
  opacity: 0;
  pointer-events: none;
  transform: translateY(-4px);
  transition: opacity .18s ease, transform .18s ease;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-header-hint.show {
  opacity: 1;
  transform: translateY(0);
}

#${INLINE_PANEL_ID} .tba-api-key-row {
  margin-top: 9px;
}

#${INLINE_PANEL_ID} .tba-api-key-input {
  width: 260px !important;
  font-family: monospace;
}

#${INLINE_PANEL_ID} .tba-api-key-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  align-items: center;
  margin-top: 9px;
}

#${INLINE_PANEL_ID} .tba-small-link-btn,
#${INLINE_PANEL_ID} .tba-small-action-btn,
#${INLINE_PANEL_ID} .tba-small-danger-btn {
  border: 1px solid rgba(255,255,255,.12);
  border-radius: 8px;
  color: #fff !important;
  cursor: pointer;
  font-size: 11px;
  font-weight: 900;
  padding: 6px 10px;
  line-height: 1;
  text-decoration: none !important;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

#${INLINE_PANEL_ID} .tba-small-link-btn {
  background: linear-gradient(135deg, rgba(24,34,54,.96), rgba(12,18,34,.96));
  border-color: rgba(134,183,255,.22);
}

#${INLINE_PANEL_ID} .tba-small-action-btn {
  background: linear-gradient(135deg, rgba(34, 150, 82, .95), rgba(22, 115, 64, .95));
}

#${INLINE_PANEL_ID} .tba-small-danger-btn {
  background: linear-gradient(135deg, rgba(185, 55, 65, .96), rgba(135, 34, 42, .96));
}

#${INLINE_PANEL_ID} .tba-subscription-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  margin-top: 5px;
  font-size: 10px;
  color: #9fb0c8 !important;
}

#${INLINE_PANEL_ID} .tba-sub-pill {
  display: inline-flex;
  align-items: center;
  border-radius: 999px;
  padding: 3px 7px;
  font-size: 10px;
  font-weight: 900;
  border: 1px solid rgba(255,255,255,.12);
}

#${INLINE_PANEL_ID} .tba-sub-pill-good {
  color: #59d98e !important;
  border-color: rgba(89,217,142,.45);
  background: rgba(89,217,142,.12);
}

#${INLINE_PANEL_ID} .tba-sub-pill-warn {
  color: #ffd166 !important;
  border-color: rgba(255,209,102,.45);
  background: rgba(255,209,102,.12);
}

#${INLINE_PANEL_ID} .tba-sub-pill-bad {
  color: #ff7f8f !important;
  border-color: rgba(255,127,143,.45);
  background: rgba(255,127,143,.12);
}

#${INLINE_PANEL_ID} .tba-sub-detail {
  color: #9fb0c8 !important;
  overflow-wrap: anywhere;
}

#${INLINE_PANEL_ID} .tba-visual-section {
  margin-top: 9px;
  border: 1px solid rgba(255,255,255,.08);
  background: rgba(2,6,23,.30);
  border-radius: 9px;
  padding: 8px;
}

#${INLINE_PANEL_ID} .tba-visual-title {
  color: #86b7ff !important;
  font-size: 10px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .7px;
  margin-bottom: 7px;
}

#${INLINE_PANEL_ID} .tba-compare-row + .tba-compare-row {
  margin-top: 7px;
}

#${INLINE_PANEL_ID} .tba-compare-top {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  color: #cbd5e1 !important;
  font-size: 10px;
  font-weight: 800;
  margin-bottom: 3px;
}

#${INLINE_PANEL_ID} .tba-compare-track {
  display: flex;
  height: 8px;
  overflow: hidden;
  border-radius: 999px;
  background: rgba(255,255,255,.07);
  border: 1px solid rgba(255,255,255,.06);
}

#${INLINE_PANEL_ID} .tba-compare-home {
  height: 100%;
  background: linear-gradient(90deg, rgba(134,183,255,.45), rgba(134,183,255,.78));
}

#${INLINE_PANEL_ID} .tba-compare-away {
  height: 100%;
  background: linear-gradient(90deg, rgba(89,217,142,.45), rgba(89,217,142,.78));
}

#${INLINE_PANEL_ID} .tba-form-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

#${INLINE_PANEL_ID} .tba-form-team {
  border: 1px solid rgba(255,255,255,.07);
  background: rgba(255,255,255,.035);
  border-radius: 8px;
  padding: 7px;
}

#${INLINE_PANEL_ID} .tba-form-name {
  color: #fff !important;
  font-size: 10px;
  font-weight: 900;
  margin-bottom: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-form-dots {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
}

#${INLINE_PANEL_ID} .tba-form-dot {
  width: 20px;
  height: 20px;
  border-radius: 999px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 10px;
  font-weight: 900;
  color: #fff !important;
  border: 1px solid rgba(255,255,255,.10);
  background: rgba(255,255,255,.08);
}

#${INLINE_PANEL_ID} .tba-form-dot.result-w {
  background: rgba(89,217,142,.22);
  border-color: rgba(89,217,142,.35);
}

#${INLINE_PANEL_ID} .tba-form-dot.result-d {
  background: rgba(255,209,102,.22);
  border-color: rgba(255,209,102,.35);
}

#${INLINE_PANEL_ID} .tba-form-dot.result-l {
  background: rgba(255,127,143,.22);
  border-color: rgba(255,127,143,.35);
}

#${INLINE_PANEL_ID} .tba-season-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 7px;
}

#${INLINE_PANEL_ID} .tba-season-card {
  border: 1px solid rgba(255,255,255,.07);
  background: rgba(255,255,255,.035);
  border-radius: 8px;
  padding: 7px;
}

#${INLINE_PANEL_ID} .tba-season-card b {
  display: block;
  color: #fff !important;
  font-size: 11px;
  margin-bottom: 4px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-season-card span {
  display: block;
  color: #9fb0c8 !important;
  font-size: 10px;
  line-height: 1.45;
}

#${INLINE_PANEL_ID} .tba-ai-list {
  list-style: none;
  margin: 0;
  padding: 0;
  color: #9fb0c8 !important;
  font-size: 10px;
  line-height: 1.45;
}

#${INLINE_PANEL_ID} .tba-ai-list li {
  position: relative;
  margin: 0;
  padding: 0 0 0 10px;
}

#${INLINE_PANEL_ID} .tba-ai-list li::before {
  content: "–";
  position: absolute;
  left: 0;
  top: 0;
  color: rgba(134,183,255,.85);
  font-weight: 800;
}

#${INLINE_PANEL_ID} .tba-ai-list li + li {
  margin-top: 3px;
}

#${INLINE_PANEL_ID} .tba-h2h-text {
  margin: 0 0 8px;
  padding: 7px 8px;
  border-radius: 8px;
  background: rgba(2,6,23,.32);
  border: 1px solid rgba(134,183,255,.12);
  color: rgba(255,255,255,.82) !important;
  font-size: 11px;
  font-weight: 700;
}

#${INLINE_PANEL_ID} .tba-h2h-summary {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 5px;
  margin-bottom: 7px;
}

#${INLINE_PANEL_ID} .tba-h2h-summary div {
  border: 1px solid rgba(255,255,255,.07);
  background: rgba(255,255,255,.035);
  border-radius: 7px;
  padding: 5px;
  text-align: center;
}

#${INLINE_PANEL_ID} .tba-h2h-summary span {
  display: block;
  color: #9fb0c8 !important;
  font-size: 8px;
  text-transform: uppercase;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-h2h-summary b {
  display: block;
  color: #fff !important;
  font-size: 12px;
  margin-top: 2px;
}

#${INLINE_PANEL_ID} .tba-h2h-row {
  display: grid;
  grid-template-columns: 74px 1fr auto;
  gap: 6px;
  align-items: center;
  padding: 4px 0;
  border-top: 1px solid rgba(255,255,255,.06);
  font-size: 10px;
}

#${INLINE_PANEL_ID} .tba-h2h-row span {
  color: #9fb0c8 !important;
}

#${INLINE_PANEL_ID} .tba-h2h-row b {
  color: #fff !important;
}

#${INLINE_PANEL_ID} .tba-api-key-inline-row {
  display: grid;
  grid-template-columns: 110px minmax(0, 1fr);
  gap: 10px;
  align-items: center;
  margin-top: 9px;
}

#${INLINE_PANEL_ID} .tba-api-key-inline-row label {
  color: #9fb0c8 !important;
  font-size: 11px;
  font-weight: 800;
  text-transform: uppercase;
}

#${INLINE_PANEL_ID} .tba-api-key-control {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 7px;
  min-width: 0;
}

#${INLINE_PANEL_ID} .tba-api-key-control .tba-small-link-btn,
#${INLINE_PANEL_ID} .tba-api-key-control .tba-small-action-btn,
#${INLINE_PANEL_ID} .tba-api-key-control .tba-small-danger-btn {
  flex: 0 0 auto;
  white-space: nowrap;
}

#${INLINE_PANEL_ID} .tba-subscription-help {
  margin-top: 10px;
  padding: 9px;
  border-radius: 8px;
  border: 1px solid rgba(89,217,142,.18);
  background:
    radial-gradient(circle at top right, rgba(89,217,142,.08), transparent 35%),
    rgba(255,255,255,.035);
}

#${INLINE_PANEL_ID} .tba-subscription-help-title {
  color: #59d98e !important;
  font-size: 11px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: .55px;
  margin-bottom: 6px;
}

#${INLINE_PANEL_ID} .tba-subscription-help-text {
  color: #aebbd2 !important;
  font-size: 10px;
  line-height: 1.45;
  margin-top: 5px;
}

#${INLINE_PANEL_ID} .tba-subscription-help-text b {
  color: #fff !important;
}

#${INLINE_PANEL_ID} .tba-subscription-help-text a {
  color: #86b7ff !important;
  text-decoration: none !important;
}

#${INLINE_PANEL_ID} .tba-sport-toggle-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(105px, 1fr));
  gap: 7px;
  margin-top: 8px;
}

#${INLINE_PANEL_ID} .tba-sport-toggle {
  display: flex;
  align-items: center;
  gap: 7px;
  border: 1px solid rgba(134,183,255,.16);
  background: rgba(2,6,23,.38);
  border-radius: 8px;
  padding: 8px;
  cursor: pointer;
  user-select: none;
}

#${INLINE_PANEL_ID} .tba-sport-toggle input {
  accent-color: #59d98e;
}

#${INLINE_PANEL_ID} .tba-sport-toggle span {
  color: #fff !important;
  font-size: 11px;
  font-weight: 900;
}

#${INLINE_PANEL_ID} .tba-sport-warning {
  margin-top: 8px;
  padding: 7px 8px;
  border-radius: 7px;
  border: 1px solid rgba(255,209,102,.35);
  background: rgba(255,209,102,.10);
  color: #ffd166 !important;
  font-size: 10px;
  line-height: 1.4;
}

@media (max-width: 720px) {

#${INLINE_PANEL_ID} .tba-sport-toggle-grid {
  grid-template-columns: 1fr;
}

#${INLINE_PANEL_ID} .tba-skip-action-row {
  justify-content: stretch;
}

#${INLINE_PANEL_ID} .tba-skip-btn {
  width: 100%;
}

#${INLINE_PANEL_ID} .tba-api-key-inline-row {
  grid-template-columns: 1fr;
}

#${INLINE_PANEL_ID} .tba-api-key-control {
  flex-direction: column;
  align-items: stretch;
}

#${INLINE_PANEL_ID} .tba-api-key-control .tba-api-key-input {
  order: -1;
  width: 100% !important;
  min-width: 0;
  max-width: none;
}

#${INLINE_PANEL_ID} .tba-api-key-control .tba-small-link-btn,
#${INLINE_PANEL_ID} .tba-api-key-control .tba-small-action-btn,
#${INLINE_PANEL_ID} .tba-api-key-control .tba-small-danger-btn {
  width: 100%;
}

  #${INLINE_PANEL_ID} .tba-form-grid,
  #${INLINE_PANEL_ID} .tba-season-grid {
    grid-template-columns: 1fr;
  }

#${INLINE_PANEL_ID} .tba-api-key-input {
  width: 100% !important;
}

#${INLINE_PANEL_ID} .tba-api-key-actions {
  flex-direction: column;
  align-items: stretch;
}

#${INLINE_PANEL_ID} .tba-small-link-btn,
#${INLINE_PANEL_ID} .tba-small-action-btn,
#${INLINE_PANEL_ID} .tba-small-danger-btn {
  width: 100%;
}

  #${INLINE_PANEL_ID} #tba-budget.tba-settings-budget {
  width: 100%;
}

  #${INLINE_PANEL_ID} .tba-settings-row {
  flex-direction: column;
  align-items: stretch;
}

#${INLINE_PANEL_ID} .tba-settings-budget {
  width: 100%;
}

#${INLINE_PANEL_ID} .tba-team-logo,
#${INLINE_PANEL_ID} .tba-team-logo-fallback {
  width: 58%;
  min-width: 80px;
  max-width: 120px;
}

  #${INLINE_PANEL_ID} .tba-fixture-top {
  flex-direction: column;
}

#${INLINE_PANEL_ID} .tba-fixture-actions {
  width: 100%;
}

#${INLINE_PANEL_ID} .tba-fixture-actions button {
  flex: 1;
}

#${INLINE_PANEL_ID} .tba-team-logo,
#${INLINE_PANEL_ID} .tba-team-logo-fallback {
  width: 58%;
  min-width: 80px;
  max-width: 120px;
  height: auto;
}

#${INLINE_PANEL_ID} .tba-outcome-cards {
  grid-template-columns: 1fr;
}

#${INLINE_PANEL_ID} .tba-outcome-numbers {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

  #${INLINE_PANEL_ID} {
    margin: 5px 0;
    padding: 8px;
    border-radius: 7px;
  }

  #${INLINE_PANEL_ID} .tba-head {
    flex-direction: column;
    align-items: stretch;
  }

  #${INLINE_PANEL_ID} .tba-grid {
    grid-template-columns: 1fr;
  }

  #${INLINE_PANEL_ID} .tba-title {
    font-size: 14px;
  }

  #${INLINE_PANEL_ID} .tba-sub {
    font-size: 10px;
  }

  #${INLINE_PANEL_ID} #tba-budget {
    width: 120px;
  }

  #${INLINE_PANEL_ID} .tba-matchup {
  grid-template-columns: 1fr;
}

#${INLINE_PANEL_ID} .tba-versus-box {
  order: 0;
  flex-direction: row;
  justify-content: center;
  padding: 2px 0;
}

#${INLINE_PANEL_ID} .tba-prediction-main {
  grid-template-columns: 1fr;
}

#${INLINE_PANEL_ID} .tba-mini-stats {
  grid-template-columns: 1fr;
}

#${INLINE_PANEL_ID} .tba-card-pick {
  width: 100%;
  grid-template-columns: auto 1fr auto auto;
}

#${INLINE_PANEL_ID} .tba-top-actions {
  display: flex !important;
  align-items: center !important;
  justify-content: flex-end !important;
  gap: 7px !important;
  flex-shrink: 0 !important;
  flex-wrap: nowrap !important;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn {
  width: 34px !important;
  min-width: 34px !important;
  max-width: 34px !important;
  height: 30px !important;
  min-height: 30px !important;
  max-height: 30px !important;
  flex: 0 0 34px !important;
  padding: 0 !important;
  display: inline-flex !important;
  align-items: center !important;
  justify-content: center !important;
  font-size: 15px !important;
  border-radius: 8px !important;
  background: linear-gradient(135deg, rgba(24,34,54,.96), rgba(12,18,34,.96));
  border: 1px solid rgba(134,183,255,.20) !important;
  color: #fff !important;
  overflow: hidden !important;
  outline: none !important;
  transform: none !important;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn:focus,
#${INLINE_PANEL_ID} .tba-header-icon-btn:active {
  outline: none !important;
  transform: none !important;
  filter: none !important;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn.is-active {
  border-color: rgba(89,217,142,.45) !important;
  box-shadow: 0 0 0 1px rgba(89,217,142,.18) inset !important;
  background: linear-gradient(135deg, rgba(18,54,38,.96), rgba(12,30,23,.96)) !important;
}
}

/* User Customization overrides */
#${INLINE_PANEL_ID} {
  background: linear-gradient(
    135deg,
    var(--tba-panel-bg-1, #111827),
    var(--tba-panel-bg-2, #080d19)
  ) !important;
  color: var(--tba-text-color, #e8eefc) !important;
  border-color: var(--tba-card-border, rgba(134,183,255,.24)) !important;
}

#${INLINE_PANEL_ID} .tba-title,
#${INLINE_PANEL_ID} summary,
#${INLINE_PANEL_ID} .tba-fixture-label,
#${INLINE_PANEL_ID} .tba-settings-title,
#${INLINE_PANEL_ID} .tba-subscription-help-title {
  color: var(--tba-title-color, #86b7ff) !important;
}

#${INLINE_PANEL_ID},
#${INLINE_PANEL_ID} .tba-card,
#${INLINE_PANEL_ID} .tba-fixture-card,
#${INLINE_PANEL_ID} .tba-settings-card,
#${INLINE_PANEL_ID} .tba-team-box,
#${INLINE_PANEL_ID} .tba-status,
#${INLINE_PANEL_ID} .tba-subscription-help {
  color: var(--tba-text-color, #e8eefc) !important;
}

#${INLINE_PANEL_ID} .tba-card,
#${INLINE_PANEL_ID} .tba-fixture-card,
#${INLINE_PANEL_ID} .tba-settings-card,
#${INLINE_PANEL_ID} .tba-team-box,
#${INLINE_PANEL_ID} .tba-status,
#${INLINE_PANEL_ID} .tba-subscription-help {
  background: var(--tba-card-bg, #020617) !important;
  border-color: var(--tba-card-border, #24364f) !important;
}

#${INLINE_PANEL_ID} .tba-sub,
#${INLINE_PANEL_ID} .tba-muted,
#${INLINE_PANEL_ID} .tba-kv b,
#${INLINE_PANEL_ID} .tba-table th,
#${INLINE_PANEL_ID} .tba-kickoff,
#${INLINE_PANEL_ID} .tba-sub-detail,
#${INLINE_PANEL_ID} .tba-fixture-league,
#${INLINE_PANEL_ID} .tba-settings-note,
#${INLINE_PANEL_ID} .tba-subscription-help-text {
  color: var(--tba-muted-color, #9fb0c8) !important;
}

/* Deep recommendation / visuals text overrides */
#${INLINE_PANEL_ID} .tba-advice-title,
#${INLINE_PANEL_ID} .tba-visual-title,
#${INLINE_PANEL_ID} .tba-section-title,
#${INLINE_PANEL_ID} .tba-insight-title,
#${INLINE_PANEL_ID} .tba-outcome-section-title,
#${INLINE_PANEL_ID} .tba-team-name,
#${INLINE_PANEL_ID} .tba-form-name,
#${INLINE_PANEL_ID} .tba-season-card b,
#${INLINE_PANEL_ID} .tba-mini-stats b,
#${INLINE_PANEL_ID} .tba-card-pick-key,
#${INLINE_PANEL_ID} .tba-card-pick em,
#${INLINE_PANEL_ID} .tba-kickoff-strong,
#${INLINE_PANEL_ID} .tba-prob-label,
#${INLINE_PANEL_ID} .tba-prob-value,
#${INLINE_PANEL_ID} .tba-outcome-key,
#${INLINE_PANEL_ID} .tba-outcome-title,
#${INLINE_PANEL_ID} .tba-outcome-label {
  color: var(--tba-text-color, #e8eefc) !important;
}

#${INLINE_PANEL_ID} .tba-advice-text,
#${INLINE_PANEL_ID} .tba-visual-text,
#${INLINE_PANEL_ID} .tba-comparison-row,
#${INLINE_PANEL_ID} .tba-comparison-label,
#${INLINE_PANEL_ID} .tba-comparison-value,
#${INLINE_PANEL_ID} .tba-season-card span,
#${INLINE_PANEL_ID} .tba-ai-list,
#${INLINE_PANEL_ID} .tba-ai-list li,
#${INLINE_PANEL_ID} .tba-h2h-text,
#${INLINE_PANEL_ID} .tba-stake-inline,
#${INLINE_PANEL_ID} .tba-kickoff-inline,
#${INLINE_PANEL_ID} .tba-card-pick-label,
#${INLINE_PANEL_ID} .tba-outcome-market,
#${INLINE_PANEL_ID} .tba-outcome-meta,
#${INLINE_PANEL_ID} .tba-outcome-stat,
#${INLINE_PANEL_ID} .tba-outcome-stat span,
#${INLINE_PANEL_ID} .tba-outcome-row,
#${INLINE_PANEL_ID} .tba-outcome-row span {
  color: var(--tba-muted-color, #9fb0c8) !important;
}

#${INLINE_PANEL_ID} .tba-advice-box,
#${INLINE_PANEL_ID} .tba-prob-box,
#${INLINE_PANEL_ID} .tba-visual-section,
#${INLINE_PANEL_ID} .tba-form-team,
#${INLINE_PANEL_ID} .tba-season-card,
#${INLINE_PANEL_ID} .tba-outcome-card,
#${INLINE_PANEL_ID} .tba-outcomes-card,
#${INLINE_PANEL_ID} .tba-card-pick {
  background: var(--tba-card-bg, #020617) !important;
  border-color: var(--tba-card-border, #24364f) !important;
}

#${INLINE_PANEL_ID} .tba-ai-list li::before {
  color: var(--tba-accent-color, #86b7ff) !important;
}

#${INLINE_PANEL_ID} .tba-card-pick b,
#${INLINE_PANEL_ID} .tba-profit,
#${INLINE_PANEL_ID} .tba-return,
#${INLINE_PANEL_ID} .tba-positive {
  color: var(--tba-success-color, #59d98e) !important;
}

#${INLINE_PANEL_ID} .tba-form-dot {
  color: var(--tba-text-color, #e8eefc) !important;
  border-color: var(--tba-card-border, #24364f) !important;
}

#${INLINE_PANEL_ID} .tba-form-dot.result-w {
  color: var(--tba-success-color, #59d98e) !important;
  border-color: var(--tba-success-color, #59d98e) !important;
}

#${INLINE_PANEL_ID} .tba-form-dot.result-d {
  color: var(--tba-warning-color, #ffd166) !important;
  border-color: var(--tba-warning-color, #ffd166) !important;
}

#${INLINE_PANEL_ID} .tba-form-dot.result-l {
  color: var(--tba-danger-color, #ff7f8f) !important;
  border-color: var(--tba-danger-color, #ff7f8f) !important;
}

#${INLINE_PANEL_ID} .tba-key,
#${INLINE_PANEL_ID} .tba-small-link-btn,
#${INLINE_PANEL_ID} .tba-small-action-btn {
  border-color: var(--tba-accent-color, #86b7ff) !important;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn {
  background: linear-gradient(
    135deg,
    var(--tba-button-bg-1, #182236),
    var(--tba-button-bg-2, #0c1222)
  ) !important;
  border-color: var(--tba-card-border, #24364f) !important;
}

#${INLINE_PANEL_ID} .tba-header-icon-btn.is-active {
  border-color: var(--tba-success-color, #59d98e) !important;
  box-shadow: 0 0 0 1px var(--tba-success-color, #59d98e) inset !important;
}

#${INLINE_PANEL_ID} .tba-pill-good,
#${INLINE_PANEL_ID} .tba-sub-pill-good,
#${INLINE_PANEL_ID} .tba-fixture-badge.is-bet {
  color: var(--tba-success-color, #59d98e) !important;
  border-color: var(--tba-success-color, #59d98e) !important;
}

#${INLINE_PANEL_ID} .tba-pill-warn,
#${INLINE_PANEL_ID} .tba-sub-pill-warn,
#${INLINE_PANEL_ID} .tba-fixture-badge.is-review {
  color: var(--tba-warning-color, #ffd166) !important;
  border-color: var(--tba-warning-color, #ffd166) !important;
}

#${INLINE_PANEL_ID} .tba-pill-bad,
#${INLINE_PANEL_ID} .tba-sub-pill-bad,
#${INLINE_PANEL_ID} .tba-fixture-badge.is-skip {
  color: var(--tba-danger-color, #ff7f8f) !important;
  border-color: var(--tba-danger-color, #ff7f8f) !important;
}

/* H2H customization overrides */
#${INLINE_PANEL_ID} .tba-h2h-summary,
#${INLINE_PANEL_ID} .tba-h2h-row {
  background: var(--tba-card-bg, #020617) !important;
  border-color: var(--tba-card-border, #24364f) !important;
}

#${INLINE_PANEL_ID} .tba-h2h-summary,
#${INLINE_PANEL_ID} .tba-h2h-summary span,
#${INLINE_PANEL_ID} .tba-h2h-summary b,
#${INLINE_PANEL_ID} .tba-h2h-row,
#${INLINE_PANEL_ID} .tba-h2h-row span,
#${INLINE_PANEL_ID} .tba-h2h-row b {
  color: var(--tba-muted-color, #9fb0c8) !important;
}

#${INLINE_PANEL_ID} .tba-customization-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin-top: 8px;
}

#${INLINE_PANEL_ID} .tba-color-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  border: 1px solid var(--tba-card-border, #24364f);
  background: rgba(255,255,255,.035);
  border-radius: 8px;
  padding: 7px 8px;
}

#${INLINE_PANEL_ID} .tba-color-row span {
  color: var(--tba-text-color, #e8eefc) !important;
  font-size: 11px;
  font-weight: 800;
}

#${INLINE_PANEL_ID} .tba-color-input {
  width: 42px;
  height: 28px;
  border: 1px solid var(--tba-card-border, #24364f);
  border-radius: 7px;
  background: transparent;
  padding: 0;
  cursor: pointer;
}

#${INLINE_PANEL_ID} .tba-customization-actions {
  display: flex;
  justify-content: flex-end;
  margin-top: 9px;
}

#${INLINE_PANEL_ID} .tba-default-btn {
  background: linear-gradient(135deg, rgba(185, 55, 65, .96), rgba(135, 34, 42, .96));
  border-color: var(--tba-danger-color, #ff7f8f) !important;
}

@media (max-width: 520px) {
  #${INLINE_PANEL_ID} .tba-customization-grid {
    grid-template-columns: 1fr;
  }
}
  `;
  }

  // ---------------------------------------------------------------------------
  // Recommendation-card rendering helpers
  // ---------------------------------------------------------------------------

  function formatKickoffText(s) {
    if (!s) return "";

    try {
      const d = new Date(s);
      if (Number.isNaN(d.getTime())) return String(s);

      const days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
      const months = [
        "Jan",
        "Feb",
        "Mar",
        "Apr",
        "May",
        "Jun",
        "Jul",
        "Aug",
        "Sep",
        "Oct",
        "Nov",
        "Dec",
      ];

      let hour = d.getUTCHours();
      const minute = String(d.getUTCMinutes()).padStart(2, "0");
      const ampm = hour >= 12 ? "PM" : "AM";
      hour = hour % 12 || 12;

      return `${days[d.getUTCDay()]}, ${months[d.getUTCMonth()]} ${d.getUTCDate()}, ${hour}:${minute} ${ampm} TCT`;
    } catch (_) {
      return String(s);
    }
  }

  function potentialProfit(stake, odds) {
    const s = Number(stake || 0);
    const o = Number(odds || 0);
    if (!Number.isFinite(s) || !Number.isFinite(o) || s <= 0 || o <= 1)
      return 0;
    return s * (o - 1);
  }

  function potentialReturn(stake, odds) {
    const s = Number(stake || 0);
    const o = Number(odds || 0);
    if (!Number.isFinite(s) || !Number.isFinite(o) || s <= 0 || o <= 1)
      return 0;
    return s * o;
  }

  function probabilityClassMap(home, draw, away) {
    const vals = [
      { key: "home", value: cardPct(home) },
      { key: "draw", value: cardPct(draw) },
      { key: "away", value: cardPct(away) },
    ];

    const max = Math.max(...vals.map((x) => x.value));
    const min = Math.min(...vals.map((x) => x.value));

    const maxCount = vals.filter((x) => Math.abs(x.value - max) < 0.001).length;

    const out = {};

    for (const item of vals) {
      if (maxCount > 1 && Math.abs(item.value - max) < 0.001) {
        out[item.key] = "is-tie-top";
      } else if (Math.abs(item.value - max) < 0.001) {
        out[item.key] = "is-highest";
      } else if (Math.abs(item.value - min) < 0.001) {
        out[item.key] = "is-lowest";
      } else {
        out[item.key] = "is-middle";
      }
    }

    return out;
  }

  function renderPredictionShape(d) {
    const s = d.prediction_shape || {};
    if (!s || !s.top_side) return "-";

    return `${s.top_side} ${pct(s.top_p)} / gap ${pct(s.top_gap)} / ${s.close ? "close" : s.strong ? "strong" : "normal"}`;
  }

  function safeCard(d = state.lastDecision) {
    return d?.display_card || d?.displayCard || null;
  }

  function cardPct(n) {
    const v = Number(n);
    if (!Number.isFinite(v)) return 0;

    // Worker احتمالاً 0.42 می‌فرستد، ولی اگر روزی 42 فرستاد هم درست شود.
    if (v > 1) return Math.max(0, Math.min(100, v));
    return Math.max(0, Math.min(100, v * 100));
  }

  function cardPctText(n) {
    const v = cardPct(n);
    if (!Number.isFinite(v)) return "-";
    return v.toFixed(1) + "%";
  }
  function teamInitials(name) {
    const words = String(name || "")
      .split(/\s+/)
      .map((x) => x.trim())
      .filter(Boolean);

    if (!words.length) return "?";

    return words
      .slice(0, 2)
      .map((w) => w[0])
      .join("")
      .toUpperCase();
  }

  function renderTeamLogo(team) {
    const name = team?.name || "";
    const logo = team?.logo || "";

    if (logo) {
      return `<img class="tba-team-logo" src="${htmlEscape(logo)}" alt="${htmlEscape(name)}" loading="lazy">`;
    }

    return `<div class="tba-team-logo tba-team-logo-fallback">${htmlEscape(teamInitials(name))}</div>`;
  }

  function renderProbabilityBar(label, value, extraClass = "") {
    const pctValue = cardPct(value);

    return `
    <div class="tba-prob-row ${extraClass}">
      <div class="tba-prob-top">
        <span>${htmlEscape(label)}</span>
        <b>${cardPctText(value)}</b>
      </div>
      <div class="tba-prob-track">
        <div class="tba-prob-fill" style="width:${pctValue.toFixed(2)}%"></div>
      </div>
    </div>
  `;
  }

  function renderInsightList(items, emptyText = "No clear edge") {
    const arr = Array.isArray(items) ? items.slice(0, 3) : [];

    if (!arr.length) {
      return `<div class="tba-insight-empty">${htmlEscape(emptyText)}</div>`;
    }

    return `
    <div class="tba-insight-list">
      ${arr
        .map(
          (item) => `
        <div class="tba-insight-pill">
          <span>${htmlEscape(item.label || item.key || "Edge")}</span>
          ${item.diff != null ? `<b>+${cardPctText(item.diff)}</b>` : ""}
        </div>
      `,
        )
        .join("")}
    </div>
  `;
  }

  function getRenderableSelectedOutcomes(card, d) {
    const sources = [
      d?.selected_outcomes,
      card?.advisor?.selected_outcomes,
      d?.advisor?.selected_outcomes,
      card?.selected_outcomes,
    ];

    for (const src of sources) {
      if (Array.isArray(src) && src.length) {
        return src;
      }
    }

    return [];
  }

  function renderSelectedOutcomeChips(
    card,
    d,
  ) {
    const selected =
      getRenderableSelectedOutcomes(
        card,
        d,
      );

    if (
      !Array.isArray(selected) ||
      !selected.length
    ) {
      return "";
    }

    const marketClosed =
      isMarketStartedOrFinished(
        state.lastParsed,
      );

    const oddsFilter =
      normalizeOddsFilterSettings(
        state.oddsFilter,
      );

    const bulkDirectBetActive =
      state.directBetFlow?.phase === "bet" &&
      state.directBetFlow?.eventKey ===
        currentBookieEventKey();

    return `
      <div class="tba-outcome-section">
        <div class="tba-outcome-section-title">Outcomes</div>

        <div class="tba-outcome-cards">
          ${selected
            .map((o, index) => {
              const stake = Number(
                o.stake || 0,
              );

              const odds = Number(
                o.odds || 0,
              );

              const betGuardKey =
                singleOutcomeBetGuardKey(o);

              const singleBetState = String(
                state.singleOutcomeBetStates?.[
                  betGuardKey
                ] || "",
              );

              const singleBetPending =
                singleBetState === "pending";

              const singleBetPlaced =
                singleBetState === "placed";

              const singleBetLocked =
                singleBetPending ||
                singleBetPlaced;

              const singleBetButtonText =
                singleBetPlaced
                  ? "Bet Placed"
                  : singleBetPending
                    ? "Placing..."
                    : "Bet";

              const singleBetButtonClass =
                singleBetPlaced
                  ? "is-placed"
                  : singleBetPending
                    ? "is-pending"
                    : "";

              const profit =
                potentialProfit(
                  stake,
                  odds,
                );

              const ret =
                potentialReturn(
                  stake,
                  odds,
                );

              const blockedByOddsFilter =
                oddsFilter.enabled &&
                odds <
                  Number(
                    oddsFilter.minOdds ||
                    DEFAULT_ODDS_FILTER.minOdds,
                  );

              const betDisabled =
                String(d?.action || "").toUpperCase() !== "BET" ||
                marketClosed ||
                stake <= 0 ||
                blockedByOddsFilter ||
                bulkDirectBetActive ||
                singleBetLocked;

              return `
              <div class="tba-outcome-card">
                <div class="tba-outcome-main">
                  <span class="tba-outcome-key">${htmlEscape(o.key || o.role || "-")}</span>

                  <div class="tba-outcome-copy">
                    <div class="tba-outcome-label">${htmlEscape(o.label || "")}</div>
                    <div class="tba-outcome-market">${htmlEscape(o.source_market_name || o.market || "")}</div>
                  </div>

                  <button
                    type="button"
                    class="tba-outcome-bet-btn ${singleBetButtonClass}"
                    data-tba-outcome-bet-index="${index}"
                    ${betDisabled ? "disabled" : ""}
                  >${htmlEscape(singleBetButtonText)}</button>
                </div>

                <div class="tba-outcome-numbers">
                  <div>
                    <span>Odds</span>
                    <b>${dec(odds, 2)}</b>
                  </div>

                  <div>
                    <span>Stake</span>
                    <b>${money(stake)}</b>
                  </div>

                  <div>
                    <span>Profit</span>
                    <b>${money(profit)}</b>
                  </div>

                  <div>
                    <span>Return</span>
                    <b>${money(ret)}</b>
                  </div>
                </div>
              </div>
            `;
            })
            .join("")}
        </div>
      </div>
    `;
  }

  function renderComparisonVisuals(card) {
    const fixture = card?.fixture || {};
    const home = fixture.home || {};
    const away = fixture.away || {};

    const rows = card?.visuals?.comparison_bars || card?.comparison || [];

    if (!Array.isArray(rows) || !rows.length) return "";

    return `
    <div class="tba-visual-section">
      <div class="tba-visual-title">Team Comparison</div>

      ${rows
        .map((item) => {
          const label = item.label || item.key || "Comparison";
          const hv = Number(item.home_percent ?? item.home_value ?? 0) || 0;
          const av = Number(item.away_percent ?? item.away_value ?? 0) || 0;
          const total = Math.max(1, hv + av);
          const hw = Math.max(0, Math.min(100, (hv / total) * 100));
          const aw = Math.max(0, Math.min(100, (av / total) * 100));

          return `
          <div class="tba-compare-row">
            <div class="tba-compare-top">
              <span>${htmlEscape(label)}</span>
              <span>${htmlEscape(home.name || "Home")} ${hv.toFixed(0)}% / ${htmlEscape(away.name || "Away")} ${av.toFixed(0)}%</span>
            </div>
            <div class="tba-compare-track">
              <div class="tba-compare-home" style="width:${hw.toFixed(2)}%"></div>
              <div class="tba-compare-away" style="width:${aw.toFixed(2)}%"></div>
            </div>
          </div>
        `;
        })
        .join("")}
    </div>
  `;
  }

  function renderFormDots(list) {
    const arr = Array.isArray(list) ? list : [];

    if (!arr.length) {
      return `<span class="tba-muted">No form data</span>`;
    }

    return `
    <div class="tba-form-dots">
      ${arr
        .map((x) => {
          const r = String(x || "").toUpperCase();
          const cls =
            r === "W"
              ? "result-w"
              : r === "D"
                ? "result-d"
                : r === "L"
                  ? "result-l"
                  : "";

          return `<span class="tba-form-dot ${cls}">${htmlEscape(r || "?")}</span>`;
        })
        .join("")}
    </div>
  `;
  }

  function renderRecentFormVisuals(card) {
    const v = card?.visuals?.recent_form;
    if (!v || (!v.home && !v.away)) return "";

    return `
    <div class="tba-visual-section">
      <div class="tba-visual-title">Recent Form</div>

      <div class="tba-form-grid">
        <div class="tba-form-team">
          <div class="tba-form-name">${htmlEscape(v.home?.team_name || card?.fixture?.home?.name || "Home")}</div>
          ${renderFormDots(v.home?.last_10)}
        </div>

        <div class="tba-form-team">
          <div class="tba-form-name">${htmlEscape(v.away?.team_name || card?.fixture?.away?.name || "Away")}</div>
          ${renderFormDots(v.away?.last_10)}
        </div>
      </div>
    </div>
  `;
  }

  function renderSeasonSummaryVisuals(card) {
    const v = card?.visuals?.season_summary;
    if (!v || (!v.home && !v.away)) return "";

    const block = (x, fallbackName) => `
    <div class="tba-season-card">
      <b>${htmlEscape(x?.team_name || fallbackName || "-")}</b>
      <span>Record: ${Number(x?.wins_total || 0)}W / ${Number(x?.draws_total || 0)}D / ${Number(x?.losses_total || 0)}L</span>
      <span>Played: ${Number(x?.played_total || 0)}</span>
      <span>Clean sheets: ${Number(x?.clean_sheets || 0)}</span>
      <span>Failed to score: ${Number(x?.failed_to_score || 0)}</span>
    </div>
  `;

    return `
    <div class="tba-visual-section">
      <div class="tba-visual-title">Season Summary</div>
      <div class="tba-season-grid">
        ${block(v.home, card?.fixture?.home?.name || "Home")}
        ${block(v.away, card?.fixture?.away?.name || "Away")}
      </div>
    </div>
  `;
  }

  function aiInsightText(value) {
    if (typeof value !== "string") return "";

    return cleanText(value);
  }

  function aiInsightList(value, limit) {
    if (!Array.isArray(value)) return [];

    return value
      .map((item) => {
        if (typeof item === "string") return cleanText(item);
        if (item && typeof item === "object") {
          return cleanText(item.text || item.label || item.value || "");
        }

        return "";
      })
      .filter(Boolean)
      .slice(0, limit);
  }

  function hasFootballAiInsightsContent(insights) {
    return Boolean(
      aiInsightText(insights?.advice) ||
        aiInsightList(insights?.key_factors, 4).length ||
        aiInsightList(insights?.red_flags, 3).length ||
        aiInsightText(insights?.news_summary),
    );
  }

  function getFootballAiInsights(card, decision) {
    const sport = normalizeSportName(
      card?.sport ||
        card?.fixture?.sport ||
        decision?.display_card?.sport ||
        decision?.displayCard?.sport ||
        decision?.sport ||
        state.lastParsed?.sport ||
        currentBookieSport(),
    );

    if (!["football", "basketball"].includes(sport)) return null;

    const sources = [
      card?.ai_insights,
      decision?.display_card?.ai_insights,
      decision?.displayCard?.ai_insights,
      decision?.football_ai_insights,
      decision?.basketball_ai_insights,
    ];

    for (const insights of sources) {
      if (
        insights &&
        typeof insights === "object" &&
        insights.available === true &&
        hasFootballAiInsightsContent(insights)
      ) {
        return insights;
      }
    }

    return null;
  }

  function renderFootballAiInsightsVisuals(card, decision) {
    const insights = getFootballAiInsights(card, decision);
    if (!insights) return "";

    const renderTextCard = (label, text) => {
      const body = aiInsightText(text);
      if (!body) return "";

      return `
      <div class="tba-season-card tba-ai-card">
        <b>${htmlEscape(label)}</b>
        <span>${htmlEscape(body)}</span>
      </div>
    `;
    };

    const renderListCard = (label, items, limit) => {
      const list = aiInsightList(items, limit);
      if (!list.length) return "";

      return `
      <div class="tba-season-card tba-ai-card">
        <b>${htmlEscape(label)}</b>
        <ul class="tba-ai-list">
          ${list.map((item) => `<li>${htmlEscape(item)}</li>`).join("")}
        </ul>
      </div>
    `;
    };

    const cards = [
      renderTextCard("Advice", insights.advice),
      renderListCard("Factors", insights.key_factors, 4),
      renderListCard("Risks", insights.red_flags, 3),
      renderTextCard("News", insights.news_summary),
    ]
      .filter(Boolean)
      .join("");

    if (!cards) return "";

    return `
    <div class="tba-visual-section">
      <div class="tba-visual-title">Insights</div>
      <div class="tba-season-grid tba-ai-grid">
        ${cards}
      </div>
    </div>
  `;
  }

  function renderH2hVisuals(card) {
    const h = card?.visuals?.h2h_summary;
    if (!h || !Number(h.matches_used || 0)) return "";

    const sport = String(
      card?.sport || state.lastDecision?.sport || "",
    ).toLowerCase();
    const isNoDrawSport = sport === "basketball" || sport === "baseball";

    const fixture = card?.fixture || {};
    const homeName = h.home_label || fixture.home?.name || "Home";
    const awayName = h.away_label || fixture.away?.name || "Away";

    const recentRaw = Array.isArray(h.recent)
      ? h.recent
      : Array.isArray(h.recent_matches)
        ? h.recent_matches
        : [];

    const recent = recentRaw.slice(0, 5);

    return `
    <div class="tba-visual-section">
      <div class="tba-visual-title">Head to Head</div>

      ${h.text ? `<div class="tba-h2h-text">${htmlEscape(h.text)}</div>` : ""}

      <div class="tba-h2h-summary">
        <div>
          <span>${htmlEscape(homeName)}</span>
          <b>${Number(h.home_wins || 0)}</b>
        </div>

        <div>
          <span>Draws</span>
          <b>${isNoDrawSport ? "-" : Number(h.draws || 0)}</b>
        </div>

        <div>
          <span>${htmlEscape(awayName)}</span>
          <b>${Number(h.away_wins || 0)}</b>
        </div>
      </div>

      ${recent
        .map((r) => {
          const date = r.date_utc
            ? String(r.date_utc).slice(0, 10)
            : r.date
              ? String(r.date).slice(0, 10)
              : "-";

          const teams = `${r.home_team || r.home_team_name || ""} v ${
            r.away_team || r.away_team_name || ""
          }`;

          const score =
            r.score ||
            r.score_current ||
            (r.home_runs != null && r.away_runs != null
              ? `${r.home_runs}-${r.away_runs}`
              : "-");

          return `
          <div class="tba-h2h-row">
            <span>${htmlEscape(date)}</span>
            <span>${htmlEscape(teams)}</span>
            <b>${htmlEscape(score || "-")}</b>
          </div>
        `;
        })
        .join("")}
    </div>
  `;
  }

  function renderVisualsHtml(card) {
    const d = state.lastDecision || {};

    return [
      renderComparisonVisuals(card),
      renderRecentFormVisuals(card),
      renderSeasonSummaryVisuals(card),
      renderFootballAiInsightsVisuals(card, d),
      renderH2hVisuals(card),
    ]
      .filter(Boolean)
      .join("");
  }

  function isSubscriptionBlockedDecision() {
    const d = state.lastDecision;

    if (!d) return false;

    const sub = d.subscription || state.subscription;

    const blockedReasons = new Set([
      "API_KEY_REQUIRED",
      "INVALID_TORN_API_KEY",
      "TORN_API_VERIFICATION_FAILED",
      "SUBSCRIPTION_REQUIRED",
      "SUBSCRIPTION_EXPIRED",
      "NOT_SUBSCRIBED",
    ]);

    return (
      blockedReasons.has(String(d.reason || "")) ||
      blockedReasons.has(String(d.error || "")) ||
      (sub && sub.active === false)
    );
  }

  function renderApiKeyRequiredHtml() {
    return `
    <div class="tba-card">
      <h4>API Key Required</h4>
      <div class="tba-muted">
        To use Torn Bookie Predictor, enter a Torn API key in Settings.
        The key should be Public/Custom and only needs <b>/user/basic</b> access.
        It is sent to the server only to verify your Torn user and subscription; it is not stored on the server.
      </div>

      <div class="tba-api-key-actions" style="margin-top:8px;">
        <button id="tba-open-settings" class="tba-small-action-btn">Open Settings</button>
        <a
          class="tba-small-link-btn"
          href="${TORN_API_KEY_CREATE_URL}"
          target="_blank"
          rel="noopener noreferrer"
        >Create API Key</a>
      </div>
    </div>
  `;
  }

  function renderSubscriptionBlockedHtml() {
    const d = state.lastDecision || {};
    const sub = state.subscription || d.subscription || {};
    const days = Number(sub.days_remaining ?? 0) || 0;
    const msg = decisionUserMessage(d) || "Your subscription is not active.";

    return `
    <div class="tba-card">
      <h4>Access Required</h4>
      <div class="tba-muted">
        ${htmlEscape(msg)}
      </div>

      <div class="tba-settings-note">
        Days remaining: <b>${htmlEscape(String(days))}</b>
        ${sub.expires_at ? `• Expires: <b>${htmlEscape(formatSubscriptionDate(sub.expires_at))}</b>` : ""}
      </div>

      ${
        sub.name
          ? `
        <div class="tba-settings-note">
          Verified Torn user: <b>${htmlEscape(sub.name)}</b>
        </div>
      `
          : ""
      }
    </div>
  `;
  }

  function isSkippableDecision(d = state.lastDecision) {
    if (!d) return false;

    const action = String(d.action || "").toUpperCase();
    const text = [
      d.reason,
      d.error,
      d.user_message,
      d.message,
      decisionUserMessage(d),
    ]
      .filter(Boolean)
      .join(" ");

    if (action === "SKIP") return true;

    return /LOW_CONFIDENCE_FIXTURE_MATCH_HARD_STOP|FIXTURE_MATCH|NO_FIXTURE|NOT_FOUND|NO_USABLE_API_PREDICTION|NO_USABLE_PREDICTION|NO_PREDICTION/i.test(
      text,
    );
  }

  function renderAdviceTextHtml(text) {
    const normalized = String(text || "")
      // Force Total score range to a new line after the main advice sentence
      .replace(/\.\s+(Total score range:)/i, ".\n$1");

    return htmlEscape(normalized).replace(/\r?\n/g, "<br>");
  }

  function normalizeDisplayTeam(raw = {}, fallbackName = "") {
    const name =
      raw.name ||
      raw.team_name ||
      raw.teamName ||
      raw.label ||
      fallbackName ||
      "";

    const logo =
      raw.logo || raw.team_logo || raw.teamLogo || raw.image || raw.icon || "";

    return {
      ...raw,
      name,
      logo,
    };
  }

  function normalizeDisplayLeague(rawFixture = {}) {
    const rawLeague = rawFixture.league;

    if (rawLeague && typeof rawLeague === "object") {
      return {
        ...rawLeague,
        name: rawLeague.name || rawFixture.league_name || "",
        country: rawLeague.country || rawFixture.country || "",
      };
    }

    return {
      name: rawFixture.league_name || rawFixture.league || "",
      country: rawFixture.country || "",
    };
  }

  function normalizeDisplayCardForRendering(card) {
    if (!card || typeof card !== "object") return card;

    const fixture = card.fixture || {};
    const teams = card.teams || {};

    const home = normalizeDisplayTeam(
      fixture.home || teams.home || {},
      fixture.home_team_name || teams.home?.team_name || "Home",
    );

    const away = normalizeDisplayTeam(
      fixture.away || teams.away || {},
      fixture.away_team_name || teams.away?.team_name || "Away",
    );

    const league = normalizeDisplayLeague(fixture);

    return {
      ...card,
      fixture: {
        ...fixture,
        home,
        away,
        league,
      },
    };
  }

  function getCurrentTctTimestamp() {
    return Math.floor(Date.now() / 1000);
  }

  function getParsedMarketText(parsed = state.lastParsed) {
    const parts = [parsed?.title, parsed?.marketName, parsed?.startText];

    if (parsed?.root && document.body.contains(parsed.root)) {
      parts.push(parsed.root.innerText || parsed.root.textContent || "");
    }

    return cleanText(parts.filter(Boolean).join(" "));
  }

  function getParsedMarketStartTimestamp(parsed = state.lastParsed) {
    const text = getParsedMarketText(parsed);

    return (
      parseTornStartTimestamp(parsed?.startText) ||
      parseTornStartTimestamp(parsed?.marketName) ||
      parseTornStartTimestamp(text) ||
      null
    );
  }

  function isMarketStartedOrFinished(parsed = state.lastParsed) {
    const text = getParsedMarketText(parsed);
    const startTs = getParsedMarketStartTimestamp(parsed);
    const nowTs = getCurrentTctTimestamp();

    // Finished is always closed.
    if (/\bfinished\s+at\b/i.test(text)) {
      return true;
    }

    // Started is always closed.
    if (/\bstarted\s+at\b/i.test(text)) {
      return true;
    }

    // If we have a start timestamp, close actions once the match start time has passed.
    if (startTs && nowTs >= startTs) {
      return true;
    }

    return false;
  }

  function renderDisplayCardHtml() {
    const d = state.lastDecision;
    const rawCard = safeCard(d);

    if (!rawCard) {
      return "";
    }

    const card = normalizeDisplayCardForRendering(rawCard);
    const fixture = card.fixture || {};
    const home = fixture.home || {};
    const away = fixture.away || {};
    const league = fixture.league || {};
    const prediction = card.prediction || {};
    const probs = prediction.probabilities || {};
    const insights = card.insights || {};
    const advisor = card.advisor || {};
    const oddsFilterBlockInfo = getOddsFilterBlockInfo(d);

    const kickoffText = formatKickoffText(
      fixture.kickoff_utc || fixture.date_utc || fixture.date || "",
    );

    const cardSport = String(
      card.sport ||
        d?.sport ||
        fixture.sport ||
        state.lastParsed?.sport ||
        currentBookieSport() ||
        "",
    ).toLowerCase();

    const isTwoWayCard = cardSport === "basketball" || cardSport === "baseball";

    const probClasses = probabilityClassMap(
      probs.home,
      isTwoWayCard ? null : probs.draw,
      probs.away,
    );

    const stakePct = advisor.stake_pct ?? d?.stake_pct ?? null;

    const leagueText =
      [league.name, league.country].filter(Boolean).join(" • ") || "-";

    let adviceText =
      prediction.advice ||
      card?.ui_hints?.main_advice ||
      card?.ui_hints?.winner_text ||
      prediction?.winner?.name ||
      d?.reason ||
      "-";

    const adviceTextWithAverage = adviceText;

    return `
    <div class="tba-fixture-card">
      <div class="tba-fixture-glow"></div>

      <div class="tba-fixture-top">
        <div>
          <div class="tba-fixture-label">${htmlEscape(leagueText)}</div>
          <div class="tba-fixture-league">
          <span class="tba-kickoff-strong">${htmlEscape(kickoffText || "-")}</span>
          ${stakePct != null ? `<span class="tba-stake-inline"> • Stake ${htmlEscape(pct(stakePct))}</span>` : ""}
        </div>
        </div>

        <div class="tba-fixture-actions">
  ${
    d?.action === "BET" &&
    !isMarketStartedOrFinished(state.lastParsed) &&
    !oddsFilterBlockInfo
      ? `
      <button id="tba-fill" class="tba-fill-btn">Next Match</button>
      <button id="tba-direct-bet" class="tba-bet-btn">${htmlEscape(directBetButtonLabel())}</button>
    `
      : d?.action === "BET" && oddsFilterBlockInfo
        ? `
      <span class="tba-filtered-pill">Odds Filtered</span>
      <button id="tba-skip-next" class="tba-skip-btn">SKIP</button>
    `
        : isSkippableDecision(d)
          ? `
      <button id="tba-skip-next" class="tba-skip-btn">SKIP</button>
  `
          : ""
  }
</div>
      </div>

      <div class="tba-matchup">
        <div class="tba-team-box">
          <div class="tba-team-role">Home</div>
          ${renderTeamLogo(home)}
          <div class="tba-team-name">${htmlEscape(home.name || "Home")}</div>

          <div class="tba-team-insights">
            <div class="tba-insight-title">Strengths</div>
            ${renderInsightList(insights.home?.strengths, "No clear strength")}
          </div>
        </div>

        <div class="tba-versus-box">
          <div class="tba-vs">VS</div>
        </div>

        <div class="tba-team-box">
          <div class="tba-team-role">Away</div>
          ${renderTeamLogo(away)}
          <div class="tba-team-name">${htmlEscape(away.name || "Away")}</div>

          <div class="tba-team-insights">
            <div class="tba-insight-title">Strengths</div>
            ${renderInsightList(insights.away?.strengths, "No clear strength")}
          </div>
        </div>
      </div>

      <div class="tba-prediction-main">
        <div class="tba-advice-box">
          <div class="tba-advice-title">Prediction</div>
          <div class="tba-advice-text">${renderAdviceTextHtml(adviceTextWithAverage)}</div>
        </div>

        <div class="tba-prob-box">
          ${renderProbabilityBar(home.name || "Home", probs.home, `home ${probClasses.home}`)}
          ${isTwoWayCard ? "" : renderProbabilityBar("Draw", probs.draw, `draw ${probClasses.draw}`)}
          ${renderProbabilityBar(away.name || "Away", probs.away, `away ${probClasses.away}`)}
        </div>
      </div>

      ${renderVisualsHtml(card)}
      ${renderSelectedOutcomeChips(card, d)}
    </div>
  `;
  }

  function renderLoadingResultHtml() {
    return `
    <div class="tba-card">
      <h4>Loading advisor...</h4>
      <div class="tba-muted">
        The interface is ready. Waiting for the full betting bundle and prediction result.
      </div>
    </div>
  `;
  }

  // ---------------------------------------------------------------------------
  // Additional-market expansion
  // ---------------------------------------------------------------------------

  function findAdditionalOptionsToggle() {
    const nodes = [
      ...document.querySelectorAll(
        'a, button, li[class*="extraOddsActivator"] a, li[class*="extraOddsActivator"]',
      ),
    ]
      .filter((el) => isDomVisible(el) || isVisible(el))
      .map((el) => {
        const text = cleanText(
          el.innerText || el.textContent || el.value || "",
        );
        const r = el.getBoundingClientRect();

        return {
          el,
          text,
          lower: text.toLowerCase(),
          score: -Math.abs(r.top + r.height / 2 - window.innerHeight * 0.5),
        };
      })
      .filter((x) => /additional betting options/i.test(x.text))
      .sort((a, b) => b.score - a.score);

    return nodes[0] || null;
  }

  function isAdditionalOptionsAlreadyOpen() {
    const toggle = findAdditionalOptionsToggle();

    if (!toggle) return false;

    // After expansion Torn changes Show -> Hide.
    // If we see Hide, options are already open. Never click it.
    return /\bhide\b/i.test(toggle.text);
  }

  function findShowAdditionalOptionsButton() {
    const toggle = findAdditionalOptionsToggle();

    if (!toggle) return null;

    const text = toggle.text;

    // Very strict: only click real SHOW, never HIDE.
    if (!/\bshow\b/i.test(text)) return null;
    if (/\bhide\b/i.test(text)) return null;
    if (!/additional betting options/i.test(text)) return null;

    return toggle.el;
  }

  async function expandAdditionalBettingOptionsIfNeeded() {
    const before = parseBookieMarket();
    const sport = before?.sport || currentBookieSport();

    if (sport === "basketball") {
      // For basketball, H/A is enough for betting, but we still want Over/Under
      // for display if Torn exposes it under Additional Betting Options.
      if (hasBasketballMoneyline(before) && hasBasketballOverUnder(before)) {
        return false;
      }

      // If already open, never click again because that would hide options.
      if (isAdditionalOptionsAlreadyOpen()) {
        return false;
      }

      const sig =
        marketAutoSignature(before) ||
        normalizeLoose(
          [
            location.href,
            before?.title || "",
            before?.marketName || "",
            before?.startText || "",
          ].join("|"),
        );

      const now = Date.now();

      if (
        sig &&
        state.lastExpandSignature === sig &&
        now - Number(state.lastExpandAt || 0) < 8000
      ) {
        return false;
      }

      const btn = findShowAdditionalOptionsButton();

      if (!btn) {
        return false;
      }

      state.lastExpandSignature = sig || state.lastExpandSignature;
      state.lastExpandAt = now;

      try {
        btn.scrollIntoView({ behavior: "smooth", block: "center" });
      } catch (_) {}

      await sleep(100);

      if (isAdditionalOptionsAlreadyOpen()) {
        return false;
      }

      const stillShowBtn = findShowAdditionalOptionsButton();

      if (!stillShowBtn) {
        return false;
      }

      dispatchActivation(stillShowBtn);

      const started = Date.now();

      while (Date.now() - started < 3500) {
        const parsed = parseBookieMarket();

        if (hasBasketballMoneyline(parsed) && hasBasketballOverUnder(parsed)) {
          return true;
        }

        await sleep(100);
      }

      return true;
    }

    if (sport === "baseball") {
      // Baseball only needs Home/Away moneyline. Do not auto-expand additional options.
      return false;
    }

    if (hasFullPrimaryBundle(before)) {
      return false;
    }

    const sig =
      marketAutoSignature(before) ||
      normalizeLoose(
        [
          location.href,
          before?.title || "",
          before?.marketName || "",
          before?.startText || "",
        ].join("|"),
      );

    const now = Date.now();

    if (isAdditionalOptionsAlreadyOpen()) {
      return false;
    }

    if (
      sig &&
      state.lastExpandSignature === sig &&
      now - Number(state.lastExpandAt || 0) < 8000
    ) {
      return false;
    }

    const btn = findShowAdditionalOptionsButton();

    if (!btn) {
      return false;
    }

    state.lastExpandSignature = sig || state.lastExpandSignature;
    state.lastExpandAt = now;

    try {
      btn.scrollIntoView({ behavior: "smooth", block: "center" });
    } catch (_) {}

    await sleep(100);

    if (isAdditionalOptionsAlreadyOpen()) {
      return false;
    }

    const stillShowBtn = findShowAdditionalOptionsButton();

    if (!stillShowBtn) {
      return false;
    }

    dispatchActivation(stillShowBtn);

    const started = Date.now();

    while (Date.now() - started < 3500) {
      const parsed = parseBookieMarket();

      if (hasFullPrimaryBundle(parsed)) {
        return true;
      }

      if (isAdditionalOptionsAlreadyOpen()) {
        return true;
      }

      await sleep(100);
    }

    return true;
  }

  // ---------------------------------------------------------------------------
  // Navigation identity and automatic resolution
  // ---------------------------------------------------------------------------

  function getCurrentBookieMarketWatchKey(domParsed = null) {
    const eventKey = currentBookieEventKey();

    // Best identity source: sport + eventId from the URL.
    if (eventKey) {
      return `event:${eventKey}`;
    }

    // Fallback for views such as Your Bets / Popular where the selected
    // match may change without placing an eventId in the URL.
    const parsed = domParsed || parseBookieMarketFromDom();

    const sport = String(
      parsed?.sport || currentBookieSport() || "unknown",
    ).toLowerCase();

    const title = normalizeLoose(parsed?.title || "");
    const home = normalizeLoose(parsed?.home || "");
    const away = normalizeLoose(parsed?.away || "");

    if (!title && !home && !away) {
      return "";
    }

    return `dom:${sport}:${title}:${home}:${away}`;
  }

  function handleRealBookieMarketChange(reason, domParsed = null) {
    const nextKey = getCurrentBookieMarketWatchKey(domParsed);

    if (!nextKey || nextKey === state.marketWatchKey) {
      return false;
    }

    state.marketWatchKey = nextKey;

    clearPanelForNewNavigation(reason);
    fastRenderAfterNavigation(reason);

    return true;
  }

  function currentBookieEventKey() {
    const sport = currentBookieSport();
    const eventId = getBookieEventIdFromUrl();

    if (!sport || sport === "unknown" || !eventId) return "";

    return `${sport}:${eventId}`;
  }

  function isSameStableEvent() {
    const key = currentBookieEventKey();

    return !!(key && state.stableEventKey && key === state.stableEventKey);
  }

  function lockPanelToCurrentEvent() {
    const key = currentBookieEventKey();

    if (!key) return;

    state.stableEventKey = key;

    // Keep old fallback too, but the real lock is stableEventKey.
    state.panelStableUntil = Date.now() + 24 * 60 * 60 * 1000;
  }

  function marketAutoSignature(parsed) {
    if (!parsed || !parsed.publicOutcomes?.length) return "";

    const sport = parsed.sport || currentBookieSport();

    const oddsSig = parsed.publicOutcomes
      .map(
        (o) =>
          `${o.key || "?"}:${Number(o.odds || 0).toFixed(4)}:${normalizeLoose(o.label || "")}`,
      )
      .sort()
      .join("|");

    return [
      sport,
      normalizeLoose(parsed.title || ""),
      cleanText(parsed.startText || ""),
      parsed.marketType || "",
      state.budget || 0,
      oddsSig,
    ].join("::");
  }

  function marketIdentitySignature(parsed) {
    if (!parsed) return "";

    const sport = parsed.sport || currentBookieSport();
    const title = normalizeLoose(parsed.title || "");
    const home = normalizeLoose(parsed.home || "");
    const away = normalizeLoose(parsed.away || "");
    const start = cleanText(parsed.startText || "");

    if (!title && !home && !away) return "";

    return [sport, title, home, away, start].join("::");
  }

  function clearDecisionForNewMarket(parsed, reason = "new_market") {
    if (isSameStableEvent() && hasStableVisibleDecision()) {
      return false;
    }

    const identity = marketIdentitySignature(parsed);

    if (!identity) return false;

    if (
      state.lastSeenMarketIdentity &&
      state.lastSeenMarketIdentity !== identity
    ) {
      state.lastDecision = null;
      state.lastParsed = parsed || null;
      state.lastFill = null;
      state.lastAutoSignature = "";
      state.lastAutoAt = 0;
      state.initialAutoDone = false;
      state.lastRenderedHtml = "";
      state.decisionReadyToShow = false;

      setStatus("New market detected. Loading advisor result...", "busy");
      render();

      state.lastSeenMarketIdentity = identity;
      state.lastSeenMarketSignature = marketAutoSignature(parsed) || "";
      return true;
    }

    if (!state.lastSeenMarketIdentity) {
      state.lastSeenMarketIdentity = identity;
    }

    const sig = marketAutoSignature(parsed);
    if (sig) {
      state.lastSeenMarketSignature = sig;
    }

    return false;
  }

  function resetNavigationScopedState({ clearObserverTimer = false } = {}) {
    state.navToken++;
    clearTimeout(state.autoTimer);

    if (clearObserverTimer) {
      clearTimeout(state.observerTimer);
    }

    state.lastDecision = null;
    state.lastParsed = null;
    state.lastFill = null;
    state.directBetFlow = null;
    state.lastAutoSignature = "";
    state.lastAutoAt = 0;
    state.lastSeenMarketSignature = "";
    state.lastSeenMarketIdentity = "";
    state.lastExpandSignature = "";
    state.lastExpandAt = 0;
    state.lastRenderedHtml = "";
    state.initialAutoDone = false;
    state.decisionReadyToShow = false;
    state.bookieApiCache = null;
    state.stableEventKey = "";
  }

  function resetAdvisorStateForNavigation(
    statusText = "Opening next Bookie market...",
    opts = {},
  ) {
    resetNavigationScopedState();

    setStatus(statusText, "busy");
    if (opts.renderNow !== false) {
      render();
    }
  }

  function clearPanelForNewNavigation(reason = "navigation") {
    resetNavigationScopedState({ clearObserverTimer: true });

    setStatus("New Bookie market opened. Loading...", "busy");

    // اگر پنل از قبل وجود دارد، فقط محتوایش را فوری Loading کن.
    // اگر پنل وجود ندارد، render نزن که زودتر از Torn جای اشتباه inject نشود.
    if (document.getElementById(INLINE_PANEL_ID)) {
      render();
    }
  }

  function fastRenderAfterNavigation(reason = "navigation") {
    clearTimeout(state.observerTimer);

    setTimeout(() => {
      renderWhenSafeAfterNavigation(reason, 0);
    }, 180);
  }

  function scheduleAutoResolve(reason = "auto", delay = 120) {
    if (!isSupportedBookieView()) return;
    if (!isCurrentBookieSportEnabled()) return;
    if (isSameStableEvent() && hasStableVisibleDecision()) return;

    if (state.autoBusy || state.busy || state.resolveInFlightPromise) {
      state.autoQueued = true;
      return;
    }

    clearTimeout(state.autoTimer);

    state.autoTimer = setTimeout(() => {
      if (state.autoBusy || state.busy || state.resolveInFlightPromise) {
        state.autoQueued = true;
        return;
      }

      Promise.resolve()
        .then(() => autoExpandParseResolve(reason))
        .catch((err) => {
          state.decisionReadyToShow = false;
          setStatus(String(err?.message || err || "AUTO_RESOLVE_ERROR"), "err");
          render();
        });
    }, delay);
  }

  function hasFullPrimaryBundle(parsed) {
    if (!parsed) return false;

    const sport = parsed.sport || currentBookieSport();
    const keys = new Set((parsed.publicOutcomes || []).map((o) => o.key));

    if (sport === "basketball" || sport === "baseball") {
      return ["H", "A"].every((k) => keys.has(k));
    }

    if (sport === "handball" || sport === "rugby") {
      return ["H", "D", "A"].every((k) => keys.has(k));
    }

    if (sport !== "football") {
      return false;
    }

    if (parsed.marketType !== "football_bundle") return false;

    const full8 = ["H", "D", "A", "H_DNB", "A_DNB", "HD", "HA", "AD"].every(
      (k) => keys.has(k),
    );

    const core6 = ["H", "D", "A", "HD", "HA", "AD"].every((k) => keys.has(k));

    return full8 || core6;
  }

  async function waitForFullPrimaryBundle(timeoutMs = 1200) {
    const started = Date.now();
    let best = null;

    while (Date.now() - started < timeoutMs) {
      const parsed = parseBookieMarket();

      if (parsed?.publicOutcomes?.length) {
        best = parsed;
      }

      if (hasFullPrimaryBundle(parsed)) {
        return parsed;
      }

      await sleep(100);
    }

    return best;
  }

  function currentMarketMatchesLastDecision() {
    try {
      if (isSameStableEvent()) return true;

      const parsed = state.lastParsed || parseBookieMarket();
      const currentIdentity = marketIdentitySignature(parsed);

      return (
        !!currentIdentity &&
        !!state.lastSeenMarketIdentity &&
        currentIdentity === state.lastSeenMarketIdentity
      );
    } catch (_) {
      return false;
    }
  }

  function hasStableVisibleDecision() {
    return !!(
      state.lastDecision &&
      safeCard(state.lastDecision) &&
      state.decisionReadyToShow
    );
  }

  function keepPanelStable() {
    return (
      isSameStableEvent() ||
      state.autoBusy ||
      state.busy ||
      Date.now() < Number(state.panelStableUntil || 0)
    );
  }

  async function autoExpandParseResolve(reason = "auto") {
    if (!isSupportedBookieView()) return;
    if (isSameStableEvent() && hasStableVisibleDecision()) {
      return;
    }
    if (!isCurrentBookieSportEnabled()) {
      state.lastDecision = null;
      state.lastParsed = null;
      state.lastFill = null;
      state.lastRenderedHtml = "";
      state.decisionReadyToShow = false;
      setStatus(
        `${currentBookieSportName()} interface is disabled in Settings.`,
        "warn",
      );
      render();
      return;
    }
    if (!hasTornApiKey()) {
      state.lastDecision = null;
      state.lastParsed = null;
      state.lastFill = null;
      state.subscription = null;
      state.authChecked = false;
      setStatus(
        "Torn API key required. Open Settings and enter a Public/Custom key with only user/basic access.",
        "warn",
      );
      state.initialAutoDone = true;
      render();
      return;
    }

    if (state.autoBusy) {
      // Do not queue duplicate auto-runs while one resolve is already in progress.
      return;
    }

    if (state.busy) {
      state.autoQueued = true;
      return;
    }

    state.autoBusy = true;
    const runToken = state.navToken;

    const isStaleRun = () => {
      return runToken !== state.navToken || !isSupportedBookieView();
    };

    try {
      saveSettingsFromUi();

      const apiEarlyParsed =
        await refreshTornBookieApiMarketIfPossible("auto_early");
      const earlyParsed = apiEarlyParsed || parseBookieMarket();

      if (earlyParsed?.publicOutcomes?.length) {
        clearDecisionForNewMarket(earlyParsed, reason);
        state.lastParsed = earlyParsed;
      }

      const earlyIdentity = marketIdentitySignature(earlyParsed);

      const sameMarketStillHasDecision =
        state.lastDecision &&
        state.lastSeenMarketIdentity &&
        earlyIdentity &&
        earlyIdentity === state.lastSeenMarketIdentity;

      const canKeepCurrentCard = !!(
        state.lastDecision &&
        safeCard(state.lastDecision) &&
        sameMarketStillHasDecision
      );

      // Critical anti-flicker rule:
      // If we already have a card for this same market, never hide it during auto checks.
      if (canKeepCurrentCard) {
        state.decisionReadyToShow = true;
        lockPanelToCurrentEvent();
      } else if (!state.lastDecision) {
        state.decisionReadyToShow = false;
      }

      setStatus("Auto: checking betting options...", "busy");

      if (!canKeepCurrentCard) {
        render();
      }

      const hasApiMarket = !!getCachedTornBookieApiParsed();

      if (!hasApiMarket) {
        await expandAdditionalBettingOptionsIfNeeded();
        if (isStaleRun()) return;
      }

      setStatus("Auto: waiting for full betting bundle...", "busy");

      if (!canKeepCurrentCard) {
        render();
      }

      const parsed =
        getCachedTornBookieApiParsed() || (await waitForFullPrimaryBundle(800));
      if (isStaleRun()) return;
      state.lastParsed = parsed;

      if (!parsed?.publicOutcomes?.length) {
        setStatus("Auto: no market detected yet.", "warn");
        state.initialAutoDone = true;
        render();
        return;
      }

      if (!isAdvisorSupportedSport(parsed.sport)) {
        setStatus(
          `Unsupported sport: ${parsed.sport || "unknown"}. Advisor currently supports Football, Basketball, Baseball, Handball, and Rugby only.`,
          "warn",
        );

        state.lastDecision = {
          ok: true,
          action: "SKIP",
          reason: "UNSUPPORTED_SPORT",
          user_message: `Unsupported sport: ${parsed.sport || "unknown"}.`,
          sport: parsed.sport || "unknown",
        };

        state.decisionReadyToShow = true;
        state.initialAutoDone = true;
        render();
        return;
      }

      if (!isSportEnabled(parsed.sport)) {
        state.lastDecision = null;
        state.lastParsed = parsed;
        state.lastFill = null;
        state.lastRenderedHtml = "";
        state.decisionReadyToShow = false;
        setStatus(
          `${parsed.sport || "This sport"} interface is disabled in Settings.`,
          "warn",
        );
        render();
        return;
      }

      if (!hasFullPrimaryBundle(parsed)) {
        const sport = parsed?.sport || currentBookieSport();
        const neededText =
          sport === "basketball" || sport === "baseball"
            ? "Need at least H/A."
            : sport === "handball" || sport === "rugby"
              ? "Need at least H/D/A."
              : "Need at least H/D/A + HD/HA/AD.";

        const msg = `Auto: market incomplete. Parsed ${parsed.publicOutcomes.length} outcome(s). ${neededText}`;

        setStatus(msg, "warn");

        state.lastDecision = {
          ok: true,
          action: "SKIP",
          reason: "MARKET_INCOMPLETE",
          user_message: msg,
          sport: sport || "unknown",
        };

        state.lastParsed = parsed;
        state.lastFill = null;
        state.decisionReadyToShow = true;
        state.initialAutoDone = true;

        // اگر stable-event logic فعال داری، پنل روی همین مچ ثابت بمونه.
        if (typeof lockPanelToCurrentEvent === "function") {
          lockPanelToCurrentEvent();
        } else {
          state.panelStableUntil = Date.now() + 30000;
        }

        render();
        return;
      }

      const sig = marketAutoSignature(parsed);
      const now = Date.now();

      state.lastSeenMarketSignature = sig || state.lastSeenMarketSignature;

      const identity = marketIdentitySignature(parsed);

      if (
        state.lastDecision &&
        safeCard(state.lastDecision) &&
        state.lastResolvedIdentity &&
        identity &&
        state.lastResolvedIdentity === identity &&
        state.lastResolvedSignature &&
        sig &&
        state.lastResolvedSignature === sig &&
        now - Number(state.lastResolvedAt || 0) < 30000
      ) {
        state.decisionReadyToShow = true;
        lockPanelToCurrentEvent();
        setStatus(
          `Auto: already resolved this exact market. ${parsed.publicOutcomes.length} outcome(s).`,
          "ok",
        );
        render();
        return;
      }

      if (
        sig &&
        sig === state.lastAutoSignature &&
        now - Number(state.lastAutoAt || 0) < 30000 &&
        state.lastDecision &&
        safeCard(state.lastDecision)
      ) {
        state.decisionReadyToShow = true;
        lockPanelToCurrentEvent();
        setStatus(
          `Auto: already resolved this market. ${parsed.publicOutcomes.length} outcome(s).`,
          "ok",
        );
        render();
        return;
      }

      // مهم: اینجا دیگر lastAutoSignature را ست نکن.
      // فقط بعد از جواب موفق Worker باید ست شود.

      setStatus(
        `Auto: parsed full bundle (${parsed.publicOutcomes.length}), resolving...`,
        "busy",
      );

      if (!canKeepCurrentCard) {
        render();
      }

      const sigBeforeResolve = marketAutoSignature(parsed);
      const eventKeyBeforeWorker = currentBookieEventKey();

      const payload = buildResolvePayload(parsed);
      const data = await resolveMarketViaWorker(parsed, payload, {
        signature: sigBeforeResolve,
      });

      if (isStaleRun()) return;

      const eventKeyAfterWorker = currentBookieEventKey();

      if (
        eventKeyBeforeWorker &&
        eventKeyAfterWorker &&
        eventKeyBeforeWorker !== eventKeyAfterWorker
      ) {
        setStatus(
          "Market changed while resolving. Ignoring old result.",
          "warn",
        );
        state.initialAutoDone = true;
        render();
        scheduleAutoResolve("event_changed_after_worker", 300);
        return;
      }

      const closedMarketReviewOnly =
        parsed?.closedAdvisorOutcomesUsed ||
        parsed?.marketClosedForAdvisorOnly ||
        isMarketStartedOrFinished(parsed);

      if (closedMarketReviewOnly) {
        data.action = "SKIP";
        data.market_closed = true;
        data.review_only_closed_market = true;

        data.user_message =
          data.user_message ||
          "This market has already started or finished. Prediction is shown for review only. Betting actions are disabled.";

        data.safety = {
          ...(data.safety || {}),
          auto_bet_allowed: false,
          reason: "Closed market. Betting actions are disabled.",
        };
      }

      state.lastDecision = data;

      if (data.display_card) {
        state.lastDecision.display_card = data.display_card;
      }

      state.lastFill = null;
      state.lastResolvedSignature = sigBeforeResolve || "";
      state.lastResolvedIdentity = marketIdentitySignature(parsed) || "";
      state.lastResolvedAt = Date.now();

      state.lastAutoSignature = sigBeforeResolve || sig || "";
      state.lastAutoAt = Date.now();

      lockPanelToCurrentEvent();

      if (data.action === "BET") {
        const fallback = data.selected_strategy?.fallback ? " fallback" : "";
        setStatus(
          `BET ${data.grade || ""}${fallback}: ${decisionUserMessage(data) || data.reason}`,
          data.selected_strategy?.fallback ? "warn" : "ok",
        );
      } else if (data.action === "REVIEW") {
        setStatus(
          `REVIEW: ${decisionUserMessage(data) || data.reason}`,
          "warn",
        );
      } else {
        setStatus(`SKIP: ${decisionUserMessage(data) || data.reason}`, "warn");
      }

      // فقط بعد از final log کارت‌ها را نشان بده
      state.decisionReadyToShow = true;
      state.initialAutoDone = true;
      render();
    } catch (err) {
      state.decisionReadyToShow = false;
      state.lastAutoSignature = "";
      state.lastAutoAt = 0;
      setStatus(String(err?.message || err), "err");
      state.initialAutoDone = true;
      render();
    } finally {
      state.autoBusy = false;

      if (state.autoQueued) {
        state.autoQueued = false;

        // Give TornPDA/Torn DOM time to settle.
        setTimeout(() => {
          if (!state.autoBusy && !state.busy && !state.resolveInFlightPromise) {
            scheduleAutoResolve("queued_auto", 250);
          }
        }, 250);
      }
    }
  }

  // ---------------------------------------------------------------------------
  // Inline panel rendering
  // ---------------------------------------------------------------------------

  function removeFloatingPanelIfExists() {
    const old = document.getElementById("tba-v4-panel");
    if (old) old.remove();
  }

  function getInlineMount(parsed = state.lastParsed) {
    const root =
      parsed?.root && document.body.contains(parsed.root)
        ? parsed.root
        : document.querySelector('ul[class*="bets-wrap"]');

    if (!root) return null;

    const infoWrap =
      root.closest('div[class*="info-wrap"]') ||
      root.closest('li[class*="c-pointer"]') ||
      root.closest("li") ||
      root.parentElement;

    if (!infoWrap) {
      return root.parentElement ? { anchor: root, where: "afterend" } : null;
    }

    const extra =
      infoWrap.querySelector('li[class*="extraOddsActivator"]') ||
      [...infoWrap.querySelectorAll("a, button, li")].find((el) =>
        /additional betting options/i.test(
          cleanText(el.textContent || el.innerText || ""),
        ),
      );

    if (extra && extra.parentElement) {
      return { anchor: extra, where: "afterend" };
    }

    const firstBetsWrap =
      infoWrap.querySelector('ul[class*="bets-wrap"]') || root;

    if (firstBetsWrap && firstBetsWrap.parentElement) {
      return { anchor: firstBetsWrap, where: "afterend" };
    }

    return null;
  }

  function inlineDecisionHtml() {
    const d = state.lastDecision;

    if (!d) {
      return `
      <div class="tba-card">
        <h4>No decision yet</h4>
        <div class="tba-muted">Select/open a Bookie market. The advisor will auto-expand, parse, and resolve.</div>
      </div>
    `;
    }

    const match = d.match || {};
    const fx = match.fixture || {};
    const pred = d.prediction || {};
    const metrics = d.metrics || {};
    const selected = Array.isArray(d.selected_outcomes)
      ? d.selected_outcomes
      : [];
    const strategy = d.selected_strategy || {};
    const fallback = !!strategy.fallback;

    const edge = metrics.edge ?? metrics.evPerUnit ?? metrics.dnbEdge ?? null;
    const eff = metrics.effectiveOdds ?? metrics.odds ?? null;
    const implied = metrics.implied ?? null;

    const badgeClass = fallback
      ? "tba-pill-warn"
      : d.action === "BET"
        ? "tba-pill-good"
        : d.action === "REVIEW"
          ? "tba-pill-warn"
          : "tba-pill-bad";

    const badgeText = fallback
      ? "API advice fallback"
      : d.action === "BET"
        ? "Value/Advisor"
        : d.action || "No bet";

    const selectedHtml = selected.length
      ? `
    <div class="tba-card">
      <h4>Outcomes</h4>
      <table class="tba-table">
        <thead>
          <tr>
            <th>Pick</th>
            <th>Odds</th>
            <th>Stake</th>
          </tr>
        </thead>
        <tbody>
          ${selected
            .map(
              (o) => `
            <tr>
              <td>
                <span class="tba-key">${htmlEscape(o.key || o.role || "-")}</span>
                ${htmlEscape(o.label || "")}
              </td>
              <td>${dec(o.odds, 2)}</td>
              <td>${money(o.stake)}</td>
            </tr>
          `,
            )
            .join("")}
        </tbody>
      </table>
    </div>
  `
      : "";

    return `
    <div class="tba-card">
      <h4>
        ${htmlEscape(d.action || "-")} ${d.grade ? "/ Grade " + htmlEscape(d.grade) : ""}
        <span class="tba-pill ${badgeClass}">${htmlEscape(badgeText)}</span>
      </h4>

      <div class="tba-kv">
        <b>Reason</b><span>${htmlEscape(d.reason || "-")}</span>
        <b>Strategy</b><span>${htmlEscape([strategy.kind, strategy.role].filter(Boolean).join(" / ") || "-")}</span>
        <b>Fixture</b><span>${htmlEscape(fx.home_team_name || "-")} vs ${htmlEscape(fx.away_team_name || "-")}</span>
        <b>League</b><span>${htmlEscape([fx.league_name, fx.country].filter(Boolean).join(", ") || "-")}</span>
        <b>Match score</b><span>${dec(match.score, 4)}</span>
        <b>API advice</b><span>${htmlEscape(pred.advice || d.classifier?.type || "-")}</span>
        <b>Prediction</b><span>${
          String(
            d.sport || state.lastParsed?.sport || currentBookieSport(),
          ).toLowerCase() === "basketball"
            ? `H ${pct(pred.p_home)} / A ${pct(pred.p_away)}`
            : `H ${pct(pred.p_home)} / D ${pct(pred.p_draw)} / A ${pct(pred.p_away)}`
        }</span>
        <b>Shape</b><span>${htmlEscape(renderPredictionShape(d))}</span>
        <b>Edge/EV</b><span>${edge == null ? "-" : pct(edge)}</span>
        <b>Implied</b><span>${implied == null ? "-" : pct(implied)}</span>
        <b>Eff. odds</b><span>${eff == null ? "-" : dec(eff, 4)}</span>
        <b>Stake</b><span>${pct(d.stake_pct)}</span>
      </div>
    </div>

    ${selectedHtml}
  `;
  }

  function inlineParsedHtml() {
    const p = state.lastParsed;

    if (!p) {
      return `
      <div class="tba-card">
        <h4>Parsed market details</h4>
        <div class="tba-muted">No parsed market yet.</div>
      </div>
    `;
    }

    const groupsHtml =
      Array.isArray(p.groups) && p.groups.length
        ? p.groups.map((g) => `${g.family}:${g.rows}`).join(" | ")
        : "-";

    return `
    <div class="tba-card">
      <h4>Parsed market details</h4>
      <div class="tba-kv">
        <b>Parser</b><span>${htmlEscape(p.parser || "-")}</span>
        <b>Title</b><span>${htmlEscape(p.title || "-")}</span>
        <b>Home/Away</b><span>${htmlEscape(p.home || "-")} / ${htmlEscape(p.away || "-")}</span>
        <b>Market</b><span>${htmlEscape(p.marketName || "-")}</span>
        <b>Start</b><span>${htmlEscape(p.startText || "-")}</span>
        <b>Groups</b><span>${htmlEscape(groupsHtml)}</span>
        <b>Outcomes</b><span>${htmlEscape(
          (p.publicOutcomes || [])
            .map(
              (o) =>
                `${o.key || "?"}:${o.label}@${o.odds}${o.source_market_family ? "[" + o.source_market_family + "]" : ""}`,
            )
            .join(" | "),
        )}</span>
      </div>
    </div>
  `;
  }

  function renderFallbackActionButtons() {
    if (!state.lastDecision) return "";

    const d = state.lastDecision;

    if (d.action !== "BET") {
      const canSkip = isSkippableDecision(d);

      return `
    <div class="tba-card">
      <h4>${htmlEscape(d.action || "No bet")}</h4>
      <div class="tba-muted">${htmlEscape(decisionUserMessage(d) || d.reason || "No betting action recommended.")}</div>

      ${
        canSkip
          ? `
        <div class="tba-skip-action-row">
          <button id="tba-skip-next" class="tba-skip-btn">SKIP</button>
        </div>
      `
          : ""
      }
    </div>
  `;
    }

    if (isMarketStartedOrFinished(state.lastParsed)) {
      return "";
    }

    const oddsFilterBlockInfo = getOddsFilterBlockInfo(d);

    if (oddsFilterBlockInfo) {
      return `
    <div class="tba-card">
      <div class="tba-fixture-actions">
        <span class="tba-filtered-pill">Odds Filtered</span>
        <button id="tba-skip-next" class="tba-skip-btn">SKIP</button>
      </div>
    </div>
  `;
    }

    return `
    <div class="tba-card">
      <h4>Actions</h4>
      <div class="tba-fixture-actions">
        <button id="tba-fill" class="tba-fill-btn">Next Match</button>
        <button id="tba-direct-bet" class="tba-bet-btn">${htmlEscape(directBetButtonLabel())}</button>
      </div>
      <div class="tba-muted" style="margin-top:6px;">
        Display card was not available, but betting actions are still available.
      </div>
    </div>
  `;
  }

  function renderSportVisibilitySettingsHtml() {
    const enabled = normalizeEnabledSports(state.enabledSports);
    const showWarning = !allAdvisorSportsEnabled();
    const oddsFilter = normalizeOddsFilterSettings(state.oddsFilter);

    return `
    <div class="tba-settings-card">
      <div class="tba-settings-title">Enabled Sports</div>

      <div class="tba-settings-note" style="margin-top:0;">
        Choose which sports should show the Torn Bookie Predictor interface.
      </div>

      <div class="tba-sport-toggle-grid">
        <label class="tba-sport-toggle">
          <input
            id="tba-sport-football"
            type="checkbox"
            ${enabled.football ? "checked" : ""}
          >
          <span>⚽ Football</span>
        </label>

        <label class="tba-sport-toggle">
          <input
            id="tba-sport-basketball"
            type="checkbox"
            ${enabled.basketball ? "checked" : ""}
          >
          <span>🏀 Basketball</span>
        </label>

        <label class="tba-sport-toggle">
          <input
            id="tba-sport-baseball"
            type="checkbox"
            ${enabled.baseball ? "checked" : ""}
          >
          <span>⚾ Baseball</span>
        </label>

        <label class="tba-sport-toggle">
          <input
            id="tba-sport-handball"
            type="checkbox"
            ${enabled.handball ? "checked" : ""}
          >
          <span>🤾 Handball</span>
        </label>

        <label class="tba-sport-toggle">
          <input
            id="tba-sport-rugby"
            type="checkbox"
            ${enabled.rugby ? "checked" : ""}
          >
          <span>🏉 Rugby</span>
        </label>
      </div>

      ${
        showWarning
          ? `
        <div class="tba-sport-warning">
          We recommend keeping all supported sports enabled so the advisor can correctly detect Football, Basketball, Handball, and Rugby markets, especially inside Your Bets.
        </div>
      `
          : ""
      }

      <div class="tba-settings-divider"></div>

      <div class="tba-settings-title" style="font-size:12px;margin-top:2px;">Odds Filters</div>

      <div class="tba-settings-note" style="margin-top:0;">
        Optional filter for hiding recommendations below your minimum selected odds.
      </div>

      <div class="tba-odds-filter-row">
        <label class="tba-sport-toggle tba-odds-filter-toggle">
          <input
            id="tba-odds-filter-enabled"
            type="checkbox"
            ${oddsFilter.enabled ? "checked" : ""}
          >
          <span>Enable Odds Filters</span>
        </label>

        <label class="tba-odds-filter-min-wrap" for="tba-odds-filter-min">
          <span>Minimum odds</span>
          <input
            id="tba-odds-filter-min"
            class="tba-odds-filter-input"
            value="${htmlEscape(oddsFilterMinText(oddsFilter.minOdds))}"
            inputmode="decimal"
            autocomplete="off"
            ${oddsFilter.enabled ? "" : "disabled"}
          >
        </label>
      </div>

      ${
        oddsFilter.enabled
          ? `
        <div class="tba-sport-warning">
          We recommend keeping Odds Filters disabled. The advisor already tries to find the best entry based on prediction, value, and your budget.
        </div>
      `
          : ""
      }
    </div>
  `;
  }

    function renderCustomizationPanelHtml() {
    if (!state.showCustomization) return "";

    const theme = normalizeCustomTheme(state.customTheme);

    const rows = CUSTOM_THEME_FIELDS.map(([key, label]) => {
      return `
        <label class="tba-color-row">
          <span>${htmlEscape(label)}</span>
          <input
            class="tba-color-input"
            type="color"
            value="${htmlEscape(theme[key])}"
            data-tba-color-key="${htmlEscape(key)}"
          >
        </label>
      `;
    }).join("");

    return `
      <div class="tba-settings-card tba-customization-card">
        <div class="tba-settings-title">Customization</div>

        <div class="tba-settings-note" style="margin-top:0;">
          Change the Predictor panel colors. Changes are saved automatically on this device.
        </div>

        <div class="tba-customization-grid">
          ${rows}
        </div>

        <div class="tba-customization-actions">
          <button id="tba-customization-default" class="tba-default-btn">Default</button>
        </div>
      </div>
    `;
  }

  function renderSettingsPanelHtml() {
    if (!state.showSettings) return "";

    return `
    <div class="tba-settings-card">
      <div class="tba-settings-title">Settings</div>

      <div class="tba-settings-row">
        <label for="tba-budget">Budget</label>
        <input
          id="tba-budget"
          class="tba-settings-budget"
          value="${htmlEscape(formatIntegerInputValue(state.budget))}"
          inputmode="numeric"
        >
      </div>

      <div class="tba-api-key-inline-row">
          <label for="tba-api-key">Torn API Key</label>

          <div class="tba-api-key-control">
              <a
                id="tba-create-api-key"
                class="tba-small-link-btn"
                href="${TORN_API_KEY_CREATE_URL}"
                target="_blank"
                rel="noopener noreferrer"
              >Generate Key</a>

              <button id="tba-save-api-key" class="tba-small-action-btn">Save</button>
              <button id="tba-clear-api-key" class="tba-small-danger-btn">Delete</button>

              <input
                id="tba-api-key"
                class="tba-settings-budget tba-api-key-input"
                value="${htmlEscape(String(state.tornApiKey || ""))}"
                placeholder="Paste Torn API key"
                autocomplete="off"
                spellcheck="false"
              >
            </div>
        </div>

      <div class="tba-settings-note">
        Required permission: Public/Custom key with only <b>/user/basic</b>.
        The key is sent to the server only to verify your Torn user/subscription and is not stored on the server.
        Locally, it is saved in browser storage and IndexedDB so the script can keep working after reload.
      </div>

      ${renderSportVisibilitySettingsHtml()}
      ${renderSubscriptionInstructionsHtml()}
      ${renderBettingDisclaimerHtml()}
    </div>
  `;
  }

  function renderDecisionAreaHtml() {
    if (!hasTornApiKey()) {
      return renderApiKeyRequiredHtml();
    }

    if (isSubscriptionBlockedDecision()) {
      return renderSubscriptionBlockedHtml();
    }

    // مهم:
    // تا وقتی لاگ نهایی BET / REVIEW / SKIP نگرفتیم، کارت نتیجه را نشان نده.
    if (!state.decisionReadyToShow) {
      if (
        state.lastDecision &&
        safeCard(state.lastDecision) &&
        currentMarketMatchesLastDecision()
      ) {
        return renderDisplayCardHtml();
      }

      return renderLoadingResultHtml();
    }

    return (
      renderDisplayCardHtml() ||
      renderFallbackActionButtons() ||
      renderLoadingResultHtml()
    );
  }

  function render() {
    injectStyle();
    removeFloatingPanelIfExists();

    if (!isSupportedBookieView()) {
      const existing = document.getElementById(INLINE_PANEL_ID);
      if (existing) existing.remove();
      return;
    }

    if (!isCurrentBookieSportEnabled()) {
      const existing = document.getElementById(INLINE_PANEL_ID);
      if (existing) existing.remove();
      return;
    }

    const existingPanel = document.getElementById(INLINE_PANEL_ID);
    const shouldKeepStable =
      existingPanel &&
      keepPanelStable() &&
      document.body.contains(existingPanel);

    let mount = null;

    if (!shouldKeepStable) {
      mount = getInlineMount();

      if (!mount) {
        if (existingPanel && keepPanelStable()) {
          return;
        }

        if (existingPanel) existingPanel.remove();
        return;
      }
    }

    let panel = existingPanel;
    const panelWasMissing = !panel;

    if (!panel) {
      panel = document.createElement("div");
      panel.id = INLINE_PANEL_ID;
    }

    if (!shouldKeepStable && mount && mount.anchor) {
      const panelAlreadyInSameParent =
        document.body.contains(panel) &&
        panel.parentElement &&
        panel.parentElement === mount.anchor.parentElement;

      if (!document.body.contains(panel)) {
        mount.anchor.insertAdjacentElement(mount.where || "afterend", panel);
      } else if (
        !panelAlreadyInSameParent &&
        panel.previousElementSibling !== mount.anchor
      ) {
        mount.anchor.insertAdjacentElement(mount.where || "afterend", panel);
      }
    } else if (!document.body.contains(panel)) {
      const fallbackMount = getInlineMount();

      if (fallbackMount?.anchor) {
        fallbackMount.anchor.insertAdjacentElement(
          fallbackMount.where || "afterend",
          panel,
        );
      } else {
        return;
      }
    }

    const statusClass = state.statusKind || "info";

    const sportIcon = currentBookieSportIcon();
    const sportName = currentBookieSportName();

    const nextHtml = `
  <div class="tba-head">
    <div>
      <div class="tba-title">${sportIcon} Torn Bookie Predictor</div>
      <div class="tba-sub">${htmlEscape(sportName)} betting insights with smarter value checks</div>
      ${renderSubscriptionBadgeHtml()}
    </div>

    <div class="tba-top-actions">
      <button
        id="tba-log-toggle"
        class="tba-header-icon-btn ${state.showLogs ? "is-active" : ""}"
        aria-label="Logs"
      >🧾</button>

      <button
        id="tba-settings-toggle"
        class="tba-header-icon-btn ${state.showSettings ? "is-active" : ""}"
        aria-label="Settings"
      >⚙️</button>

      <button
        id="tba-customization-toggle"
        class="tba-header-icon-btn ${state.showCustomization ? "is-active" : ""}"
        aria-label="Customization"
      >🎨</button>


    </div>
  </div>

  ${renderSettingsPanelHtml()}
  ${renderCustomizationPanelHtml()}

  <div class="tba-status ${statusClass}">${htmlEscape(state.lastStatus || "Ready")}</div>

  ${renderDecisionAreaHtml()}

  ${
    state.showLogs
      ? `
    <div class="tba-grid">
      ${inlineDecisionHtml()}
    </div>

    ${inlineParsedHtml()}
  `
      : ""
  }
`;

    if (
      panelWasMissing ||
      !panel.innerHTML ||
      state.lastRenderedHtml !== nextHtml
    ) {
      panel.innerHTML = nextHtml;
      state.lastRenderedHtml = nextHtml;
    }

    applyCustomThemeToPanel(panel);

    const budgetEl = panel.querySelector("#tba-budget");

    if (budgetEl) {
      budgetEl.oninput = () => {
        const n = parseIntegerInputValue(budgetEl.value, DEFAULT_BUDGET);
        state.budget = n;
        setStore("budget", n);
        scheduleAutoResolve("budget_changed", 1200);
      };

      budgetEl.onblur = () => {
        budgetEl.value = formatIntegerInputValue(state.budget);
      };

      budgetEl.onfocus = () => {
        budgetEl.value = String(state.budget || "");
      };

      budgetEl.onchange = () => {
        saveSettingsFromUi();
        budgetEl.value = formatIntegerInputValue(state.budget);
        scheduleAutoResolve("budget_changed", 300);
      };
    }

    const openSettingsBtn = panel.querySelector("#tba-open-settings");
    if (openSettingsBtn) {
      openSettingsBtn.onclick = () => openSettingsForApiKey();
    }

    const apiKeyEl = panel.querySelector("#tba-api-key");
    const saveKeyBtn = panel.querySelector("#tba-save-api-key");
    const clearKeyBtn = panel.querySelector("#tba-clear-api-key");

    if (apiKeyEl) {
      apiKeyEl.onkeydown = (e) => {
        if (e.key === "Enter") {
          e.preventDefault();
          saveTornApiKey(apiKeyEl.value).catch((err) => {
            setStatus(String(err?.message || err), "err");
            render();
          });
        }
      };
    }

    const sportFootballEl = panel.querySelector("#tba-sport-football");
    const sportBasketballEl = panel.querySelector("#tba-sport-basketball");
    const sportBaseballEl = panel.querySelector("#tba-sport-baseball");
    const sportHandballEl = panel.querySelector("#tba-sport-handball");
    const sportRugbyEl = panel.querySelector("#tba-sport-rugby");

    const bindSportToggle = (el, sport) => {
      if (!el) return;

      el.onchange = () => {
        const wanted = !!el.checked;
        const currentEnabled = normalizeEnabledSports(state.enabledSports);

        // If this is the last enabled sport, keep it checked.
        if (
          !wanted &&
          enabledSportsCount(currentEnabled) <= 1 &&
          currentEnabled[sport]
        ) {
          el.checked = true;
          setStatus("At least one sport must stay enabled.", "warn");
          render();
          return;
        }

        setSportEnabled(sport, wanted);
      };
    };

    bindSportToggle(sportFootballEl, "football");
    bindSportToggle(sportBasketballEl, "basketball");
    bindSportToggle(sportBaseballEl, "baseball");
    bindSportToggle(sportHandballEl, "handball");
    bindSportToggle(sportRugbyEl, "rugby");

    const oddsFilterEnabledEl = panel.querySelector("#tba-odds-filter-enabled");
    const oddsFilterMinEl = panel.querySelector("#tba-odds-filter-min");

    if (oddsFilterMinEl) {
      oddsFilterMinEl.onkeydown = (e) => {
        const allowedKeys = [
          "Backspace",
          "Delete",
          "ArrowLeft",
          "ArrowRight",
          "ArrowUp",
          "ArrowDown",
          "Tab",
          "Home",
          "End",
          "Enter",
        ];

        if (allowedKeys.includes(e.key) || e.ctrlKey || e.metaKey) return;

        if (!/^[0-9.]$/.test(e.key)) {
          e.preventDefault();
          return;
        }

        if (
          e.key === "." &&
          String(oddsFilterMinEl.value || "").includes(".")
        ) {
          e.preventDefault();
        }
      };

      oddsFilterMinEl.oninput = () => {
        const clean = cleanOddsFilterInputValue(oddsFilterMinEl.value);

        if (oddsFilterMinEl.value !== clean) {
          oddsFilterMinEl.value = clean;
        }

        state.oddsFilter = setOddsFilterToStore({
          ...(state.oddsFilter || DEFAULT_ODDS_FILTER),
          minOdds: clean || DEFAULT_ODDS_FILTER.minOdds,
        });

        scheduleAutoResolve("odds_filter_changed", 500);
      };

      oddsFilterMinEl.onblur = () => {
        state.oddsFilter = setOddsFilterToStore({
          ...(state.oddsFilter || DEFAULT_ODDS_FILTER),
          minOdds: oddsFilterMinEl.value || DEFAULT_ODDS_FILTER.minOdds,
        });

        oddsFilterMinEl.value = oddsFilterMinText(state.oddsFilter.minOdds);
        scheduleAutoResolve("odds_filter_changed", 250);
      };
    }

    if (oddsFilterEnabledEl) {
      oddsFilterEnabledEl.onchange = () => {
        const enabled = !!oddsFilterEnabledEl.checked;

        state.oddsFilter = setOddsFilterToStore({
          ...(state.oddsFilter || DEFAULT_ODDS_FILTER),
          enabled,
          minOdds:
            oddsFilterMinEl?.value ||
            state.oddsFilter?.minOdds ||
            DEFAULT_ODDS_FILTER.minOdds,
        });

        if (oddsFilterMinEl) {
          oddsFilterMinEl.disabled = !enabled;
          oddsFilterMinEl.value = oddsFilterMinText(state.oddsFilter.minOdds);
        }

        state.lastDecision = null;
        state.lastFill = null;
        state.directBetFlow = null;
        state.lastRenderedHtml = "";
        state.decisionReadyToShow = false;

        setStatus(
          enabled
            ? `Odds Filters enabled. Minimum odds: ${oddsFilterMinText(state.oddsFilter.minOdds)}.`
            : "Odds Filters disabled. Advisor behavior restored.",
          enabled ? "warn" : "ok",
        );

        render();
        scheduleAutoResolve("odds_filter_toggled", 250);
      };
    }

    if (saveKeyBtn && apiKeyEl) {
      saveKeyBtn.onclick = () => {
        saveTornApiKey(apiKeyEl.value).catch((err) => {
          setStatus(String(err?.message || err), "err");
          render();
        });
      };
    }

    if (clearKeyBtn) {
      clearKeyBtn.onclick = () => {
        clearTornApiKey().catch((err) => {
          setStatus(String(err?.message || err), "err");
          render();
        });
      };
    }

    const fillBtn = panel.querySelector("#tba-fill");
    if (fillBtn) {
      fillBtn.onclick = () => {
        const beforeParsed = state.lastParsed || parseBookieMarket();

        withBusy("Opening next Bookie market.", async () => {
          await openNextBookieMarketAfterCurrent(beforeParsed);
        });
      };
    }

    const directBetBtn = panel.querySelector("#tba-direct-bet");

    if (directBetBtn) {
      directBetBtn.onclick = () => {
        saveSettingsFromUi();
        directBetClick();
      };
    }

    const outcomeBetButtons =
      panel.querySelectorAll(
        "[data-tba-outcome-bet-index]",
      );

    outcomeBetButtons.forEach((button) => {
      button.onclick = () => {
        if (button.disabled) {
          return;
        }

        saveSettingsFromUi();

        const index = Number(
          button.getAttribute(
            "data-tba-outcome-bet-index",
          ),
        );

        const decision =
          state.lastDecision;

        const displayCard =
          decision?.display_card ||
          decision?.displayCard ||
          null;

        const selected =
          getRenderableSelectedOutcomes(
            displayCard,
            decision,
          );

        const outcome =
          selected[index] || null;

        if (!outcome) {
          setStatus(
            "Could not find the selected outcome. Resolve this market again.",
            "err",
          );

          render();
          return;
        }

        /*
        * Preserve the clicked outcome before withBusy()
        * redraws the panel.
        */
        const target =
          makeSingleOutcomeBetTarget(
            outcome,
          );

        const betGuardKey =
          singleOutcomeBetGuardKey(
            outcome,
          );

        const currentBetState = String(
          state.singleOutcomeBetStates?.[
            betGuardKey
          ] || "",
        );

        // Double-click / repeated-click guard.
        if (
          currentBetState === "pending" ||
          currentBetState === "placed"
        ) {
          return;
        }

        /*
        * Lock synchronously before any await or redraw.
        * This prevents a rapid double-click from sending two bets.
        */
        state.singleOutcomeBetStates[
          betGuardKey
        ] = "pending";

        button.disabled = true;
        button.textContent = "Placing...";
        button.classList.add("is-pending");

        render();

        withBusy(
          `Direct Bet: placing only ${target.label || "selected outcome"}...`,
          async () => {
            try {
              await directBetSingleOutcome(
                target,
              );

              /*
              * The Torn request completed successfully.
              * Keep this outcome permanently locked for this page session.
              */
              state.singleOutcomeBetStates[
                betGuardKey
              ] = "placed";
            } catch (err) {
              /*
              * Bet was not successfully completed.
              * Unlock it so the user may retry.
              */
              delete state.singleOutcomeBetStates[
                betGuardKey
              ];

              throw err;
            } finally {
              render();
            }
          },
        );
      };
    });

    const skipBtn = panel.querySelector("#tba-skip-next");
    if (skipBtn) {
      skipBtn.onclick = () => {
        const beforeParsed = state.lastParsed || parseBookieMarket();

        withBusy("Skipping this market and opening next...", async () => {
          await openNextBookieMarketAfterCurrent(beforeParsed);
        });
      };
    }

    const logBtn = panel.querySelector("#tba-log-toggle");
    if (logBtn) {
      logBtn.onclick = () => {
        state.showLogs = !state.showLogs;
        setStore("showLogs", state.showLogs);
        render();
        showHeaderHint(state.showLogs ? "Log enabled" : "Log disabled");
      };
    }

    const settingsBtn = panel.querySelector("#tba-settings-toggle");
    if (settingsBtn) {
      settingsBtn.onclick = () => {
        state.showSettings = !state.showSettings;
        setStore("showSettings", state.showSettings);
        render();
        showHeaderHint(
          state.showSettings ? "Settings enabled" : "Settings disabled",
        );
      };
    }

    const customizationBtn = panel.querySelector("#tba-customization-toggle");
    if (customizationBtn) {
      customizationBtn.onclick = () => {
        state.showCustomization = !state.showCustomization;
        setStore("showCustomization", state.showCustomization);
        render();
        showHeaderHint(
          state.showCustomization
            ? "Customization enabled"
            : "Customization disabled",
        );
      };
    }

    const customizationColorInputs = panel.querySelectorAll("[data-tba-color-key]");
    customizationColorInputs.forEach((input) => {
      input.oninput = () => {
        const key = input.getAttribute("data-tba-color-key") || "";
        setCustomThemeColor(key, input.value);
      };

      input.onchange = () => {
        const key = input.getAttribute("data-tba-color-key") || "";
        setCustomThemeColor(key, input.value);
      };
    });

    const customizationDefaultBtn = panel.querySelector("#tba-customization-default");
    if (customizationDefaultBtn) {
      customizationDefaultBtn.onclick = () => {
        resetCustomThemeToDefault();
        showHeaderHint("Default colors restored");
      };
    }
  }

  // ---------------------------------------------------------------------------
  // Safe mounting and boot
  // ---------------------------------------------------------------------------

  function hasSafeInlinePanelTarget() {
    if (!isSupportedBookieView()) return false;

    const eventId = getBookieEventIdFromUrl();

    // For sport landing pages, render is fine.
    if (!eventId) return true;

    const candidates = [
      ...document.querySelectorAll(
        'div[class*="info-wrap"], ul[class*="bets-wrap"], li[class*="matchName"], div[class*="matchName"], [class*="eventName"]',
      ),
    ].filter(isDomVisible);

    if (!candidates.length) return false;

    // Avoid rendering while only the huge list is visible but the selected match panel is not mounted yet.
    const hasBetsWrap = candidates.some((el) => {
      return (
        el.matches?.('ul[class*="bets-wrap"]') ||
        !!el.querySelector?.('ul[class*="bets-wrap"]')
      );
    });

    const hasInfoWrap = candidates.some((el) => {
      return (
        el.matches?.('div[class*="info-wrap"]') ||
        !!el.querySelector?.('div[class*="info-wrap"]')
      );
    });

    return hasBetsWrap || hasInfoWrap;
  }

  function renderWhenSafeAfterNavigation(reason = "navigation", attempt = 0) {
    const panelExists = !!document.getElementById(INLINE_PANEL_ID);

    if (panelExists || hasSafeInlinePanelTarget()) {
      render();
      scheduleAutoResolve(reason, 120);
      return;
    }

    if (attempt >= 8) {
      render();
      scheduleAutoResolve(reason, 180);
      return;
    }

    setTimeout(
      () => {
        renderWhenSafeAfterNavigation(reason, attempt + 1);
      },
      attempt < 3 ? 120 : 180,
    );
  }

  function observeBookieDom() {
    const obs = new MutationObserver((mutations) => {
      const onlyPredictorChanges = mutations.every((mutation) => {
        const target = mutation.target;

        return (
          target instanceof Element &&
          !!target.closest(`#${INLINE_PANEL_ID}`)
        );
      });

      if (onlyPredictorChanges) return;

      clearTimeout(state.observerTimer);

      state.observerTimer = setTimeout(() => {
        if (!isSupportedBookieView()) return;

        // Read directly from Torn's DOM here.
        // Do not use parseBookieMarket(), because its BookieAPI cache may
        // still belong to the previously opened match.
        const domParsed = parseBookieMarketFromDom();

        const domIdentity = marketIdentitySignature(domParsed);

        const hasDetectedMarket = !!(
          domIdentity &&
          domParsed?.publicOutcomes?.length
        );

        // Only react if an actual new match/event has appeared.
        if (
          hasDetectedMarket &&
          handleRealBookieMarketChange(
            "bookie_market_detected",
            domParsed,
          )
        ) {
          return;
        }

        // Ordinary Torn DOM changes must not redraw or reload the panel.
        if (document.getElementById(INLINE_PANEL_ID)) {
          return;
        }

        // Torn may remove the panel while rebuilding the selected match.
        if (!hasSafeInlinePanelTarget()) {
          return;
        }

        render();

        // If the panel was removed and mounted again while a market exists,
        // make sure the automatic resolve process is restarted.
        if (hasDetectedMarket) {
          state.marketWatchKey =
            getCurrentBookieMarketWatchKey(domParsed);

          scheduleAutoResolve("panel_remounted", 120);
        }
      }, 180);
    });

    obs.observe(document.documentElement, {
      childList: true,
      subtree: true,

      // Torn may select a match by changing the active class before
      // inserting the complete match content.
      attributes: true,
      attributeFilter: ["class", "style"],
    });
  }

  function bindHistoryNavigation() {
    let lastRoute =
      location.pathname + location.search + location.hash;

    let routeTimer = null;

    function checkForRealRouteChange(reason) {
      const nextRoute =
        location.pathname + location.search + location.hash;

      if (nextRoute === lastRoute) {
        return;
      }

      lastRoute = nextRoute;

      clearTimeout(routeTimer);

      routeTimer = setTimeout(() => {
        const domParsed = parseBookieMarketFromDom();
        const nextMarketKey =
          getCurrentBookieMarketWatchKey(domParsed);

        // The DOM observer may already have handled this exact match.
        if (
          nextMarketKey &&
          nextMarketKey === state.marketWatchKey
        ) {
          return;
        }

        state.marketWatchKey = nextMarketKey || "";

        clearPanelForNewNavigation(reason);
        fastRenderAfterNavigation(reason);
      }, 30);
    }

    window.addEventListener("hashchange", () => {
      checkForRealRouteChange("hashchange");
    });

    window.addEventListener("popstate", () => {
      checkForRealRouteChange("popstate");
    });

    // Torn sometimes changes its internal route without dispatching
    // hashchange or popstate. This watches the URL itself and only reacts
    // when the route string actually changes.
    const routeWatcher = setInterval(() => {
      checkForRealRouteChange("route_watch");
    }, 200);

    window.addEventListener(
      "beforeunload",
      () => {
        clearInterval(routeWatcher);
        clearTimeout(routeTimer);
      },
      { once: true },
    );
  }

  function migrateStoredApiKey() {
    const rawStoredKey = getStore(API_KEY_STORE_KEY, "") || "";
    const migratedKey = normalizeTornApiKey(rawStoredKey);

    if (migratedKey !== rawStoredKey) {
      state.tornApiKey = migratedKey;
      setStore(API_KEY_STORE_KEY, migratedKey);
      idbSet(API_KEY_STORE_KEY, migratedKey);
    }
  }

  function boot() {
    removeFloatingPanelIfExists();
    migrateStoredApiKey();

    // Remember the initial route/event so ordinary first-page DOM
    // mutations are not mistaken for opening another match.
    state.marketWatchKey =
      getCurrentBookieMarketWatchKey();

    render();
    hydrateApiKeyFromIndexedDb();

    observeBookieDom();
    bindHistoryNavigation();

    setTimeout(() => {
      fastRenderAfterNavigation("initial_load");
    }, 80);
  }

  boot();
})();