Torn Maxxed Command Center

Mobile Torn command center for manual next-action planning, crimes, training, FFScouter targets and Sell Losses, money, progression, and page tools.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Torn Maxxed Command Center
// @namespace    https://maxxedtechnicalsystems.com/torn/
// @version      1.0.4
// @description  Mobile Torn command center for manual next-action planning, crimes, training, FFScouter targets and Sell Losses, money, progression, and page tools.
// @author       Maxxed Technical Systems
// @homepageURL  https://techmaxxed.com/torn/
// @supportURL   https://techmaxxed.com/support/?app=Torn%20Maxxed%20Command%20Center
// @license      MIT
// @match        https://www.torn.com/*
// @match        https://torn.com/*
// @run-at       document-end
// @noframes
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_setClipboard
// @grant        GM_xmlhttpRequest
// @connect      api.torn.com
// @connect      ffscouter.com
// @connect      yata.yt
// ==/UserScript==

/*
PRIVACY AND SERVICE DISCLOSURE
--------------------------------
Local storage:
- Torn API key, settings, checklist state, recommendation preferences, target
  notes, local outcome history, and cached API/service results are stored only
  in this userscript manager on this device.
- Backup export intentionally excludes the Torn API key.

Network use:
- Torn API: enabled account modules refresh automatically at the configured
  interval. The Torn key is sent to https://api.torn.com only for those calls.
- FFScouter: target searches and Sell Losses status checks are manual. Each
  feature asks for scoped consent before sending the Torn API key plus the
  requested target filters or seller-status request to https://ffscouter.com.
  FFScouter is independent and applies its own privacy policy and terms.
- Referral handling: normal FFScouter links are non-referral links. The script
  can copy the current player's own Sell Losses referral URL for private
  chat/mail sharing; it never inserts the author's referral ID.
- YATA: foreign-stock lookup is manual and disabled by default. The request is
  sent to https://yata.yt without the Torn API key.
- No analytics, advertising, author tracking, remote executable code, or hidden
  telemetry is included.

Torn interaction:
- The script never automatically commits crimes, trains, attacks, buys, sells,
  travels, revives, plays casino games, submits forms, or bypasses CAPTCHA.
- Non-API page tools only process the Torn page currently visible to the user.
- Any field-filling helper requires a direct user click and never submits.
*/

(() => {
  "use strict";

  const ID = "torn-maxxed-command-center";
  const VERSION = "1.0.4";
  const API_USER = "https://api.torn.com/v2/user/";
  const API_KEY_INFO = "https://api.torn.com/v2/key/info";
  // API v2 is required for selections such as organizedcrime. Most of the
  // console was originally written against API v1 response shapes, so the
  // generic v2 endpoint requests legacy-compatible payloads for selections
  // that exist in both versions while leaving v2-only selections native.
  const V2_ONLY_SELECTIONS = new Set(["organizedcrime"]);
  const SELECTION_RECOVERY_CODES = new Set([4, 16, 22, 23, 27]);
  const FF_API = "https://ffscouter.com/api/v1";
  const PROJECT_URL = "https://techmaxxed.com/torn/";
  const FF_SELL_LOSSES_URL = "https://ffscouter.com/sell-losses";
  const FF_SELL_LOSSES_GUIDE = "https://ffscouter.com/guides/selling-losses";
  const TORN_DUMP_URL = "https://www.torn.com/dump.php";
  const YATA_TRAVEL = "https://yata.yt/api/v1/travel/export/";
  const PREFIX = `${ID}:`;
  const SENSITIVE_STORAGE = new Set(["apiKey"]);
  const PDA_KEY_TOKEN = "###PDA-APIKEY###";
  const PDA_KEY_SENTINEL = "###" + "PDA-APIKEY" + "###";

  const DEFAULTS = {
    refresh: 60,
    walletWarning: 500000,
    cashReserve: 200000,
    factionVaultUrl: "",
    safeMode: false,
    compactMode: true,
    crimeName: "Your best reliable crime",
    crimeNerve: 5,
    crimeProfit: 5000,
    crimeGoal: "balanced",
    preferredCrime: "Auto",
    crimeFallback: "Pickpocketing",
    crimeMinimumSkill: 5,
    gymTarget: "best-dots",
    gymPlan: "balanced",
    gymRatios: { strength: 25, speed: 25, defense: 25, dexterity: 25 },
    ffTargetGoal: "leveling",
    ffMinLevel: 1,
    ffMaxLevel: 100,
    ffMinFF: 1,
    ffMaxFF: 3,
    ffInactive: true,
    ffFactionless: false,
    ffLimit: 20,
    warPay: 0,
    attackEnergy: 25,
    travelName: "Configured travel run",
    travelCapital: 500000,
    travelProfit: 250000,
    travelMinutes: 120,
    xanaxCost: 850000,
    xanaxEnergy: 250,
    reviveUrl: "https://www.torn.com/hospitalview.php",
    shown: 3,
    modules: {
      energy: true,
      crimes: true,
      crimeGuide: true,
      crimeLearning: true,
      gymGuide: true,
      targetFinder: true,
      ffLosses: true,
      dumpSearch: true,
      money: true,
      travel: false,
      war: false,
      xanax: false,
      refill: true,
      bank: true,
      jobpoints: true,
      checklist: true,
      bars: true,
      cooldowns: true,
      education: true,
      merits: true,
      missions: true,
      notifications: true,
      weaponexp: true,
      stocks: true,
      inventory: true,
      workstats: true,
      battleStats: true,
      personalstats: false,
      organizedCrime: true,
      levelProgress: true,
      eggFinder: true,
      attackSummary: true,
      reviveHelper: true,
      marketTools: true,
      inventoryTools: true,
      cityFinds: true,
      chatTools: false,
      yata: false,
    },
  };

  const TASKS = [
    ["bars", "Check energy and nerve caps", 30, "checklist", "https://www.torn.com/index.php"],
    ["crime", "Use profitable nerve", 30, "crimes", "https://www.torn.com/loader.php?sid=crimes"],
    ["gym", "Spend available energy", 60, "energy", "https://www.torn.com/gym.php"],
    ["cash", "Review exposed wallet cash", 60, "money", "https://www.torn.com/city.php#/p=bank"],
    ["travel", "Review travel profitability", 240, "travel", "https://www.torn.com/travelagency.php"],
    ["refill", "Review daily energy refill", 1440, "refill", "https://www.torn.com/points.php"],
    ["jobpoints", "Review job points", 1440, "jobpoints", "https://www.torn.com/joblist.php"],
    ["bank", "Review idle capital and City Bank", 1440, "bank", "https://www.torn.com/city.php#/p=bank"],
    ["education", "Review education and start the next course", 1440, "education", "https://www.torn.com/education.php"],
    ["missions", "Review missions and mission credits", 1440, "missions", "https://www.torn.com/loader.php?sid=missions"],
    ["merits", "Review merits and nearby awards", 10080, "merits", "https://www.torn.com/awards.php"],
    ["stocks", "Review stock benefit progress", 10080, "stocks", "https://www.torn.com/page.php?sid=stocks"],
    ["tools", "Review inventory and market utilities", 10080, "inventoryTools", "https://www.torn.com/item.php"],
    ["organizedcrime", "Review organized-crime readiness", 1440, "organizedCrime", "https://www.torn.com/factions.php?step=your#/tab=crimes"],
  ];

  const ERROR_HELP = {
    1: "The API key field is empty.",
    2: "The API key is incorrect or malformed.",
    4: "One requested API selection is invalid.",
    5: "The API rate limit was reached. Wait about one minute.",
    8: "The current IP is temporarily blocked by the API.",
    9: "The Torn API is currently disabled.",
    13: "The key is disabled because its owner has been inactive.",
    16: "The key lacks permission for one requested selection.",
    17: "The Torn API reported a backend error.",
    18: "The key is paused.",
    22: "A requested selection is available only in API v1. Compatibility routing will isolate it.",
    23: "A requested selection is available only in API v2. Install the API-v2-compatible Torn Maxxed build.",
    27: "The organized-crime selection must use Organized Crimes 2.0.",
    24: "The requested API feature is temporarily closed.",
  };


  const CRIME_NAMES = [
    "Search For Cash", "Bootlegging", "Graffiti", "Shoplifting",
    "Pickpocketing", "Card Skimming", "Burglary", "Hustling",
    "Disposal", "Cracking", "Forgery", "Scamming", "Arson"
  ];

  const GYM_NAMES = [
    "Premier Fitness", "Average Joes", "Woody's Workout", "Beach Bods",
    "Silver Gym", "Pour Femme", "Davies Den", "Global Gym",
    "Knuckle Heads", "Pioneer Fitness", "Anabolic Anomalies", "Core",
    "Racing Fitness", "Complete Cardio", "Legs Bums and Tums", "Deep Burn",
    "Apollo Gym", "Gun Shop", "Force Training", "Cha Cha's", "Atlas",
    "Last Round", "The Edge", "George's", "Balboas Gym", "Frontline Fitness",
    "Gym 3000", "Mr Isoyamas", "Total Rebound", "Elites", "Sports Science Lab"
  ];

  const STAT_NAMES = ["Strength", "Speed", "Defense", "Dexterity"];

  let apiKey = "";
  let cfg = copy(DEFAULTS);
  let user = null;
  let tasks = {};
  let prefs = {};
  let partial = null;
  let latestError = null;
  let tab = "today";
  let panel;
  let body;
  let status;
  let timer;
  let clock;
  let nextRefresh = 0;
  let refreshing = false;
  let transportUsed = "not tested";
  let pageCoachState = { crime: null, gym: null };
  let ffTargets = [];
  let ffTargetStatus = "Not loaded";
  let ffTargetError = null;
  let ffTargetFetchedAt = 0;
  let ffTargetIndex = 0;
  let ffConsent = { targets: false, sellLosses: false };
  let ffKeyInfo = null;
  let ffKeyStatus = "Not checked";
  let ffLossContracts = [];
  let ffLossClaims = [];
  let ffLossStatus = "Not loaded";
  let ffLossError = null;
  let ffLossFetchedAt = 0;
  let pageCoachTimer = null;
  let pageObserver = null;
  let pageCoachListenersBound = false;
  let crimeOutcomes = [];
  let targetPrefs = {};
  let actionHistory = [];
  let lastSuccessAt = 0;
  let activePageScanAt = 0;
  let yataStocks = [];
  let yataStatus = "Not loaded";
  let yataFetchedAt = 0;
  let selectionHealth = { signature: "", working: [], failed: [] };
  let pageContext = { eggs: 0, levelProgress: null, pickpocket: null, skimming: null, cracking: null, attack: null };
  let apiHistory = [];

  function copy(value) {
    return JSON.parse(JSON.stringify(value));
  }

  function merge(saved) {
    return {
      ...copy(DEFAULTS),
      ...(saved || {}),
      modules: {
        ...copy(DEFAULTS.modules),
        ...(saved?.modules || {}),
      },
    };
  }

  function enabled(name) {
    return cfg.modules?.[name] !== false;
  }

  function num(value, fallback = 0) {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : fallback;
  }

  function int(value, fallback = 0) {
    return Math.trunc(num(value, fallback));
  }

  function clamp(value, min, max) {
    return Math.min(max, Math.max(min, value));
  }

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

  function tornUrl(value, fallback = "") {
    if (!value) return fallback;
    try {
      const url = new URL(value, "https://www.torn.com/");
      return ["torn.com", "www.torn.com"].includes(url.hostname) ? url.href : fallback;
    } catch { return fallback; }
  }

  function cash(value) {
    const amount = num(value);
    if (Math.abs(amount) >= 1e9) return `$${(amount / 1e9).toFixed(2)}b`;
    if (Math.abs(amount) >= 1e6) return `$${(amount / 1e6).toFixed(2)}m`;
    if (Math.abs(amount) >= 1e3) return `$${(amount / 1e3).toFixed(1)}k`;
    return `$${Math.round(amount).toLocaleString()}`;
  }

  function time(seconds) {
    let left = Math.max(0, int(seconds));
    if (!left) return "ready";
    const days = Math.floor(left / 86400);
    left %= 86400;
    const hours = Math.floor(left / 3600);
    left %= 3600;
    const minutes = Math.floor(left / 60);
    const parts = [];
    if (days) parts.push(`${days}d`);
    if (hours) parts.push(`${hours}h`);
    if (minutes || !parts.length) parts.push(`${minutes}m`);
    return parts.join(" ");
  }

  function relative(timestamp) {
    const minutes = Math.round((timestamp - Date.now()) / 60000);
    if (Math.abs(minutes) < 1) return minutes >= 0 ? "due now" : "just overdue";
    if (Math.abs(minutes) < 60) {
      return minutes >= 0 ? `in ${minutes}m` : `${Math.abs(minutes)}m overdue`;
    }
    const hours = Math.round(minutes / 60);
    if (Math.abs(hours) < 48) {
      return hours >= 0 ? `in ${hours}h` : `${Math.abs(hours)}h overdue`;
    }
    const days = Math.round(hours / 24);
    return days >= 0 ? `in ${days}d` : `${Math.abs(days)}d overdue`;
  }

  function style(css) {
    try {
      if (typeof GM_addStyle === "function") return GM_addStyle(css);
      if (typeof GM !== "undefined" && typeof GM.addStyle === "function") {
        return GM.addStyle(css);
      }
    } catch {}
    const node = document.createElement("style");
    node.textContent = css;
    (document.head || document.documentElement).appendChild(node);
  }

  async function load(name, fallback) {
    const key = PREFIX + name;
    try {
      if (typeof GM_getValue === "function") {
        const value = await GM_getValue(key, fallback);
        return value === undefined ? fallback : value;
      }
      if (typeof GM !== "undefined" && typeof GM.getValue === "function") {
        const value = await GM.getValue(key, fallback);
        return value === undefined ? fallback : value;
      }
    } catch {}
    // Never expose the Torn API key to the page's localStorage. Supported
    // userscript managers and Torn PDA provide isolated GM storage.
    if (SENSITIVE_STORAGE.has(name)) return fallback;
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? fallback : JSON.parse(raw);
    } catch {
      return fallback;
    }
  }

  async function save(name, value) {
    const key = PREFIX + name;
    try {
      if (typeof GM_setValue === "function") return GM_setValue(key, value);
      if (typeof GM !== "undefined" && typeof GM.setValue === "function") {
        return GM.setValue(key, value);
      }
    } catch {}
    if (SENSITIVE_STORAGE.has(name)) {
      console.warn("[Torn Maxxed] Isolated userscript storage is unavailable; the API key was not persisted.");
      return false;
    }
    localStorage.setItem(key, JSON.stringify(value));
    return true;
  }

  function apiPlan() {
    const selections = new Set(["basic"]);
    let access = "Public";
    const minimal = (name) => { selections.add(name); if (access === "Public") access = "Minimal"; };
    const limited = (name) => { selections.add(name); access = "Limited"; };

    if (enabled("energy") || enabled("crimes") || enabled("bars") || enabled("gymGuide")) minimal("bars");
    if (enabled("xanax") || enabled("cooldowns")) minimal("cooldowns");
    if (enabled("travel")) minimal("travel");
    if (enabled("refill")) minimal("refills");
    if (enabled("jobpoints")) minimal("jobpoints");
    if (enabled("education")) minimal("education");
    if (enabled("merits")) minimal("merits");
    if (enabled("missions")) minimal("missions");
    if (enabled("notifications")) minimal("notifications");
    if (enabled("weaponexp")) minimal("weaponexp");
    if (enabled("workstats")) minimal("workstats");
    if (enabled("battleStats")) limited("battlestats");
    if (enabled("inventory")) limited("inventory");
    if (enabled("stocks")) limited("stocks");
    if (enabled("personalstats")) limited("personalstats");
    if (enabled("organizedCrime")) minimal("organizedcrime");
    if (enabled("money") || enabled("travel") || enabled("xanax") || enabled("refill") || enabled("bank")) limited("money");

    return { selections: [...selections], access };
  }

  function makeApiError(data, statusCode) {
    const code = int(data?.error?.code);
    const message = data?.error?.error || data?.error?.message || "Unknown API error";
    const error = new Error(`Torn API error ${code || "?"}: ${message}`);
    error.code = code;
    error.statusCode = statusCode;
    error.help = ERROR_HELP[code] || "Review the key and enabled selections.";
    error.isServiceError = true;
    return error;
  }

  async function request(url) {
    const host = new URL(url, location.href).hostname;
    const isFFScouter = /(?:^|\.)ffscouter\.com/i.test(host);
    const isYata = /(?:^|\.)yata\.yt/i.test(host);
    const serviceName = isFFScouter ? "FFScouter" : isYata ? "YATA" : "Torn";
    const parse = (text, statusCode = 200) => {
      let data;
      try {
        data = typeof text === "string" ? JSON.parse(text) : text;
      } catch {
        throw new Error(`${serviceName} returned invalid data (HTTP ${statusCode}).`);
      }
      if (data?.error) {
        if (isFFScouter) {
          const error = new Error(String(data.error));
          error.code = data.code;
          error.help = "Check that this exact 16-character Torn key is registered with FFScouter and has the permission required by the requested feature.";
          error.isServiceError = true;
          throw error;
        }
        if (isYata) {
          const error = new Error(String(data.error?.message || data.error));
          error.help = "YATA did not return travel data. Retry later; your Torn key was not sent.";
          error.isServiceError = true;
          throw error;
        }
        throw makeApiError(data, statusCode);
      }
      return data;
    };

    const failures = [];

    // Torn PDA's documented native cross-origin transport. The handler is
    // exposed through flutter_inappwebview.callHandler, not as a global
    // PDA_httpGet() function.
    // At document-end the Torn PDA bridge is already available when this is
    // running inside Torn PDA. Do not wait for it on desktop browsers; doing so
    // would add a needless delay to every request before trying GM APIs.
    const pdaBridge = isTornPdaRuntime() ? window.flutter_inappwebview : null;
    if (pdaBridge) {
      try {
        const nativeRequest = pdaBridge.callHandler(
          "PDA_httpGet",
          url,
          { Accept: "application/json" }
        );
        const response = await Promise.race([
          nativeRequest,
          new Promise((_, reject) =>
            setTimeout(() => reject(new Error("native request timed out")), 15000)
          ),
        ]);
        transportUsed = "Torn PDA native HTTP bridge";
        return parse(
          response?.responseText ?? response?.response ?? response ?? "",
          response?.status ?? 200
        );
      } catch (error) {
        if (error?.isServiceError) throw error;
        failures.push(`PDA native bridge: ${error?.message || error}`);
      }
    } else {
      failures.push("PDA native bridge: unavailable");
    }

    // Modern Greasemonkey-compatible API.
    if (typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function") {
      try {
        const response = await GM.xmlHttpRequest({
          method: "GET",
          url,
          timeout: 15000,
          headers: { Accept: "application/json" },
          responseType: "text",
        });
        transportUsed = "GM.xmlHttpRequest";
        return parse(response?.responseText ?? response?.response ?? "", response?.status ?? 200);
      } catch (error) {
        if (error?.isServiceError) throw error;
        failures.push(`GM.xmlHttpRequest: ${error?.message || error}`);
      }
    }

    // Legacy callback API.
    if (typeof GM_xmlhttpRequest === "function") {
      try {
        const response = await new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method: "GET",
            url,
            timeout: 15000,
            headers: { Accept: "application/json" },
            onload: resolve,
            onerror: (error) => reject(error || new Error("request failed")),
            ontimeout: () => reject(new Error("request timed out")),
          });
        });
        transportUsed = "GM_xmlhttpRequest";
        return parse(response?.responseText ?? response?.response ?? "", response?.status ?? 200);
      } catch (error) {
        if (error?.isServiceError) throw error;
        failures.push(`GM_xmlhttpRequest: ${error?.message || error}`);
      }
    }

    // Last resort for normal browsers. Torn PDA should normally use one of the
    // native transports above.
    {
      const controller = new AbortController();
      const timeout = setTimeout(() => controller.abort(), 15000);
      try {
        const response = await fetch(url, {
          method: "GET",
          credentials: "omit",
          headers: { Accept: "application/json" },
          signal: controller.signal,
        });
        transportUsed = "browser fetch";
        return parse(await response.text(), response.status);
      } catch (error) {
        if (error?.isServiceError) throw error;
        failures.push(`fetch: ${error?.message || error}`);
      } finally {
        clearTimeout(timeout);
      }
    }

    const detail = failures.filter(Boolean).join(" | ");
    const networkServiceName = /ffscouter\.com/i.test(url) ? "FFScouter" : /yata\.yt/i.test(url) ? "YATA" : "Torn API";
    const error = new Error(
      `Network error while contacting ${networkServiceName}.${detail ? ` ${detail}` : ""}`
    );
    error.help = /ffscouter\.com/i.test(url)
      ? "Check that this exact API key is registered at ffscouter.com and retry."
      : /yata\.yt/i.test(url)
        ? "YATA could not be reached. Retry later; no Torn key was sent."
        : "The script could not reach api.torn.com. Torn PDA native transport was attempted first.";
    throw error;
  }

  async function requestSelections(key, selections) {
    const uniqueSelections = [...new Set(selections.filter(Boolean))];
    const legacySelections = uniqueSelections.filter(
      (selection) => !V2_ONLY_SELECTIONS.has(selection)
    );
    const query = new URLSearchParams({
      selections: uniqueSelections.join(","),
      key,
      comment: "Torn_Maxxed_Command_Center",
    });
    // Torn's API v2 generic user endpoint can return legacy v1-shaped data for
    // selected fields. This keeps the established normalizer stable while
    // allowing v2-only selections in the same request.
    if (legacySelections.length) {
      query.set("legacy", legacySelections.join(","));
    }
    try {
      return await request(`${API_USER}?${query}`);
    } catch (error) {
      error.selections = uniqueSelections;
      throw error;
    }
  }

  async function requestKeyInfo(key) {
    const query = new URLSearchParams({
      key,
      comment: "Torn_Maxxed_Key_Test",
    });
    return request(`${API_KEY_INFO}?${query}`);
  }

  async function diagnose(key, selections) {
    const working = [];
    const failed = [];

    for (const selection of selections) {
      try {
        await requestSelections(key, [selection]);
        working.push(selection);
      } catch (error) {
        failed.push({
          selection,
          code: error.code || "?",
          message: error.message,
        });
        if ([2, 13, 18].includes(error.code)) break;
      }
    }

    return { working, failed };
  }

  async function requestData(key, forceDiagnose = false) {
    const plan = apiPlan();
    const signature = plan.selections.slice().sort().join(",");

    if (!forceDiagnose && selectionHealth.signature === signature && selectionHealth.working?.length) {
      try {
        const data = await requestSelections(key, selectionHealth.working);
        apiHistory.push({ at: Date.now(), type: "api", selections: selectionHealth.working, mode: "cached", ok: true });
        apiHistory = apiHistory.slice(-30); await save("apiHistory", apiHistory);
        return { data, working: selectionHealth.working, failed: selectionHealth.failed || [] };
      } catch (error) {
        selectionHealth = { signature: "", working: [], failed: [] };
        await save("selectionHealth", selectionHealth);
        if (!SELECTION_RECOVERY_CODES.has(error.code)) throw error;
      }
    }

    try {
      const data = await requestSelections(key, plan.selections);
      selectionHealth = { signature, working: plan.selections, failed: [] };
      apiHistory.push({ at: Date.now(), type: "api", selections: plan.selections, mode: "full", ok: true });
      apiHistory = apiHistory.slice(-30);
      await Promise.all([save("selectionHealth", selectionHealth), save("apiHistory", apiHistory)]);
      return { data, working: plan.selections, failed: [] };
    } catch (error) {
      if (!SELECTION_RECOVERY_CODES.has(error.code)) throw error;
      const result = await diagnose(key, plan.selections);
      if (!result.working.length) throw error;
      const data = await requestSelections(key, result.working);
      selectionHealth = { signature, working: result.working, failed: result.failed };
      apiHistory.push({ at: Date.now(), type: "api", selections: result.working, mode: "partial", ok: true, failed: result.failed.map((item) => item.selection) });
      apiHistory = apiHistory.slice(-30);
      await Promise.all([save("selectionHealth", selectionHealth), save("apiHistory", apiHistory)]);
      return { data, ...result };
    }
  }

  function readBar(raw, name) {
    const source = raw?.bars?.[name] || raw?.[name] || {};
    const current = num(source.current ?? source.amount ?? source.value);
    const maximum = num(source.maximum ?? source.max);
    return {
      current,
      maximum,
      fulltime: int(source.fulltime ?? source.full_time),
      percentage: maximum > 0 ? clamp((current / maximum) * 100, 0, 100) : 0,
    };
  }

  function normalize(raw) {
    const moneyData = raw?.money || raw || {};
    const cooldownData = raw?.cooldowns || raw || {};
    const travelData = raw?.travel || {};
    const refillData = raw?.refills || {};
    const basic = raw?.basic || raw || {};
    const destination = String(travelData.destination || "Torn");
    const travelTime = int(travelData.time_left ?? travelData.timeLeft);
    const vaultValue = moneyData.vault ?? moneyData.vault_amount ?? raw?.vault_amount;
    const education = raw?.education || {};
    const missions = raw?.missions || {};
    const notifications = raw?.notifications || {};
    const merits = raw?.merits || {};
    const stocks = raw?.stocks || {};
    const inventory = Array.isArray(raw?.inventory) ? raw.inventory : [];
    const weaponexp = Array.isArray(raw?.weaponexp) ? raw.weaponexp : [];
    const jobpoints = raw?.jobpoints || raw?.job_points || {};
    const workstats = raw?.workstats || {};
    const personalstats = raw?.personalstats || {};
    const organizedcrime = raw?.organizedcrime || raw?.organized_crime || {};
    const statusData = raw?.status || basic?.status || {};
    const battleRaw = raw?.battlestats || raw || {};
    const battleStats = {
      strength: num(battleRaw.strength), speed: num(battleRaw.speed),
      defense: num(battleRaw.defense), dexterity: num(battleRaw.dexterity),
    };

    return {
      name: String(raw?.name || basic?.name || "Player"),
      level: int(raw?.level ?? basic?.level),
      playerId: int(raw?.player_id ?? basic?.player_id ?? raw?.id),
      status: typeof statusData === "string" ? { state: statusData, description: statusData } : statusData,
      energy: readBar(raw, "energy"),
      nerve: readBar(raw, "nerve"),
      happy: readBar(raw, "happy"),
      life: readBar(raw, "life"),
      wallet: num(moneyData.wallet ?? moneyData.money_onhand ?? raw?.money_onhand),
      vault: vaultValue == null ? 0 : num(vaultValue),
      hasVault: vaultValue !== null && vaultValue !== undefined,
      points: num(moneyData.points ?? raw?.points),
      cityBank: num(moneyData.city_bank?.amount ?? raw?.city_bank?.amount),
      cityBankTime: int(moneyData.city_bank?.time_left ?? raw?.city_bank?.time_left),
      drug: int(cooldownData.drug),
      booster: int(cooldownData.booster),
      medical: int(cooldownData.medical),
      refillUsed: Boolean(refillData.energy_refill_used ?? refillData.energy?.used),
      destination,
      travelTime,
      inFlight: travelTime > 0,
      abroad: !["", "torn", "torn city"].includes(destination.toLowerCase()),
      education,
      missions,
      notifications,
      merits,
      stocks,
      inventory,
      weaponexp,
      jobpoints,
      workstats,
      battleStats,
      personalstats,
      organizedcrime,
      fetchedAt: Date.now(),
    };
  }


  function isTornPdaRuntime() {
    try {
      return Boolean(
        window.flutter_inappwebview &&
        typeof window.flutter_inappwebview.callHandler === "function"
      );
    } catch {
      return false;
    }
  }

  function isActivelyViewed() {
    if (cfg.safeMode || document.visibilityState !== "visible") return false;
    // Torn PDA's current WebView is the active in-app page. Desktop managers
    // must also report that the Torn document has focus. No time-based grace
    // period is used, so an unfocused desktop tab is never page-parsed.
    if (isTornPdaRuntime()) return true;
    try {
      return typeof document.hasFocus !== "function" || document.hasFocus();
    } catch {
      return false;
    }
  }

  function currentPageKind() {
    const href = location.href.toLowerCase();
    if (/sid=crimes|page\.php\?sid=crimes/.test(href)) return "crimes";
    if (/gym\.php/.test(href)) return "gym";
    if (/item\.php/.test(href)) return "inventory";
    if (/sid=itemmarket|imarket\.php|bazaar\.php/.test(href)) return "market";
    if (/city\.php/.test(href)) return "city";
    return "other";
  }

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

  function isVisibleElement(element) {
    if (!element || !element.isConnected) return false;
    const rect = element.getBoundingClientRect();
    const style = getComputedStyle(element);
    return rect.width > 20 && rect.height > 10 && style.display !== "none" && style.visibility !== "hidden";
  }

  function findNamedElement(name, root = document) {
    const wanted = cleanText(name).toLowerCase();
    const candidates = root.querySelectorAll(
      "h1,h2,h3,h4,h5,h6,[role=heading],button,a,span,strong,[class*=title],[class*=name]"
    );
    let best = null;
    for (const element of candidates) {
      if (!isVisibleElement(element)) continue;
      const text = cleanText(element.textContent).toLowerCase();
      if (text !== wanted) continue;
      if (!best || cleanText(element.textContent).length < cleanText(best.textContent).length) best = element;
    }
    return best;
  }

  function findCard(element, name) {
    let node = element;
    let fallback = element?.parentElement || element;
    for (let depth = 0; node && depth < 7; depth += 1, node = node.parentElement) {
      const rect = node.getBoundingClientRect();
      const text = cleanText(node.innerText || node.textContent);
      if (text.toLowerCase().includes(name.toLowerCase())) fallback = node;
      if (rect.width >= 180 && rect.height >= 45 && rect.height <= 520 && text.length <= 1800) {
        const interactive = node.querySelector("button,a,[role=button]");
        if (interactive || depth >= 2) return node;
      }
    }
    return fallback;
  }

  function removeCoachMarks() {
    document.querySelectorAll(".mx-coach-badge").forEach((node) => node.remove());
    document.querySelectorAll(".mx-coach-highlight").forEach((node) => {
      node.classList.remove("mx-coach-highlight", "mx-coach-second", "mx-coach-third");
    });
  }

  function parseSkill(card) {
    const explicit = [...card.querySelectorAll("[aria-label],[title],[data-skill],[class*=skill]")]
      .map((element) => `${element.getAttribute("aria-label") || ""} ${element.title || ""} ${element.dataset.skill || ""} ${element.textContent || ""}`)
      .join(" ");
    const match = explicit.match(/(?:crime\s*)?skill(?:\s*(?:level|rank))?\s*[:#-]?\s*(100|[1-9]?\d)(?!\d)/i);
    return match ? Number(match[1]) : 0;
  }

  function crimeScore(item) {
    const goal = cfg.crimeGoal || "balanced";
    const tier = item.tier;
    const skill = item.skill || 0;
    const minSkill = Math.max(1, num(cfg.crimeMinimumSkill, 5));
    const learned = crimeOutcomeStats(item.name);
    const learningBonus = learned.samples >= 3
      ? (learned.successRate * 900) + Math.min(900, Math.max(-900, num(learned.averageProfit) / 1000))
      : 0;
    if (cfg.preferredCrime && cfg.preferredCrime !== "Auto") {
      return item.name.toLowerCase() === cfg.preferredCrime.toLowerCase() ? 100000 : -tier;
    }
    if (goal === "safest") return skill * 100 - tier + learningBonus;
    if (goal === "master-lowest") return (101 - skill) * 60 + tier + learningBonus;
    if (goal === "latest-reliable") {
      return tier * 100 + skill * 2 - Math.max(0, minSkill - skill) * 500 + learningBonus;
    }
    return tier * 55 + skill * 18 - Math.max(0, minSkill - skill) * 250 + learningBonus;
  }

  function addCoachBadge(card, text, rank = 1) {
    if (!card || card.querySelector(":scope > .mx-coach-badge")) return;
    card.classList.add("mx-coach-highlight");
    if (rank === 2) card.classList.add("mx-coach-second");
    if (rank === 3) card.classList.add("mx-coach-third");
    if (getComputedStyle(card).position === "static") card.style.position = "relative";
    const badge = document.createElement("div");
    badge.className = "mx-coach-badge";
    badge.textContent = text;
    card.appendChild(badge);
  }

  function coachCrimePage() {
    if (!enabled("crimeGuide") || !/sid=crimes|page\.php\?sid=crimes/i.test(location.href)) {
      pageCoachState.crime = null;
      return;
    }
    const found = [];
    CRIME_NAMES.forEach((name, index) => {
      const label = findNamedElement(name);
      if (!label) return;
      const card = findCard(label, name);
      if (!card || /locked|coming soon/i.test(card.innerText || "")) return;
      found.push({ name, tier: index + 1, skill: parseSkill(card), card });
    });
    const unique = [...new Map(found.map((item) => [item.name, item])).values()];
    unique.forEach((item) => { item.score = crimeScore(item); });
    unique.sort((a, b) => b.score - a.score);
    unique.slice(0, 3).forEach((item, index) => {
      const labels = ["DO THIS CRIME", "SECOND CHOICE", "SAFE FALLBACK"];
      const skill = item.skill ? ` · crime skill ${item.skill}` : "";
      addCoachBadge(item.card, `${labels[index]}${skill}`, index + 1);
    });
    pageCoachState.crime = unique[0]
      ? {
          ...unique[0],
          choices: unique.slice(0, 3).map(({ name, skill, score }) => ({ name, skill, score })),
          reason: cfg.preferredCrime && cfg.preferredCrime !== "Auto"
            ? `Manual preference: ${cfg.preferredCrime}`
            : `Selected by ${String(cfg.crimeGoal || "balanced").replaceAll("-", " ")} strategy`,
        }
      : null;
  }

  function coloredSegmentScore(element) {
    const rect = element.getBoundingClientRect();
    if (rect.width < 2 || rect.width > 48 || rect.height < 2 || rect.height > 24) return 0;
    const style = getComputedStyle(element);
    if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) < 0.25) return 0;
    const color = style.backgroundColor;
    if (!color || color === "transparent" || /rgba?\(0,\s*0,\s*0(?:,\s*0)?\)/.test(color)) return 0;
    return 1;
  }

  function inferDots(section) {
    const attributeText = [...section.querySelectorAll("[aria-label],[title],[data-value],[data-dots]")]
      .map((element) => `${element.getAttribute("aria-label") || ""} ${element.title || ""} ${element.dataset.value || ""} ${element.dataset.dots || ""}`)
      .join(" ");
    let match = attributeText.match(/(\d+(?:\.\d+)?)\s*(?:\/\s*10|dots?)/i);
    if (match) return Number(match[1]);
    const active = [...section.querySelectorAll("[class*=dot],[class*=bar],[class*=segment],[class*=progress]")]
      .filter((element) => /active|filled|selected|on|green|orange/i.test(element.className || ""));
    if (active.length) return active.length;
    const visual = [...section.querySelectorAll("span,i,b,em,div")].reduce((sum, element) => sum + coloredSegmentScore(element), 0);
    return visual >= 2 && visual <= 20 ? visual : 0;
  }

  function collectActiveGymStats(root = document.querySelector("#gymroot") || document) {
    const stats = [];
    STAT_NAMES.forEach((name) => {
      const heading = findNamedElement(name, root);
      if (!heading) return;
      const section = findCard(heading, name);
      stats.push({ name, dots: inferDots(section), card: section });
    });
    return stats;
  }

  function plannedGymStat() {
    const fixed = String(cfg.gymTarget || "best-dots").toLowerCase();
    if (fixed !== "best-dots") return fixed;
    const stats = user?.battleStats || {};
    const entries = ["strength", "speed", "defense", "dexterity"].map((name) => [name, num(stats[name])]);
    if (!entries.some(([, value]) => value > 0)) return "best-dots";
    const total = entries.reduce((sum, [, value]) => sum + value, 0) || 1;
    const plan = String(cfg.gymPlan || "balanced");
    let targets = { strength: 25, speed: 25, defense: 25, dexterity: 25 };
    if (plan === "glass-cannon") targets = { strength: 40, speed: 40, defense: 10, dexterity: 10 };
    if (plan === "defensive") targets = { strength: 15, speed: 15, defense: 45, dexterity: 25 };
    if (plan === "dexterity") targets = { strength: 15, speed: 20, defense: 20, dexterity: 45 };
    if (plan === "custom") targets = cfg.gymRatios || targets;
    if (plan === "lowest-stat") return entries.sort((a, b) => a[1] - b[1])[0][0];
    const deficits = entries.map(([name, value]) => ({ name, deficit: num(targets[name], 25) - (value / total * 100) }));
    return deficits.sort((a, b) => b.deficit - a.deficit)[0].name;
  }

  function collectVisibleGyms() {
    const gyms = [];
    GYM_NAMES.forEach((gymName) => {
      const label = findNamedElement(gymName);
      if (!label) return;
      const card = findCard(label, gymName);
      const text = cleanText(card?.innerText || card?.textContent);
      if (!card || /locked|requires .*gym|not available|buy membership/i.test(text)) return;
      const stats = collectActiveGymStats(card);
      if (!stats.some((item) => item.dots > 0)) return;
      gyms.push({ gymName, card, stats });
    });
    return [...new Map(gyms.map((gym) => [gym.gymName, gym])).values()];
  }

  function bestStatForGym(gym, configured) {
    const stats = [...gym.stats];
    if (configured !== "best-dots") {
      const target = stats.find((item) => item.name.toLowerCase() === configured);
      if (target) return target;
    }
    return stats.sort((a, b) => b.dots - a.dots)[0] || null;
  }

  function coachGymPage() {
    if (!enabled("gymGuide") || !/gym\.php/i.test(location.href)) {
      pageCoachState.gym = null;
      return;
    }
    const configured = plannedGymStat();
    const visibleGyms = collectVisibleGyms();
    const rankedGyms = visibleGyms
      .map((gym) => ({ ...gym, best: bestStatForGym(gym, configured) }))
      .filter((gym) => gym.best)
      .sort((a, b) => b.best.dots - a.best.dots);

    if (rankedGyms.length) {
      const bestGym = rankedGyms[0];
      const dotText = bestGym.best.dots ? ` · ${bestGym.best.dots} gain bars` : "";
      addCoachBadge(bestGym.card, `★ Best gym: ${bestGym.best.name}${dotText}`, 1);
      pageCoachState.gym = {
        stat: bestGym.best.name,
        dots: bestGym.best.dots,
        gymName: bestGym.gymName,
        card: bestGym.card,
        comparedGyms: rankedGyms.length,
      };
      return;
    }

    const stats = collectActiveGymStats();
    if (!stats.length) return;
    let ranked = [...stats];
    if (configured !== "best-dots") {
      ranked.sort((a, b) => (a.name.toLowerCase() === configured ? -1 : b.name.toLowerCase() === configured ? 1 : b.dots - a.dots));
    } else {
      ranked.sort((a, b) => b.dots - a.dots);
    }
    const best = ranked[0];
    const gymHeading = document.querySelector("#gymroot h1,#gymroot h2,#gymroot [class*=title]");
    const gymName = cleanText(gymHeading?.textContent).replace(/gym$/i, "").trim() || "current gym";
    const dotText = best.dots ? ` · ${best.dots} visible gain bars` : "";
    addCoachBadge(best.card, `★ Train ${best.name}${dotText}`, 1);
    pageCoachState.gym = { stat: best.name, dots: best.dots, gymName, card: best.card, comparedGyms: 1 };
  }

  function explicitPercentNear(label) {
    const nodes = [...document.querySelectorAll('body *')].filter(isVisibleElement);
    for (const node of nodes) {
      const text = cleanText(node.textContent);
      if (!text || text.length > 180 || !text.toLowerCase().includes(label.toLowerCase())) continue;
      const match = text.match(/(100|\d{1,2}(?:\.\d+)?)\s*%/);
      if (match) return num(match[1]);
    }
    return null;
  }

  function scanEggs() {
    if (!enabled("eggFinder")) return 0;
    document.querySelectorAll(".mx-egg-find").forEach((node) => node.classList.remove("mx-egg-find"));
    const nodes = [...document.querySelectorAll('img,button,a,[role=button],[class*=egg]')].filter((node) => {
      if (!isVisibleElement(node) || node.closest(`#${ID}`)) return false;
      const text = `${node.getAttribute?.("alt") || ""} ${node.getAttribute?.("title") || ""} ${node.getAttribute?.("aria-label") || ""} ${node.getAttribute?.("src") || ""} ${node.className || ""}`;
      return /(?:^|[^a-z])(?:easter\s*)?egg(?:[^a-z]|$)/i.test(text) && !/egg.?timer|egg.?count/i.test(text);
    });
    nodes.forEach((node) => node.classList.add("mx-egg-find"));
    return nodes.length;
  }

  function scanPickpocketContext() {
    if (currentPageKind() !== "crimes" || !/pickpocket/i.test(document.body.innerText || "")) return null;
    const targetWords = [
      ["distracted", 90], ["drunk", 85], ["elderly", 80], ["homeless", 75], ["student", 70],
      ["tourist", 65], ["jogger", 55], ["cyclist", 50], ["police", 5], ["guard", 10], ["soldier", 15],
    ];
    const rows = [...document.querySelectorAll('button,[role=button],li,[class*=target],[class*=option]')].filter(isVisibleElement)
      .map((node) => ({ node, text: cleanText(node.innerText || node.textContent) }))
      .filter((row) => row.text.length > 2 && row.text.length < 240);
    const ranked = rows.map((row) => {
      let score = 0; let reason = "No known safety keyword";
      targetWords.forEach(([word, value]) => { if (row.text.toLowerCase().includes(word) && value > score) { score = value; reason = word; } });
      return { ...row, score, reason };
    }).filter((row) => row.score > 0).sort((a, b) => b.score - a.score);
    const best = ranked[0];
    if (best) addCoachBadge(best.node, `PREFERRED TARGET · ${best.reason}`, 1);
    return best ? { label: best.text.slice(0, 100), reason: best.reason, score: best.score } : null;
  }

  function scanSkimmingContext() {
    if (currentPageKind() !== "crimes" || !/card skimming/i.test(document.body.innerText || "")) return null;
    const text = cleanText(document.body.innerText || "");
    const deployed = text.match(/(?:deployed|installed|active)\s*[:-]?\s*(\d+)/i);
    const cards = text.match(/(?:card details|cards collected|details)\s*[:-]?\s*([\d,]+)/i);
    const rate = text.match(/([\d.]+)\s*(?:cards?|details?)\s*(?:per|\/)\s*hour/i);
    return { deployed: deployed ? int(deployed[1]) : null, cards: cards ? int(cards[1].replaceAll(",", "")) : null, rate: rate ? num(rate[1]) : null };
  }

  function scanCrackingContext() {
    if (currentPageKind() !== "crimes" || !/cracking/i.test(document.body.innerText || "")) return null;
    const text = cleanText(document.body.innerText || "");
    const patterns = text.match(/\b[A-Z0-9][A-Z0-9_?*.-]{3,}[A-Z0-9_?*.-]\b/gi) || [];
    const pattern = patterns.find((item) => /[_?*]/.test(item));
    return pattern ? { pattern } : null;
  }

  function scanAttackContext() {
    if (!enabled("attackSummary") || !/sid=attack|attack\.php/i.test(location.href)) return null;
    const text = cleanText(document.body.innerText || "");
    const respect = text.match(/respect\s*(?:gain|earned)?\s*[:+]?\s*([\d.]+)/i);
    const ff = text.match(/fair\s*fight\s*[:x]?\s*([\d.]+)/i);
    const result = /you\s+(?:won|defeated)/i.test(text) ? "win" : /you\s+(?:lost|were defeated)/i.test(text) ? "loss" : "in progress";
    return { result, respect: respect ? num(respect[1]) : null, fairFight: ff ? num(ff[1]) : null };
  }

  function scanPageContext() {
    if (!isActivelyViewed()) return;
    pageContext = {
      eggs: scanEggs(),
      levelProgress: enabled("levelProgress") ? explicitPercentNear("level") : null,
      pickpocket: scanPickpocketContext(),
      skimming: scanSkimmingContext(),
      cracking: scanCrackingContext(),
      attack: scanAttackContext(),
    };
  }

  function applyChatTools() {
    document.querySelectorAll(".mx-chat-compact").forEach((node) => node.classList.remove("mx-chat-compact"));
    if (!enabled("chatTools") || !isActivelyViewed()) return;
    document.querySelectorAll('#chatRoot,[class*="chatRoot"],[class*="chatBox"],[class*="chat-box"]').forEach((node) => node.classList.add("mx-chat-compact"));
  }

  function visibleMarketInput() {
    const focused = document.activeElement;
    if (focused && focused.matches?.('input[type="number"],input[class*=amount],input[class*=quantity]') && isVisibleElement(focused)) return focused;
    const inputs = [...document.querySelectorAll('input[type="number"],input[class*=amount],input[class*=quantity]')].filter(isVisibleElement);
    return inputs.length === 1 ? inputs[0] : null;
  }

  function fillMaxAffordableVisible() {
    if (!isActivelyViewed() || currentPageKind() !== "market") return { ok: false, message: "Open the market listing and tap its quantity field first." };
    const input = visibleMarketInput();
    if (!input) return { ok: false, message: "Tap one visible quantity field, then run Max affordable again." };
    const container = input.closest('li,[class*=item],[class*=listing],[class*=row],form') || input.parentElement;
    const text = cleanText(container?.innerText || container?.textContent);
    const prices = [...text.matchAll(/\$\s*([\d,]+)/g)].map((match) => num(match[1].replaceAll(",", ""))).filter((value) => value > 0);
    if (!prices.length) return { ok: false, message: "A price could not be read from the selected listing." };
    const price = Math.min(...prices);
    const stockMatch = text.match(/(?:stock|available|left)\s*[:x]?\s*([\d,]+)/i);
    const stock = stockMatch ? int(stockMatch[1].replaceAll(",", ""), 999999999) : 999999999;
    const maximum = Math.max(0, Math.min(stock, Math.floor(Math.max(0, user?.wallet || 0) / price)));
    input.value = String(maximum);
    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    return { ok: true, message: `Filled ${maximum} at ${cash(price)} each. Review before buying.` };
  }

  function sortVisibleInventoryCards() {
    if (!isActivelyViewed() || currentPageKind() !== "inventory") return { ok: false, message: "Open Inventory first." };
    const nodes = [...document.querySelectorAll('[class*=item],[data-item]')].filter((node) => isVisibleElement(node) && !node.closest(`#${ID}`));
    const grouped = new Map();
    nodes.forEach((node) => {
      const parent = node.parentElement;
      if (parent) grouped.set(parent, [...(grouped.get(parent) || []), node]);
    });
    const best = [...grouped.entries()].sort((a, b) => b[1].length - a[1].length)[0];
    if (!best || best[1].length < 2) return { ok: false, message: "Visible inventory cards could not be identified." };
    best[1].map((node) => ({ node, name: cleanText(node.querySelector('[class*=name],[class*=title],strong,b')?.textContent || node.textContent) }))
      .sort((a, b) => a.name.localeCompare(b.name))
      .forEach((item, index) => { item.node.style.order = String(index); });
    return { ok: true, message: `${best[1].length} visible cards sorted by name.` };
  }

  function runPageCoach() {
    if (!isActivelyViewed()) return;
    activePageScanAt = Date.now();
    removeCoachMarks();
    coachCrimePage();
    coachGymPage();
    applyChatTools();
    scanPageContext();
  }

  function schedulePageCoach() {
    clearTimeout(pageCoachTimer);
    pageCoachTimer = setTimeout(runPageCoach, 350);
  }

  function startPageCoach() {
    if (cfg.safeMode) { pageObserver?.disconnect(); removeCoachMarks(); return; }
    schedulePageCoach();
    pageObserver?.disconnect();
    pageObserver = new MutationObserver((mutations) => {
      if (!isActivelyViewed()) return;
      if (mutations.some((mutation) => [...mutation.addedNodes].some((node) => node.nodeType === 1 && !node.classList?.contains("mx-coach-badge")))) schedulePageCoach();
    });
    pageObserver.observe(document.body, { childList: true, subtree: true });
    if (!pageCoachListenersBound) {
      pageCoachListenersBound = true;
      window.addEventListener("hashchange", schedulePageCoach);
      window.addEventListener("popstate", schedulePageCoach);
      document.addEventListener("visibilitychange", () => { if (isActivelyViewed()) schedulePageCoach(); });
      window.addEventListener("focus", schedulePageCoach);
    }
  }

  function taskList() {
    return TASKS
      .filter((task) => enabled(task[3]))
      .map((task) => {
        const saved = tasks[task[0]] || {};
        return {
          id: task[0],
          label: task[1],
          minutes: task[2],
          url: task[4],
          dueAt: num(saved.dueAt, Date.now()),
          snoozedUntil: num(saved.snoozedUntil),
        };
      })
      .sort((a, b) => dueTime(a) - dueTime(b));
  }

  function dueTime(task) {
    return Math.max(task.dueAt, task.snoozedUntil);
  }

  function taskBoost(id) {
    const task = taskList().find((item) => item.id === id);
    if (!task || dueTime(task) > Date.now()) return 0;
    return 900 + Math.min(1600, ((Date.now() - dueTime(task)) / 60000) * 20);
  }

  async function completeTask(id) {
    const task = TASKS.find((item) => item[0] === id);
    if (!task) return;
    tasks[id] = {
      dueAt: Date.now() + task[2] * 60000,
      snoozedUntil: 0,
    };
    await save("tasks", tasks);
    render();
  }

  async function snoozeTask(id, minutes = 30) {
    tasks[id] = {
      ...(tasks[id] || {}),
      snoozedUntil: Date.now() + minutes * 60000,
    };
    await save("tasks", tasks);
    render();
  }

  function visible(id) {
    const pref = prefs[id] || {};
    return !pref.disabled && num(pref.until) <= Date.now();
  }

  async function snoozeAction(id, minutes) {
    prefs[id] = { ...(prefs[id] || {}), until: Date.now() + minutes * 60000 };
    await save("prefs", prefs);
    render();
  }

  async function hideAction(id) {
    prefs[id] = { ...(prefs[id] || {}), disabled: true, until: 0 };
    await save("prefs", prefs);
    render();
  }

  async function restoreActions() {
    prefs = {};
    await save("prefs", prefs);
    render();
  }

  function action(data) {
    return {
      tone: "normal",
      metric: "",
      reasons: [],
      taskId: "",
      ...data,
    };
  }

  function objectNumberSum(value) {
    if (!value || typeof value !== "object") return 0;
    return Object.values(value).reduce((sum, item) => sum + (Number.isFinite(Number(item)) ? Number(item) : 0), 0);
  }

  function educationInfo() {
    const source = user?.education || {};
    return {
      current: int(source.current ?? source.education_current ?? user?.personalstats?.education_current),
      timeLeft: int(source.timeleft ?? source.time_left ?? source.education_timeleft),
      completed: Array.isArray(source.completed) ? source.completed.length : Array.isArray(source.education_completed) ? source.education_completed.length : 0,
    };
  }

  function jobPointTotal() {
    const points = user?.jobpoints || {};
    if (Number.isFinite(Number(points))) return Number(points);
    return objectNumberSum(points);
  }

  function notificationTotal() {
    return objectNumberSum(user?.notifications || {});
  }

  function missionCount() {
    const missions = user?.missions;
    if (Array.isArray(missions)) return missions.length;
    if (missions && typeof missions === "object") return Object.keys(missions).length;
    return 0;
  }

  function weaponExperienceOpportunity() {
    const rows = Array.isArray(user?.weaponexp) ? user.weaponexp : [];
    const normalized = rows.map((item) => ({
      name: String(item?.name ?? item?.weapon_name ?? `Weapon ${item?.itemID ?? item?.item_id ?? ""}`).trim(),
      experience: num(item?.exp ?? item?.experience ?? item?.percentage),
    })).filter((item) => item.name && item.experience > 0 && item.experience < 100);
    return normalized.sort((a, b) => b.experience - a.experience)[0] || null;
  }

  function stockCount() {
    const stocks = user?.stocks;
    if (!stocks || typeof stocks !== "object") return 0;
    return Object.keys(stocks).length;
  }

  function crimeOutcomeStats(name) {
    const rows = crimeOutcomes.filter((row) => row.crime === name).slice(-30);
    const successes = rows.filter((row) => row.result === "success");
    const failures = rows.filter((row) => row.result !== "success");
    const profit = rows.reduce((sum, row) => sum + num(row.profit), 0);
    return {
      samples: rows.length,
      successRate: rows.length ? successes.length / rows.length : null,
      averageProfit: rows.length ? profit / rows.length : null,
      failures: failures.length,
    };
  }

  async function logCrimeOutcome(result, crimeName, profit = 0, note = "") {
    crimeOutcomes.push({ result, crime: crimeName, profit: num(profit), note: String(note || "").slice(0, 160), at: Date.now() });
    crimeOutcomes = crimeOutcomes.slice(-300);
    await save("crimeOutcomes", crimeOutcomes);
    render();
  }

  function openCrimeOutcomeModal(crimeName) {
    document.getElementById(`${ID}-crime-result`)?.remove();
    const modal = document.createElement("div");
    modal.id = `${ID}-crime-result`;
    modal.className = "mx-modal-bg";
    modal.innerHTML = `
      <div class="mx-modal small">
        <header><div><b>Log crime result</b><small>${html(crimeName)}</small></div><button data-close>×</button></header>
        <main><section>
          ${field("Result", "Stored locally to improve your crime preference.", `<select name="result"><option value="success">Success</option><option value="failure">Failure</option><option value="critical">Critical failure</option></select>`)}
          ${field("Cash profit or loss", "Enter zero when the result was an item or progression only.", `<input type="number" name="profit" step="100" value="0">`)}
          ${field("Note", "Optional target, subcrime, equipment, or outcome note.", `<input type="text" name="note" maxlength="160" placeholder="Optional">`)}
        </section></main>
        <footer><button data-close>Cancel</button><button data-save class="primary">Save result</button></footer>
      </div>`;
    modal.addEventListener("click", async (event) => {
      if (event.target.closest("[data-close]") || event.target === modal) return modal.remove();
      if (event.target.closest("[data-save]")) {
        await logCrimeOutcome(
          modal.querySelector('[name="result"]').value,
          crimeName,
          modal.querySelector('[name="profit"]').value,
          modal.querySelector('[name="note"]').value
        );
        modal.remove();
      }
    });
    document.body.appendChild(modal);
  }

  function targetPref(id) {
    return targetPrefs[String(id)] || {};
  }

  async function updateTargetPref(id, patch) {
    targetPrefs[String(id)] = { ...targetPref(id), ...patch, updatedAt: Date.now() };
    await save("targetPrefs", targetPrefs);
    ffTargets = [...ffTargets].sort(targetSort);
    render();
  }

  function targetSort(a, b) {
    const ap = targetPref(a.id);
    const bp = targetPref(b.id);
    if (Boolean(ap.favorite) !== Boolean(bp.favorite)) return ap.favorite ? -1 : 1;
    if (cfg.ffTargetGoal === "respect") return b.fairFight - a.fairFight || b.level - a.level;
    return b.level - a.level || b.fairFight - a.fairFight;
  }

  function availableFfTargets() {
    return ffTargets.filter((target) => !targetPref(target.id).blacklisted);
  }

  function waitingEvents() {
    if (!user) return [];
    const events = [];
    const add = (label, seconds, url) => { if (num(seconds) > 0) events.push({ label, seconds: num(seconds), url }); };
    if (enabled("energy")) add("Energy reaches cap", user.energy.fulltime, "https://www.torn.com/gym.php");
    if (enabled("crimes")) add("Nerve reaches cap", user.nerve.fulltime, "https://www.torn.com/loader.php?sid=crimes");
    if (enabled("xanax")) add("Drug cooldown ready", user.drug, "https://www.torn.com/item.php");
    if (enabled("cooldowns")) add("Booster cooldown ready", user.booster, "https://www.torn.com/item.php");
    if (enabled("travel") && user.inFlight) add(`Arrive in ${user.destination}`, user.travelTime, "https://www.torn.com/index.php");
    if (enabled("bank") && user.cityBankTime > 0) add("City Bank matures", user.cityBankTime, "https://www.torn.com/city.php#/p=bank");
    const edu = educationInfo();
    if (enabled("education") && edu.timeLeft > 0) add("Education completes", edu.timeLeft, "https://www.torn.com/education.php");
    return events.sort((a, b) => a.seconds - b.seconds).slice(0, 8);
  }

  function blockedActions() {
    if (!user) return [];
    const blocked = [];
    if (enabled("travel") && user.level < 15) blocked.push({ label: "Travel", reason: "Unlocks at level 15" });
    if (enabled("travel") && user.wallet < cfg.cashReserve + cfg.travelCapital) blocked.push({ label: cfg.travelName, reason: `${cash(cfg.cashReserve + cfg.travelCapital - user.wallet)} more cash needed` });
    if (enabled("xanax") && user.wallet < cfg.cashReserve + cfg.xanaxCost) blocked.push({ label: "Xanax plan", reason: `${cash(cfg.cashReserve + cfg.xanaxCost - user.wallet)} short after reserve` });
    if (enabled("targetFinder") && !availableFfTargets().length) blocked.push({ label: "Target session", reason: ffTargetError ? "FFScouter search failed" : "Load or refresh targets" });
    if (enabled("money") && user.wallet >= cfg.walletWarning && !user.hasVault && !cfg.factionVaultUrl) blocked.push({ label: "Secure cash", reason: "No property or configured faction vault" });
    return blocked;
  }

  function recommendations() {
    if (!user) return [];
    const list = [];
    const spendable = Math.max(0, user.wallet - cfg.cashReserve);

    if (enabled("travel") && user.inFlight && visible("flight")) {
      list.push(action({
        id: "flight",
        title: `Prepare for ${user.destination}`,
        detail: "You are currently traveling. Plan your manual purchase or return step.",
        category: "Travel",
        score: 10000,
        url: "https://www.torn.com/index.php",
        metric: `${time(user.travelTime)} remaining`,
        tone: "info",
        reasons: ["Travel is already in progress"],
      }));
    }

    if (enabled("money") && user.wallet >= cfg.walletWarning) {
      if (user.hasVault && visible("cash-vault")) {
        list.push(action({
          id: "cash-vault",
          taskId: "cash",
          title: "Secure exposed cash",
          detail: `Your wallet exceeds ${cash(cfg.walletWarning)} and a property vault was detected.`,
          category: "Money",
          score: 9300 + taskBoost("cash"),
          url: "https://www.torn.com/properties.php#/p=options&tab=vault",
          metric: `${cash(user.wallet)} exposed`,
          tone: "danger",
          reasons: ["Wallet safety threshold exceeded", "Property vault detected"],
        }));
      } else if (!user.hasVault && visible("cash-no-vault")) {
        list.push(action({
          id: "cash-no-vault",
          taskId: "cash",
          title: "Review exposed cash options",
          detail:
            `Your wallet exceeds ${cash(cfg.walletWarning)}, but no property vault was detected. ` +
            "This warning will not block gym or crime recommendations.",
          category: "Money",
          score: 1600 + taskBoost("cash") * 0.25,
          url: "https://www.torn.com/city.php#/p=bank",
          metric: "No property vault detected",
          tone: "warn",
          reasons: ["Consider City Bank, a trusted faction vault, supplies, or a higher warning level"],
        }));
      }
    }

    if (
      enabled("crimes") &&
      cfg.crimeNerve > 0 &&
      user.nerve.current >= cfg.crimeNerve &&
      visible("crime")
    ) {
      const attempts = Math.floor(user.nerve.current / cfg.crimeNerve);
      const perNerve = cfg.crimeProfit / cfg.crimeNerve;
      list.push(action({
        id: "crime",
        taskId: "crime",
        title: `DO THIS CRIME: ${(pageCoachState.crime?.name || cfg.preferredCrime || cfg.crimeName).toUpperCase()}`,
        detail:
          pageCoachState.crime
            ? `Choose ${pageCoachState.crime.name}. ${pageCoachState.crime.reason}. ` +
              `${attempts} estimated attempt${attempts === 1 ? "" : "s"} for about ${cash(attempts * cfg.crimeProfit)} using your configured average.`
            : `Choose ${cfg.preferredCrime && cfg.preferredCrime !== "Auto" ? cfg.preferredCrime : cfg.crimeName}. ` +
              `If it is unavailable, use Pickpocketing as the fallback. ${attempts} estimated attempt${attempts === 1 ? "" : "s"}.`,
        category: "Crime",
        score:
          2600 +
          user.nerve.percentage * 55 +
          Math.min(2500, perNerve / 2) +
          taskBoost("crime"),
        url: "https://www.torn.com/loader.php?sid=crimes",
        metric: `${cash(perNerve)}/nerve`,
        tone: user.nerve.percentage >= 90 ? "danger" : user.nerve.percentage >= 70 ? "warn" : "normal",
        reasons: [
          `Nerve is ${Math.round(user.nerve.percentage)}% full`,
          pageCoachState.crime?.reason || `Preferred crime setting: ${cfg.preferredCrime || "Auto"}`,
          "Fallback when Card Skimming is unavailable: Pickpocketing",
        ],
      }));
    }

    const ffTarget = currentFfTarget();
    if (
      enabled("targetFinder") &&
      ffTarget &&
      user.energy.current >= cfg.attackEnergy &&
      visible("ff-target")
    ) {
      list.push(action({
        id: "ff-target",
        taskId: "gym",
        title: `TARGET: ${ffTarget.name} [${ffTarget.id}]`,
        detail: `FFScouter estimate: Fair Fight ${ffTarget.fairFight ? ffTarget.fairFight.toFixed(2) : "unknown"}, level ${ffTarget.level || "unknown"}, battle stats ${ffTarget.stats}.`,
        category: "Target",
        score: 3250 + user.energy.percentage * 45,
        url: targetAttackUrl(ffTarget),
        metric: `FF ${ffTarget.fairFight ? ffTarget.fairFight.toFixed(2) : "?"}`,
        tone: "info",
        reasons: [
          `Target goal: ${String(cfg.ffTargetGoal || "leveling").replaceAll("-", " ")}`,
          `Last action: ${ffTarget.lastAction}`,
          "FFScouter estimates are not spy reports",
        ],
      }));
    }

    if (
      enabled("ffLosses") &&
      ffLossContracts.length &&
      user.energy.current >= cfg.attackEnergy &&
      visible("ff-losses")
    ) {
      const bestLoss = [...ffLossContracts].sort((a, b) => num(b?.price_per_loss) - num(a?.price_per_loss))[0];
      const possible = Math.max(1, Math.floor(user.energy.current / Math.max(1, cfg.attackEnergy)));
      list.push(action({
        id: "ff-losses",
        title: "Review FFScouter Sell Losses",
        detail: `${ffLossContracts.length} open contract${ffLossContracts.length === 1 ? "" : "s"} are cached. Review the seller page, claim only what you can finish, and use a pillow-only loadout.`,
        category: "Money",
        score: 2550 + user.energy.percentage * 30,
        url: FF_SELL_LOSSES_URL,
        metric: `${cash(bestLoss?.price_per_loss || 0)}/loss · about ${possible} energy slots`,
        tone: "info",
        reasons: ["Claims expire quickly", "The script never claims or attacks automatically", "FFScouter verifies attack logs using your registered key"],
      }));
    }

    if (
      enabled("war") &&
      cfg.warPay > 0 &&
      user.energy.current >= cfg.attackEnergy &&
      visible("war")
    ) {
      const hits = Math.floor(user.energy.current / cfg.attackEnergy);
      list.push(action({
        id: "war",
        taskId: "gym",
        title: "Use energy on paid war hits",
        detail: `${hits} possible hit${hits === 1 ? "" : "s"} project ${cash(hits * cfg.warPay)}.`,
        category: "Energy",
        score: 3500 + user.energy.percentage * 55 + taskBoost("gym"),
        url: "https://www.torn.com/factions.php?step=your",
        metric: `${cash(cfg.warPay)}/hit`,
        tone: user.energy.percentage >= 90 ? "danger" : user.energy.percentage >= 70 ? "warn" : "normal",
        reasons: ["Paid-war mode is enabled"],
      }));
    } else if (enabled("energy") && user.energy.current >= 5 && visible("gym")) {
      list.push(action({
        id: "gym",
        taskId: "gym",
        title: pageCoachState.gym
          ? `Train ${pageCoachState.gym.stat} in ${pageCoachState.gym.gymName}`
          : "Train energy in the gym",
        detail: pageCoachState.gym
          ? `The gym coach marked ${pageCoachState.gym.gymName} for ${pageCoachState.gym.stat} as the best configured training choice${pageCoachState.gym.dots ? ` with ${pageCoachState.gym.dots} visible gain bars` : ""}${pageCoachState.gym.comparedGyms > 1 ? ` after comparing ${pageCoachState.gym.comparedGyms} visible gyms` : ""}.`
          : "Open the Gym page; the coach will highlight the best stat for your configured target.",
        category: "Energy",
        score: 3000 + user.energy.percentage * 58 + taskBoost("gym"),
        url: "https://www.torn.com/gym.php",
        metric: `${Math.round(user.energy.percentage)}% full`,
        tone: user.energy.percentage >= 90 ? "danger" : user.energy.percentage >= 70 ? "warn" : "normal",
        reasons: [`Energy is ${Math.round(user.energy.percentage)}% full`],
      }));
    }

    if (
      enabled("xanax") &&
      user.drug === 0 &&
      user.wallet >= cfg.cashReserve + cfg.xanaxCost &&
      visible("xanax")
    ) {
      list.push(action({
        id: "xanax",
        title: "Xanax is available under your plan",
        detail: "Reminder only. Consider addiction, overdose risk, and immediate energy use.",
        category: "Energy",
        score: 1800,
        url: "https://www.torn.com/item.php",
        metric: `${cash(cfg.xanaxCost / Math.max(1, cfg.xanaxEnergy))}/energy`,
        tone: "warn",
        reasons: ["Drug cooldown is ready"],
      }));
    }

    if (
      enabled("refill") &&
      !user.refillUsed &&
      user.points >= 30 &&
      user.energy.percentage <= 35 &&
      visible("refill")
    ) {
      list.push(action({
        id: "refill",
        taskId: "refill",
        title: "Review the daily energy refill",
        detail: "The refill appears available. Spend the energy immediately after purchase.",
        category: "Daily",
        score: 1900 + taskBoost("refill"),
        url: "https://www.torn.com/points.php",
        metric: `${Math.floor(user.points)} points`,
        tone: "info",
        reasons: ["Refill appears unused"],
      }));
    }

    if (
      enabled("travel") &&
      user.level >= 15 &&
      !user.abroad &&
      user.wallet >= cfg.cashReserve + cfg.travelCapital &&
      visible("travel")
    ) {
      const hourly = cfg.travelMinutes > 0
        ? cfg.travelProfit * (60 / cfg.travelMinutes)
        : 0;
      const penalty = Math.max(user.energy.percentage, user.nerve.percentage) >= 70 ? 1800 : 0;
      list.push(action({
        id: "travel",
        taskId: "travel",
        title: `Start: ${cfg.travelName}`,
        detail: "Configured capital is available after reserve. Spend high bars before leaving.",
        category: "Travel",
        score: 2600 + Math.min(4500, hourly / 2500) - penalty + taskBoost("travel"),
        url: "https://www.torn.com/travelagency.php",
        metric: `${cash(hourly)}/hour`,
        tone: penalty ? "warn" : "normal",
        reasons: [penalty ? "High bars reduce travel value" : "Bars are safe for travel"],
      }));
    }

    if (
      enabled("bank") &&
      user.cityBank <= 0 &&
      spendable > Math.max(cfg.travelCapital, 1000000) &&
      visible("bank")
    ) {
      list.push(action({
        id: "bank",
        taskId: "bank",
        title: "Review a City Bank investment",
        detail: "Only lock money not needed for travel, supplies, faction needs, or your reserve.",
        category: "Money",
        score: 1400 + Math.min(1200, spendable / 5000000) + taskBoost("bank"),
        url: "https://www.torn.com/city.php#/p=bank",
        metric: `${cash(spendable)} above reserve`,
        tone: "info",
        reasons: ["No active City Bank amount detected"],
      }));
    }

    if (enabled("education") && visible("education")) {
      const edu = educationInfo();
      if (!edu.current && !edu.timeLeft) {
        list.push(action({
          id: "education", taskId: "education", title: "Start your next education course",
          detail: "No active education timer was detected. Review available courses and begin the next useful course manually.",
          category: "Progress", score: 2050 + taskBoost("education"), url: "https://www.torn.com/education.php",
          metric: `${edu.completed} completed`, tone: "info", reasons: ["Education time should not sit idle"],
        }));
      }
    }

    if (enabled("missions") && missionCount() > 0 && visible("missions")) {
      list.push(action({
        id: "missions", taskId: "missions", title: "Review available missions",
        detail: `${missionCount()} mission record${missionCount() === 1 ? "" : "s"} detected. Check deadlines, rewards, and mission-credit value manually.`,
        category: "Progress", score: 1500 + taskBoost("missions"), url: "https://www.torn.com/loader.php?sid=missions",
        metric: `${missionCount()} available`, tone: "info", reasons: ["Mission opportunities can expire"],
      }));
    }

    if (enabled("jobpoints") && jobPointTotal() > 0 && visible("jobpoints")) {
      list.push(action({
        id: "jobpoints", taskId: "jobpoints", title: "Review useful job specials",
        detail: `You have ${Math.floor(jobPointTotal())} detected job points across current job categories. Compare specials before spending.`,
        category: "Progress", score: 1250 + taskBoost("jobpoints"), url: "https://www.torn.com/joblist.php",
        metric: `${Math.floor(jobPointTotal())} points`, tone: "info", reasons: ["Unused job points may represent idle value"],
      }));
    }

    const weaponOpportunity = enabled("weaponexp") ? weaponExperienceOpportunity() : null;
    if (weaponOpportunity && weaponOpportunity.experience >= 90 && visible("weaponexp")) {
      list.push(action({
        id: "weaponexp", title: `Finish ${weaponOpportunity.name} weapon experience`,
        detail: `${weaponOpportunity.experience.toFixed(1)}% weapon experience detected. A small amount remains to reach 100%.`,
        category: "Progress", score: 1450 + weaponOpportunity.experience * 5, url: "https://www.torn.com/item.php",
        metric: `${(100 - weaponOpportunity.experience).toFixed(1)}% remaining`, tone: "info", reasons: ["Near-complete progression opportunity"],
      }));
    }

    if (enabled("notifications") && notificationTotal() > 0 && visible("notifications")) {
      list.push(action({
        id: "notifications", title: "Review unread Torn notifications",
        detail: `${Math.floor(notificationTotal())} notification indicator${notificationTotal() === 1 ? "" : "s"} detected across enabled API categories.`,
        category: "Progress", score: 900 + Math.min(1000, notificationTotal() * 15), url: "https://www.torn.com/index.php",
        metric: `${Math.floor(notificationTotal())} notices`, tone: "info", reasons: ["Unread notices can contain time-sensitive information"],
      }));
    }

    if (enabled("stocks") && stockCount() > 0 && taskBoost("stocks") > 0 && visible("stocks")) {
      list.push(action({
        id: "stocks", taskId: "stocks", title: "Review stock benefit progress",
        detail: `${stockCount()} stock holding record${stockCount() === 1 ? "" : "s"} detected. Review benefit-block progress and idle capital.`,
        category: "Money", score: 1050 + taskBoost("stocks"), url: "https://www.torn.com/page.php?sid=stocks",
        metric: `${stockCount()} holdings`, tone: "info", reasons: ["Weekly stock review is due"],
      }));
    }

    if (enabled("organizedCrime") && user.organizedcrime && Object.keys(user.organizedcrime).length && visible("organizedcrime")) {
      const ocText = cleanText(JSON.stringify(user.organizedcrime));
      list.push(action({
        id: "organizedcrime", taskId: "organizedcrime", title: "Review organized-crime readiness",
        detail: "Organized-crime data was returned for your account. Check role, readiness, missing items, and start time with your faction.",
        category: "Faction", score: 1350 + taskBoost("organizedcrime"), url: "https://www.torn.com/factions.php?step=your#/tab=crimes",
        metric: ocText.length > 80 ? "OC data available" : ocText, tone: "info", reasons: ["Daily organized-crime review is enabled"],
      }));
    }

    const statusState = String(user.status?.state || user.status?.description || "").toLowerCase();
    if (enabled("reviveHelper") && /hospital/.test(statusState) && visible("revive")) {
      list.push(action({
        id: "revive", title: "Request or arrange a revive", detail: "Your API status indicates that you are hospitalized. Open the configured revive destination manually.",
        category: "Status", score: 9900, url: cfg.reviveUrl, metric: "Hospitalized", tone: "danger", reasons: ["Hospital status blocks many actions"],
      }));
    }

    const dueTask = taskList().find((item) => dueTime(item) <= Date.now());
    if (enabled("checklist") && dueTask && visible(`task-${dueTask.id}`)) {
      list.push(action({
        id: `task-${dueTask.id}`,
        taskId: dueTask.id,
        title: `Checklist due: ${dueTask.label}`,
        detail: "This scheduled check is due or overdue.",
        category: "Checklist",
        score: 1700 + taskBoost(dueTask.id),
        url: dueTask.url,
        metric: relative(dueTime(dueTask)),
        tone: "info",
        reasons: ["Scheduled review is due"],
      }));
    }

    if (enabled("dumpSearch") && visible("dump-search")) {
      list.push(action({
        id: "dump-search",
        title: "Search the Dump",
        detail: "A low-priority manual idea for downtime. Open the Dump and inspect visible items yourself; availability is unpredictable and no click is automated.",
        category: "Opportunity",
        score: 225,
        url: TORN_DUMP_URL,
        metric: "Manual opportunity",
        tone: "info",
        reasons: ["Useful only after urgent bars, cooldowns, cash, and progression actions are handled"],
      }));
    }

    if (!list.length) {
      list.push(action({
        id: "wait",
        title: "No urgent configured action",
        detail: "Bars can regenerate safely. Review education, missions, events, or markets manually.",
        category: "General",
        score: 1,
        url: "https://www.torn.com/index.php",
        metric: "Low urgency",
        tone: "info",
        reasons: ["All enabled thresholds are clear"],
      }));
    }

    return list.sort((a, b) => b.score - a.score);
  }

  function bar(label, data, className) {
    const percentage = clamp(data.percentage, 0, 100);
    return `
      <div class="mx-bar">
        <div><span>${html(label)}</span><b>${Math.round(data.current)}/${Math.round(data.maximum)}</b></div>
        <i><em class="${className}" style="width:${percentage}%"></em></i>
        <small>${data.fulltime > 0 ? `caps in ${time(data.fulltime)}` : percentage >= 99 ? "capped" : ""}</small>
      </div>`;
  }


  function ffTargetParams() {
    const goal = String(cfg.ffTargetGoal || "leveling");
    const limit = clamp(int(cfg.ffLimit, 20), 1, 50);
    if (goal === "leveling") return { preset: "level", limit };
    if (goal === "respect") return { preset: "respect", limit };
    return {
      minlevel: clamp(int(cfg.ffMinLevel, 1), 1, 100),
      maxlevel: clamp(int(cfg.ffMaxLevel, 100), 1, 100),
      minff: Math.max(1, num(cfg.ffMinFF, 1)),
      maxff: Math.max(1, num(cfg.ffMaxFF, 3)),
      inactiveonly: cfg.ffInactive ? 1 : 0,
      factionless: cfg.ffFactionless ? 1 : 0,
      limit,
    };
  }

  function normalizeFfTarget(raw) {
    const id = int(raw?.player_id ?? raw?.playerId ?? raw?.id, 0);
    const lastActionValue = raw?.last_action_relative ?? raw?.last_action ?? raw?.lastAction ?? raw?.last_seen ?? "Unknown";
    const lastAction = typeof lastActionValue === "object"
      ? String(lastActionValue.relative || lastActionValue.status || lastActionValue.timestamp || "Unknown")
      : String(lastActionValue || "Unknown");
    return {
      id,
      name: String(raw?.player_name ?? raw?.name ?? raw?.username ?? (id ? `Player ${id}` : "Unknown player")),
      level: int(raw?.level, 0),
      fairFight: num(raw?.fair_fight ?? raw?.fairFight ?? raw?.ff, 0),
      stats: String(raw?.bs_estimate_human ?? raw?.battle_stat_estimate ?? raw?.bs_estimate ?? raw?.stats_estimate ?? "Unknown"),
      lastAction,
      status: String(raw?.status?.state ?? raw?.status ?? raw?.state ?? "Unknown"),
    };
  }

  function targetAttackUrl(target) {
    return `https://www.torn.com/loader.php?sid=attack&user2ID=${target.id}`;
  }

  function targetProfileUrl(target) {
    return `https://www.torn.com/profiles.php?XID=${target.id}`;
  }

  function currentFfTarget() {
    const available = availableFfTargets();
    if (!available.length) return null;
    ffTargetIndex = clamp(ffTargetIndex, 0, available.length - 1);
    return available[ffTargetIndex] || available[0];
  }

  function normalizeFfConsent(value) {
    if (value === true) return { targets: true, sellLosses: false };
    if (!value || typeof value !== "object") return { targets: false, sellLosses: false };
    return { targets: Boolean(value.targets), sellLosses: Boolean(value.sellLosses) };
  }

  function ffKeyFingerprint() {
    const key = String(apiKey || "");
    if (!key) return "No key";
    if (key.length < 8) return "Invalid length";
    return `${key.slice(0, 4)}••••••••••${key.slice(-2)}`;
  }

  function ffConsentDescription(scope) {
    if (scope === "sellLosses") {
      return {
        title: "Enable FFScouter Sell Losses",
        summary: "This manually loads available seller contracts and your recent claim/delivery status.",
        sharing: "Your current Torn API key is sent to FFScouter seller endpoints. FFScouter uses it to identify your registered account and verify attack logs.",
        permission: "FFScouter states Sell Losses requires basic and attacks access; a Torn Limited Access key normally includes these selections.",
      };
    }
    return {
      title: "Enable FFScouter target search",
      summary: "This manually searches FFScouter for target estimates using your selected goal or filters.",
      sharing: "Your current Torn API key and target-search preset or filters are sent to FFScouter.",
      permission: "The exact key must already be registered with FFScouter. Estimates are external estimates, not spy reports.",
    };
  }

  function openFfConsentModal(scope) {
    return new Promise((resolve) => {
      document.getElementById(`${ID}-ff-consent`)?.remove();
      const copy = ffConsentDescription(scope);
      const modal = document.createElement("div");
      modal.id = `${ID}-ff-consent`;
      modal.className = "mx-modal-bg";
      modal.innerHTML = `
        <div class="mx-modal small mx-consent-card">
          <header><div><b>${html(copy.title)}</b><small>Scoped external-service consent</small></div><button data-consent="decline" aria-label="Close">×</button></header>
          <main>
            <section class="mx-consent-hero"><span class="mx-logo">F</span><div><h3>${html(copy.summary)}</h3><p>${html(copy.sharing)}</p></div></section>
            <section>
              <table class="mx-tos">
                <tbody>
                  <tr><th>Service</th><td>FFScouter, an independent third-party Torn community service.</td></tr>
                  <tr><th>Key in use</th><td>${html(ffKeyFingerprint())}</td></tr>
                  <tr><th>Purpose</th><td>${html(copy.summary)}</td></tr>
                  <tr><th>Data sent</th><td>${html(copy.sharing)}</td></tr>
                  <tr><th>Permission note</th><td>${html(copy.permission)}</td></tr>
                  <tr><th>Storage</th><td>Torn Maxxed stores only your consent choice and cached results locally. FFScouter applies its own policy.</td></tr>
                </tbody>
              </table>
              <p class="mx-help">This feature runs only after you press its load/search button. Consent can be revoked in Settings.</p>
              <div class="mx-buttons"><a href="https://ffscouter.com/privacy/" target="_blank" rel="noopener noreferrer">Read FFScouter privacy</a><a href="https://ffscouter.com/api-docs" target="_blank" rel="noopener noreferrer">API documentation</a></div>
            </section>
          </main>
          <footer><button data-consent="decline">Not now</button><button data-consent="accept" class="primary">Allow this feature</button></footer>
        </div>`;
      const finish = (accepted) => { modal.remove(); resolve(accepted); };
      modal.addEventListener("click", (event) => {
        const action = event.target.closest("[data-consent]")?.dataset.consent;
        if (action === "accept") finish(true);
        if (action === "decline" || event.target === modal) finish(false);
      });
      document.body.appendChild(modal);
    });
  }

  async function ensureFfConsent(scope = "targets") {
    ffConsent = normalizeFfConsent(ffConsent);
    if (ffConsent[scope]) return true;
    const accepted = await openFfConsentModal(scope);
    if (!accepted) {
      if (scope === "sellLosses") ffLossStatus = "Sell Losses consent not granted";
      else ffTargetStatus = "Target-search consent not granted";
      render();
      return false;
    }
    ffConsent[scope] = true;
    await save("ffConsent", ffConsent);
    return true;
  }

  async function checkFfKey(scope = "targets", renderAfter = true) {
    if (!apiKey) throw new Error("A Torn API key is required.");
    if (!(await ensureFfConsent(scope))) return null;
    ffKeyStatus = "Checking registered key…";
    if (renderAfter) render();
    try {
      const info = await request(`${FF_API}/check-key?${new URLSearchParams({ key: apiKey })}`);
      ffKeyInfo = info;
      ffKeyStatus = info?.is_registered
        ? `Registered · policy v${info.policy_version || "?"}${info.policy_update_required ? " · policy update required" : ""}`
        : "This exact key is not registered";
      return info;
    } catch (error) {
      ffKeyInfo = null;
      ffKeyStatus = `Key check failed: ${error.message}`;
      throw error;
    } finally {
      if (renderAfter) render();
    }
  }

  async function fetchFfTargets(force = false) {
    if (!enabled("targetFinder")) return;
    if (!apiKey) {
      ffTargetError = new Error("A Torn API key is required for FFScouter target search.");
      ffTargetStatus = "Target search unavailable";
      render();
      return;
    }
    if (!(await ensureFfConsent("targets"))) return;
    if (!force && ffTargets.length && Date.now() - ffTargetFetchedAt < 6 * 60 * 60 * 1000) return;
    ffTargetStatus = "Checking FFScouter key…";
    ffTargetError = null;
    render();
    try {
      const info = await checkFfKey("targets", false);
      if (!info?.is_registered) {
        const error = new Error(`FFScouter does not recognize ${ffKeyFingerprint()} as a registered key.`);
        error.help = "The key saved in Torn Maxxed must be the same key shown in your FFScouter API Keys page.";
        throw error;
      }
      ffTargetStatus = "Searching FFScouter…";
      render();
      const query = new URLSearchParams({ key: apiKey });
      Object.entries(ffTargetParams()).forEach(([name, value]) => query.set(name, String(value)));
      const response = await request(`${FF_API}/get-targets?${query}`);
      const rawTargets = Array.isArray(response?.targets) ? response.targets : [];
      ffTargets = rawTargets.map(normalizeFfTarget).filter((target) => target.id > 0);
      ffTargets.sort(targetSort);
      ffTargetFetchedAt = Date.now();
      ffTargetIndex = 0;
      ffTargetStatus = ffTargets.length ? `${ffTargets.length} targets loaded` : "No matching targets";
      await save("ffTargets", { targets: ffTargets, fetchedAt: ffTargetFetchedAt });
    } catch (error) {
      ffTargetError = error;
      ffTargetStatus = "Target search failed";
    }
    render();
  }

  async function fetchFfLosses(force = false) {
    if (!enabled("ffLosses")) return;
    if (!apiKey) {
      ffLossError = new Error("A Torn API key is required for FFScouter Sell Losses.");
      ffLossStatus = "Sell Losses unavailable";
      render();
      return;
    }
    if (!(await ensureFfConsent("sellLosses"))) return;
    if (!force && ffLossFetchedAt && Date.now() - ffLossFetchedAt < 60 * 1000) return;
    ffLossStatus = "Checking FFScouter key…";
    ffLossError = null;
    render();
    try {
      const info = await checkFfKey("sellLosses", false);
      if (!info?.is_registered) {
        const error = new Error(`FFScouter does not recognize ${ffKeyFingerprint()} as a registered key.`);
        error.help = "Confirm that Torn Maxxed contains the same exact key registered in FFScouter. Sell Losses also needs basic and attacks permission.";
        throw error;
      }
      ffLossStatus = "Loading contracts and claims…";
      render();
      const query = new URLSearchParams({ key: apiKey });
      const [contractsResponse, claimsResponse] = await Promise.all([
        request(`${FF_API}/losses/seller/contracts?${query}`),
        request(`${FF_API}/losses/seller/claims?${query}`),
      ]);
      ffLossContracts = Array.isArray(contractsResponse?.contracts) ? contractsResponse.contracts : [];
      ffLossClaims = Array.isArray(claimsResponse?.claims) ? claimsResponse.claims : [];
      ffLossFetchedAt = Date.now();
      const openSlots = ffLossContracts.reduce((sum, item) => sum + int(item?.slots_available), 0);
      const active = ffLossClaims.filter((item) => ["active", "settling"].includes(String(item?.state))).length;
      ffLossStatus = `${ffLossContracts.length} contract${ffLossContracts.length === 1 ? "" : "s"} · ${openSlots} open slot${openSlots === 1 ? "" : "s"} · ${active} active/settling`;
      await save("ffLossCache", {
        contracts: ffLossContracts,
        claims: ffLossClaims,
        fetchedAt: ffLossFetchedAt,
      });
    } catch (error) {
      ffLossError = error;
      ffLossStatus = "Sell Losses load failed";
    }
    render();
  }

  function lossClaimValue(claim) {
    return int(claim?.slots_delivered) * num(claim?.price_per_loss);
  }

  function normalizeYataStocks(raw) {
    const countries = raw?.stocks && typeof raw.stocks === "object" ? raw.stocks : {};
    const rows = [];
    Object.entries(countries).forEach(([country, data]) => {
      const updated = num(data?.update ?? data?.timestamp);
      const stocks = Array.isArray(data?.stocks) ? data.stocks : [];
      stocks.forEach((item) => rows.push({
        country: String(data?.name || country), name: String(item?.name || `Item ${item?.id || ""}`),
        id: int(item?.id), quantity: int(item?.quantity), cost: num(item?.cost), updated,
      }));
    });
    return rows.sort((a, b) => b.quantity - a.quantity || a.cost - b.cost);
  }

  async function fetchYataStocks(force = false) {
    if (!enabled("yata")) return;
    if (!force && yataStocks.length && Date.now() - yataFetchedAt < 15 * 60000) return;
    yataStatus = "Loading YATA travel stock…"; render();
    try {
      const raw = await request(YATA_TRAVEL);
      yataStocks = normalizeYataStocks(raw);
      yataFetchedAt = Date.now();
      yataStatus = yataStocks.length ? `${yataStocks.length} stock rows loaded` : "No YATA stock data returned";
      await save("yataCache", { stocks: yataStocks, fetchedAt: yataFetchedAt });
    } catch (error) { yataStatus = `YATA error: ${error.message}`; }
    render();
  }

  function targetNotesModal(target) {
    document.getElementById(`${ID}-target-note`)?.remove();
    const pref = targetPref(target.id);
    const modal = document.createElement("div");
    modal.id = `${ID}-target-note`;
    modal.className = "mx-modal-bg";
    modal.innerHTML = `
      <div class="mx-modal small">
        <header><div><b>Target notes</b><small>${html(target.name)} [${target.id}]</small></div><button data-close>×</button></header>
        <main><section>
          ${field("Favorite", "Favorite targets sort to the top.", `<input type="checkbox" name="favorite" ${pref.favorite ? "checked" : ""}>`)}
          ${field("Blacklist", "Blacklisted targets are removed from current recommendations.", `<input type="checkbox" name="blacklisted" ${pref.blacklisted ? "checked" : ""}>`)}
          ${field("Last result", "Manual outcome used only for your local target history.", `<select name="lastResult"><option value="">No change</option><option value="win">Win</option><option value="loss">Loss</option><option value="too-strong">Too strong</option><option value="retaliated">Retaliated</option><option value="good-target">Good target</option></select>`)}
          ${field("Note", "Examples: good leveling target, retaliated, too strong.", `<input type="text" name="note" maxlength="180" value="${html(pref.note || "")}">`)}
        </section></main>
        <footer><button data-close>Cancel</button><button data-save class="primary">Save</button></footer>
      </div>`;
    modal.addEventListener("click", async (event) => {
      if (event.target.closest("[data-close]") || event.target === modal) return modal.remove();
      if (event.target.closest("[data-save]")) {
        const lastResult = modal.querySelector('[name="lastResult"]').value;
        const resultLog = Array.isArray(pref.resultLog) ? [...pref.resultLog] : [];
        if (lastResult) resultLog.push({ result: lastResult, at: Date.now() });
        await updateTargetPref(target.id, {
          favorite: modal.querySelector('[name="favorite"]').checked,
          blacklisted: modal.querySelector('[name="blacklisted"]').checked,
          note: modal.querySelector('[name="note"]').value.trim(),
          resultLog: resultLog.slice(-30),
          lastResult: lastResult || pref.lastResult || "",
        });
        modal.remove();
      }
    });
    document.body.appendChild(modal);
  }

  function scanVisibleInventory() {
    if (!isActivelyViewed() || currentPageKind() !== "inventory") return [];
    const candidates = [...document.querySelectorAll('[class*=item],[class*=inventory] li,[data-item]')]
      .filter(isVisibleElement)
      .map((node) => {
        const text = cleanText(node.innerText || node.textContent);
        const name = cleanText(node.querySelector('[class*=name],[class*=title],strong,b')?.textContent) || text.slice(0, 80);
        const quantityMatch = text.match(/(?:x|qty\.?|quantity)\s*[:x]?\s*([\d,]+)/i) || text.match(/\b([\d,]+)\s*x\b/i);
        return { name, quantity: quantityMatch ? int(quantityMatch[1].replaceAll(",", ""), 1) : 1 };
      })
      .filter((item) => item.name && item.name.length > 1);
    return [...new Map(candidates.map((item) => [item.name, item])).values()].sort((a, b) => a.name.localeCompare(b.name));
  }

  function scanVisibleMarket() {
    if (!isActivelyViewed() || currentPageKind() !== "market") return null;
    const prices = [...document.querySelectorAll('body *')]
      .filter(isVisibleElement)
      .map((node) => cleanText(node.textContent))
      .filter((text) => /^\$[\d,]+(?:\.\d{1,2})?$/.test(text))
      .map((text) => num(text.replace(/[$,]/g, "")))
      .filter((value) => value > 0 && value < 1e15);
    if (!prices.length) return null;
    const sorted = [...prices].sort((a, b) => a - b);
    const median = sorted[Math.floor(sorted.length / 2)];
    return { count: sorted.length, low: sorted[0], high: sorted.at(-1), median };
  }

  function fillFocusedMarketPrice() {
    if (!isActivelyViewed() || currentPageKind() !== "market") return { ok: false, message: "Open a market or bazaar listing first." };
    const focused = document.activeElement;
    const inputs = [...document.querySelectorAll('input[type="number"],input[class*=price]')].filter(isVisibleElement);
    const input = focused?.matches?.('input[type="number"],input[class*=price]') ? focused : inputs.length === 1 ? inputs[0] : null;
    if (!input) return { ok: false, message: "Tap the price field, then run Set focused price." };
    const requested = window.prompt("Enter the listing price. Review it before submitting.", input.value || "");
    if (requested === null || !Number.isFinite(Number(String(requested).replace(/[$,]/g, "")))) return { ok: false, message: "Price was not changed." };
    input.value = String(Number(String(requested).replace(/[$,]/g, "")));
    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    return { ok: true, message: `Price field set to ${cash(input.value)}. Review before submitting.` };
  }

  function highlightCityFinds() {
    if (!isActivelyViewed() || currentPageKind() !== "city") return 0;
    const candidates = [...document.querySelectorAll('img,button,a,[role=button]')].filter((node) => {
      if (!isVisibleElement(node) || node.closest(`#${ID}`)) return false;
      const text = `${node.alt || ""} ${node.title || ""} ${node.getAttribute("aria-label") || ""} ${node.className || ""}`;
      return /city.?find|found.?item|item.?find|collect/i.test(text);
    });
    candidates.forEach((node) => node.classList.add("mx-city-find"));
    return candidates.length;
  }

  async function copyText(value, promptTitle = "Copy this value") {
    try {
      if (typeof GM_setClipboard === "function") await GM_setClipboard(value);
      else if (typeof GM !== "undefined" && typeof GM.setClipboard === "function") await GM.setClipboard(value);
      else await navigator.clipboard.writeText(value);
      return true;
    } catch {
      window.prompt(promptTitle, value);
      return false;
    }
  }

  async function copyLocalExport() {
    const payload = JSON.stringify({
      version: VERSION,
      exportedAt: new Date().toISOString(),
      config: cfg,
      tasks,
      prefs,
      crimeOutcomes,
      targetPrefs,
      actionHistory,
    }, null, 2);
    return copyText(payload, "Copy this Torn Maxxed backup");
  }

  async function importLocalBackup() {
    const raw = window.prompt("Paste a Torn Maxxed JSON backup. Existing local settings will be replaced where provided.", "");
    if (!raw) return { ok: false, message: "Import cancelled" };
    try {
      const data = JSON.parse(raw);
      if (!data || typeof data !== "object") throw new Error("Backup is not an object");
      if (data.config) cfg = merge(data.config);
      if (data.tasks && typeof data.tasks === "object") tasks = data.tasks;
      if (data.prefs && typeof data.prefs === "object") prefs = data.prefs;
      if (Array.isArray(data.crimeOutcomes)) crimeOutcomes = data.crimeOutcomes.slice(-300);
      if (data.targetPrefs && typeof data.targetPrefs === "object") targetPrefs = data.targetPrefs;
      if (Array.isArray(data.actionHistory)) actionHistory = data.actionHistory.slice(-300);
      selectionHealth = { signature: "", working: [], failed: [] };
      await Promise.all([
        save("config", cfg), save("tasks", tasks), save("prefs", prefs), save("crimeOutcomes", crimeOutcomes),
        save("targetPrefs", targetPrefs), save("actionHistory", actionHistory), save("selectionHealth", selectionHealth),
      ]);
      panel?.classList.toggle("compact", cfg.compactMode);
      startPageCoach(); render();
      return { ok: true, message: "Backup imported" };
    } catch (error) { return { ok: false, message: `Import failed: ${error.message}` }; }
  }

  async function clearAllLocalData() {
    if (!window.confirm("Clear the Torn Maxxed API key, settings, checklists, target notes, and learning history on this device?")) return false;
    apiKey = ""; cfg = copy(DEFAULTS); tasks = {}; prefs = {}; crimeOutcomes = []; targetPrefs = {}; actionHistory = [];
    ffTargets = []; ffConsent = { targets: false, sellLosses: false }; ffKeyInfo = null; ffLossContracts = []; ffLossClaims = []; yataStocks = []; apiHistory = []; selectionHealth = { signature: "", working: [], failed: [] };
    const entries = {
      apiKey: "", config: cfg, tasks: {}, prefs: {}, crimeOutcomes: [], targetPrefs: {}, actionHistory: [], ffTargets: null,
      yataCache: null, ffConsent: { targets: false, sellLosses: false }, ffLossCache: null, apiHistory: [], selectionHealth, lastSuccessAt: 0, tab: "today", position: null, collapsed: false,
    };
    await Promise.all(Object.entries(entries).map(([name, value]) => save(name, value)));
    tab = "today"; latestError = null; partial = null; user = null; render();
    return true;
  }

  function tabs() {
    const rows = [
      ["today", "⌂", "Today"], ["crimes", "♦", "Crimes"], ["train", "▲", "Train"],
      ["targets", "◎", "Targets"], ["money", "$", "Money"], ["tools", "⋯", "Tools"],
    ];
    return `<nav class="mx-tabs" aria-label="Torn Maxxed sections">${rows.map(([id, icon, label]) => `<button data-tab="${id}" class="${tab === id ? "active" : ""}" aria-label="${label}"><span>${icon}</span><small>${label}</small></button>`).join("")}</nav>`;
  }

  function apiBanner() {
    const notices = [];
    if (cfg.safeMode) notices.push(`<div class="mx-warning"><b>Safe mode active</b><span>Page parsing and page highlighting are disabled. API-only advice remains active.</span></div>`);
    if (latestError && user) notices.push(`<div class="mx-warning"><b>Latest refresh failed</b><span>${html(latestError.message)}</span><span>Showing the last successful data.</span><button data-action="diagnostic">Details</button></div>`);
    if (partial?.failed?.length) {
      const failed = partial.failed.map((item) => `${item.selection} (${item.code})`).join(", ");
      notices.push(`<div class="mx-warning"><b>Partial API mode</b><span>Working: ${html(partial.working.join(", "))}</span><span>Unavailable: ${html(failed)}</span><button data-action="diagnostic">Details</button></div>`);
    }
    return notices.join("");
  }

  function buttons(item, primary = false) {
    return `<div class="mx-buttons">
      <a href="${html(item.url)}" class="${primary ? "primary" : ""}">${primary ? "Open now" : "Open"}</a>
      ${item.taskId ? `<button data-action="done" data-id="${html(item.id)}" data-task="${html(item.taskId)}">Done</button>` : ""}
      ${item.id === "crime" && enabled("crimeLearning") ? `<button data-action="log-crime" data-crime="${html(pageCoachState.crime?.name || cfg.preferredCrime || cfg.crimeName)}">Log result</button>` : ""}
      <button data-action="later" data-id="${html(item.id)}">Later</button>
      <button data-action="hide" data-id="${html(item.id)}">Not for me</button>
      ${primary ? `<button data-action="next-option" data-id="${html(item.id)}">Next option</button>` : ""}
    </div>`;
  }

  function summaryCard() {
    if (!user) return "";
    return `<section class="mx-summary">
      <div class="mx-player"><b>${html(user.name)}</b><span>Level ${user.level || "?"}</span></div>
      ${enabled("bars") ? `${bar("Energy", user.energy, "energy")}${bar("Nerve", user.nerve, "nerve")}` : ""}
      ${enabled("money") ? `<div class="mx-pairs"><span>Wallet <b>${cash(user.wallet)}</b></span><span>${user.hasVault ? `Vault <b>${cash(user.vault)}</b>` : `<b>No property vault</b>`}</span></div>` : ""}
      ${enabled("cooldowns") ? `<div class="mx-pairs"><span>Drug <b>${time(user.drug)}</b></span><span>Booster <b>${time(user.booster)}</b></span></div>` : ""}
    </section>`;
  }

  function actionCard(item, primary = false) {
    return `<section class="mx-next ${html(item.tone)}">
      <small>${primary ? "DO THIS NOW" : "NEXT OPTION"} · ${html(item.category)}</small>
      <h3>${html(item.title)}</h3><p>${html(item.detail)}</p>
      <div class="mx-reasons">${item.reasons.map((reason) => `<span>• ${html(reason)}</span>`).join("")}</div>
      <strong>${html(item.metric)}</strong>${buttons(item, primary)}
    </section>`;
  }

  function renderToday() {
    if (!user) return `<div class="mx-empty">Reading Torn data…</div>`;
    const ready = recommendations();
    const first = ready[0];
    const next = ready.slice(1, clamp(cfg.shown, 2, 6));
    const waiting = waitingEvents();
    const blocked = blockedActions();
    return `${apiBanner()}${summaryCard()}
      ${enabled("targetFinder") ? `<section class="mx-quick-target"><button data-action="quick-target">◎ Find next target</button><small>Opens the Targets screen and loads FFScouter results when needed.</small></section>` : ""}
      ${actionCard(first, true)}
      ${next.length ? `<section class="mx-section"><h4>DO THIS NEXT</h4>${next.map((item) => `<article class="mx-plan-row"><div><b>${html(item.title)}</b><small>${html(item.metric)}</small></div>${buttons(item)}</article>`).join("")}</section>` : ""}
      <section class="mx-section"><h4>WAITING ON</h4>${waiting.length ? waiting.map((item) => `<a class="mx-plan-row" href="${html(item.url)}"><div><b>${html(item.label)}</b><small>${time(item.seconds)}</small></div><strong>›</strong></a>`).join("") : `<p class="mx-muted">No configured timers are currently pending.</p>`}</section>
      <section class="mx-section"><h4>BLOCKED BECAUSE</h4>${blocked.length ? blocked.map((item) => `<article class="mx-plan-row"><div><b>${html(item.label)}</b><small>${html(item.reason)}</small></div></article>`).join("") : `<p class="mx-muted">No configured actions are blocked.</p>`}</section>
      <p class="mx-note">Read-only advisor. Every Torn action remains manual.</p>`;
  }

  function crimeChoiceName() {
    if (pageCoachState.crime?.name) return pageCoachState.crime.name;
    if (cfg.preferredCrime && cfg.preferredCrime !== "Auto") return cfg.preferredCrime;
    return cfg.crimeName;
  }

  function renderCrimes() {
    if (!enabled("crimes")) return `<div class="mx-empty"><b>Crime advisor disabled</b><p>Enable it in Settings.</p></div>`;
    const choice = crimeChoiceName();
    const fallback = cfg.crimeFallback || "Pickpocketing";
    const stats = crimeOutcomeStats(choice);
    const attempts = user && cfg.crimeNerve > 0 ? Math.floor(user.nerve.current / cfg.crimeNerve) : 0;
    return `${apiBanner()}<section class="mx-next ${user?.nerve?.percentage >= 90 ? "danger" : "info"}">
      <small>DO THIS CRIME</small><h3>${html(choice.toUpperCase())}</h3>
      <p>${pageCoachState.crime?.reason ? html(pageCoachState.crime.reason) : "Open the Crimes page while Torn is foregrounded so the coach can compare visible unlocked crime cards."}</p>
      <div class="mx-reasons"><span>• ${attempts} configured attempt${attempts === 1 ? "" : "s"} available</span><span>• Fallback: ${html(fallback)}</span><span>• Strategy: ${html(String(cfg.crimeGoal).replaceAll("-", " "))}</span></div>
      <strong>${cash(cfg.crimeProfit / Math.max(1, cfg.crimeNerve))}/nerve estimate</strong>
      <div class="mx-buttons"><a href="https://www.torn.com/loader.php?sid=crimes">Open Crimes</a>${enabled("crimeLearning") ? `<button data-action="log-crime" data-crime="${html(choice)}">Log result</button>` : ""}</div>
    </section>
    <section class="mx-section"><h4>PERSONAL RESULTS</h4>${stats.samples ? `<p>${stats.samples} samples · ${(stats.successRate * 100).toFixed(0)}% success · ${cash(stats.averageProfit)} average</p><p>${stats.failures} failure${stats.failures === 1 ? "" : "s"}</p>` : `<p class="mx-muted">No results logged for ${html(choice)} yet. After several entries, this becomes a personal safety/profit signal.</p>`}</section>
    <section class="mx-section"><h4>VISIBLE PAGE COACH</h4><p>${isActivelyViewed() && currentPageKind() === "crimes" ? `Active now · last scan ${activePageScanAt ? relative(activePageScanAt) : "pending"}` : "Open the Crimes page and keep Torn foregrounded. Background page scanning is intentionally disabled."}</p>
      ${pageContext.pickpocket ? `<p><b>Pickpocket target:</b> ${html(pageContext.pickpocket.label)} · signal ${html(pageContext.pickpocket.reason)}</p>` : ""}
      ${pageContext.skimming ? `<p><b>Card skimming:</b> ${pageContext.skimming.deployed ?? "?"} deployed · ${pageContext.skimming.cards ?? "?"} details${pageContext.skimming.rate != null ? ` · ${pageContext.skimming.rate}/hour` : ""}</p>` : ""}
      ${pageContext.cracking ? `<p><b>Cracking pattern detected:</b> ${html(pageContext.cracking.pattern)}</p>` : ""}</section>`;
  }

  function renderTrain() {
    if (!enabled("energy")) return `<div class="mx-empty"><b>Training advisor disabled</b><p>Enable it in Settings.</p></div>`;
    const coach = pageCoachState.gym;
    const planned = plannedGymStat();
    const desired = planned === "best-dots" ? "the best visible gain" : planned;
    const bs = user?.battleStats || {};
    return `${apiBanner()}<section class="mx-next info"><small>TRAIN THIS STAT</small>
      <h3>${html((coach?.stat || desired).toUpperCase())}</h3>
      <p>${coach ? `Use ${coach.gymName}. It produced the strongest visible ${coach.stat} gain indicator among ${coach.comparedGyms} gym${coach.comparedGyms === 1 ? "" : "s"} inspected.` : "Open the Gym page while Torn is foregrounded. The optimizer will compare visible unlocked gyms and mark the strongest configured stat."}</p>
      <div class="mx-reasons"><span>• Energy: ${Math.round(user?.energy?.current || 0)}/${Math.round(user?.energy?.maximum || 0)}</span><span>• Happy: ${Math.round(user?.happy?.current || 0)}/${Math.round(user?.happy?.maximum || 0)}</span><span>• Plan: ${html(cfg.gymPlan)}</span></div>
      ${Object.values(bs).some((value) => num(value) > 0) ? `<div class="mx-stat-grid"><span>STR ${Math.round(bs.strength).toLocaleString()}</span><span>SPD ${Math.round(bs.speed).toLocaleString()}</span><span>DEF ${Math.round(bs.defense).toLocaleString()}</span><span>DEX ${Math.round(bs.dexterity).toLocaleString()}</span></div>` : ""}
      <strong>${coach?.dots ? `${coach.dots} visible gain bars` : "Waiting for active Gym page"}</strong>
      <div class="mx-buttons"><a href="https://www.torn.com/gym.php">Open Gym</a></div>
    </section>
    <section class="mx-section"><h4>TRAINING RULE</h4><p>The coach compares visible gain indicators and your selected stat plan. It does not press Train or switch gyms automatically.</p></section>`;
  }

  async function openNextTarget() {
    if (!enabled("targetFinder")) {
      tab = "targets";
      await save("tab", tab);
      render();
      return;
    }

    const wasAlreadyViewingTargets = tab === "targets";
    tab = "targets";
    await save("tab", tab);

    let available = availableFfTargets();
    if (!available.length) {
      await fetchFfTargets(true);
      available = availableFfTargets();
    } else if (wasAlreadyViewingTargets) {
      ffTargetIndex = (ffTargetIndex + 1) % available.length;
    }

    render();
  }

  function renderTargets() {
    if (!enabled("targetFinder") && !enabled("ffLosses")) return `<div class="mx-empty"><b>FFScouter tools disabled</b><p>Enable target finding or Sell Losses in Settings.</p></div>`;
    const available = availableFfTargets();
    const current = currentFfTarget();
    const keyLine = `${ffKeyFingerprint()} · ${ffKeyStatus}`;
    const targetSection = enabled("targetFinder") ? `
      <section class="mx-target-summary">
        <div class="mx-section-title"><div><small>FFSCOUTER</small><b>Target Finder</b></div><span class="mx-pill">${html(String(cfg.ffTargetGoal || "leveling"))}</span></div>
        <p>${html(ffTargetStatus)}</p><p class="mx-muted">${html(keyLine)}</p>
        <div class="mx-buttons"><button data-action="quick-target" class="primary">Find next target</button><button data-action="refresh-targets">Refresh</button><button data-action="check-ff-key" data-scope="targets">Check key</button><a href="https://ffscouter.com/" target="_blank" rel="noopener">FFScouter</a></div>
        ${ffTargetError ? `<div class="mx-warning"><b>${html(ffTargetError.message)}</b><span>${html(ffTargetError.help || "")}</span></div>` : ""}
      </section>
      ${current ? `<section class="mx-next info mx-current-target"><small>CURRENT TARGET · ${html(String(cfg.ffTargetGoal).toUpperCase())}</small><h3>${targetPref(current.id).favorite ? "★ " : ""}${html(current.name)} [${current.id}]</h3><p>Level ${current.level || "?"} · Fair Fight ${current.fairFight ? current.fairFight.toFixed(2) : "?"} · Stats ${html(current.stats)}</p><div class="mx-reasons"><span>Last action: ${html(current.lastAction)}</span><span>Status: ${html(current.status)}</span>${targetPref(current.id).note ? `<span>Note: ${html(targetPref(current.id).note)}</span>` : ""}${targetPref(current.id).lastResult ? `<span>Last result: ${html(targetPref(current.id).lastResult)}</span>` : ""}</div><div class="mx-buttons"><a class="primary" href="${targetAttackUrl(current)}">Attack page</a><a href="${targetProfileUrl(current)}">Profile</a><button data-action="target-note" data-target-id="${current.id}">Notes</button><button data-action="next-target">Skip</button></div></section>` : ""}
      ${available.length ? `<details class="mx-disclosure"><summary>Target list <span>${available.length}</span></summary><section class="mx-target-list">${available.slice(0, 30).map((target, index) => `<article class="${index === ffTargetIndex ? "active" : ""}"><div><b>${targetPref(target.id).favorite ? "★ " : ""}${html(target.name)} [${target.id}]</b><small>Level ${target.level || "?"} · FF ${target.fairFight ? target.fairFight.toFixed(2) : "?"} · ${html(target.stats)}</small>${targetPref(target.id).note ? `<small>${html(targetPref(target.id).note)}</small>` : ""}</div><div class="mx-buttons"><a href="${targetAttackUrl(target)}">Attack</a><button data-action="target-note" data-target-id="${target.id}">⋮</button></div></article>`).join("")}</section></details>` : ""}
      ${Object.values(targetPrefs).some((item) => item.blacklisted) ? `<button data-action="restore-targets">Restore blacklisted targets</button>` : ""}` : "";

    const activeClaims = ffLossClaims.filter((claim) => ["active", "settling"].includes(String(claim?.state)));
    const historyClaims = ffLossClaims.filter((claim) => !["active", "settling"].includes(String(claim?.state))).slice(0, 8);
    const lossesSection = enabled("ffLosses") ? `
      <section class="mx-target-summary mx-losses">
        <div class="mx-section-title"><div><small>FFSCOUTER</small><b>Sell Losses</b></div><span class="mx-pill">Manual</span></div>
        <p>${html(ffLossStatus)}</p><p class="mx-muted">Requires a registered key with basic and attacks access. Use a pillow only and review FFScouter's current rules before claiming.</p>
        <div class="mx-buttons"><button data-action="refresh-losses" class="primary">Load seller data</button><button data-action="check-ff-key" data-scope="sellLosses">Check key</button><a href="${FF_SELL_LOSSES_URL}" target="_blank" rel="noopener noreferrer">Open Sell Losses</a><a href="${FF_SELL_LOSSES_GUIDE}" target="_blank" rel="noopener noreferrer">Guide</a></div>
        ${ffLossError ? `<div class="mx-warning"><b>${html(ffLossError.message)}</b><span>${html(ffLossError.help || "")}</span></div>` : ""}
      </section>
      ${activeClaims.length ? `<section class="mx-section"><h4>ACTIVE / SETTLING CLAIM</h4>${activeClaims.map((claim) => `<article class="mx-loss-card"><div><b>${html(String(claim.state || "active").toUpperCase())} · ${html(claim.order_number || "Order")}</b><small>${int(claim.slots_delivered)}/${int(claim.slots_claimed)} delivered · ${cash(claim.price_per_loss)} each</small><small>${claim.expires_at ? `Expires ${relative(int(claim.expires_at) * 1000)}` : ""}</small></div><div class="mx-buttons">${int(claim.target_player_id) ? `<a class="primary" href="https://www.torn.com/profiles.php?XID=${int(claim.target_player_id)}">Target</a>` : ""}<a href="${FF_SELL_LOSSES_URL}" target="_blank" rel="noopener">Manage</a></div></article>`).join("")}</section>` : ""}
      ${ffLossContracts.length ? `<details class="mx-disclosure" open><summary>Available contracts <span>${ffLossContracts.length}</span></summary><section class="mx-loss-list">${ffLossContracts.slice(0, 12).map((contract) => `<article><div><b>${cash(contract.price_per_loss)} per loss</b><small>${int(contract.slots_available)} slot${int(contract.slots_available) === 1 ? "" : "s"} available · order ${html(contract.order_number || "")}</small></div><a href="${FF_SELL_LOSSES_URL}" target="_blank" rel="noopener">Review</a></article>`).join("")}</section></details>` : ""}
      ${historyClaims.length ? `<details class="mx-disclosure"><summary>Recent claim history <span>${historyClaims.length}</span></summary><section class="mx-loss-list">${historyClaims.map((claim) => `<article><div><b>${html(String(claim.state || "unknown"))} · ${int(claim.slots_delivered)}/${int(claim.slots_claimed)}</b><small>${cash(lossClaimValue(claim))} delivered value · payment ${html(claim.payment_status || "not recorded")}</small></div></article>`).join("")}</section></details>` : ""}
      <section class="mx-section mx-referral-note"><h4>PRIVATE REFERRAL SHARING</h4><p>FFScouter says referral links may be shared only through private chat or mail. Torn Maxxed never substitutes the author's referral. This button copies <b>your own</b> link using player ID ${user?.playerId || "unknown"}.</p><div class="mx-buttons"><button data-action="copy-own-referral" ${user?.playerId ? "" : "disabled"}>Copy my referral link</button></div></section>` : "";

    return `${apiBanner()}${targetSection}${lossesSection}`;
  }

  function renderMoney() {
    if (!user) return `<div class="mx-empty">Reading Torn data…</div>`;
    const spendable = Math.max(0, user.wallet - cfg.cashReserve);
    const hourly = cfg.travelMinutes > 0 ? cfg.travelProfit * 60 / cfg.travelMinutes : 0;
    const destination = user.hasVault ? { label: "Property vault", url: "https://www.torn.com/properties.php#/p=options&tab=vault" } : cfg.factionVaultUrl ? { label: "Configured faction vault", url: cfg.factionVaultUrl } : { label: "City Bank / supplies review", url: "https://www.torn.com/city.php#/p=bank" };
    return `${apiBanner()}${summaryCard()}<section class="mx-next ${user.wallet >= cfg.walletWarning ? "warn" : "info"}"><small>CASH PLAN</small><h3>${user.wallet >= cfg.walletWarning ? "Review exposed cash" : "Wallet is within your limit"}</h3><p>${user.wallet >= cfg.walletWarning ? `${cash(user.wallet)} is exposed. Best configured destination: ${destination.label}.` : `Your wallet is below the ${cash(cfg.walletWarning)} warning threshold.`}</p><div class="mx-reasons"><span>• Reserve: ${cash(cfg.cashReserve)}</span><span>• Spendable above reserve: ${cash(spendable)}</span><span>• City Bank: ${cash(user.cityBank)}</span></div><div class="mx-buttons"><a href="${html(destination.url)}">Open ${html(destination.label)}</a></div></section>
      ${enabled("travel") ? `<section class="mx-section"><h4>TRAVEL PLAN</h4><p><b>${html(cfg.travelName)}</b> · ${cash(cfg.travelProfit)} per run · approximately ${cash(hourly)}/hour</p><p>Capital required after reserve: ${cash(cfg.travelCapital)} · ${user.wallet >= cfg.cashReserve + cfg.travelCapital ? "Affordable now" : `${cash(cfg.cashReserve + cfg.travelCapital - user.wallet)} short`}</p><a class="mx-link" href="https://www.torn.com/travelagency.php">Open Travel Agency</a></section>` : ""}
      ${enabled("stocks") ? `<section class="mx-section"><h4>STOCKS</h4><p>${stockCount()} holding record${stockCount() === 1 ? "" : "s"} detected.</p><a class="mx-link" href="https://www.torn.com/page.php?sid=stocks">Review stocks</a></section>` : ""}
      ${enabled("yata") ? `<section class="mx-section"><h4>YATA FOREIGN STOCK</h4><p>${html(yataStatus)}</p><p>Public YATA data is fetched only when you tap Refresh YATA; your Torn key is not sent.</p><div class="mx-buttons"><button data-action="refresh-yata">Refresh YATA</button></div>${yataStocks.length ? yataStocks.slice(0, 12).map((item) => `<p><b>${html(item.country)} · ${html(item.name)}</b> — ${item.quantity.toLocaleString()} at ${cash(item.cost)}</p>`).join("") : ""}</section>` : ""}`;
  }

  function renderChecklist() {
    if (!enabled("checklist")) return `<div class="mx-empty">Checklist is disabled in Settings.</div>`;
    return `<section>${taskList().map((item) => `<article class="mx-task ${dueTime(item) <= Date.now() ? "due" : ""}"><div><b>${html(item.label)}</b><small>${relative(dueTime(item))}</small></div><div><a href="${html(item.url)}">Open</a><button data-task-action="done" data-id="${item.id}">Done</button><button data-task-action="snooze" data-id="${item.id}">+30m</button></div></article>`).join("")}</section>`;
  }

  function renderTools() {
    const inventoryRows = scanVisibleInventory();
    const market = scanVisibleMarket();
    const weapon = weaponExperienceOpportunity();
    return `${apiBanner()}<section class="mx-section"><h4>DAILY & WEEKLY CHECKLIST</h4>${renderChecklist()}</section>
      <section class="mx-section"><h4>ACTIVE PAGE TOOLS</h4><p>Page tools run only while Torn is visible and focused.</p><div class="mx-buttons"><button data-action="scan-page">Scan current page</button>${enabled("marketTools") ? `<button data-action="max-buy">Max affordable</button><button data-action="set-price">Set focused price</button>` : ""}${enabled("inventoryTools") ? `<button data-action="sort-inventory">Sort visible inventory</button>` : ""}${enabled("cityFinds") ? `<button data-action="find-city">Highlight City finds</button>` : ""}</div>
      ${inventoryRows.length ? `<p><b>Visible inventory:</b> ${inventoryRows.slice(0, 12).map((item) => `${html(item.name)} ×${item.quantity}`).join(" · ")}</p>` : ""}
      ${market ? `<p><b>Visible prices:</b> ${market.count} found · low ${cash(market.low)} · median ${cash(market.median)} · high ${cash(market.high)}${market.high > market.median * 3 ? " · overpricing warning" : ""}</p>` : ""}
      ${pageContext.eggs ? `<p><b>Egg finder:</b> ${pageContext.eggs} genuine visible candidate${pageContext.eggs === 1 ? "" : "s"} highlighted.</p>` : ""}
      ${pageContext.levelProgress != null ? `<p><b>Visible level progress:</b> ${pageContext.levelProgress}%</p>` : ""}
      ${pageContext.attack ? `<p><b>Attack page:</b> ${html(pageContext.attack.result)}${pageContext.attack.respect != null ? ` · respect ${pageContext.attack.respect}` : ""}${pageContext.attack.fairFight != null ? ` · FF ${pageContext.attack.fairFight}` : ""}</p>` : ""}</section>
      <section class="mx-section"><h4>PROGRESSION</h4><p>Education: ${educationInfo().timeLeft ? `${time(educationInfo().timeLeft)} remaining` : "review/start course"}</p><p>Job points: ${Math.floor(jobPointTotal())} · Missions: ${missionCount()} · Notifications: ${Math.floor(notificationTotal())}</p>${weapon ? `<p>Nearest weapon XP: ${html(weapon.name)} ${weapon.experience.toFixed(1)}%</p>` : ""}</section>
      ${enabled("dumpSearch") ? `<section class="mx-section"><div class="mx-section-title"><div><small>LOW PRIORITY</small><h4>DUMP SEARCH IDEA</h4></div><span class="mx-pill">Manual</span></div><p>Use this only after urgent energy, nerve, cooldown, cash, and progression work is handled. Item availability is unpredictable.</p><div class="mx-buttons"><a href="${TORN_DUMP_URL}">Search the Dump</a></div></section>` : ""}
      <section class="mx-section"><h4>DATA & DIAGNOSTICS</h4><p>Last successful API refresh: ${lastSuccessAt ? new Date(lastSuccessAt).toLocaleTimeString() : "not yet"}</p><p>Transport: ${html(transportUsed)}</p><div class="mx-buttons"><button data-action="diagnostic">API diagnostic</button><button data-action="export-data">Copy backup</button><button data-action="import-data">Import backup</button><button data-action="reset-position">Reset panel position</button><button data-action="restore-actions">Restore hidden actions</button></div></section>`;
  }

  function renderError(error) {
    const selections = Array.isArray(error?.selections) ? error.selections.join(", ") : apiPlan().selections.join(", ");
    return `${tabs()}<div class="mx-error"><b>API refresh failed</b><p><strong>${html(error?.message || "Unknown error")}</strong></p><p>${html(error?.help || "Review the key and enabled selections.")}</p><p>Requested: ${html(selections)}</p><div class="mx-buttons center"><button data-action="diagnostic" class="primary">Run diagnostic</button><button data-action="settings">Settings</button><button data-action="refresh">Retry</button></div></div>`;
  }

  function render() {
    if (!body) return;
    if (!apiKey) {
      body.innerHTML = `${tabs()}<div class="mx-empty"><b>API setup required</b><p>The setup screen shows the required permission and tests the exact enabled selections.</p><button data-action="settings" class="primary">Open API setup</button></div>`;
      status.textContent = "API setup required";
      return;
    }
    if (latestError && !user) { body.innerHTML = renderError(latestError); return; }
    const screens = { today: renderToday, crimes: renderCrimes, train: renderTrain, targets: renderTargets, money: renderMoney, tools: renderTools };
    body.innerHTML = `${tabs()}${(screens[tab] || renderToday)()}`;
  }

  function field(label, description, control) {
    return `
      <label class="mx-field">
        <span><b>${html(label)}</b><small>${html(description)}</small></span>
        ${control}
      </label>`;
  }

  function toggle(name, label, description) {
    return field(
      label,
      description,
      `<input type="checkbox" name="module:${name}" ${enabled(name) ? "checked" : ""}>`
    );
  }

  function openSettings() {
    document.getElementById(`${ID}-settings`)?.remove();
    const modal = document.createElement("div");
    modal.id = `${ID}-settings`;
    modal.className = "mx-modal-bg";
    const plan = apiPlan();
    const crimeOptions = ["Auto", ...CRIME_NAMES].map((name) => `<option value="${html(name)}">${html(name)}</option>`).join("");
    modal.innerHTML = `
      <div class="mx-modal">
        <header><div><b>Torn Maxxed Settings</b><small>Version ${VERSION} · API v2 least-privilege plan</small></div><button data-modal="close">×</button></header>
        <main>
          <section><h3>Getting started</h3>
            <div class="mx-permission" id="mx-plan"><b>Minimum required: ${html(plan.access)} Access</b><p>Selections: ${html(plan.selections.join(", "))}</p><p>The key is stored locally. Automatic account refreshes send it only to api.torn.com. Manual FFScouter target and Sell Losses requests use separate scoped consent before sending the key and requested filters/status request. Manual YATA lookups do not send the key.</p></div>
            <table class="mx-tos" aria-label="Torn API terms of service disclosure">
              <tbody>
                <tr><th>Data storage</th><td>Only locally in isolated userscript storage until you clear script data.</td></tr>
                <tr><th>Data sharing</th><td>Normal account data is not shared with the author or other users. Optional manual FFScouter target or Sell Losses requests share the key and only the requested filters or seller-status request after scoped consent.</td></tr>
                <tr><th>Purpose of use</th><td>Personal quality-of-life recommendations, progress review, page presentation, manually requested target estimates, and Sell Losses seller-status review.</td></tr>
                <tr><th>Key storage &amp; sharing</th><td>Stored locally in userscript storage; sent to api.torn.com for enabled modules. Sent to ffscouter.com only for a manually requested target or Sell Losses request after scoped consent. Never sent to YATA.</td></tr>
                <tr><th>Key access level</th><td>${html(plan.access)} for the currently enabled modules. Exact selections are shown above and update with module switches.</td></tr>
              </tbody>
            </table>
            ${field("Torn API key", "Limited Access supports the default modules. Requests use Torn API v2; compatible selections retain their established response format.", `<div class="mx-key-row"><input type="password" name="apiKey" value="${html(apiKey)}" placeholder="Paste API key" autocomplete="off"><button type="button" data-modal="test">Test</button></div>`)}
            <div id="mx-test-result" class="mx-test">Not tested in this session.</div>
            ${field("Refresh interval", "Read-only API refresh; 30 seconds minimum.", `<input type="number" name="refresh" min="30" max="1800" value="${cfg.refresh}">`)}
            ${field("Safe mode", "Disables all page parsing and highlighting while keeping API-only advice active.", `<input type="checkbox" name="safeMode" ${cfg.safeMode ? "checked" : ""}>`)}
            ${field("Compact mobile layout", "Reduces spacing and panel height for smaller screens.", `<input type="checkbox" name="compactMode" ${cfg.compactMode ? "checked" : ""}>`)}
          </section>

          <section><h3>Daily advisor modules</h3>
            ${toggle("energy", "Energy advisor", "Energy urgency and gym recommendations.")}
            ${toggle("crimes", "Crime advisor", "Nerve usage and explicit crime recommendation.")}
            ${toggle("money", "Money safety", "Wallet, vault, reserve, and cash destination guidance.")}
            ${toggle("travel", "Travel advisor", "Configured route, affordability, timing, and profit-per-hour.")}
            ${toggle("war", "Ranked-war advisor", "Configured pay-per-hit valuation.")}
            ${toggle("xanax", "Xanax reminder", "Cooldown and affordability reminder only.")}
            ${toggle("refill", "Daily refill advisor", "Review energy refill availability.")}
            ${toggle("bank", "City Bank advisor", "Maturity and idle-capital checks.")}
            ${toggle("checklist", "Timed checklist", "Half-hour, hourly, daily, and weekly checks.")}
            ${toggle("bars", "Show bars", "Energy and nerve meters.")}
            ${toggle("cooldowns", "Show cooldowns", "Drug and booster cooldown display.")}
          </section>

          <section><h3>Progress modules</h3>
            ${toggle("education", "Education", "Course completion and idle-course checks.")}
            ${toggle("jobpoints", "Job points", "Job-point balance and daily review.")}
            ${toggle("missions", "Missions", "Available mission and mission-credit review.")}
            ${toggle("notifications", "Notifications", "Unread API notification counts.")}
            ${toggle("merits", "Merits and awards", "Weekly merit and near-award review.")}
            ${toggle("weaponexp", "Weapon experience", "Highlight near-complete weapon experience.")}
            ${toggle("stocks", "Stocks", "Holding and benefit-block review.")}
            ${toggle("inventory", "Inventory API", "Inventory summary; requires broader access.")}
            ${toggle("workstats", "Work stats", "Work-stat context for progression.")}
            ${toggle("battleStats", "Battle stats", "Stat-balance planning and gym target selection.")}
            ${toggle("personalstats", "Personal stats", "Optional expanded progression context.")}
            ${toggle("organizedCrime", "Organized crime", "Faction organized-crime readiness and daily review.")}
            ${toggle("levelProgress", "Visible level progress", "Reads an explicitly displayed level percentage on the active page.")}
            ${toggle("eggFinder", "Genuine egg finder", "Highlights visible egg elements; never creates fake eggs.")}
            ${toggle("attackSummary", "Attack summary", "Summarizes visible attack result, respect, and Fair Fight.")}
            ${toggle("reviveHelper", "Revive helper", "Shows a manual revive destination while hospitalized.")}
          </section>

          <section><h3>Crime planner</h3>
            ${toggle("crimeGuide", "Active Crimes page coach", "Highlights preferred, second choice, and fallback only while the Crimes page is foregrounded.")}
            ${toggle("crimeLearning", "Personal crime results", "Stores manual success/failure and profit entries locally.")}
            ${field("Crime strategy", "Balanced, reliable, safest, mastery, or manual preference.", `<select name="crimeGoal"><option value="balanced">Balanced</option><option value="latest-reliable">Latest reliable</option><option value="safest">Safest / highest skill</option><option value="master-lowest">Master lowest skill</option></select>`)}
            ${field("Preferred crime", "Auto lets the visible-page coach decide. Manual selection always wins.", `<select name="preferredCrime">${crimeOptions}</select>`)}
            ${field("Fallback crime", "Used when the preferred crime is unavailable.", `<select name="crimeFallback">${CRIME_NAMES.map((name) => `<option value="${html(name)}">${html(name)}</option>`).join("")}</select>`)}
            ${field("Minimum reliable Crime Skill", "Penalty threshold for Latest reliable mode; it is not an official Torn requirement.", `<input type="number" name="crimeMinimumSkill" min="1" max="100" value="${cfg.crimeMinimumSkill}">`)}
            ${field("Nerve per attempt", "Used for estimated attempt count and value per nerve.", `<input type="number" name="crimeNerve" min="1" value="${cfg.crimeNerve}">`)}
            ${field("Profit per attempt", "Use your recent average including failures.", `<input type="number" name="crimeProfit" min="0" step="100" value="${cfg.crimeProfit}">`)}
          </section>

          <section><h3>Training planner</h3>
            ${toggle("gymGuide", "Active Gym page optimizer", "Compares visible unlocked gym/stat gain indicators only while foregrounded.")}
            ${field("Stat target", "Best visible gain or a fixed battle stat.", `<select name="gymTarget"><option value="best-dots">Best visible gain</option><option value="strength">Strength</option><option value="speed">Speed</option><option value="defense">Defense</option><option value="dexterity">Dexterity</option></select>`)}
            ${field("Stat plan", "Describes your intended battle-stat structure.", `<select name="gymPlan"><option value="balanced">Balanced</option><option value="lowest-stat">Train lowest stat</option><option value="glass-cannon">Glass cannon</option><option value="defensive">Defensive</option><option value="dexterity">Dexterity focus</option><option value="maintain">Maintain current ratio</option><option value="custom">Custom ratio</option></select>`)}
            ${field("Custom stat percentages", "Strength, Speed, Defense, Dexterity; used as planning context.", `<div class="mx-range four"><input type="number" name="ratioStrength" min="0" max="100" value="${cfg.gymRatios?.strength ?? 25}"><input type="number" name="ratioSpeed" min="0" max="100" value="${cfg.gymRatios?.speed ?? 25}"><input type="number" name="ratioDefense" min="0" max="100" value="${cfg.gymRatios?.defense ?? 25}"><input type="number" name="ratioDexterity" min="0" max="100" value="${cfg.gymRatios?.dexterity ?? 25}"></div>`)}
          </section>

          <section><h3>Targets and attacks</h3>
            ${toggle("targetFinder", "FFScouter target finder", "Manual third-party target search with scoped consent, key registration check, and official presets.")}
            ${toggle("ffLosses", "FFScouter Sell Losses", "Manually loads seller contracts and recent claims after separate scoped consent. No contracts are claimed by the script.")}
            <p class="mx-help">Targets and Sell Losses each ask for separate consent. The key fingerprint shown in Targets helps confirm that Torn Maxxed is using the same exact key registered with FFScouter.</p>
            ${field("Target goal", "Leveling, respect/chain, or custom filters.", `<select name="ffTargetGoal"><option value="leveling">Leveling</option><option value="respect">Respect / chain</option><option value="custom">Custom</option></select>`)}
            ${field("Target level range", "Minimum and maximum target level.", `<div class="mx-range"><input type="number" name="ffMinLevel" min="1" max="100" value="${cfg.ffMinLevel}"><input type="number" name="ffMaxLevel" min="1" max="100" value="${cfg.ffMaxLevel}"></div>`)}
            ${field("Fair Fight range", "Custom mode only.", `<div class="mx-range"><input type="number" name="ffMinFF" min="1" step="0.1" value="${cfg.ffMinFF}"><input type="number" name="ffMaxFF" min="1" step="0.1" value="${cfg.ffMaxFF}"></div>`)}
            ${field("Inactive only", "Reduces retaliation risk but does not guarantee continued inactivity.", `<input type="checkbox" name="ffInactive" ${cfg.ffInactive ? "checked" : ""}>`)}
            ${field("Factionless only", "Optional complication filter.", `<input type="checkbox" name="ffFactionless" ${cfg.ffFactionless ? "checked" : ""}>`)}
            ${field("Target count", "Up to 50 estimates.", `<input type="number" name="ffLimit" min="1" max="50" value="${cfg.ffLimit}">`)}
            ${field("War pay per hit", "Set to zero when no paid war is active.", `<input type="number" name="warPay" min="0" step="1000" value="${cfg.warPay}">`)}
            ${field("Energy per attack", "Usually 25.", `<input type="number" name="attackEnergy" min="1" value="${cfg.attackEnergy}">`)}
          </section>

          <section><h3>Money and travel</h3>
            ${field("Wallet warning", "No-vault warnings are informational and do not dominate ranking.", `<input type="number" name="walletWarning" min="0" step="10000" value="${cfg.walletWarning}">`)}
            ${field("Cash reserve", "Never treat this amount as spendable.", `<input type="number" name="cashReserve" min="0" step="10000" value="${cfg.cashReserve}">`)}
            ${field("Faction vault URL", "Optional Torn URL used when no property vault exists.", `<input type="text" name="factionVaultUrl" value="${html(cfg.factionVaultUrl || "")}" placeholder="https://www.torn.com/factions.php?...">`)}
            ${field("Travel run", "Example: Mexico plushie run.", `<input type="text" name="travelName" value="${html(cfg.travelName)}">`)}
            ${field("Travel capital", "Purchase cost plus safety margin.", `<input type="number" name="travelCapital" min="0" step="10000" value="${cfg.travelCapital}">`)}
            ${field("Travel net profit", "Expected profit per complete round trip.", `<input type="number" name="travelProfit" min="0" step="1000" value="${cfg.travelProfit}">`)}
            ${field("Round-trip minutes", "Include buying and selling time.", `<input type="number" name="travelMinutes" min="1" value="${cfg.travelMinutes}">`)}
            ${field("Xanax cost", "Manual current estimate.", `<input type="number" name="xanaxCost" min="0" step="10000" value="${cfg.xanaxCost}">`)}
            ${field("Xanax energy", "Used for cost-per-energy.", `<input type="number" name="xanaxEnergy" min="1" value="${cfg.xanaxEnergy}">`)}
            ${field("Revive destination", "Torn URL opened manually when hospital status is detected.", `<input type="text" name="reviveUrl" value="${html(cfg.reviveUrl)}">`)}
          </section>

          <section><h3>Page tools</h3>
            ${toggle("marketTools", "Market analysis", "Summarizes visible prices and flags extreme outliers; never buys or submits.")}
            ${toggle("inventoryTools", "Inventory analysis", "Builds a sorted summary from the actively viewed inventory page.")}
            ${toggle("cityFinds", "City-find highlighter", "Highlights likely finds on the actively viewed City page only.")}
            ${toggle("dumpSearch", "Dump search idea", "Adds a low-priority manual Dump option after higher-value actions.")}
            ${toggle("chatTools", "Compact chat", "Optional presentation-only chat compaction.")}
            ${toggle("yata", "YATA travel lookup", "Optional external manual travel-stock integration; disabled by default.")}
          </section>

          <section><h3>Privacy and external services</h3>
            <p class="mx-help"><b>Torn API:</b> enabled modules refresh automatically and send the key only to api.torn.com.</p>
            <p class="mx-help"><b>FFScouter:</b> target and Sell Losses tools are manual and use separate in-panel consent. Normal links are non-referral. Torn Maxxed can copy the current player’s own referral URL for private chat/mail sharing.</p>
            <p class="mx-help"><b>YATA:</b> manual foreign-stock lookup sends no Torn key.</p>
            <p class="mx-help"><b>Local data:</b> settings, notes, histories, and caches remain in this userscript manager. Backup export excludes the API key. No analytics or author tracking is included.</p>
            <div class="mx-buttons"><a href="https://ffscouter.com/privacy" target="_blank" rel="noopener noreferrer">FFScouter privacy</a><a href="https://yata.yt/tos" target="_blank" rel="noopener noreferrer">YATA terms</a><button type="button" data-modal="revoke-ff">Revoke FFScouter consent</button></div>
          </section>

          <section><h3>Display and local controls</h3>
            ${field("Options shown", "Number of ranked recommendations retained for Today.", `<input type="number" name="shown" min="2" max="6" value="${cfg.shown}">`)}
            <div class="mx-buttons"><button type="button" data-modal="restore">Restore hidden recommendations</button><button type="button" data-modal="clear-learning">Clear crime and target learning</button><button type="button" data-modal="reset-settings">Reset settings</button><button type="button" data-modal="clear-all">Clear all local data</button></div>
          </section>
        </main>
        <footer><button data-modal="close">Cancel</button><button data-modal="save" class="primary">Save and refresh</button></footer>
      </div>`;

    const setSelect = (name, value) => { const input = modal.querySelector(`[name="${name}"]`); if (input) input.value = value; };
    setSelect("crimeGoal", cfg.crimeGoal || "balanced");
    setSelect("preferredCrime", cfg.preferredCrime || "Auto");
    setSelect("crimeFallback", cfg.crimeFallback || "Pickpocketing");
    setSelect("gymTarget", cfg.gymTarget || "best-dots");
    setSelect("gymPlan", cfg.gymPlan || "balanced");
    setSelect("ffTargetGoal", cfg.ffTargetGoal || "leveling");

    function draftModules() {
      const modules = {};
      modal.querySelectorAll('input[name^="module:"]').forEach((input) => { modules[input.name.split(":")[1]] = input.checked; });
      return modules;
    }

    function previewPlan() {
      const prior = cfg.modules;
      cfg.modules = { ...cfg.modules, ...draftModules() };
      const draft = apiPlan();
      cfg.modules = prior;
      modal.querySelector("#mx-plan").innerHTML = `<b>Minimum required: ${html(draft.access)} Access</b><p>Selections: ${html(draft.selections.join(", "))}</p><p>The key is stored locally. Automatic account refreshes send it only to api.torn.com. Manual FFScouter target and Sell Losses requests use separate scoped consent before sending the key and requested filters/status request. Manual YATA lookups do not send the key.</p>`;
      const accessCell = modal.querySelector(".mx-tos tr:last-child td");
      if (accessCell) accessCell.textContent = `${draft.access} for the currently enabled modules. Exact selections are shown above and update with module switches.`;
    }
    modal.addEventListener("change", (event) => { if (event.target.matches('input[name^="module:"]')) previewPlan(); });

    modal.addEventListener("click", async (event) => {
      const button = event.target.closest("[data-modal]");
      if (!button) { if (event.target === modal) modal.remove(); return; }
      const actionName = button.dataset.modal;
      if (actionName === "close") return modal.remove();
      if (actionName === "restore") { await restoreActions(); button.textContent = "Restored"; return; }
      if (actionName === "clear-learning") {
        crimeOutcomes = []; targetPrefs = {}; actionHistory = [];
        await Promise.all([save("crimeOutcomes", []), save("targetPrefs", {}), save("actionHistory", [])]);
        button.textContent = "Cleared"; render(); return;
      }
      if (actionName === "revoke-ff") {
        ffConsent = { targets: false, sellLosses: false };
        ffTargets = [];
        ffTargetFetchedAt = 0;
        ffTargetStatus = "FFScouter consent revoked";
        ffLossContracts = [];
        ffLossClaims = [];
        ffLossFetchedAt = 0;
        ffLossStatus = "FFScouter consent revoked";
        ffKeyInfo = null;
        ffKeyStatus = "Not checked";
        await Promise.all([save("ffConsent", ffConsent), save("ffTargets", null), save("ffLossCache", null)]);
        button.textContent = "Consent revoked";
        render();
        return;
      }
      if (actionName === "reset-settings") {
        cfg = copy(DEFAULTS); await save("config", cfg); modal.remove(); openSettings(); return;
      }
      if (actionName === "clear-all") {
        const cleared = await clearAllLocalData();
        if (cleared) modal.remove();
        return;
      }
      if (actionName === "test") {
        const result = modal.querySelector("#mx-test-result");
        const testKey = String(modal.querySelector('[name="apiKey"]').value || "").trim();
        if (!testKey) { result.innerHTML = "<b>Paste a key first.</b>"; return; }
        const oldModules = cfg.modules;
        cfg.modules = { ...cfg.modules, ...draftModules() };
        const testPlan = apiPlan();
        result.textContent = `Testing ${testPlan.selections.join(", ")}…`;
        try {
          const info = await requestKeyInfo(testKey);
          const response = await requestData(testKey, true);
          const access = info?.access?.type || info?.access_type || info?.access_level || "recognized";
          result.innerHTML = response.failed.length
            ? `<b>Key works in partial mode.</b><br>Reported access: ${html(access)}<br>Working: ${html(response.working.join(", "))}<br>Unavailable:<br>${response.failed.map((item) => `${html(item.selection)} — ${html(item.message)}`).join("<br>")}`
            : `<b>Key works.</b><br>Reported access: ${html(access)}<br>All enabled selections passed.`;
        } catch (error) { result.innerHTML = `<b>Test failed.</b><br>${html(error.message)}<br>${html(error.help || "")}`; }
        finally { cfg.modules = oldModules; }
        return;
      }
      if (actionName !== "save") return;

      const get = (name) => modal.querySelector(`[name="${name}"]`)?.value;
      apiKey = String(get("apiKey") || "").trim();
      cfg = merge({
        ...cfg,
        refresh: clamp(int(get("refresh"), 60), 30, 1800),
        safeMode: Boolean(modal.querySelector('[name="safeMode"]')?.checked),
        compactMode: Boolean(modal.querySelector('[name="compactMode"]')?.checked),
        walletWarning: Math.max(0, num(get("walletWarning"), 500000)),
        cashReserve: Math.max(0, num(get("cashReserve"), 200000)),
        factionVaultUrl: tornUrl(get("factionVaultUrl"), ""),
        crimeGoal: String(get("crimeGoal") || "balanced"),
        preferredCrime: String(get("preferredCrime") || "Auto"),
        crimeFallback: String(get("crimeFallback") || "Pickpocketing"),
        crimeMinimumSkill: clamp(int(get("crimeMinimumSkill"), 5), 1, 100),
        crimeNerve: Math.max(1, num(get("crimeNerve"), 5)),
        crimeProfit: Math.max(0, num(get("crimeProfit"), 5000)),
        gymTarget: String(get("gymTarget") || "best-dots"),
        gymPlan: String(get("gymPlan") || "balanced"),
        gymRatios: {
          strength: clamp(num(get("ratioStrength"), 25), 0, 100), speed: clamp(num(get("ratioSpeed"), 25), 0, 100),
          defense: clamp(num(get("ratioDefense"), 25), 0, 100), dexterity: clamp(num(get("ratioDexterity"), 25), 0, 100),
        },
        ffTargetGoal: String(get("ffTargetGoal") || "leveling"),
        ffMinLevel: clamp(int(get("ffMinLevel"), 1), 1, 100), ffMaxLevel: clamp(int(get("ffMaxLevel"), 100), 1, 100),
        ffMinFF: Math.max(1, num(get("ffMinFF"), 1)), ffMaxFF: Math.max(1, num(get("ffMaxFF"), 3)),
        ffInactive: Boolean(modal.querySelector('[name="ffInactive"]')?.checked), ffFactionless: Boolean(modal.querySelector('[name="ffFactionless"]')?.checked),
        ffLimit: clamp(int(get("ffLimit"), 20), 1, 50),
        warPay: Math.max(0, num(get("warPay"), 0)), attackEnergy: Math.max(1, num(get("attackEnergy"), 25)),
        travelName: String(get("travelName") || DEFAULTS.travelName).trim(), travelCapital: Math.max(0, num(get("travelCapital"), 500000)),
        travelProfit: Math.max(0, num(get("travelProfit"), 250000)), travelMinutes: Math.max(1, num(get("travelMinutes"), 120)),
        xanaxCost: Math.max(0, num(get("xanaxCost"), 850000)), xanaxEnergy: Math.max(1, num(get("xanaxEnergy"), 250)),
        reviveUrl: tornUrl(get("reviveUrl"), "https://www.torn.com/hospitalview.php"),
        shown: clamp(int(get("shown"), 4), 2, 6), modules: draftModules(),
      });
      selectionHealth = { signature: "", working: [], failed: [] };
      await Promise.all([save("apiKey", apiKey), save("config", cfg), save("selectionHealth", selectionHealth)]);
      modal.remove(); user = null; partial = null; latestError = null;
      panel?.classList.toggle("compact", cfg.compactMode); render(); startPageCoach();
      if (apiKey) await refresh();
    });
    document.body.appendChild(modal);
  }

  function openDiagnostic() {
    document.getElementById(`${ID}-diagnostic`)?.remove();
    const modal = document.createElement("div");
    modal.id = `${ID}-diagnostic`;
    modal.className = "mx-modal-bg";
    const plan = apiPlan();
    const failed = partial?.failed || [];
    modal.innerHTML = `
      <div class="mx-modal small">
        <header>
          <div><b>API diagnostic</b><small>Exact Torn response details</small></div>
          <button data-close>×</button>
        </header>
        <main>
          <section>
            <h3>Current request</h3>
            <p>Required access: ${html(plan.access)}</p>
            <p>Selections: ${html(plan.selections.join(", "))}</p>
            <p>Transport: ${html(transportUsed)}</p>
          </section>
          ${latestError ? `
            <section>
              <h3>Last error</h3>
              <p>${html(latestError.message)}</p>
              <p>${html(latestError.help || "")}</p>
            </section>` : ""}
          ${partial ? `
            <section>
              <h3>Partial mode</h3>
              <p>Working: ${html(partial.working.join(", "))}</p>
              ${failed.map((item) => `<p><b>${html(item.selection)}</b> — code ${html(item.code)} — ${html(item.message)}</p>`).join("")}
            </section>` : ""}
          <section><h3>Recent API calls</h3>${apiHistory.slice(-10).reverse().map((item) => `<p>${new Date(item.at).toLocaleTimeString()} · ${html(item.mode || item.type)} · ${html((item.selections || []).join(", "))}${item.failed?.length ? ` · unavailable ${html(item.failed.join(", "))}` : ""}</p>`).join("") || "<p>No API call history yet.</p>"}</section>
          <section><p>Open Settings and tap Test to rerun the full selection check.</p></section>
        </main>
        <footer><button data-close>Close</button></footer>
      </div>`;
    modal.addEventListener("click", (event) => {
      if (event.target.closest("[data-close]") || event.target === modal) modal.remove();
    });
    document.body.appendChild(modal);
  }

  function updateClock() {
    if (!status || !apiKey) return;
    if (refreshing) {
      status.textContent = "Refreshing…";
      return;
    }
    const seconds = Math.max(0, Math.ceil((nextRefresh - Date.now()) / 1000));
    status.textContent = user ? `Refresh in ${seconds}s` : "Waiting for API";
  }

  function schedule(seconds = cfg.refresh) {
    clearTimeout(timer);
    const delay = Math.max(30, seconds);
    nextRefresh = Date.now() + delay * 1000;
    timer = setTimeout(refresh, delay * 1000);
    updateClock();
  }

  async function refresh() {
    if (!apiKey || refreshing) return;
    refreshing = true;
    latestError = null;
    updateClock();

    try {
      const result = await requestData(apiKey);
      user = normalize(result.data);
      lastSuccessAt = Date.now();
      await save("lastSuccessAt", lastSuccessAt);
      partial = result.failed.length
        ? { working: result.working, failed: result.failed }
        : null;
      render();
    } catch (error) {
      latestError = error;
      apiHistory.push({ at: Date.now(), type: "api", mode: "error", ok: false, code: error.code || "?", message: error.message, selections: error.selections || apiPlan().selections });
      apiHistory = apiHistory.slice(-30); await save("apiHistory", apiHistory);
      if (!user) render();
      status.textContent = `API error ${error.code || ""}`.trim();
    } finally {
      refreshing = false;
      schedule(latestError?.code === 5 ? 75 : cfg.refresh);
    }
  }

  const COLLAPSED_WIDTH = 54;

  function snapCollapsedToSide(preferredSide, preferredTop) {
    const side = preferredSide === "left" ? "left" : "right";
    const top = clamp(num(preferredTop, panel?.getBoundingClientRect().top || 72), 8, Math.max(8, innerHeight - 78));
    panel.dataset.side = side;
    panel.style.left = side === "left" ? "0px" : `${Math.max(0, innerWidth - COLLAPSED_WIDTH)}px`;
    panel.style.top = `${top}px`;
    panel.style.right = "auto";
    const button = panel.querySelector('[data-action="collapse"]');
    if (button) button.textContent = side === "left" ? "›" : "‹";
  }

  async function setCollapsedState(collapsedNow) {
    const collapseButton = panel.querySelector('[data-action="collapse"]');

    if (collapsedNow) {
      const rect = panel.getBoundingClientRect();
      const expandedPosition = { left: Math.round(rect.left), top: Math.round(rect.top) };
      const side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
      await Promise.all([
        save("expandedPosition", expandedPosition),
        save("position", expandedPosition),
        save("collapsedSide", side),
        save("collapsedTop", Math.round(rect.top)),
        save("collapsed", true),
      ]);
      panel.classList.add("collapsed");
      snapCollapsedToSide(side, rect.top);
      return;
    }

    panel.classList.remove("collapsed");
    panel.dataset.side = "";
    const restored = await load("expandedPosition", await load("position", null));
    if (restored && Number.isFinite(restored.left) && Number.isFinite(restored.top)) {
      panel.style.left = `${clamp(restored.left, 0, Math.max(0, innerWidth - panel.offsetWidth))}px`;
      panel.style.top = `${clamp(restored.top, 8, Math.max(8, innerHeight - 60))}px`;
      panel.style.right = "auto";
    } else {
      panel.style.left = "auto";
      panel.style.top = "72px";
      panel.style.right = "8px";
    }
    if (collapseButton) collapseButton.textContent = "−";
    await save("collapsed", false);
  }

  function drag(handle) {
    if (!handle) return;
    let offset = null;
    let pointerId = null;

    const move = (event) => {
      if (!offset || (pointerId !== null && event.pointerId !== pointerId)) return;
      event.preventDefault();
      panel.style.left = `${clamp(event.clientX - offset.x, 0, Math.max(0, innerWidth - panel.offsetWidth))}px`;
      panel.style.top = `${clamp(event.clientY - offset.y, 8, Math.max(8, innerHeight - 48))}px`;
      panel.style.right = "auto";
    };

    const stop = async (event) => {
      if (!offset || (pointerId !== null && event.pointerId !== pointerId)) return;
      offset = null;
      pointerId = null;
      panel.classList.remove("dragging");
      window.removeEventListener("pointermove", move);
      window.removeEventListener("pointerup", stop);
      window.removeEventListener("pointercancel", stop);

      const rect = panel.getBoundingClientRect();
      if (panel.classList.contains("collapsed")) {
        const side = rect.left + rect.width / 2 < innerWidth / 2 ? "left" : "right";
        snapCollapsedToSide(side, rect.top);
        await Promise.all([
          save("collapsedSide", side),
          save("collapsedTop", Math.round(rect.top)),
        ]);
      } else {
        const position = { left: Math.round(rect.left), top: Math.round(rect.top) };
        await Promise.all([save("position", position), save("expandedPosition", position)]);
      }
    };

    handle.addEventListener("pointerdown", (event) => {
      if (event.pointerType === "mouse" && event.button !== 0) return;
      if (event.target.closest("button,a,input,select,textarea")) return;
      const rect = panel.getBoundingClientRect();
      offset = { x: event.clientX - rect.left, y: event.clientY - rect.top };
      pointerId = event.pointerId;
      panel.classList.add("dragging");
      try { handle.setPointerCapture(event.pointerId); } catch {}
      window.addEventListener("pointermove", move, { passive: false });
      window.addEventListener("pointerup", stop);
      window.addEventListener("pointercancel", stop);
      event.preventDefault();
      event.stopPropagation();
    }, { passive: false });
  }

  async function createPanel() {
    document.getElementById(ID)?.remove();
    panel = document.createElement("aside");
    panel.id = ID;
    panel.innerHTML = `
      <header class="mx-head">
        <div class="mx-head-brand"><span class="mx-drag-grip" title="Hold and drag">⠿</span><span class="mx-logo">M</span><span class="mx-brand-text"><b>Torn Maxxed</b><small id="mx-status">Starting…</small></span></div>
        <nav>
          <a class="mx-head-link" href="${PROJECT_URL}" target="_blank" rel="noopener noreferrer" title="Project page" aria-label="Project page">?</a>
          <button data-action="quick-target" title="Find next target" aria-label="Find next target">◎</button>
          <button data-action="refresh" title="Refresh Torn data" aria-label="Refresh Torn data">↻</button>
          <button data-action="settings" title="Settings" aria-label="Settings">⚙</button>
          <button data-action="collapse" title="Minimize" aria-label="Minimize">−</button>
        </nav>
      </header>
      <main class="mx-body"></main>`;
    document.body.appendChild(panel);
    body = panel.querySelector(".mx-body");
    status = panel.querySelector("#mx-status");

    const position = await load("position", null);
    if (position && Number.isFinite(position.left) && Number.isFinite(position.top)) {
      panel.style.left = `${clamp(position.left, 0, innerWidth - 280)}px`;
      panel.style.top = `${clamp(position.top, 0, innerHeight - 60)}px`;
      panel.style.right = "auto";
    }

    const collapsed = Boolean(await load("collapsed", false));
    panel.classList.toggle("compact", cfg.compactMode);
    if (collapsed) {
      panel.classList.add("collapsed");
      const collapsedSide = String(await load("collapsedSide", "right"));
      const collapsedTop = num(await load("collapsedTop", position?.top ?? 72), 72);
      snapCollapsedToSide(collapsedSide, collapsedTop);
    } else {
      panel.querySelector('[data-action="collapse"]').textContent = "−";
    }

    panel.addEventListener("click", async (event) => {
      const tabButton = event.target.closest("[data-tab]");
      if (tabButton) {
        tab = tabButton.dataset.tab;
        await save("tab", tab);
        render();
        return;
      }

      const taskButton = event.target.closest("[data-task-action]");
      if (taskButton) {
        if (taskButton.dataset.taskAction === "done") await completeTask(taskButton.dataset.id);
        else await snoozeTask(taskButton.dataset.id, 30);
        return;
      }

      const button = event.target.closest("[data-action]");
      if (!button) return;
      const actionName = button.dataset.action;
      if (actionName === "settings") return openSettings();
      if (actionName === "refresh") return refresh();
      if (actionName === "diagnostic") return openDiagnostic();
      if (actionName === "quick-target") return openNextTarget();
      if (actionName === "refresh-targets") return fetchFfTargets(true);
      if (actionName === "refresh-losses") return fetchFfLosses(true);
      if (actionName === "check-ff-key") {
        try { await checkFfKey(button.dataset.scope || "targets", true); }
        catch (error) { button.textContent = "Key check failed"; }
        return;
      }
      if (actionName === "copy-own-referral") {
        if (!user?.playerId) { button.textContent = "Player ID unavailable"; return; }
        const link = `${FF_SELL_LOSSES_URL}?ref=${user.playerId}`;
        const copied = await copyText(link, "Copy your FFScouter referral link for private chat or mail");
        button.textContent = copied ? "My link copied" : "Link opened";
        return;
      }
      if (actionName === "next-target") {
        const available = availableFfTargets();
        if (available.length) ffTargetIndex = (ffTargetIndex + 1) % available.length;
        render(); return;
      }
      if (actionName === "target-note") {
        const target = ffTargets.find((item) => item.id === int(button.dataset.targetId));
        if (target) targetNotesModal(target);
        return;
      }
      if (actionName === "restore-targets") {
        Object.keys(targetPrefs).forEach((id) => { targetPrefs[id] = { ...targetPrefs[id], blacklisted: false }; });
        await save("targetPrefs", targetPrefs); render(); return;
      }
      if (actionName === "log-crime") return openCrimeOutcomeModal(button.dataset.crime || crimeChoiceName());
      if (actionName === "later") return snoozeAction(button.dataset.id, 60);
      if (actionName === "next-option") return snoozeAction(button.dataset.id, 10);
      if (actionName === "hide") return hideAction(button.dataset.id);
      if (actionName === "done") {
        if (button.dataset.task) await completeTask(button.dataset.task);
        actionHistory.push({ id: button.dataset.id, at: Date.now(), type: "done" });
        actionHistory = actionHistory.slice(-300);
        await save("actionHistory", actionHistory);
        await snoozeAction(button.dataset.id, 30);
        return;
      }
      if (actionName === "scan-page") { schedulePageCoach(); setTimeout(render, 450); return; }
      if (actionName === "max-buy") { const result = fillMaxAffordableVisible(); button.textContent = result.message; return; }
      if (actionName === "set-price") { const result = fillFocusedMarketPrice(); button.textContent = result.message; return; }
      if (actionName === "sort-inventory") { const result = sortVisibleInventoryCards(); button.textContent = result.message; return; }
      if (actionName === "refresh-yata") return fetchYataStocks(true);
      if (actionName === "find-city") {
        const count = highlightCityFinds();
        button.textContent = count ? `${count} highlighted` : "No visible finds";
        return;
      }
      if (actionName === "export-data") {
        const copied = await copyLocalExport();
        button.textContent = copied ? "Backup copied" : "Backup opened";
        return;
      }
      if (actionName === "import-data") {
        const result = await importLocalBackup(); button.textContent = result.message; return;
      }
      if (actionName === "restore-actions") return restoreActions();
      if (actionName === "reset-position") {
        panel.classList.remove("collapsed"); panel.dataset.side = "";
        panel.style.left = "auto"; panel.style.top = "72px"; panel.style.right = "8px";
        panel.querySelector('[data-action="collapse"]').textContent = "−";
        await Promise.all([save("position", null), save("expandedPosition", null), save("collapsed", false)]); return;
      }
      if (actionName === "collapse") {
        await setCollapsedState(!panel.classList.contains("collapsed"));
      }
    });


    drag(panel.querySelector(".mx-head"));
  }

  style(`
    #${ID}{position:fixed;top:72px;right:8px;z-index:2147483000;width:min(338px,calc(100vw - 12px));background:#111419;color:#edf1f5;border:1px solid #363c45;border-radius:15px;box-shadow:0 16px 44px #000c;font:12px/1.35 Inter,Arial,sans-serif;overflow:hidden;overscroll-behavior:contain}
    #${ID} *{box-sizing:border-box}#${ID}.dragging{opacity:.94;box-shadow:0 20px 55px #000e}#${ID}.collapsed{width:50px;height:66px;border-radius:0 14px 14px 0}#${ID}.collapsed[data-side="right"]{border-radius:14px 0 0 14px}#${ID}.collapsed .mx-body{display:none}
    .mx-head{display:flex;justify-content:space-between;align-items:center;min-height:44px;padding:4px 5px;background:linear-gradient(180deg,#232831,#1a1e25);border-bottom:1px solid #343a43;cursor:grab;user-select:none;touch-action:none}.mx-head:active{cursor:grabbing}.mx-head-brand{display:flex;align-items:center;gap:5px;min-width:0}.mx-drag-grip{display:grid;place-items:center;width:25px;height:34px;border:0;border-radius:8px;color:#8d96a3;font-size:19px;line-height:1;touch-action:none}.mx-brand-text{display:flex;flex-direction:column;min-width:0}.mx-brand-text b{font-size:12px;letter-spacing:.1px}.mx-head small{color:#98a0aa;font-size:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:110px}.mx-logo{display:grid;place-items:center;width:27px;height:27px;border-radius:8px;background:linear-gradient(135deg,#ee6259,#b6322c);color:#fff;font-weight:900;flex:0 0 auto}.mx-head nav{display:flex;align-items:center;gap:1px}.mx-head button,.mx-head-link{display:grid;place-items:center;width:29px;height:32px;padding:0;border:0;border-radius:8px;background:transparent;color:#d7dce2;text-decoration:none;font-size:15px}.mx-head button:active,.mx-head-link:active{background:#363d47}
    #${ID}.collapsed .mx-head{height:66px;min-height:66px;padding:4px;flex-direction:column;justify-content:center;gap:1px}#${ID}.collapsed .mx-drag-grip,#${ID}.collapsed .mx-brand-text{display:none}#${ID}.collapsed .mx-head-brand{display:block}#${ID}.collapsed .mx-logo{width:32px;height:32px}#${ID}.collapsed .mx-head nav>*{display:none}#${ID}.collapsed .mx-head nav [data-action="collapse"]{display:grid;width:32px;height:23px;padding:0;font-size:20px;line-height:1}
    .mx-body{max-height:calc(100vh - 96px);overflow:auto;padding:0 8px 9px;scrollbar-width:thin;scrollbar-color:#414854 transparent}.mx-tabs{position:sticky;top:0;z-index:5;display:grid;grid-template-columns:repeat(6,minmax(45px,1fr));gap:2px;margin:0 -8px 7px;padding:5px 6px;background:#111419ee;backdrop-filter:blur(8px);border-bottom:1px solid #252b33;overflow-x:auto}.mx-tabs button{min-width:45px;padding:5px 2px 4px;border:0;border-radius:8px;background:transparent;color:#7f8996}.mx-tabs button span{display:block;font-size:14px;line-height:1}.mx-tabs button small{display:block;margin-top:2px;font-size:7px}.mx-tabs .active{background:#d84a42;color:#fff;box-shadow:0 3px 10px #d84a4233}
    .mx-summary{padding:7px 8px;border:1px solid #2c323b;border-radius:10px;background:#171b21}.mx-player,.mx-pairs{display:flex;justify-content:space-between;gap:8px}.mx-player{margin-bottom:4px}.mx-player b{font-size:12px}.mx-player span,.mx-pairs{font-size:9px;color:#929ba7}.mx-pairs{margin-top:5px}.mx-pairs b{color:#e7ebef}
    .mx-bar{margin:4px 0}.mx-bar>div{display:flex;justify-content:space-between;font-size:9px}.mx-bar i{display:block;height:5px;background:#090b0e;border-radius:99px;overflow:hidden}.mx-bar em{display:block;height:100%}.mx-bar .energy{background:#72bb58}.mx-bar .nerve{background:#e0a33b}.mx-bar small{display:block;text-align:right;color:#68717c;font-size:7px}
    .mx-warning{display:flex;flex-direction:column;gap:3px;margin:6px 0;padding:8px;border:1px solid #74592d;border-radius:9px;background:#292218;color:#e9c98f;font-size:9px}.mx-warning button{align-self:flex-start}
    .mx-next{margin-top:6px;padding:9px;border:1px solid #303640;border-left:3px solid #707986;border-radius:10px;background:#181d23}.mx-next.danger{border-left-color:#e35a53}.mx-next.warn{border-left-color:#e4a63f}.mx-next.info{border-left-color:#4d91d8}.mx-next>small,.mx-others>small{color:#8d97a3;font-size:7px;letter-spacing:.8px}.mx-next h3{margin:2px 0 3px;color:#fff;font-size:14px;line-height:1.18}.mx-next p{margin:3px 0;color:#b8c0ca;font-size:10px}.mx-next>strong{display:inline-block;margin-top:5px;padding:2px 6px;border-radius:99px;background:#252b33;color:#d5dbe2;font-size:9px}.mx-reasons{display:flex;flex-wrap:wrap;gap:2px 8px;color:#89939f;font-size:8px}.mx-reasons span:before{content:"• ";color:#59626e}
    .mx-buttons{display:flex;flex-wrap:wrap;gap:4px;margin-top:6px}.mx-buttons.center{justify-content:center}.mx-buttons a,.mx-buttons button,.mx-warning button,.mx-loss-list a{min-height:28px;padding:6px 8px;border:1px solid #404751;border-radius:7px;background:#252b33;color:#e6eaee;text-decoration:none;font-size:8px;line-height:1.1}.mx-buttons a.primary,.mx-buttons button.primary,.primary{background:#d84a42!important;border-color:#d84a42!important;color:#fff!important;font-weight:800}.mx-buttons button:disabled{opacity:.45}
    .mx-note{margin:7px 2px;color:#69727d;font-size:7px;text-align:center}.mx-quick-target{display:flex;align-items:center;justify-content:space-between;gap:7px;margin:6px 0;padding:6px;border:1px solid #30485f;border-radius:9px;background:#17222c}.mx-quick-target button{min-height:32px;padding:7px 10px;border:0;border-radius:7px;background:#397fc0;color:#fff;font-size:9px;font-weight:800}.mx-quick-target small{color:#8e9ba8;font-size:7px;text-align:right}
    .mx-section,.mx-target-summary{margin-top:6px;padding:8px;border:1px solid #2d333c;border-radius:10px;background:#171b21}.mx-section h4{margin:0 0 5px;color:#aab2bd;font-size:8px;letter-spacing:.9px}.mx-section p,.mx-target-summary p{margin:4px 0;color:#aeb7c2;font-size:9px}.mx-section-title{display:flex;align-items:center;justify-content:space-between;gap:7px}.mx-section-title>div{display:flex;flex-direction:column}.mx-section-title small{color:#77818d;font-size:7px;letter-spacing:.8px}.mx-section-title b,.mx-section-title h4{margin:0;color:#eef2f6;font-size:12px;letter-spacing:0}.mx-pill{padding:2px 6px;border:1px solid #3b4652;border-radius:99px;background:#20262e;color:#9eabb8;font-size:7px;text-transform:capitalize}
    .mx-plan-row{display:flex;justify-content:space-between;align-items:center;gap:7px;padding:6px 1px;border-top:1px solid #292f37;color:#eee;text-decoration:none}.mx-plan-row:first-of-type{border-top:0}.mx-plan-row>div{display:flex;flex-direction:column}.mx-plan-row b{font-size:9px}.mx-plan-row small,.mx-muted{color:#818b97;font-size:8px}.mx-link{display:inline-block;margin-top:4px;color:#f0a29c}.mx-range{display:grid;grid-template-columns:1fr 1fr;gap:5px}.mx-range.four{grid-template-columns:repeat(4,1fr)}
    .mx-target-list article,.mx-loss-list article,.mx-loss-card{display:flex;justify-content:space-between;align-items:center;gap:6px;padding:7px 2px;border-top:1px solid #292f37}.mx-target-list article.active{border-left:3px solid #4d91d8;padding-left:6px;background:#1b222a}.mx-target-list article>div:first-child,.mx-loss-list article>div:first-child,.mx-loss-card>div:first-child{display:flex;flex-direction:column;min-width:0}.mx-target-list b,.mx-loss-list b,.mx-loss-card b{font-size:9px}.mx-target-list small,.mx-loss-list small,.mx-loss-card small{color:#858f9b;font-size:8px}.mx-loss-list>a{flex:0 0 auto}.mx-current-target{background:linear-gradient(180deg,#1a222a,#171b21)}.mx-losses{border-color:#3f4932;background:#1a2018}.mx-referral-note{border-style:dashed}
    .mx-disclosure{margin-top:6px;border:1px solid #2c323a;border-radius:9px;background:#15191e;overflow:hidden}.mx-disclosure summary{display:flex;justify-content:space-between;padding:8px;color:#cbd1d8;font-size:9px;font-weight:700;cursor:pointer;list-style:none}.mx-disclosure summary::-webkit-details-marker{display:none}.mx-disclosure summary span{padding:1px 6px;border-radius:99px;background:#262d35;color:#95a0ac;font-size:8px}.mx-disclosure>section{padding:0 7px 5px}
    .mx-task{display:flex;justify-content:space-between;gap:6px;padding:6px;border-top:1px solid #292f37}.mx-task:first-child{border-top:0}.mx-task.due{border-left:3px solid #e5a33d}.mx-task>div:first-child{display:flex;flex-direction:column}.mx-task small{color:#89939f;font-size:8px}.mx-task>div:last-child{display:flex;gap:3px;align-items:center}.mx-task a,.mx-task button{padding:4px 5px;border:1px solid #3d444e;border-radius:6px;background:#242a31;color:#eee;text-decoration:none;font-size:7px}
    .mx-empty,.mx-error{text-align:center;padding:18px 7px}.mx-empty p,.mx-error p{color:#9ca5b0;font-size:9px}.mx-error p strong{color:#f0b5b2}
    .mx-modal-bg{position:fixed;inset:0;z-index:2147483647;display:grid;place-items:center;padding:8px;background:#020305d9;backdrop-filter:blur(4px)}.mx-modal{width:min(680px,96vw);max-height:94vh;background:#14171c;color:#eee;border:1px solid #454c56;border-radius:14px;overflow:hidden;box-shadow:0 24px 70px #000e}.mx-modal.small{width:min(540px,96vw)}.mx-modal>header,.mx-modal>footer{display:flex;justify-content:space-between;align-items:center;padding:9px 11px;background:#20252c}.mx-modal>header>div{display:flex;flex-direction:column}.mx-modal>header small{color:#8c96a2;font-size:8px}.mx-modal>header button{border:0;background:transparent;color:#fff;font-size:22px}.mx-modal>main{max-height:calc(94vh - 100px);overflow:auto;padding:7px}.mx-modal section{margin-bottom:7px;padding:9px;border:1px solid #2f353e;border-radius:9px;background:#191d23}.mx-modal h3{margin:0 0 6px;font-size:12px}.mx-permission,.mx-test{padding:8px;border:1px solid #414b57;border-radius:7px;background:#101318;font-size:9px}.mx-permission p{margin:4px 0;color:#9ca5b0}.mx-tos{width:100%;margin:7px 0;border-collapse:collapse;font-size:8px}.mx-tos th,.mx-tos td{padding:5px;border:1px solid #343b45;text-align:left;vertical-align:top}.mx-tos th{width:29%;color:#d7dce2}.mx-tos td{color:#aeb6c0}.mx-test{margin-top:6px;line-height:1.4}.mx-key-row{display:grid;grid-template-columns:1fr auto;gap:5px}.mx-consent-hero{display:flex;gap:9px;align-items:flex-start}.mx-consent-hero .mx-logo{width:38px;height:38px;flex:0 0 auto}.mx-consent-hero p{margin:3px 0;color:#aab3bd;font-size:9px}
    .mx-field{display:grid;grid-template-columns:minmax(0,1fr) minmax(145px,1fr);gap:8px;align-items:center;padding:6px 0;border-top:1px solid #292f37}.mx-field>span{display:flex;flex-direction:column}.mx-field small,.mx-help{color:#858f9b;font-size:8px}.mx-field input[type=text],.mx-field input[type=password],.mx-field input[type=number],.mx-field select{width:100%;padding:7px;border:1px solid #3e4650;border-radius:7px;background:#0e1115;color:#fff}.mx-field input[type=checkbox]{width:18px;height:18px;justify-self:end;accent-color:#d84a42}.mx-modal>footer button,.mx-modal section>button,.mx-key-row button{padding:7px 10px;border:1px solid #414852;border-radius:7px;background:#272d35;color:#eee}.mx-modal>footer .primary{border:0;background:#d84a42}
    .mx-coach-highlight{outline:3px solid #55c96f!important;outline-offset:-3px!important;box-shadow:0 0 0 2px #0b0d0f,0 0 18px #55c96f66!important}.mx-coach-second{outline-color:#e6b64b!important;box-shadow:0 0 0 2px #0b0d0f,0 0 14px #e6b64b55!important}.mx-coach-third{outline-color:#68a7e8!important;box-shadow:0 0 0 2px #0b0d0f,0 0 12px #68a7e855!important}.mx-coach-badge{position:absolute!important;z-index:9999!important;top:5px!important;right:5px!important;max-width:72%!important;padding:4px 7px!important;border-radius:999px!important;background:#17843a!important;color:#fff!important;font:700 10px/1.2 Arial,sans-serif!important;box-shadow:0 2px 8px #0009!important;pointer-events:none!important}.mx-coach-second>.mx-coach-badge{background:#9a6b13!important}.mx-coach-third>.mx-coach-badge{background:#24669d!important}
    .mx-stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:4px;margin:6px 0;padding:6px;border-radius:7px;background:#101318;color:#bec5cd;font-size:8px}.mx-egg-find{outline:4px solid #b8ff66!important;outline-offset:2px!important;filter:drop-shadow(0 0 10px #b8ff66)!important}.mx-city-find{outline:4px solid #ffdd57!important;outline-offset:2px!important;filter:drop-shadow(0 0 8px #ffdd57)!important}.mx-chat-compact [class*=message]{padding:2px 4px!important;margin:1px 0!important;font-size:11px!important}.mx-chat-compact img{max-width:24px!important;max-height:24px!important}.compact .mx-body{padding-left:6px;padding-right:6px}.compact .mx-next,.compact .mx-section,.compact .mx-target-summary{padding:7px;margin-top:4px}.compact .mx-task{padding:5px}
    @media(max-width:650px){#${ID}{width:min(326px,calc(100vw - 8px));right:4px}.mx-field{grid-template-columns:1fr}.mx-field input[type=checkbox]{justify-self:start}.mx-brand-text small{max-width:90px}.mx-tabs{grid-template-columns:repeat(6,48px);justify-content:space-between}.mx-tabs button{min-width:46px}.mx-body{max-height:calc(100vh - 82px)}}
  `);

  async function init() {
    apiKey = String(await load("apiKey", ""));
    if (!apiKey && PDA_KEY_TOKEN !== PDA_KEY_SENTINEL && PDA_KEY_TOKEN.length > 8) {
      apiKey = PDA_KEY_TOKEN;
    }
    cfg = merge(await load("config", null));
    tasks = await load("tasks", {});
    prefs = await load("prefs", {});
    crimeOutcomes = await load("crimeOutcomes", []);
    targetPrefs = await load("targetPrefs", {});
    actionHistory = await load("actionHistory", []);
    apiHistory = await load("apiHistory", []);
    lastSuccessAt = num(await load("lastSuccessAt", 0), 0);
    selectionHealth = await load("selectionHealth", { signature: "", working: [], failed: [] });
    tab = String(await load("tab", "today"));
    ffConsent = normalizeFfConsent(await load("ffConsent", { targets: false, sellLosses: false }));
    const savedFfTargets = await load("ffTargets", null);
    if (savedFfTargets?.targets && Array.isArray(savedFfTargets.targets)) {
      ffTargets = savedFfTargets.targets.sort(targetSort);
      ffTargetFetchedAt = num(savedFfTargets.fetchedAt, 0);
      ffTargetStatus = ffTargets.length ? `${ffTargets.length} cached targets` : "Not loaded";
    }
    const savedFfLosses = await load("ffLossCache", null);
    if (savedFfLosses) {
      ffLossContracts = Array.isArray(savedFfLosses.contracts) ? savedFfLosses.contracts : [];
      ffLossClaims = Array.isArray(savedFfLosses.claims) ? savedFfLosses.claims : [];
      ffLossFetchedAt = num(savedFfLosses.fetchedAt, 0);
      if (ffLossFetchedAt) ffLossStatus = `${ffLossContracts.length} cached contracts · ${ffLossClaims.length} cached claims`;
    }
    const savedYata = await load("yataCache", null);
    if (savedYata?.stocks && Array.isArray(savedYata.stocks)) {
      yataStocks = savedYata.stocks; yataFetchedAt = num(savedYata.fetchedAt);
      yataStatus = yataStocks.length ? `${yataStocks.length} cached stock rows` : "Not loaded";
    }
    await createPanel();
    startPageCoach();
    render();
    if (apiKey) await refresh();
    else schedule();
    clearInterval(clock);
    clock = setInterval(() => {
      updateClock();
      if (tab === "today") render();
    }, 1000);
  }

  init().catch((error) => {
    console.error("[Torn Maxxed] startup failed", error);
    const notice = document.createElement("div");
    notice.textContent = `Torn Maxxed failed to start: ${error?.message || error}`;
    notice.style.cssText =
      "position:fixed;top:72px;left:8px;right:8px;z-index:2147483647;" +
      "background:#8b1e1e;color:#fff;padding:10px;border-radius:8px;font:13px Arial";
    document.body.appendChild(notice);
  });
})();