Zed.City – QOL Update By MathewPerry

Could I *BE* any more scripts!? (Market Favs, Profit Helper, Inventory Net Worth, Timer Bar, Exploration Data, Auto fill for selling, Store Remaining Amount + Rad ROI Tracker + Durability converter + Market Buy Tracker). Each feature can be toggled from the cog in the header.

// ==UserScript==
// @name         Zed.City – QOL Update By MathewPerry
// @namespace    zed.city.aio
// @version      1.6.2
// @description  Could I *BE* any more scripts!? (Market Favs, Profit Helper, Inventory Net Worth, Timer Bar, Exploration Data, Auto fill for selling, Store Remaining Amount + Rad ROI Tracker + Durability converter + Market Buy Tracker). Each feature can be toggled from the cog in the header.
// @author       MathewPerry
// @match        https://www.zed.city/*
// @run-at       document-idle
// @icon         https://www.google.com/s2/favicons?sz=64&domain=zed.city
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function() {
  'use strict';

  const SETTINGS_KEY = 'zed-aio-settings';
  const DefaultSettings = {
    marketFavs: true,
    profitHelper: true,
    networth: true,
    timerBar: true,
    explorationData: true,
    marketSelling: true,
    storeRemainingAmounts: true,
    radTracker: true,
    durability: true,
    marketBuyTracker: true

  };
  function readSettings(){
    try {
      const raw = localStorage.getItem(SETTINGS_KEY);
      return Object.assign({}, DefaultSettings, raw ? JSON.parse(raw) : {});
    } catch(e) {
      return Object.assign({}, DefaultSettings);
    }
  }
  function writeSettings(s){ localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); }
  let SETTINGS = readSettings();

  // Find the exact toolbar row and insert Options button as FIRST child (left)
  function findToolbarRow(){
    const exact = document.querySelector('div.q-gutter-xs.row.items-center.no-wrap.col-xs-4.order-xs-first.order-sm-none.col-sm-auto.justify-end');
    if (exact) return exact;
    return document.querySelector('.q-toolbar .row.justify-end') || document.querySelector('.q-header .row.justify-end');
  }

  function mountOptions(){
    const row = findToolbarRow();
    if (!row) return;
    if (row.querySelector('.zed-aio-opts-btn')) return;

    const btn = document.createElement('button');
    btn.className = 'zed-aio-opts-btn q-btn q-btn-item non-selectable no-outline q-btn--flat q-btn--round text-grey-7 q-btn--actionable q-focusable q-hoverable';
    btn.type = 'button';
    btn.setAttribute('aria-label', 'Options');
    btn.innerHTML = '<span class="q-focus-helper"></span><span class="q-btn__content text-center col items-center q-anchor--skip justify-center row"><i class="q-icon fal fa-cog" aria-hidden="true"></i></span>';
    row.insertBefore(btn, row.firstChild);

    const panel = document.createElement('div');
    panel.className = 'zed-aio-opts-panel';
    panel.style.cssText = 'position:absolute;z-index:9999;margin-top:8px;padding:10px 12px;border-radius:8px;background:rgba(20,20,20,.98);border:1px solid rgba(255,255,255,.12);box-shadow:0 6px 20px rgba(0,0,0,.35);color:#ddd;font-size:10px;display:none;left:0;transform:translateX(-24px);width:200px;';
    panel.innerHTML = [
      '<div style="font-weight:200;margin-bottom:8px">Options</div>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="marketFavs">Market Favs</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="profitHelper">Profit Helper</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="networth">Networth</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="timerBar">Timer Bar</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="explorationData">Exploration Data</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="marketSelling">Auto Fill sell price</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="storeRemainingAmounts">Shows Store amounts</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="radTracker">Shows ROI on Rad Spent</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="durability">Shows durability on items</label>',
      '<label style="display:flex;gap:8px;align-items:center;margin:4px 0"><input type="checkbox" data-k="marketBuyTracker">Shows Market buy history</label>',

      '<div style="opacity:.75;margin-top:8px">Page will reload to apply.</div>'
    ].join('');
    btn.style.position = 'relative';
    btn.appendChild(panel);

    panel.querySelectorAll('input[type=checkbox][data-k]').forEach(cb => {
      const k = cb.getAttribute('data-k');
      cb.checked = !!SETTINGS[k];
      cb.addEventListener('change', () => {
        SETTINGS[k] = cb.checked;
        writeSettings(SETTINGS);
        location.reload();
      });
    });

    let open = false;
    function show(){ panel.style.display = 'block'; open = true; }
    function hide(){ panel.style.display = 'none'; open = false; }
    btn.addEventListener('click', (e) => { e.stopPropagation(); open ? hide() : show(); });
    document.addEventListener('click', (e) => { if (open && !btn.contains(e.target)) hide(); });
    document.addEventListener('keydown', (e) => { if (open && e.key === 'Escape') hide(); });
  }

  // Retry mount in case SPA header loads late
  (function retryMount(){
    let tries = 0;
    const t = setInterval(() => {
      tries++;
      mountOptions();
      if (findToolbarRow() && tries > 1 || tries > 60) clearInterval(t);
    }, 250);
  })();

  // ---------- Module gates ----------
  const RUN_MARKET = !!SETTINGS.marketFavs;
  const RUN_PROFIT = !!SETTINGS.profitHelper;
  const RUN_NETWORTH = !!SETTINGS.networth;
  const RUN_TIMERS = !!SETTINGS.timerBar;
  const RUN_EXPLORATION = !!SETTINGS.explorationData;
  const RUN_MARKETSELLING = !!SETTINGS.marketSelling;
  const RUN_STOREREMAININGAMOUNTS = !!SETTINGS.storeRemainingAmounts;
  const RUN_RADTRACKER = !!SETTINGS.radTracker;
  const RUN_DURABILITY = !!SETTINGS.durability;
  const RUN_MARKETBUYTRACKER = !!SETTINGS.marketBuyTracker;


// ===============================
// NetBus (hardened, private, no CSRF exposure)
// ===============================
(function NetBus(){
  const FLAG_XHR = '__zedNetBusXHR';
  const FLAG_FETCH = '__zedNetBusFetch';

  // Private in-closure bus (not on window)
  const Bus = new EventTarget();

  // Compat shim: same API name your modules already use
  // Returns an unsubscribe function.
  window.addNetListener = function addNetListener(pattern, handler){
    const re = pattern instanceof RegExp ? pattern : new RegExp(String(pattern), 'i');
    const listener = (e) => {
      const { url = '', response, source } = e.detail || {};
      if (re.test(url)) handler(url, response, source);
    };
    Bus.addEventListener('net', listener);
    return () => Bus.removeEventListener('net', listener);
  };

  // Scrub likely secrets before emitting to modules (belt & braces)
  function scrub(obj){
    try {
      return JSON.parse(JSON.stringify(obj, (k, v) => (
        /csrf|token|auth|session|secret|cookie/i.test(k) ? undefined : v
      )));
    } catch { return null; }
  }

  // ---- XHR wrapper (guarded) ----
  if (!XMLHttpRequest.prototype[FLAG_XHR]) {
    const originalOpen = XMLHttpRequest.prototype.open;
    const originalSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function (method, url) {
      this._zedURL = url;
      return originalOpen.apply(this, arguments);
    };
    XMLHttpRequest.prototype.send = function (body) {
      this.addEventListener('readystatechange', function(){
        if (this.readyState === 4) {
          try {
            if (this.responseType && this.responseType !== '' && this.responseType !== 'text') return;
            const text = this.responseText || '';
            const first = text[0];
            if (!text || (first !== '{' && first !== '[')) return; // quick JSON gate
            const json = JSON.parse(text);
            const clean = scrub(json);
            // Emit ONLY on the private bus
            Bus.dispatchEvent(new CustomEvent('net', {
              detail: { url: this._zedURL || '', response: clean, source: 'xhr' }
            }));
          } catch(e){}
        }
      });
      return originalSend.apply(this, arguments);
    };
    Object.defineProperty(XMLHttpRequest.prototype, FLAG_XHR, { value: true, configurable: false });
  }

  // ---- fetch wrapper (guarded) ----
  if (!window[FLAG_FETCH]) {
    const origFetch = window.fetch;
    window.fetch = async function(){
      const res = await origFetch.apply(this, arguments);
      try {
        const clone = res.clone();
        const ct = clone.headers.get('content-type') || '';
        if (ct.includes('application/json')) {
          const json = await clone.json();
          const clean = scrub(json);
          const req = arguments[0];
          const url = typeof req === 'string' ? req : (req && req.url) || '';
          Bus.dispatchEvent(new CustomEvent('net', {
            detail: { url, response: clean, source: 'fetch' }
          }));
        }
      } catch(e){}
      return res;
    };
    Object.defineProperty(window, FLAG_FETCH, { value: true, configurable: false });
  }
})();




    //-----Marketdata, always run -------
    // ===============================
      // Config
      // ===============================
      const PINNED_ITEM_LIMIT = 22;
      const MARKET_KEY = "Zed-market-data";
      const HISTORY_KEY = "Zed-market-data-history";
      const PINNED_KEY = "Zed-pinned-items";
      const COLLAPSE_KEY = "Zed-pinned-collapsed";
      const LAST_SUCCESS_KEY = "Zed-market-last-success";// last successful save (ms)
      const NEXT_ALLOWED_KEY = "Zed-market-next-allowed";// earliest next attempt (ms)
      const HISTORY_LIMIT = 50;
      const STALE_MS = 10 * 60 * 1000;
      const POLL_MS = 60 * 1000;
      const RETRY_MIN_MS = 5 * 1000;// at least 5s between retries
      const RETRY_MAX_MS = 5 * 60 * 1000;// cap at 5m
      const DEBUG = false;

      // Compact UI tuning
      const SPARK_W = 100;
      const SPARK_H = 22;

      const isCollapsed = () => localStorage.getItem(COLLAPSE_KEY) === "1";
      const setCollapsed = (v) => localStorage.setItem(COLLAPSE_KEY, v ? "1" : "0");
      const log = (...a) => DEBUG && console.log("[zed-market-data]", ...a);

      // Init stores if missing
      for (const k of [MARKET_KEY, HISTORY_KEY]) {
        if (!localStorage.getItem(k)) localStorage.setItem(k, JSON.stringify({}));
      }

      // ===============================
      // Utils
      // ===============================
      const normalizeName = (name) => (name || "").replace(/[★☆]/g, "").trim().toLowerCase();
      const getPinnedItems = () => [...new Set(JSON.parse(localStorage.getItem(PINNED_KEY) || "[]").map(normalizeName))];
      const setPinnedItems = (items) => localStorage.setItem(PINNED_KEY, JSON.stringify([...new Set(items.map(normalizeName))]));
      const getMarket = () => JSON.parse(localStorage.getItem(MARKET_KEY) || "{}");
      const setMarket = (obj) => localStorage.setItem(MARKET_KEY, JSON.stringify(obj || {}));
      const getHistory = () => JSON.parse(localStorage.getItem(HISTORY_KEY) || "{}");
      const setHistory = (obj) => localStorage.setItem(HISTORY_KEY, JSON.stringify(obj || {}));

      function appendHistory(name, price) {
  try {
    const now = Date.now();
    const bag = getHistory();
    const k = normalizeName(name);
    const cur = bag[k] || { v: 2, h: [] };
    const last = cur.h.at?.(-1);
    const lastTs = Number(last?.[0] || 0);
    const lastVal = Number(last?.[1]);

    // append if price changed OR last update was > 60 minutes ago
    if (!last || lastVal !== Number(price) || (now - lastTs) > 60 * 60 * 1000) {
      cur.h.push([now, Number(price)]);
      if (cur.h.length > HISTORY_LIMIT) cur.h = cur.h.slice(-HISTORY_LIMIT);
      bag[k] = cur;
      setHistory(bag);
    }
  } catch (e) {
    console.error("[zed-market-data] appendHistory failed:", e);
  }
}

      function extractItems(apiResponse) {
        if (!apiResponse) return null;
        if (Array.isArray(apiResponse.items)) return apiResponse.items;
        if (Array.isArray(apiResponse)) return apiResponse; // tolerate bare array
        if (apiResponse.data && Array.isArray(apiResponse.data.items)) return apiResponse.data.items;
        return null;
      }



      // ===============================
      // Capture market prices (XHR + fetch hooks)
      // ===============================
      const _open = XMLHttpRequest.prototype.open;
      const _send = XMLHttpRequest.prototype.send;
      XMLHttpRequest.prototype.open = function (method, url) { this._zed_url = url; return _open.apply(this, arguments); };
      XMLHttpRequest.prototype.send = function () {
        this.addEventListener("readystatechange", function () {
          if (this.readyState === 4) {
            try {
              const url = this._zed_url || "";
              if (typeof url === "string" && url.includes("getMarket") && !url.includes("getMarketUser")) {
                const resp = JSON.parse(this.responseText);
                saveMarketPrices(resp);
              }

              // Also capture stats for instant timers
              if (typeof url === "string" && url.includes("getStats")) {
                try {
                  const stats = JSON.parse(this.responseText);
                  if (window.zedApplyStats) window.zedApplyStats(stats);
                } catch(_) {}
              }

            } catch (_) {}
          }
        });
        return _send.apply(this, arguments);
      };

      const _origFetch = window.fetch;
      window.fetch = async function (...args) {
        const res = await _origFetch.apply(this, args);
        try {
          const req = args[0];
          const url = typeof req === "string" ? req : (req && req.url) || "";
          if (url.includes("getMarket") && !url.includes("getMarketUser")) {
            const clone = res.clone();
            const json = await clone.json();
            saveMarketPrices(json);
          }
        } catch (_) {}
        return res;
      };

      // ===============================
      // Active poller with backoff
      // ===============================
      let pollInFlight = false;
      let backoffMs = RETRY_MIN_MS;

      function scheduleNextAllowed(delayMs) {
        const when = Date.now() + delayMs;
        localStorage.setItem(NEXT_ALLOWED_KEY, String(when));
        return when;
      }

      async function pollMarketOnce() {
        if (pollInFlight) return;
        pollInFlight = true;
        try {
          const controller = new AbortController();
          const timeout = setTimeout(() => controller.abort(), 15000);
          const res = await _origFetch("https://api.zed.city/getMarket", {
      signal: controller.signal,
      cache: "no-store",
      mode: "cors",
      credentials: "include",
    });
          clearTimeout(timeout);
          if (!res.ok) throw new Error(`HTTP ${res.status}`);
          const json = await res.json();
          saveMarketPrices(json);

          // success → reset backoff & schedule next at POLL_MS
          backoffMs = RETRY_MIN_MS;
          scheduleNextAllowed(POLL_MS);
        } catch (e) {
          log("poll error:", e?.message || e);
          // failure → exponential backoff (min 5s) + jitter
          const jitter = Math.floor(Math.random() * 1000);
          backoffMs = Math.min(Math.max(backoffMs * 2, RETRY_MIN_MS), RETRY_MAX_MS);
          scheduleNextAllowed(backoffMs + jitter);
        } finally {
          pollInFlight = false;
        }
      }

          function startMarketPoller() {
        if (!localStorage.getItem(NEXT_ALLOWED_KEY)) {
          localStorage.setItem(NEXT_ALLOWED_KEY, "0");
        }

        const now = Date.now();
        const nextAllowed = Number(localStorage.getItem(NEXT_ALLOWED_KEY) || 0);
        const lastSuccess = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0);
        if (now >= nextAllowed && (now - lastSuccess >= POLL_MS)) {
          pollMarketOnce();
        }

        // 1s tick to decide whether we can poll
        setInterval(() => {
          const now = Date.now();
          const nextAllowed = Number(localStorage.getItem(NEXT_ALLOWED_KEY) || 0);
          const lastSuccess = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0);
          if (now >= nextAllowed && (now - lastSuccess >= POLL_MS)) {
            pollMarketOnce();
          }
        }, 1000);

        document.addEventListener("visibilitychange", () => {
          if (!document.hidden) {
            const now = Date.now();
            const nextAllowed = Number(localStorage.getItem(NEXT_ALLOWED_KEY) || 0);
            const lastSuccess = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0);
            if (now >= nextAllowed && (now - lastSuccess >= POLL_MS)) {
              pollMarketOnce();
            }
          }
        });
      }

          function saveMarketPrices(apiResponse) {
        try {
          const items = extractItems(apiResponse);
          if (!items) return;

          const market = getMarket();
          for (const item of items) {
            if (!item || typeof item.name !== "string") continue;
            const name = item.name;
            const price = Number(item.market_price);
            if (!Number.isFinite(price)) continue;
            market[name] = price;
            appendHistory(name, price);
          }
          setMarket(market);

          const now = Date.now();
          localStorage.setItem(LAST_SUCCESS_KEY, String(now));
          // After success, next attempt is after POLL_MS
          localStorage.setItem(NEXT_ALLOWED_KEY, String(now + POLL_MS));

          window.dispatchEvent(new CustomEvent("zed:marketDataUpdated", { detail: now }));
        } catch (e) {
          console.error("[zed-market-data] saveMarketPrices failed:", e);
        }
      }


      // ---------- Market Favs ----------
      function run_MarketFavs(){



    (function () {



      // ===============================
      // UI helpers
      // ===============================
      function getMarketHost() {
        return (
          document.querySelector(".zed-grid.has-title.has-content") ||
          document.querySelector(".zed-grid.has-content") ||
          document.querySelector(".zed-grid") ||
          document.querySelector(".q-page-container") ||
          document.querySelector(".q-px-xs")
        );
      }

    function findNavShell() {
      // inner row with the <a> tabs
      const tabsContent = document.querySelector(
        ".q-tabs__content.scroll--mobile.row.no-wrap.items-center.self-stretch.hide-scrollbar.relative-position.q-tabs__content--align-center"
      );
      if (!tabsContent) return null;

      // the q-tabs component
      const qtabs = tabsContent.closest(".q-tabs");

      // the outer bar wrapper you showed (preferred anchor)
      const shell =
        qtabs?.closest(".gt-xs.bg-grey-3.text-grey-5.text-h6") ||
        qtabs?.parentElement || // fallback: parent of .q-tabs
        tabsContent;

      return shell;
    }

    function findStickyAncestor(el) {
      let cur = el;
      while (cur && cur !== document.body) {
        const cs = getComputedStyle(cur);
        if (cs.position === "sticky" || cs.position === "fixed") return cur;
        cur = cur.parentElement;
      }
      return null;
    }

    function ensurePinnedBar() {
      // the inner row with the <a> tabs
      const tabsContent = document.querySelector(
        ".q-tabs__content.scroll--mobile.row.no-wrap.items-center.self-stretch.hide-scrollbar.relative-position.q-tabs__content--align-center"
      );
      if (!tabsContent) return null;

      // (re)use/create bar
      let pinnedDiv = document.getElementById("pinnedItems");
      if (!pinnedDiv) {
        pinnedDiv = document.createElement("div");
        pinnedDiv.id = "pinnedItems";
        pinnedDiv.style = `
          background: rgba(0,0,0,0.5);
          border: 1px solid #666;
          padding: 5px 0px;
          margin: 0px 0 0px;
          border-radius: 0px;
          color: #fff;
          font-size: 12px;
          width: 100%;
          box-sizing: border-box;
          display: block;
          flex: 0 0 100%;
          align-self: stretch;
          position: relative; /* normal flow so it scrolls away */
        `;
      }

      // Find the sticky/fixed ancestor (likely the header)
      const sticky = findStickyAncestor(tabsContent) ||
                     tabsContent.closest(".q-header, .q-layout__header");

      // Prefer to insert as the FIRST child of the main scrolling container
      const pageContainer = (sticky && sticky.nextElementSibling && sticky.nextElementSibling.matches(".q-page-container"))
        ? sticky.nextElementSibling
        : document.querySelector(".q-page-container") ||
          document.querySelector(".zed-grid.has-title.has-content, .zed-grid.has-content, .zed-grid") ||
          document.querySelector(".q-page"); // fallbacks

      if (pageContainer) {
        // Put our bar at the very top of the scrolled page content (below the header)
        pageContainer.insertBefore(pinnedDiv, pageContainer.firstChild);
        pinnedDiv.dataset.anchor = "below-sticky-header";
        return pinnedDiv;
      }

      // Last resort: place right after the sticky header element itself
      if (sticky && sticky.parentNode) {
        sticky.insertAdjacentElement("afterend", pinnedDiv);
        pinnedDiv.dataset.anchor = "below-sticky-header";
        return pinnedDiv;
      }

      // If we got here, just append to body (still scrolls, but not ideal layout-wise)
      if (!pinnedDiv.parentElement) document.body.appendChild(pinnedDiv);
      return pinnedDiv;
    }



      function renderSparkline(el, series, w = SPARK_W, h = SPARK_H, pad = 2) {
        el.innerHTML = "";
        if (!series || series.length < 2) return;
        const c = document.createElement("canvas");
        c.width = w; c.height = h;
        el.appendChild(c);
        const ctx = c.getContext("2d");

        const ys = series.map(p => Number(p[1])).filter(Number.isFinite);
        const n = ys.length; if (n < 2) return;

        const min = Math.min(...ys), max = Math.max(...ys);
        const span = Math.max(1, max - min);
        const x = (i) => pad + (i * (w - pad * 2)) / (n - 1);
        const y = (val) => h - pad - ((val - min) * (h - pad * 2)) / span;

        ctx.lineWidth = 1;
        ctx.beginPath();
        ctx.moveTo(x(0), y(ys[0]));
        for (let i = 1; i < n; i++) ctx.lineTo(x(i), y(ys[i]));
        ctx.stroke();

        ctx.beginPath();
        ctx.arc(x(n - 1), y(ys[n - 1]), 1.5, 0, Math.PI * 2);
        ctx.fill();
      }

      // ===============================
      // UI: Pinned bar + toggle
      // ===============================
      function renderPinnedItems() {
          if (document.documentElement.hasAttribute('data-mail')) return;
        try {
          const host = getMarketHost();
          if (!host) { setTimeout(renderPinnedItems, 500); return; }
          const pinnedDiv = ensurePinnedBar(host);
          const pinned = getPinnedItems();
          const market = getMarket();
          const history = getHistory();

          pinnedDiv.innerHTML = "";

          // Header
          const header = document.createElement("div");
          header.style = "display:flex;align-items:center;gap:10px;justify-content:space-between;flex-wrap:wrap;";

          const leftHdr = document.createElement("div");
          leftHdr.style = "display:flex;align-items:center;gap:8px;";

          const title = document.createElement("strong");
          title.textContent = "⭐ Pinned Items";

          const tsSpan = document.createElement("span");
          tsSpan.id = "pinned-last-updated";
          tsSpan.style = "opacity:.8;font-size:12px;";

          leftHdr.appendChild(title);
          leftHdr.appendChild(tsSpan);

          const rightHdr = document.createElement("div");
          rightHdr.style = "display:flex;align-items:center;gap:8px;";

          const tip = document.createElement("div");
          tip.textContent = "";
          tip.style = "opacity:.8;font-size:12px;";

          const toggleBtn = document.createElement("button");
          toggleBtn.textContent = isCollapsed() ? "▸" : "▾";
          toggleBtn.title = (isCollapsed() ? "Show" : "Hide") + " market ticker";
          toggleBtn.style = `
            cursor:pointer;border:1px solid #666;border-radius:6px;background:rgba(255,255,255,0.08);
            color:#fff;font-size:12px;line-height:1;padding:4px 8px;
          `;
          toggleBtn.onclick = () => { setCollapsed(!isCollapsed()); renderPinnedItems(); };

          rightHdr.appendChild(tip);
          rightHdr.appendChild(toggleBtn);
          header.appendChild(leftHdr);
          header.appendChild(rightHdr);
          pinnedDiv.appendChild(header);

          if (pinned.length === 0) {
            pinnedDiv.appendChild(Object.assign(document.createElement("i"), { textContent: "No pinned items" }));
            return;
          }

          const lastTs = Number(localStorage.getItem(LAST_SUCCESS_KEY) || 0) ||
            Object.values(history).map(v => v?.h?.at?.(-1)?.[0]).filter(Boolean).sort((a, b) => b - a)[0];
          if (lastTs) {
            const age = Date.now() - lastTs;
            const mins = Math.floor(age / 60000);
            tsSpan.textContent = `Last updated: ${mins ? `${mins}m` : "just now"} ago`;
          } else {
            tsSpan.textContent = `(waiting for market data…)`;
          }

          // Collapsed? keep header only.
          if (isCollapsed()) return;

          // Rows container (compact)
          const rows = document.createElement("div");
          rows.style = `
            display:grid;
            grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); /* was 260px */
            gap: 4px 8px;            /* tighter spacing */
            margin-top: 6px;         /* compact */
            width: 100%;
          `;
          pinnedDiv.appendChild(rows);

          for (const pinnedName of pinned) {
            const displayKey = Object.keys(market).find(k => normalizeName(k) === pinnedName);
            const displayName = displayKey || pinnedName;
            const price = displayKey ? market[displayKey] : null;

            const hist = history[pinnedName] || history[normalizeName(displayName)] || history[normalizeName(pinnedName)];
            const series = hist?.h || [];
            const first = series.length ? series[0][1] : null;
            const last = series.length ? series.at?.(-1)?.[1] : price;

            const pct = (first && last) ? ((Number(last) - Number(first)) / Number(first)) * 100 : null;
            const isUp = pct != null && pct >= 0;

            const isStale = series.length
              ? (Date.now() - (series.at?.(-1)?.[0] || 0) > STALE_MS)
              : true;

            const row = document.createElement("div");
            row.style = `
              display:flex; align-items:center; justify-content:space-between;
              column-gap: 8px;
              padding:4px 6px;                 /* compact */
              border-radius:6px;
              border:1px solid rgba(255,255,255,0.08);
              background: rgba(255,255,255,0.04);
              ${isStale ? "opacity:.85;" : ""}
            `;

            const left = document.createElement("div");
            left.style = "display:flex;align-items:center;gap:6px;min-width:0;flex:1;";

            const nameSpan = document.createElement("span");
            nameSpan.textContent = displayName;
            nameSpan.style = `
              cursor:pointer;
              white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
              flex: 1 1 50%;     /* ensure ~1/2+ width before truncation */
              min-width: 45%;
            `;
            nameSpan.title = "Click to search this item";
            nameSpan.onclick = () => {
              const input = document.querySelector("input[type='text']");
              if (input) {
                input.value = displayName;
                input.dispatchEvent(new Event("input", { bubbles: true }));
              }
            };

            const spark = document.createElement("span");
            spark.className = "spark";
            spark.style = `
              display:inline-block;
              width:${SPARK_W}px; height:${SPARK_H}px;
              flex: 0 0 ${SPARK_W}px;
            `;

            left.appendChild(nameSpan);
            left.appendChild(spark);

            const right = document.createElement("div");
            right.style = "display:flex;align-items:center;gap:6px;flex:0 0 auto;";

            const priceSpan = document.createElement("span");
            priceSpan.textContent = price != null ? `$${Number(price).toLocaleString()}` : "N/A";
            priceSpan.style = "font-variant-numeric: tabular-nums;";

            const pctSpan = document.createElement("span");
            if (pct != null && isFinite(pct)) {
              pctSpan.textContent = `${isUp ? "+" : ""}${pct.toFixed(1)}%`;
              pctSpan.style = `
                font-size:12px;padding:1px 6px;border-radius:999px;
                background:${isUp ? "rgba(0,200,0,.15)" : "rgba(220,0,0,.15)"};
                color:${isUp ? "#7CFC9A" : "#FFAAAA"};
                white-space:nowrap;
              `;
            }

            right.appendChild(priceSpan);
            if (pctSpan.textContent) right.appendChild(pctSpan);

            row.appendChild(left);
            row.appendChild(right);
            rows.appendChild(row);

            try { renderSparkline(spark, series); } catch (_) {}
          }
        } catch (e) {
          console.error("[zed-market-data] renderPinnedItems failed:", e);
        }
      }

      // ===============================
      // Stars in market list
      // ===============================
      function injectStarsIntoMarketItems() {
        const items = document.querySelectorAll(".q-item");
        if (!items.length) return;
        const pinned = getPinnedItems();

        items.forEach((item) => {
          const label = item.querySelector(".q-item__label");
          if (!label) return;

          const rawText = label.textContent.trim();
          const itemName = rawText.split("\n")[0].trim();
          if (!itemName) return;

          const normName = normalizeName(itemName);

          let star = label.querySelector(".market-fav-star");
          const storedName = star?.dataset?.itemName;

          if (!star || normalizeName(storedName) !== normName) {
            if (star) star.remove();

            label.style.position = "relative";

            star = document.createElement("span");
            star.className = "market-fav-star";
            star.innerHTML = pinned.includes(normName) ? "★" : "☆";
            star.dataset.itemName = itemName;
            star.title = "Click to pin/unpin";
            star.style.cssText = `
              position:absolute; top:2px; right:5px; font-size:1.2em;
              cursor:pointer; color:gold; z-index:10; user-select:none;
            `;

            star.onclick = (e) => {
              e.stopPropagation();
              e.preventDefault();

              let list = getPinnedItems();
              const index = list.findIndex((p) => p === normName);

              if (index >= 0) {
                list.splice(index, 1);
                star.innerHTML = "☆";
              } else {
                if (list.length >= PINNED_ITEM_LIMIT) {
                  alert(`You can only pin up to ${PINNED_ITEM_LIMIT} items.`);
                  return;
                }
                list.push(normName);
                star.innerHTML = "★";
              }

              setPinnedItems(list);
              renderPinnedItems();
            };

            label.appendChild(star);
            log("Injected/Updated star:", itemName);
          } else {
            star.innerHTML = pinned.includes(normName) ? "★" : "☆";
          }
        });
      }

      function hookSearchBar() {
        const input = document.querySelector("input[type='text']");
        if (!input) { setTimeout(hookSearchBar, 500); return; }
        input.addEventListener("input", () => setTimeout(injectStarsIntoMarketItems, 300));
      }

      function observeMarket() {
          if (document.documentElement.hasAttribute('data-mail')) return;
        const container =
          document.querySelector(".zed-grid.has-title.has-content") ||
          document.querySelector(".zed-grid.has-content") ||
          document.querySelector(".zed-grid");

        if (!container) { setTimeout(observeMarket, 500); return; }

        const observer = new MutationObserver(() => { debounceInjectStars(); });
        observer.observe(container, { childList: true, subtree: true });

        injectStarsIntoMarketItems();
        renderPinnedItems();
        hookSearchBar();
      }

      let lastInjectTime = 0;
      function debounceInjectStars() {
        const now = Date.now();
        if (now - lastInjectTime > 300) { lastInjectTime = now; injectStarsIntoMarketItems(); }
      }

        // ===============================
        // URL handling
        // ===============================
        function updateMailFlag() {
            document.documentElement.toggleAttribute('data-mail', location.pathname.startsWith('/mail'));
        }

        // rAF-throttled renderer to avoid bursty reflows
        let _renderScheduled = false;
        function scheduleRender() {
            if (_renderScheduled) return;
            _renderScheduled = true;
            requestAnimationFrame(() => {
                _renderScheduled = false;
                renderPinnedItems();
            });
        }

        function handleURL() {
            // set/clear the mail flag every time we "navigate"
            updateMailFlag();

            // Render the bar anywhere a suitable host exists (not just /market)
            setTimeout(() => { if (getMarketHost()) scheduleRender(); }, 300);

            // Stars only make sense on the market page
            if (location.pathname.includes("/market")) setTimeout(observeMarket, 500);
        }

        // ===============================
        // Boot
        // ===============================
        // Hide pinned ticker completely on /mail via attribute on <html>
        (() => {
            const hideTickerCSS = document.createElement('style');
            hideTickerCSS.textContent = `html[data-mail] #pinnedItems{display:none!important;}`;
            document.head.appendChild(hideTickerCSS);
        })();

        window.addEventListener("zed:marketDataUpdated", scheduleRender);
        window.addEventListener("hashchange", handleURL);
        window.addEventListener("popstate", handleURL);

        // Wrap history once (don’t re-wrap elsewhere)
        const _pushState = history.pushState;
        history.pushState = function (...args) { const r = _pushState.apply(this, args); handleURL(); return r; };
        const _replaceState = history.replaceState;
        history.replaceState = function (...args) { const r = _replaceState.apply(this, args); handleURL(); return r; };

        // Lightweight route watcher: history hooks + mild pathname poll
        (() => {
            document.addEventListener('visibilitychange', () => { if (!document.hidden) handleURL(); });

            let last = location.pathname;
            setInterval(() => {
                const cur = location.pathname;
                if (cur !== last) { last = cur; handleURL(); }
            }, 1500); // was 250ms; reduce wakeups
        })();

        console.log("[zed-market-data] loaded");
        startMarketPoller();
        handleURL(); // initial run

    })();
      }

      // ---------- Profit Helper ----------
      function run_ProfitHelper(){


    (() => {
      // ====== DEBUG ======
      const DEBUG_NUMBER_BADGES = false;
      const DEBUG_CONSOLE = false;

      // ====== PRICING CONFIG ======
      const MARKET_KEY = "Zed-market-data";
      const PRICE_FALLBACK = "buy"; // fallback to vars.buy (else vars.sell) per unit

      // ====== BENCH RECIPE CACHE (1 hour) ======
      const RECIPES_CACHE_KEY = "Zed-recipes-cache-v1";
      const RECIPES_CACHE_TTL = 3600_000; // 1 hour

      // ====== ROUTING ======
      const STRONGHOLD_RE = /\/stronghold\/\d+/;
      const isOnStronghold = () => STRONGHOLD_RE.test(location.pathname) || location.href.includes("/stronghold/");

      // ====== BENCH TYPES ======
      const BENCH_TYPES = [
        "crafting_bench","medical_bay","tech_lab","materials_bench",
        "armour_bench","ammo_bench","chem_bench","kitchen","furnace","weapon_bench"
      ];

      // ====== ACTION WORDS ======
      const ACTIONS = ["Craft","Salvage","Combine","Recycle","Bulk Recycle","Smelt","Forge","Burn","Purify","Create"];
      const ACTION_RE = new RegExp(`^(?:${ACTIONS.map(a => a.replace(/\s+/g,"\\s+")).join("|")})\\b`, "i");

      // ====== RADIO TOWER (card detection) ======
      function getRadioCardButtons(root=document){
        const spans = root.querySelectorAll('.q-btn .q-btn__content .block');
        return [...spans].filter(s => (s.textContent || "").trim().toLowerCase() === "trade")
          .map(s => s.closest('.q-btn'));
      }
      const RADIO_CARD_CONTAINER = (btn) => btn?.closest('.q-pa-md') || btn?.closest('.grid-cont') || btn?.closest('.zed-grid');

      // ====== STATE ======
      const processedBenchNodes = new WeakSet();
      const processedRadio = new WeakSet();
      const lastVals = new WeakMap();
      let recipeIndexes = {};
      let recipeIndexesNorm = {};
      let radioTrades = null;
      let rerenderQueued = false;
      let mo = null;
      let globalBadgeSeq = 0;

      // ====== UTILS ======
      const fmt = (n) => (Number.isFinite(n) ? `$${Math.round(n).toLocaleString()}` : "N/A");
      const safeNum = (v) => (Number.isFinite(+v) ? +v : 0);
      const collapseSpaces = (s) => String(s || "").replace(/\s+/g," ").trim();
      const normalizeTitle = (s) => collapseSpaces(s).replace(/\s*blueprint\s*$/i,"").toLowerCase();
      const log = (...a) => { if (DEBUG_CONSOLE) console.log("[ZedProfit]", ...a); };

      const getMarket = () => {
        try { return JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); }
        catch { return {}; }
      };
      const bestPriceForName = (name, vars = {}) => {
        const m = getMarket();
        if (m && m[name] != null) return +m[name]; // unit price from market cache
        const fb = PRICE_FALLBACK === "buy" ? vars?.buy : vars?.sell; // unit fallback
        if (fb != null) return +fb;
        return null;
      };
      const getQty = (obj) => {
        const v = obj?.qty ?? obj?.quantity ?? obj?.count ?? obj?.amount ?? obj?.vars?.qty;
        const n = Number(v);
        return Number.isFinite(n) && n > 0 ? n : 1;
      };

      // ====== CSS ======
      (function injectCSS(){
        if (document.getElementById("zed-craft-css")) return;
        const css = `
          .zed-craft-badge{
            margin-left:6px;
            font-size:12px;
            padding:1px 6px;
            border-radius:999px;
            display:inline-block;
            font-weight:bold;
            white-space:nowrap;
          }
          .zed-pos{ background: rgba(0,200,0,.15); color:#7CFC9A; } /* Market Favs positive */
          .zed-neg{ background: rgba(220,0,0,.15); color:#FFAAAA; } /* Market Favs negative */
          .zed-dim{ opacity:.85 }
        `;
        const style = document.createElement("style");
        style.id = "zed-craft-css";
        style.textContent = css;
        document.head.appendChild(style);
      })();

      // ====== CACHE HELPERS ======
      function readRecipesCache() {
        try { return JSON.parse(localStorage.getItem(RECIPES_CACHE_KEY) || "{}"); }
        catch { return {}; }
      }
      function writeRecipesCache(cacheObj) {
        try { localStorage.setItem(RECIPES_CACHE_KEY, JSON.stringify(cacheObj)); } catch {}
      }
      function getCachedBenchJson(type) {
        const cache = readRecipesCache();
        const entry = cache[type];
        if (!entry) return null;
        if ((Date.now() - (entry.ts || 0)) > RECIPES_CACHE_TTL) return null;
        return entry.json || null;
      }
      function setCachedBenchJson(type, json) {
        const cache = readRecipesCache();
        cache[type] = { ts: Date.now(), json };
        writeRecipesCache(cache);
      }

      // ====== FETCHERS: BENCHES ======
      async function fetchRecipesForBench(type) {
        const cached = getCachedBenchJson(type);
        if (cached) return indexRecipes(cached);

        const res = await fetch(`https://api.zed.city/getRecipes?type=${encodeURIComponent(type)}`, { credentials:"include" });
        if (!res.ok) throw new Error(`getRecipes failed for ${type}`);
        const json = await res.json();
        setCachedBenchJson(type, json);
        return indexRecipes(json);
      }

      function indexRecipes(recipesJson) {
        const orig = new Map();
        const norm = new Map();
        const list = Array.isArray(recipesJson?.items) ? recipesJson.items : [];
        for (const r of list) {
          const displayName = collapseSpaces(r?.name || "");
          const reqs = r?.vars?.items || {};
          const outObj = r?.vars?.output || {};
          const outputs = [];
          for (const k of Object.keys(outObj)) {
            const item = outObj[k];
            if (!item) continue;
            outputs.push({ name: item.name || k, item, qty: getQty(item) });
          }
          const rec = { reqs, outputs, displayName };
          orig.set(displayName, rec);
          const keyNorm = normalizeTitle(displayName);
          if (keyNorm) norm.set(keyNorm, rec);
        }
        return { orig, norm };
      }

      async function loadAllRecipes() {
        recipeIndexes = {};
        recipeIndexesNorm = {};
        await Promise.all(BENCH_TYPES.map(async type => {
          try {
            const { orig, norm } = await fetchRecipesForBench(type);
            recipeIndexes[type] = orig;
            recipeIndexesNorm[type] = norm;
          } catch {
            recipeIndexes[type] = new Map();
            recipeIndexesNorm[type] = new Map();
          }
        }));
      }

      // ====== FETCHERS: RADIO TOWER (no cache on purpose) ======
      async function getStrongholdJson() {
        const res = await fetch(`https://api.zed.city/getStronghold`, { credentials:"include" });
        if (!res.ok) throw new Error("getStronghold failed");
        return res.json();
      }
      function findRadioTowerId(strongholdJson) {
        const buildings = strongholdJson?.stronghold || strongholdJson || {};
        for (const k of Object.keys(buildings)) {
          const b = buildings[k];
          if (b?.codename === "radio_tower") return Number(b.id || k);
        }
        return null;
      }
      async function fetchRadioTrades(radioId) {
        let res = await fetch(`https://api.zed.city/getRadioTower?id=${encodeURIComponent(radioId)}`, { credentials:"include" });
        if (!res.ok) {
          res = await fetch(`https://api.zed.city/getRadioTower`, { credentials:"include" });
          if (!res.ok) throw new Error("getRadioTower failed");
        }
        const json = await res.json();
        return indexRadioTrades(json);
      }
      function indexRadioTrades(json) {
        const orig = new Map();
        const norm = new Map();
        const list = [];
        const items = Array.isArray(json?.items) ? json.items : (Array.isArray(json) ? json : []);
        for (const t of items) {
          const displayName = collapseSpaces(t?.name || "");
          const reqs = t?.vars?.items || {};
          const outObj = t?.vars?.output || {};
          const outputs = [];
          for (const k of Object.keys(outObj)) {
            const item = outObj[k];
            if (!item) continue;
            outputs.push({ name: item.name || k, item, qty: getQty(item) });
          }
          const rec = { reqs, outputs, displayName, isRadio:true };
          list.push(rec);
          if (displayName) {
            orig.set(displayName, rec);
            norm.set(normalizeTitle(displayName), rec);
          }
        }
        return { list, orig, norm };
      }

      // ====== COMPUTE ======
      function sumIngredientCost(itemReqs) {
        let total = 0, missing = [];
        for (const key of Object.keys(itemReqs || {})) {
          const req = itemReqs[key];
          if (!req?.name) continue;
          const unit = bestPriceForName(req.name, req.vars || {});
          const qty = safeNum(req.req_qty ?? req.qty ?? 0);
          if (unit == null) missing.push(req.name);
          total += (unit ?? 0) * qty;
        }
        return { total, missing };
      }

      function lookupRecipeOrTrade(displayName) {
        const text = collapseSpaces(displayName);
        const keyNorm = normalizeTitle(text);

        // benches
        for (const type of BENCH_TYPES) {
          const byOrig = recipeIndexes[type];
          if (byOrig?.has(text)) return byOrig.get(text);
          const byNorm = recipeIndexesNorm[type];
          if (keyNorm && byNorm?.has(keyNorm)) return byNorm.get(keyNorm);
        }
        // radio
        if (radioTrades?.orig?.has(text)) return radioTrades.orig.get(text);
        if (keyNorm && radioTrades?.norm?.has(keyNorm)) return radioTrades.norm.get(keyNorm);
        return null;
      }

      function computeRec(rec) {
        if (!rec) return null;
        const { total: cost, missing } = sumIngredientCost(rec.reqs);

        let valueTotal = 0;
        let anyVal = false;
        const outQtyNotes = [];

        for (const out of rec.outputs || []) {
          const unitFromMarket = bestPriceForName(out.name, out.item?.vars || {});
          let unitVal = unitFromMarket;
          if (unitVal == null && Number.isFinite(out.item?.value)) unitVal = +out.item.value; // per-unit fallback
          if (unitVal == null) continue;

          const qty = getQty(out.item) || out.qty || 1;
          valueTotal += unitVal * qty;
          anyVal = true;
          if (qty > 1) outQtyNotes.push(`${out.name}×${qty}`);
        }

        const value = anyVal ? valueTotal : null;
        const profit = (Number.isFinite(cost) && Number.isFinite(value)) ? (value - cost) : null;
        return { cost, value, profit, missing, outQtyNotes };
      }

      // ====== BENCH MATCHER (permissive) ======
      function isBenchRow(el) {
        if (!el || el.nodeType !== 1) return false;
        const hasClassCol = el.classList?.contains("col");
        const likelyContainer = hasClassCol || /^(DIV|SPAN|A|BUTTON)$/i.test(el.tagName);
        if (!likelyContainer) return false;
        const t = collapseSpaces(el.textContent || "");
        if (!t) return false;
        if (ACTION_RE.test(t)) return true;
        if (/\bBlueprint$/i.test(t)) return true;
        return false;
      }

      // ====== DOM ======
      function makeOrUpdateBadge(el, data, debugId=null) {
        const prev = lastVals.get(el) || {};
        const round = (x) => Number.isFinite(x) ? Math.round(x) : NaN;
        if (round(prev.cost) === round(data.cost) &&
            round(prev.value) === round(data.value) &&
            round(prev.profit) === round(data.profit)) return;

        let badge = el.querySelector(":scope > .zed-craft-badge");
        let txtCore = `${fmt(data.cost)} → ${fmt(data.value)} (${data.profit > 0 ? "+" : ""}${fmt(data.profit).replace("$","")})`;
        if (DEBUG_NUMBER_BADGES && debugId != null) txtCore += ` [#${debugId}]`;

        if (!badge) {
          badge = document.createElement("span");
          badge.className = "zed-craft-badge";
          el.appendChild(badge);
        }
        badge.textContent = txtCore;

        // Market Favs logic: zero treated as positive
        badge.classList.remove("zed-pos", "zed-neg");
        if (!Number.isFinite(data.profit) || data.profit >= 0) badge.classList.add("zed-pos");
        else badge.classList.add("zed-neg");

        const missingNames = (data.missing || []);
        badge.classList.toggle("zed-dim", missingNames.length > 0);
        const parts = [];
        if (missingNames.length) parts.push(`Missing prices: ${missingNames.join(", ")}`);
        if (data.outQtyNotes?.length) parts.push(`Outputs: ${data.outQtyNotes.join(", ")}`);
        badge.title = parts.join(" • ");

        lastVals.set(el, { cost: data.cost, value: data.value, profit: data.profit });
      }

      // ====== RENDER ======
      function renderBenches(root=document){
        if (!Object.keys(recipeIndexes).length) return;

        const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
          acceptNode(node) {
            return (!processedBenchNodes.has(node) && isBenchRow(node))
              ? NodeFilter.FILTER_ACCEPT
              : NodeFilter.FILTER_SKIP;
          }
        });

        let node;
        while ((node = walker.nextNode())) {
          // If this match is inside an .item-row that already has *any* badge, skip.
          const row = node.closest?.('.item-row');
          if (row && (row.dataset.zedBadgeDone === "1" || row.querySelector('.zed-craft-badge'))) {
            processedBenchNodes.add(node);
            continue;
          }

          const title = collapseSpaces(node.textContent || "");
          const rec = lookupRecipeOrTrade(title);
          const data = computeRec(rec);
          if (!data) { processedBenchNodes.add(node); continue; }

          // Place badge here (first hit in this row wins)
          const host = document.createElement("span");
          node.appendChild(host);
          const id = ++globalBadgeSeq;
          makeOrUpdateBadge(host, data, id);

          // Mark the entire row as done so later matches inside it are ignored
          if (row) row.dataset.zedBadgeDone = "1";

          processedBenchNodes.add(node);
        }
      }

      function renderRadio(root=document){
        if (!radioTrades?.list?.length) return;
        const buttons = getRadioCardButtons(root);
        buttons.forEach((btn, i) => {
          const container = RADIO_CARD_CONTAINER(btn) || btn.parentElement;
          if (!container || processedRadio.has(container) || container.querySelector(".zed-craft-badge")) return;

          const rec = radioTrades.list[i];
          const data = computeRec(rec);
          if (!data) { processedRadio.add(container); return; }

          const hostWrap = document.createElement("div");
          hostWrap.style.marginTop = "6px";
          (btn.parentElement || container).appendChild(hostWrap);

          const id = ++globalBadgeSeq;
          makeOrUpdateBadge(hostWrap, data, id);

          processedRadio.add(container);
        });
      }

      function renderPass(root=document){
        renderBenches(root);
        renderRadio(root);
      }

      function queueRender(root=document){
        if (rerenderQueued) return;
        rerenderQueued = true;
        requestAnimationFrame(() => {
          rerenderQueued = false;
          renderPass(root);
        });
      }

      // ====== OBSERVERS & ROUTING ======
      function connectObserver() {
        if (mo) mo.disconnect();
        mo = new MutationObserver((muts) => {
          for (const m of muts) for (const n of m.addedNodes || []) {
            if (n.nodeType === 1) queueRender(n);
          }
        });
        mo.observe(document.body, { childList:true, subtree:true });
      }
      function disconnectObserver(){ if (mo) { mo.disconnect(); mo = null; } }

      async function initForPage() {
        try {
          if (!isOnStronghold()) {
            recipeIndexes = {};
            recipeIndexesNorm = {};
            radioTrades = null;
            connectObserver();
            return;
          }

          // Benches (cached)
          await loadAllRecipes();

          // Radio Tower (live)
          try {
            const sh = await getStrongholdJson();
            const radioId = findRadioTowerId(sh);
            radioTrades = radioId ? await fetchRadioTrades(radioId) : null;
          } catch {
            radioTrades = null;
          }

          connectObserver();
          queueRender(document);
        } catch (e) {
          if (DEBUG_CONSOLE) console.warn(e);
        }
      }

      let lastURL = location.href;
      function onRouteChange() {
        if (location.href === lastURL) return;
        lastURL = location.href;

        processedBenchNodes.clear?.();
        processedRadio.clear?.();
        lastVals.clear?.();
        disconnectObserver();

        // Keep localStorage cache; just clear in-memory
        recipeIndexes = {};
        recipeIndexesNorm = {};
        radioTrades = null;

        requestAnimationFrame(() => {
          initForPage();
          setTimeout(() => queueRender(document), 150);
        });
      }

      // SPA hooks
      const _ps = history.pushState;
      history.pushState = function (...a) { const r = _ps.apply(this, a); onRouteChange(); return r; };
      const _rs = history.replaceState;
      history.replaceState = function (...a) { const r = _rs.apply(this, a); onRouteChange(); return r; };
      window.addEventListener("popstate", onRouteChange);
      document.addEventListener("click", (e) => {
        const a = e.target.closest?.("a[href]");
        if (!a) return;
        setTimeout(onRouteChange, 0);
      }, { capture:true });

      // Safety net
      const hintObserver = new MutationObserver((muts) => {
        for (const m of muts) for (const n of m.addedNodes || []) {
          if (n.nodeType !== 1) continue;
          if (/\b(Craft|Salvage|Combine|Recycle|Blueprint|Trade|Smelt|Forge|Burn|Purify|Create)\b/i.test(n.textContent || "")) {
            initForPage(); hintObserver.disconnect(); return;
          }
        }
      });
      hintObserver.observe(document.documentElement, { childList:true, subtree:true });

      // Re-render when market cache changes
      window.addEventListener("zed:marketDataUpdated", () => queueRender(document));
      window.addEventListener("storage", (e) => { if (e.key === MARKET_KEY) queueRender(document); });

      // Kick off
      initForPage();
    })();
      }






    /* ===== Inventory Net Worth ===== */
           function run_networth(){

    (() => {
      const MARKET_KEY = "Zed-market-data";

      const API_ITEMS = "https://api.zed.city/loadItems";
      const API_STATS = "https://api.zed.city/getStats";

      const INVENTORY_GRID_SEL = 'div.zed-grid.has-title.has-content';
      const PANEL_ID = 'inventory-networth-api';
      const INVENTORY_RE = /\/inventory(?:$|[?#])/;

      // --- state
      let panel = null;
      let itemsCache = null;
      let cashBalance = null;
      let lastMarketStr = null;

      // --- utils
      const onInventory = () => INVENTORY_RE.test(location.pathname) || location.href.includes('/inventory');
      const $ = (sel, root=document) => root.querySelector(sel);
      const money = n => Number.isFinite(n) ? ('$' + Math.round(n).toLocaleString()) : 'N/A';
      const parseCash = (obj) => {
        // Try a few common shapes safely
        const cands = [
          obj?.money, obj?.cash, obj?.balance, obj?.stats?.money, obj?.user?.money, obj?.data?.money
        ].filter(v => Number.isFinite(+v));
        return cands.length ? +cands[0] : null;
      };
      const readMarket = () => {
        const s = localStorage.getItem(MARKET_KEY) || "{}";
        if (s === lastMarketStr) return null;
        lastMarketStr = s;
        try { return JSON.parse(s); } catch { return {}; }
      };

      // Find the inventory grid by structure (hash-proof)
      function findInventoryGrid() {
        try {
          const q = document.querySelector(`${INVENTORY_GRID_SEL}:has(.grid-cont .q-list)`);
          if (q) return q;
        } catch {}
        // Fallback: scan candidates for the expected inner structure
        const grids = document.querySelectorAll(INVENTORY_GRID_SEL);
        for (const el of grids) {
          if (el.querySelector('.grid-cont .q-list')) return el;
        }
        return null;
      }

      function ensurePanel() {
        if (panel && panel.isConnected) return panel;
        const bottom = findInventoryGrid();
        if (!bottom || !bottom.parentNode) return null;

        const p = document.createElement('div');
        p.id = PANEL_ID;
        p.style.margin = '8px 0 10px';
        p.style.padding = '4px 4px';
        p.style.borderRadius = '4px';
        p.style.background = 'rgba(255,255,255,0.06)';
        p.style.border = '1px solid rgba(255,255,255,0.08)';
        p.style.display = 'flex';
        p.style.flexWrap = 'wrap';
        p.style.alignItems = 'center';
        p.style.gap = '4px';
        p.style.fontSize = '12px';

        const h = document.createElement('div');
        h.textContent = 'Net Worth';
        h.style.fontWeight = '700';
        h.style.marginRight = '8px';
        p.appendChild(h);

        const v = document.createElement('div');
        v.className = 'zed-nw-total';
        v.style.fontWeight = '700';
        v.style.padding = '2px 8px';
        v.style.borderRadius = '999px';
        v.style.background = 'rgba(0,200,0,.15)';
        v.style.color = '#7CFC9A';
        v.textContent = '$…';
        p.appendChild(v);

        const b = document.createElement('div');
        b.className = 'zed-nw-breakdown';
        b.style.opacity = '0.85';
        b.textContent = 'items: $0 + cash: $0';
        p.appendChild(b);

        bottom.parentNode.insertBefore(p, bottom);
        panel = p;
        return panel;
      }

      function setPanel(itemsVal, cashVal, priced, unpriced) {
        if (!panel) return;
        const total = (Number(itemsVal)||0) + (Number(cashVal)||0);
        const v = panel.querySelector('.zed-nw-total');
        const b = panel.querySelector('.zed-nw-breakdown');
        if (v) v.textContent = money(total);
        if (b) b.textContent = `items: ${money(itemsVal||0)} + cash: ${money(cashVal||0)} · unpriced: ${unpriced}`;
      }

      function computeFromCaches() {
        const market = readMarket() ?? JSON.parse(lastMarketStr || "{}");
        // items
        let itemsVal = 0, priced = 0, unpriced = 0;
        if (Array.isArray(itemsCache)) {
          for (const it of itemsCache) {
            const name = it?.name;
            const qty = Number(it?.quantity) || 0;
            const unit = (name && market[name] != null) ? Number(market[name]) : null;
            if (unit != null && Number.isFinite(unit)) { itemsVal += unit * qty; priced++; }
            else { unpriced++; }
          }
        }
        setPanel(itemsVal, cashBalance||0, priced, unpriced);
      }

      async function fetchOnce(url, postBodyIfNoGet=false) {

        try {
          const r = await fetch(url, { credentials: 'include' });
          if (!r.ok) throw new Error('GET failed');
          return await r.json();
        } catch {}


      }

      async function initInventoryNW() {
        if (!onInventory()) return;

        // Wait briefly for the inventory grid (debounced observer, short timeout)
        const waitForGrid = () => new Promise(res => {
          const now = findInventoryGrid();
          if (now) return res(true);

          let done = false, rafId = 0, timer = 0;
          const debounce = (() => {
            let queued = false;
            return () => {
              if (queued) return;
              queued = true;
              rafId = requestAnimationFrame(() => { queued = false; check(); });
            };
          })();

          const mo = new MutationObserver(debounce);

          function check() {
            if (done) return;
            if (findInventoryGrid()) {
              done = true;
              mo.disconnect();
              cancelAnimationFrame(rafId);
              clearTimeout(timer);
              res(true);
            }
          }

          mo.observe(document.documentElement, { childList: true, subtree: true });
          timer = setTimeout(() => { if (!done) { done = true; mo.disconnect(); res(false); } }, 1500);
          // kick once
          debounce();
        });

        const ok = await waitForGrid();
        if (!ok) return;
        if (!ensurePanel()) return;

        // Fetch once if we haven't intercepted yet
        if (!Array.isArray(itemsCache)) {
          const ji = await fetchOnce(API_ITEMS, true);
          if (ji && Array.isArray(ji.items)) itemsCache = ji.items;
        }
        if (!Number.isFinite(cashBalance)) {
          const js = await fetchOnce(API_STATS, true);
          const cash = js ? parseCash(js) : null;
          if (Number.isFinite(cash)) cashBalance = cash;
        }

        computeFromCaches();

        // tiny self-heal: reattach if grid/panel moves (debounced)
        (function liteReattach(){
          let queued = false;
          const reattach = () => {
            if (queued) return;
            queued = true;
            requestAnimationFrame(() => {
              queued = false;
              const grid = findInventoryGrid();
              if (!grid) return;
              if (!panel || !panel.isConnected || panel.nextSibling !== grid) {
                if (panel && panel.isConnected) panel.remove();
                panel = null;
                ensurePanel();
                computeFromCaches();
              }
            });
          };
          const mo = new MutationObserver(reattach);
          mo.observe(document.documentElement, { childList: true, subtree: true });

          // hook route changes
          const _ps = history.pushState;
          history.pushState = function(...a){ const r = _ps.apply(this, a); reattach(); return r; };
          const _rs = history.replaceState;
          history.replaceState = function(...a){ const r = _rs.apply(this, a); reattach(); return r; };
          window.addEventListener('popstate', reattach);
          window.addEventListener('hashchange', reattach);
        })();
      }

      // Market Favs updates → recompute only
      window.addEventListener('storage', (e) => { if (e.key === MARKET_KEY) computeFromCaches(); });
      window.addEventListener('zed:marketDataUpdated', computeFromCaches);

      // Minimal network hooks (ONLY intercept our two endpoints)
      ;(function hookFetchOnce(){
        if (window.__nw2_fetch_hooked__) return;
        window.__nw2_fetch_hooked__ = true;
        const orig = window.fetch;
        window.fetch = async function(input, init) {
          const resp = await orig.apply(this, arguments);
          try {
            const url = typeof input === 'string' ? input : (input?.url || '');
            if (url.includes('loadItems') || url.includes('getStats')) {
              const clone = resp.clone();
              clone.json().then(j => {
                if (url.includes('loadItems') && Array.isArray(j?.items)) {
                  itemsCache = j.items;
                }
                if (url.includes('getStats')) {
                  const cash = parseCash(j);
                  if (Number.isFinite(cash)) cashBalance = cash;
                }
                if (onInventory() && panel) computeFromCaches();
              }).catch(()=>{});
            }
          } catch {}
          return resp;
        };
      })();

      ;(function hookXHROnce(){
        if (window.__nw2_xhr_hooked__) return;
        window.__nw2_xhr_hooked__ = true;
        const open = XMLHttpRequest.prototype.open;
        const send = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.open = function(method, url) {
          this.__nw2_url = url || '';
          return open.apply(this, arguments);
        };
        XMLHttpRequest.prototype.send = function(body) {
          const url = this.__nw2_url || '';
          if (url.includes('loadItems') || url.includes('getStats')) {
            this.addEventListener('readystatechange', function() {
              if (this.readyState === 4) {
                try {
                  const j = JSON.parse(this.responseText);
                  if (url.includes('loadItems') && Array.isArray(j?.items)) {
                    itemsCache = j.items;
                  }
                  if (url.includes('getStats')) {
                    const cash = parseCash(j);
                    if (Number.isFinite(cash)) cashBalance = cash;
                  }
                  if (onInventory() && panel) computeFromCaches();
                } catch {}
              }
            });
          }
          return send.apply(this, arguments);
        };
      })();

      // SPA routing trigger + first load
      function onRouteChange(){ if (onInventory()) initInventoryNW(); }
      const _ps = history.pushState;
      history.pushState = function(...a){ const r = _ps.apply(this, a); onRouteChange(); return r; };
      const _rs = history.replaceState;
      history.replaceState = function(...a){ const r = _rs.apply(this, a); onRouteChange(); return r; };
      window.addEventListener('popstate', onRouteChange);
      window.addEventListener('hashchange', onRouteChange);

      // First load
      initInventoryNW();
    })();


      }

// ---------- Timers ----------
function run_Timers(){

(function () {
  'use strict';

  /** CONFIG **/
  const API = {
    stats: 'https://api.zed.city/getStats',
    stronghold: 'https://api.zed.city/getStronghold',
  };

  const ICON = {
    energy: '⚡',
    rads: '☢',
    xp: '🏆',
    raid: '🎯',
    furnace: '🔥',
    mine_iron: '⛏️',
    mine_coal: '⛏️',
    junk: '🛒',
    radio: '🗼',
  };

  const TEN_MIN = 10 * 60; // seconds
  const POLL_STATS_EVERY = 60 * 1000;
  const POLL_STRONGHOLD_EVERY = 60 * 1000;

  const SAVED_KEY = 'zed-timerbar-saved'; // junk/radio
  const SH_CACHE_KEY = 'zed-timerbar-stronghold-cache'; // furnace cache for travel mode

  const saved = () => JSON.parse(localStorage.getItem(SAVED_KEY) || '{}');
  const save = (obj) => localStorage.setItem(SAVED_KEY, JSON.stringify(obj));

  let __zed_started = false;
  const __zed_intervals = { stats: null, stronghold: null, saved: null, tick: null };
  function setIntervalSafe(key, fn, ms) {
    if (__zed_intervals[key]) clearInterval(__zed_intervals[key]);
    __zed_intervals[key] = setInterval(fn, ms);
  }

  /** Cross-origin friendly JSON fetch (falls back to GM_xmlhttpRequest) **/
  function fetchJSON(url) {
    return new Promise(async (resolve) => {
      try {
        const r = await fetch(url, { method: 'GET', mode: 'cors', credentials: 'include' });
        if (r && r.ok) {
          resolve(await r.json());
          return;
        }
      } catch { /* try GM below */ }

      if (typeof GM_xmlhttpRequest === 'function') {
        try {
          GM_xmlhttpRequest({
            method: 'GET',
            url,
            onload: (res) => {
              try { resolve(JSON.parse(res.responseText)); } catch { resolve(null); }
            },
            onerror: () => resolve(null),
            ontimeout: () => resolve(null),
          });
          return;
        } catch { /* noop */ }
      }

      resolve(null);
    });
  }

  /** DOM: create bar just under the main toolbar **/
  function ensureBar() {
    const toolbar =
      document.querySelector('div.q-toolbar.row.no-wrap.items-center[role="toolbar"]') ||
      document.querySelector('div.q-toolbar[role="toolbar"]') ||
      document.querySelector('.q-header .q-toolbar') ||
      document.querySelector('header .q-toolbar');

    if (!toolbar) return null;

    let holder = document.getElementById('zed-timerbar');
    if (holder) return holder;

    holder = document.createElement('div');
    holder.id = 'zed-timerbar';
    holder.setAttribute('role', 'group');
    holder.innerHTML = `
      <div class="zed-timerbar-inner">
        ${tileHTML('energy')}
        ${tileHTML('rads')}
        ${tileHTML('xp')}
        ${tileHTML('raid')}
        <span id="zed-furnace-mount" style="display:none"></span>
        ${tileHTML('mine_iron')}
        ${tileHTML('mine_coal')}
        ${tileHTML('junk')}
        ${tileHTML('radio')}
      </div>
    `;

    if (toolbar.parentElement) {
      toolbar.parentElement.insertBefore(holder, toolbar.nextSibling);
    } else {
      document.body.insertBefore(holder, document.body.firstChild);
    }
    injectStyles();

    // Hide mines until confirmed
    setTileVisible('mine_iron', false);
    setTileVisible('mine_coal', false);

    // Remove any legacy single furnace tile
    holder.querySelectorAll('.zed-tile[data-key="furnace"]').forEach(el => el.remove());

    return holder;
  }

  function tileHTML(key) {
    return `
      <a class="zed-tile" data-key="${key}" href="#" title="${key.toUpperCase()}">
        <div class="zed-icon">${ICON[key] || '•'}</div>
        <div class="zed-time" data-seconds="-1">--:--</div>
      </a>
    `;
  }

  function injectStyles() {
    if (document.getElementById('zed-timerbar-style')) return;
    const style = document.createElement('style');
    style.id = 'zed-timerbar-style';
    style.textContent = `
      #zed-timerbar {
        display: flex;
        justify-content: center;
        padding: 2px 8px;
        background: rgba(9,10,11,0.9);
        border-top: 1px solid rgba(255,255,255,0.06);
      }
      .zed-timerbar-inner {
        display: flex;
        flex-wrap: wrap;
        gap: 0px;
        justify-content: center;
        align-items: flex-start;
      }
      .zed-tile {
        display: flex;
        flex-direction: row;
        align-items: center;
        gap: 6px;
        text-decoration: none;
        padding: 2px 8px;
      }
      .zed-tile.hidden { display: none !important; }
      .zed-icon { font-size: 12px; line-height: 1; }
      .zed-time {
        padding: 2px 6px;
        font-size: 12px;
        font-weight: 600;
        color: #fff;
        background: rgba(255,255,255,0.08);
        border-radius: 8px;
        min-width: 44px;
        text-align: center;
      }
      .zed-time.alert { color: #ff4d4f; background: rgba(255,77,79,0.12); }
      #zed-furnace-mount { display:none; }
    `;
    document.head.appendChild(style);
  }

  /** Formatting **/
  function fmt(seconds) {
  // Show D:HH:MM:SS (days shown only when needed)
  if (seconds == null || seconds <= 0) return '00:00';
  let s = Math.floor(seconds);

  const d = Math.floor(s / 86400); s -= d * 86400;
  const h = Math.floor(s / 3600); s -= h * 3600;
  const m = Math.floor(s / 60); s -= m * 60;

  const pad = (n) => (n < 10 ? '0' + n : '' + n);
  const parts = [];
  if (d) parts.push(pad(d));
  if (h || parts.length) parts.push(pad(h));
  parts.push(pad(m));
  parts.push(pad(s));

  return parts.join(':');
}
  function applyAlertClass(el, seconds) {
    if (!el) return;
    if (seconds <= TEN_MIN) el.classList.add('alert');
    else el.classList.remove('alert');
  }

  function setTileVisible(key, visible) {
    const el = document.querySelector(`.zed-tile[data-key="${key}"]`);
    if (!el) return;
    el.classList.toggle('hidden', !visible);
  }

  /** XHR intercept **/
  (function interceptXHR() {
    const openOrig = XMLHttpRequest.prototype.open;
    const sendOrig = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open = function (method, url) {
      this._zedURL = url;
      return openOrig.apply(this, arguments);
    };
    XMLHttpRequest.prototype.send = function (body) {
      this.addEventListener('readystatechange', function () {
        if (this.readyState !== 4) return;
        try {
          const url = String(this._zedURL || '');
          const res = JSON.parse(this.responseText || 'null');
          if (!res) return;
          if (url.includes('store_id') && url.includes('junk')) {
            const reset = res?.limits?.reset_time;
            if (reset) {
              const until = Date.now() + reset * 1000;
              const s = saved();
              s.junk = { until, href: '/store/junk' };
              save(s);
            }
          }
          if (url.includes('getRadioTower')) {
            const expire = res?.expire;
            if (expire) {
              const until = Date.now() + expire * 1000;
              const s = saved();
              s.radio = { until, href: '/stronghold' };
              save(s);
            }
          }
        } catch {}
      });
      return sendOrig.apply(this, arguments);
    };
  })();

  function writeStrongholdCache(entries) {
    const payload = {
      saved_at: Date.now(),
      furnaces: entries.map(e => ({ id: e.id, until: e.until })),
    };
    localStorage.setItem(SH_CACHE_KEY, JSON.stringify(payload));
  }
  function readStrongholdCache() {
    try {
      const raw = localStorage.getItem(SH_CACHE_KEY);
      if (!raw) return null;
      const data = JSON.parse(raw);
      const now = Date.now();
      return (data.furnaces || []).map(f => ({
        id: f.id,
        seconds: Math.max(0, Math.floor((f.until - now) / 1000)),
      }));
    } catch {
      return null;
    }
  }

  function zedApplyStats(data) {
  // --- Base energy max ---
  let energyMax = data?.membership ? 150 : (data?.stats?.max_energy || 100);

  // --- Add +50 if "Feeling Relaxed" effect is active ---
  const hasFeelingRelaxed = Object.values(data?.effects || {}).some(
    eff => eff.codename === 'player_effect_feeling_relaxed'
  );
  if (hasFeelingRelaxed) {
    energyMax += 50;
  }

  // --- Energy calculations ---
  const energyMissing = Math.max(0, energyMax - (data?.energy ?? 0));
  const tickSeconds = data?.energy_tick ?? (data?.membership ? 600 : 900);
  const toNextEnergy = Math.max(0, data?.energy_regen ?? 0);
  const ticksNeeded = energyMissing > 0 ? Math.ceil(energyMissing / 5) : 0;
  const energySeconds = ticksNeeded > 0 ? toNextEnergy + (ticksNeeded - 1) * tickSeconds : 0;
  setTile('energy', '/stronghold', energySeconds);

  // --- Radiation calculations ---
  const radBaseMax = (data?.stats?.max_rad ?? 60);
  const radMax = radBaseMax + (data?.membership ? 20 : 0);
  const radMissing = Math.max(0, radMax - (data?.rad ?? 0));
  const baseTick = 300;
  const toNext = Math.max(0, data?.rad_regen || 0);
  const radSeconds = radMissing > 0 ? Math.max(0, (radMissing - 1) * baseTick) + toNext : 0;
  setTile('rads', '/scavenge', radSeconds);

  // --- XP tile ---
  const xpToGo = Math.max(0, Math.round((data?.xp_end || 0)) - Math.round((data?.experience || 0)));
  setTile('xp', '/profile', null, xpToGo.toLocaleString());

  // --- Raid cooldown ---
  const raid = Math.max(0, data?.raid_cooldown ?? 0);
  setTile('raid', '/raids', raid);
}

window.zedApplyStats = zedApplyStats;

  async function refreshStats() {
    const data = await fetchJSON(API.stats);
    if (!data) return;
    if (window.zedApplyStats) window.zedApplyStats(data);
  }

  async function refreshStronghold() {
    function ensureFurnaceMount() {
      let mount = document.getElementById('zed-furnace-mount');
      if (!mount) {
        const fallback = document.querySelector('.zed-timerbar-inner');
        mount = document.createElement('span');
        mount.id = 'zed-furnace-mount';
        (fallback || document.body).appendChild(mount);
      }
      return mount;
    }

    function createFurnaceTile(id, seconds) {
      const a = document.createElement('a');
      a.className = 'zed-tile';
      a.href = `/stronghold/${id}`;
      a.title = `FURNACE ${id}`;
      const icon = document.createElement('div');
      icon.className = 'zed-icon';
      icon.textContent = '🔥';
      const time = document.createElement('div');
      time.className = 'zed-time';
      time.dataset.seconds = String(Math.max(0, seconds || 0));
      time.textContent = fmt(seconds || 0);
      a.appendChild(icon);
      a.appendChild(time);
      return a;
    }

    function renderFurnaces(list) {
      const mount = ensureFurnaceMount();
      if (!mount) return;
      const container = mount.parentElement || document;
      if (!list.length) {
        container.querySelectorAll('.zed-tile[data-furnace-id]').forEach(el => el.remove());
        return;
      }
      const existing = new Set(list.map(r => String(r.id)));
      container.querySelectorAll('.zed-tile[data-furnace-id]').forEach(el => {
        if (!existing.has(el.dataset.furnaceId)) el.remove();
      });
      list.forEach(({ id, seconds }) => {
        let tile = container.querySelector(`.zed-tile[data-furnace-id="${id}"]`);
        if (!tile) {
          tile = createFurnaceTile(id, seconds || 0);
          tile.dataset.furnaceId = String(id);
          mount.insertAdjacentElement('afterend', tile);
        } else {
          const timeEl = tile.querySelector('.zed-time');
          const s = Math.max(0, Number(seconds || 0));
          timeEl.dataset.seconds = String(s);
          timeEl.textContent = fmt(s);
          applyAlertClass(timeEl, s);
        }
      });
    }

    const data = await fetchJSON(API.stronghold);
    if (data && data.stronghold) {
      const now = Date.now();
      const buildings = Object.values(data.stronghold) || [];
      const furnacesRaw = buildings.filter(b => b?.codename === 'furnace');
      const computed = furnacesRaw.map((f) => {
        let seconds = 0;
        const bpWait = f?.items?.['item_requirement-bp']?.vars?.wait_time;
        const qtyEach = f?.items?.['item_requirement-bp']?.vars?.items?.['item_requirement-1']?.qty;
        const total = f?.items?.['item_requirement-1']?.quantity;
        if (f?.in_progress && typeof f?.timeLeft === 'number' && bpWait && qtyEach && total) {
          const done = f?.iterationsPassed || 0;
          const remainingIters = Math.max(0, Math.floor(total / qtyEach) - done - 1);
          seconds = Math.max(0, (remainingIters * bpWait) + (f.timeLeft || 0));
        } else if (typeof f?.wait === 'number') {
          seconds = Math.max(0, f.wait);
        }
        return { id: f.id, seconds, until: now + seconds * 1000 };
      });
      writeStrongholdCache(computed.map(({ id, until }) => ({ id, until })));
      const ironMine = buildings.find(b => b?.codename === 'iron_automine');
      if (ironMine) {
        const secsIron = Math.max(0,
          typeof ironMine?.timeLeft === 'number' ? ironMine.timeLeft : (ironMine?.wait || 0)
        );
        setTile('mine_iron', ironMine?.id ? `/stronghold/${ironMine.id}` : '#', secsIron);
        setTileVisible('mine_iron', true);
      } else setTileVisible('mine_iron', false);
      const coalMine = buildings.find(b => b?.codename === 'coal_automine');
      if (coalMine) {
        const secsCoal = Math.max(0,
          typeof coalMine?.timeLeft === 'number' ? coalMine.timeLeft : (coalMine?.wait || 0)
        );
        setTile('mine_coal', coalMine?.id ? `/stronghold/${coalMine.id}` : '#', secsCoal);
        setTileVisible('mine_coal', true);
      } else setTileVisible('mine_coal', false);
      renderFurnaces(computed.map(({ id, seconds }) => ({ id, seconds })));
      return;
    }
    const cached = readStrongholdCache();
    if (cached && cached.length) renderFurnaces(cached);
  }

  function setTile(key, href, seconds, overrideLabel) {
    const tile = document.querySelector(`.zed-tile[data-key="${key}"]`);
    if (!tile) return;
    tile.href = href || '#';
    const timeEl = tile.querySelector('.zed-time');
    if (!timeEl) return;
    if (overrideLabel != null) {
      timeEl.textContent = String(overrideLabel);
      timeEl.dataset.seconds = '-1';
      timeEl.classList.remove('alert');
      return;
    }
    const s = Math.max(0, Number(seconds || 0));
    timeEl.textContent = fmt(s);
    timeEl.dataset.seconds = String(s);
    applyAlertClass(timeEl, s);
  }

  function refreshSavedTimers() {
    const now = Date.now();
    const s = saved();
    if (s.junk) {
      const secs = Math.max(0, Math.floor((s.junk.until - now) / 1000));
      setTile('junk', s.junk.href || '/store/junk', secs);
    }
    if (s.radio) {
      const secs = Math.max(0, Math.floor((s.radio.until - now) / 1000));
      setTile('radio', s.radio.href || '/stronghold', secs);
    }
  }

  function tick() {
    document.querySelectorAll('#zed-timerbar [data-seconds]').forEach(el => {
      const s = parseInt(el.dataset.seconds || '-1', 10);
      if (isNaN(s) || s < 0) return;
      const next = Math.max(0, s - 1);
      el.dataset.seconds = String(next);
      el.textContent = fmt(next);
      applyAlertClass(el, next);
    });
  }

  function startIntervals() {
    if (__zed_started) return;
    __zed_started = true;
    refreshStats();
    refreshStronghold();
    refreshSavedTimers();
    setIntervalSafe('stats', refreshStats, POLL_STATS_EVERY);
    setIntervalSafe('stronghold', refreshStronghold, POLL_STRONGHOLD_EVERY);
    setIntervalSafe('saved', refreshSavedTimers, 5 * 1000);
    setIntervalSafe('tick', tick, 1000);
  }

  function boot() {
    if (ensureBar()) { startIntervals(); return; }
    const obs = new MutationObserver(() => {
      if (ensureBar()) {
        obs.disconnect();
        startIntervals();
      }
    });
    obs.observe(document.documentElement, { childList: true, subtree: true });
    setTimeout(() => {
      obs.disconnect();
      (function retryUntilReady(tries = 40) {
        if (ensureBar()) startIntervals();
        else if (tries > 0) setTimeout(() => retryUntilReady(tries - 1), 500);
      })();
    }, 20000);
  }

  boot();
})();

}


// ==========================================================
//  Exploration Data with arrival-based pruning (no legacy migration)
// ==========================================================
function run_ExplorationData(){
  (function(){
    // ---------------------------------
    // Storage (data + UI state)
    // ---------------------------------
    const DATA_KEY = "Zed-exploration-data";
    const UI_KEY = "Zed-exploration-ui"; // { [location]: { NPCs: true/false, Gates: true/false } }

    // Initialize fresh keys — no legacy migration
    if (!localStorage.getItem(DATA_KEY)) {
      localStorage.setItem(DATA_KEY, JSON.stringify({}));
    }
    if (!localStorage.getItem(UI_KEY)) {
      localStorage.setItem(UI_KEY, JSON.stringify({}));
    }

    // helpers: read/write
    function readData() { try { return JSON.parse(localStorage.getItem(DATA_KEY)) || {}; } catch { return {}; } }
    function writeData(all) { localStorage.setItem(DATA_KEY, JSON.stringify(all)); }
    function readUI() { try { return JSON.parse(localStorage.getItem(UI_KEY)) || {}; } catch { return {}; } }
    function writeUI(ui) { localStorage.setItem(UI_KEY, JSON.stringify(ui)); }

    function getSectionState(location, sectionTitle, defaultOpen = true) {
      const ui = readUI();
      return ui[location]?.[sectionTitle] ?? defaultOpen;
    }
    function setSectionState(location, sectionTitle, isOpen) {
      const ui = readUI();
      if (!ui[location]) ui[location] = {};
      ui[location][sectionTitle] = isOpen;
      writeUI(ui);
    }

    // ---------------------------------
    // Minimal state / other helpers
    // ---------------------------------
    let current_location = "City";
    const TIMER_THRESHOLD = 10 * 60;

    function slugify(s) { return String(s || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""); }

    function getDayHourMinute(seconds) {
  seconds = Math.max(0, Math.floor(Number(seconds) || 0));

  let days = Math.floor(seconds / 86400);
  seconds -= days * 86400;

  let hours = Math.floor(seconds / 3600);
  seconds -= hours * 3600;

  let minutes = Math.floor(seconds / 60);
  seconds -= minutes * 60;

  const pad = (n) => (n < 10 ? "0" + n : String(n));
  const time = [];

  if (days > 0) time.push(pad(days));
  if (hours > 0 || time.length) time.push(pad(hours));
  if (minutes > 0 || time.length) time.push(pad(minutes));
  time.push(pad(seconds));

  return time.join(":");
}


    function updateTimers() {
  document.querySelectorAll(".explore-ui .timer").forEach((el) => {
    let t = Number(el.timeLeft);
    if (isNaN(t)) return;

    // already finished?
    if (t <= -1) {
      el.classList.add("alert");
      el.textContent = getDayHourMinute(0);
      const row = el.closest('.rowline');
      const kind = row?.dataset?.kind || '';
      const state = row?.querySelector('.state');
      if (state) {
        if (kind === 'scavenge') { state.className = 'state ok'; state.textContent = 'Ready'; }
        else if (kind === 'gate') { state.className = 'state warn'; state.textContent = 'Locked'; }
      }
      return;
    }

    const next = t - 1;
    el.timeLeft = next;

    if (next <= TIMER_THRESHOLD) el.classList.add("alert"); else el.classList.remove("alert");

    // when it hits zero, set the right state and freeze
    if (next <= 0) {
      el.timeLeft = -1;
      el.textContent = getDayHourMinute(0);
      const row = el.closest('.rowline');
      const kind = row?.dataset?.kind || '';
      const state = row?.querySelector('.state');
      if (state) {
        if (kind === 'scavenge') { state.className = 'state ok'; state.textContent = 'Ready'; }
        else if (kind === 'gate') { state.className = 'state warn'; state.textContent = 'Locked'; }
      }
      return;
    }

    // normal ticking
    el.textContent = getDayHourMinute(next);
  });
}

    // ---------------------------------
    // Safe XHR intercept (guarded so we don’t double-wrap prototype)
    // ---------------------------------
    (function () {
      if (XMLHttpRequest.prototype.__zedExplorationWrapped) return;
      const originalOpen = XMLHttpRequest.prototype.open;
      const originalSend = XMLHttpRequest.prototype.send;
      XMLHttpRequest.prototype.open = function (method, url) {
        this._interceptedURL = url;
        return originalOpen.apply(this, arguments);
      };
      XMLHttpRequest.prototype.send = function (body) {
        this.addEventListener("readystatechange", function () {
          if (this.readyState === 4) {
            try {
              const response = JSON.parse(this.responseText);
              const eventData = { url: this._interceptedURL, response };
              window.dispatchEvent(new CustomEvent("xhrIntercepted", { detail: eventData }));
            } catch (e) {}
          }
        });
        return originalSend.apply(this, arguments);
      };
      Object.defineProperty(XMLHttpRequest.prototype, '__zedExplorationWrapped', { value: true, configurable: false });
    })();

    // ---------------------------------
    // Data collectors
    // ---------------------------------

    // When we receive getLocation, we treat that as "arriving" at a top-level location.
    // We store an arrival timestamp and mark that this location needs a one-time prune.
    function handleLocation(response) {
      current_location = response.name || current_location;
      const all = readData();
      const cur = all[current_location] || {};
      const meta = cur.meta || {};
      meta.lastArrival = Date.now();
      meta.needsArrivalPrune = true; // first getRoom after arrival will prune stale stuff
      all[current_location] = {
        npcs: cur.npcs || {},
        gates: cur.gates || {},
        scavenge: cur.scavenge || {},
        meta
      };
      writeData(all);
    }

    // Helper: prune entries whose timers already hit 0 *before arrival*.
    function pruneOnArrival(cur) {
      const now = Date.now();
      const arrivalTs = cur?.meta?.lastArrival || now;
      const result = {
        npcs: {},
        gates: {},
        scavenge: cur.scavenge || {},
        meta: { ...(cur.meta || {}), needsArrivalPrune: false, prunedAt: now }
      };

      // NPCs: respawn_time is an absolute ms timestamp we saved previously.
      // If respawn_time <= arrivalTs (i.e., timer would have been 0 or negative BEFORE we arrived),
      // drop it so new spawns/renames won't duplicate.
      for (const id in (cur.npcs || {})) {
        const npc = cur.npcs[id];
        const respawn = Number(npc.respawn_time || 0);
        if (respawn > arrivalTs) {
          result.npcs[id] = npc;
        }
      }

      // Gates: gate_next_change_time/gate_unlock_time are absolute ms timestamps when state changes.
      // If that time <= arrivalTs, drop it so we accept whatever the room now tells us.
      for (const id in (cur.gates || {})) {
        const g = cur.gates[id];
        const changeAt = Number(g.gate_next_change_time || g.gate_unlock_time || 0);
        if (changeAt > arrivalTs) {
          result.gates[id] = g;
        }
      }

      return result;
    }

    // Merge helper: add/replace by id without deleting other sub-area items
    function mergeById(target, incoming) {
      for (const id in incoming) {
        target[id] = incoming[id];
      }
      return target;
    }

    function saveExplorationData(response) {
      const all = readData();
      const now = Date.now();

      const cur = all[current_location] || { npcs: {}, gates: {}, scavenge: {}, meta: {} };

      // If this is the first room load after arriving, prune stale entries once.
      let working = cur;
      if (cur.meta?.needsArrivalPrune) {
        working = pruneOnArrival(cur);
      } else {
        // ensure meta object exists
        working.meta = working.meta || {};
      }

      // Build fresh payloads from this getRoom
      const npcsIncoming = {};
      if (Array.isArray(response.npcs)) {
        const timeNow = new Date();
        for (const npc of response.npcs) {
          const id = npc.id;
          const life = npc.vars?.life;
          const max_life = npc.vars?.max_life;
          const respawn_left = npc.vars?.respawn_time || 0; // seconds until respawn
          const respawn_time = timeNow.getTime() + respawn_left * 1000; // absolute ms
          npcsIncoming[id] = { name: npc.name, life, max_life, respawn_time };
        }
      }

      const gatesIncoming = {};
      if (Array.isArray(response.gates)) {
        const timeNow = new Date().getTime();
        for (const gate of response.gates) {
          const id = gate.id;
          const unlocked = !!(gate.vars?.unlocked);
          const next_secs = gate.vars?.unlock_time || 0;
          const gate_next_change_time = next_secs ? timeNow + next_secs * 1000 : 0;
          const required_items = {};
          if (gate.items) {
            for (const key in gate.items) {
              const it = gate.items[key];
              required_items[it.name] = it.req_qty;
            }
          }
          const gate_unlock_time = gate_next_change_time; // legacy fallback (renderer uses next_change)
          gatesIncoming[id] = { name: gate.name, unlocked, gate_next_change_time, gate_unlock_time, unlock_time: next_secs, required_items };
        }
      }

      // Scavenge only updated when we see it; we don't want to erase existing cooldowns from other sub-areas.
      const scavengeIncoming = {};
      const scavenge_invalid = ["Fuel Pumps","Foundation Pit","Bulk Goods Lockup","Scrap Pile","Warm Springs","Red River","Grand Lake"];
      if (Array.isArray(response.scavenge)) {
        const tNow = new Date().getTime();
        for (const s of response.scavenge) {
          if (scavenge_invalid.includes(s.name)) continue;
          const cooldown = s.vars?.cooldown || 0;
          scavengeIncoming[s.id] = { name: s.name, cooldown, cooldown_end: cooldown ? tNow + cooldown * 1000 : -1 };
        }
      }

      // Merge the new data without wiping other entries (sub-areas)
      working.npcs = mergeById(working.npcs || {}, npcsIncoming);
      working.gates = mergeById(working.gates || {}, gatesIncoming);
      working.scavenge = mergeById(working.scavenge || {}, scavengeIncoming);

      // Bookkeeping
      working.meta.lastRoomUpdate = now;

      all[current_location] = working;
      writeData(all);
    }

    function handleExplorationTrade(response) {
      const all = readData();
      const cur = all[current_location] || { npcs: {}, gates: {}, scavenge: {}, meta: {} };
      const now = new Date();
      const job = response.job;
      const wait = job?.vars?.cooldown || 0;
      cur.scavenge[job.id] = { name: job.name, cooldown: wait, cooldown_end: now.getTime() + wait * 1000 };
      all[current_location] = cur; writeData(all);
    }

    function handleStartJob(response) {
      if (response.error) return;
      const code = response.job?.codename || "";
      if (code.startsWith("job_fuel_depot_fuel_trader") || code.startsWith("job_vault_lockbox") || code.startsWith("job_demolition_site")) {
        handleExplorationTrade(response);
      }
    }

    // ---------------------------------
    // Collapsible UI (per-location persistent)
    // ---------------------------------
    function preventNav(e) { e.preventDefault(); e.stopPropagation(); }

    function makeCollapsibleSection(title, contentElement, initiallyOpen, locationName) {
      const slug = `${slugify(locationName)}-${slugify(title)}`;
      const wrapper = document.createElement("div");
      wrapper.className = "collapsible-section explore-ui";
      wrapper.setAttribute("data-exploration-ui", "1");

      const header = document.createElement("button");
      header.type = "button";
      header.className = "collapsible-header";
      header.setAttribute("aria-expanded", initiallyOpen ? "true" : "false");
      header.setAttribute("aria-controls", `${slug}-section`);
      header.innerHTML = `<span class="arrow">${initiallyOpen ? "▼" : "►"}</span> ${title}`;
      ["pointerdown","mousedown","touchstart"].forEach(evt => { header.addEventListener(evt, preventNav, true); });

      const body = document.createElement("div");
      body.id = `${slug}-section`;
      body.className = "collapsible-body";
      body.style.display = initiallyOpen ? "block" : "none";
      body.appendChild(contentElement);

      function toggle(open) {
        const isOpen = open !== undefined ? open : body.style.display === "none";
        body.style.display = isOpen ? "block" : "none";
        header.querySelector(".arrow").textContent = isOpen ? "▼" : "►";
        header.setAttribute("aria-expanded", isOpen ? "true" : "false");
        body.toggleAttribute("hidden", !isOpen);
        setSectionState(locationName, title, isOpen); // persist
      }
      header.addEventListener("click", () => toggle());
      header.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toggle(); } });

      wrapper.appendChild(header);
      wrapper.appendChild(body);
      return wrapper;
    }

    // compact renderers (3-up, with pipes)
    function makeSep(){ const s=document.createElement('span'); s.className='sep'; s.textContent=' | '; return s; }

function createNPCSection(locationName, npcs){
  const content = document.createElement('div');
  content.className = 'grid three-up';

  if (!npcs || Object.keys(npcs).length === 0){
    const empty = document.createElement('div');
    empty.className = 'rowline empty';
    empty.textContent = 'No NPCs available';
    content.appendChild(empty);
    const open = getSectionState(locationName, 'NPCs', true);
    return makeCollapsibleSection('NPCs', content, open, locationName);
  }

  const now = Date.now();

  for (const id in npcs){
    const npc = npcs[id];
    const item = document.createElement('div');
    item.className = 'rowline';
    item.title = npc.name;

    // HP column
    const life = npc.life ?? '—';
    const maxLife = npc.max_life ?? '—';

    const nameEl = document.createElement('span');
    nameEl.className = 'name';
    nameEl.textContent = npc.name;

    const hpEl = document.createElement('span');
    hpEl.className = 'hp';
    hpEl.textContent = `${life}/${maxLife}`;

    // Timer column
    const respawnMs = npc.respawn_time ? npc.respawn_time - now : 0;
    const secs = Math.max(0, Math.floor(respawnMs / 1000));

    const timerEl = document.createElement('span');
    timerEl.className = 'timer';
    timerEl.timeLeft = secs; // updateTimers() decrements this
    timerEl.textContent = getDayHourMinute(secs); // always DD:HH:MM:SS

    // Compose row
    item.appendChild(nameEl);
    item.appendChild(makeSep());
    item.appendChild(hpEl);
    item.appendChild(makeSep());
    item.appendChild(document.createTextNode('{'));
    item.appendChild(timerEl);
    item.appendChild(document.createTextNode('}'));

    content.appendChild(item);
  }

  const open = getSectionState(locationName, 'NPCs', true);
  return makeCollapsibleSection('NPCs', content, open, locationName);
}


   function createGatesSection(locationName, gates){
  const content = document.createElement('div');
  content.className = 'grid three-up';

  if (!gates || Object.keys(gates).length === 0){
    const empty = document.createElement('div');
    empty.className = 'rowline empty';
    empty.textContent = 'No gates available';
    content.appendChild(empty);
    const open = getSectionState(locationName, 'Gates', true);
    return makeCollapsibleSection('Gates', content, open, locationName);
  }

  const now = Date.now();

  for (const id in gates){
    const gate = gates[id];
    const item = document.createElement('div');
      item.className = 'rowline';
      item.dataset.kind = 'gate';

    // Tooltip: full requirement breakdown if present
    if (gate.required_items && Object.keys(gate.required_items).length){
      const list = Object.entries(gate.required_items).map(([n,q]) => `${n} (${q})`).join(', ');
      item.title = `Items: ${list}`;
    }

    // Compute time left (if any)
    let changeMillis = 0;
    if (gate.gate_next_change_time) changeMillis = gate.gate_next_change_time - now;
    else if (gate.gate_unlock_time) changeMillis = gate.gate_unlock_time - now;

    let secs = Math.max(0, Math.floor(changeMillis / 1000));
    const isUnlockedNow = !!gate.unlocked && secs > 0; // only unlocked while timer is running

    // Left + middle columns
    const nameEl = document.createElement('span');
    nameEl.className = 'name';
    nameEl.textContent = gate.name;

    const stateEl = document.createElement('span');
    stateEl.className = isUnlockedNow ? 'state ok' : 'state warn';
    stateEl.textContent = isUnlockedNow ? 'Unlocked' : 'Locked';

    // Right column
    let thirdEl;
    if (secs > 0){
      const timerEl = document.createElement('span');
      timerEl.className = 'timer';
      timerEl.timeLeft = secs;                // ticking handled by updateTimers()
      timerEl.textContent = getDayHourMinute(secs); // always DD:HH:MM:SS
      thirdEl = timerEl;
    } else {
      const needEl = document.createElement('span');
      needEl.className = 'needs';
      if (gate.required_items && Object.keys(gate.required_items).length){
        needEl.textContent = Object.entries(gate.required_items)
          .map(([n,q]) => `${n} (${q})`)
          .join(', ');
      } else {
        needEl.textContent = '—';
      }
      thirdEl = needEl;
    }

    // Compose row
    item.appendChild(nameEl);
    item.appendChild(makeSep());
    item.appendChild(stateEl);
    item.appendChild(makeSep());
    item.appendChild(document.createTextNode('{'));
    item.appendChild(thirdEl);
    item.appendChild(document.createTextNode('}'));

    content.appendChild(item);
  }

  const open = getSectionState(locationName, 'Gates', true);
  return makeCollapsibleSection('Gates', content, open, locationName);
}

      function createScavengeSection(locationName, scavenge){
  const content = document.createElement('div');
  content.className = 'grid three-up';

  if (!scavenge || Object.keys(scavenge).length === 0){
    const empty = document.createElement('div');
    empty.className = 'rowline empty';
    empty.textContent = 'No loot crates found';
    content.appendChild(empty);
    const open = getSectionState(locationName, 'Loot Crates', true);
    return makeCollapsibleSection('Loot Crates', content, open, locationName);
  }

  const now = Date.now();

  for (const id in scavenge){
    const s = scavenge[id];
    const item = document.createElement('div');
    item.className = 'rowline';
    item.dataset.kind = 'scavenge'; // so updateTimers knows how to flip state

    const nameEl = document.createElement('span');
    nameEl.className = 'name';
    nameEl.textContent = s.name || 'Loot';

    // time remaining (we persist cooldown_end in ms)
    let secs = 0;
    if (typeof s.cooldown_end === 'number' && s.cooldown_end > 0){
      secs = Math.max(0, Math.floor((s.cooldown_end - now) / 1000));
    }

    // state badge
    const stateEl = document.createElement('span');
    if (secs === 0){
      stateEl.className = 'state ok';
      stateEl.textContent = 'Ready';
    } else {
      stateEl.className = 'state warn';
      stateEl.textContent = 'Cooling';
    }

    // right column: the live timer (always show; freezes at 00:00:00:00)
    const timerEl = document.createElement('span');
    timerEl.className = 'timer';
    timerEl.timeLeft = secs === 0 ? -1 : secs; // -1 means “finished” to our updater
    timerEl.textContent = getDayHourMinute(secs);

    // compose row
    item.appendChild(nameEl);
    item.appendChild(makeSep());
    item.appendChild(stateEl);
    item.appendChild(makeSep());
    item.appendChild(document.createTextNode('{'));
    item.appendChild(timerEl);
    item.appendChild(document.createTextNode('}'));

    content.appendChild(item);
  }

  const open = getSectionState(locationName, 'Loot Crates', true);
  return makeCollapsibleSection('Loot Crates', content, open, locationName);
}



    // ---------------------------------
    // Render on Explore screen
    // ---------------------------------
    function safeInsertAfter(card, node){
      try { if (card && card.parentNode) { card.parentNode.insertBefore(node, card.nextSibling); return true; } } catch(e) {}
      return false;
    }
    function display_exploration_data(){
      try {
        const all = readData();
        if (!location.href.includes('/explore')) return;
        const titles = Array.from(document.querySelectorAll('.job-name'));
        if (titles.length === 0) return;
        for (const titleEl of titles){
          if (titleEl.hasAttribute('data-exploration-processed')) continue;
          const locationName = (titleEl.textContent || '').trim();
          const locationData = all[locationName];
          if (!locationData) { titleEl.setAttribute('data-exploration-processed','1'); continue; }
          const container = document.createElement('div');
          container.className = 'exploration-data-container explore-ui';
          container.setAttribute('data-exploration-ui', '1');
          container.appendChild(createNPCSection(locationName, locationData.npcs));
          container.appendChild(createGatesSection(locationName, locationData.gates));
            container.appendChild(createScavengeSection(locationName, locationData.scavenge));
          let card = titleEl; let p = titleEl.parentElement;
          while (p){
            const classes = [...p.classList];
            if (classes.some(c => c.startsWith('job-cont'))) { card = p; break; }
            p = p.parentElement;
          }
          if (!safeInsertAfter(card, container)) { card.insertAdjacentElement('afterend', container); }
          titleEl.setAttribute('data-exploration-processed','1');
        }
      } catch(err){ console.error('[Exploration UI] render error:', err); }
    }

    // ---------------------------------
    // Wiring (guarded push/replace wrappers)
    // ---------------------------------
    window.addEventListener('xhrIntercepted', (e) => {
      const { url, response } = e.detail;
      if (String(url).includes('getLocation')) { handleLocation(response); }
      else if (String(url).includes('getRoom')) { saveExplorationData(response); display_exploration_data(); }
      else if (String(url).includes('startJob')) { handleStartJob(response); }
    });

    function onRouteChange(){ setTimeout(() => { if (location.href.includes('/explore')) display_exploration_data(); }, 300); }

    if (!history.__zedExploreWrapped){
      const origPush = history.pushState;
      history.pushState = function(){ const r = origPush.apply(this, arguments); onRouteChange(); return r; };
      const origReplace = history.replaceState;
      history.replaceState = function(){ const r = origReplace.apply(this, arguments); onRouteChange(); return r; };
      Object.defineProperty(history, '__zedExploreWrapped', { value: true, configurable: false });
    }

    window.addEventListener('hashchange', onRouteChange);
    window.addEventListener('popstate', onRouteChange);

    const mo = new MutationObserver(() => { if (location.href.includes('/explore')) display_exploration_data(); });
    mo.observe(document.documentElement, { childList: true, subtree: true });

    const timerId = setInterval(updateTimers, 1000);
    // Stop timers if user turns the toggle off without reload (defensive)
    window.addEventListener('beforeunload', () => clearInterval(timerId));

    // ---------------------------------
    // Styling
    // ---------------------------------
    const style = document.createElement('style');
    style.textContent = `
      .exploration-data-container { margin-top:4px; padding:4px; border-radius:4px; background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.02)); backdrop-filter: blur(2px); position: relative; z-index: 1; font-size:10px; box-shadow: 0 1px 8px rgba(0,0,0,.12); }
      .collapsible-header { cursor:pointer; font-weight:700; letter-spacing:.2px; margin:4px 0 4px; user-select:none; display:flex; align-items:center; gap:4px; background:transparent; border:0; padding:0; color:inherit; font:inherit; text-align:left; }
      .collapsible-header .arrow { width:1em; text-align:center; }
      .collapsible-body { margin-left:0; }
      .grid.three-up { display:grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap:0; border:1px solid rgba(255,255,255,0.08); border-radius:8px; overflow:hidden; }
      .grid.three-up .rowline { padding:4px 4px; display:flex; align-items:baseline; gap:4px; min-width:0; white-space:nowrap; overflow:hidden; background: rgba(255,255,255,0.02); transition: background .15s ease, transform .05s ease; }
      .grid.three-up .rowline:nth-child(odd) { background: rgba(255,255,255,0.015); }
      .grid.three-up .rowline:hover { background: rgba(255,255,255,0.06); }
      .grid.three-up .rowline .name { font-weight:600; overflow:hidden; text-overflow:ellipsis; }
      .grid.three-up .sep { opacity:.5; }
      .grid.three-up .timer.alert { color:#ff4d4f; }
      .rowline.empty { padding:6px; opacity:.75; }
      .state.ok { color:#9cff9c; }
      .state.warn { color:#ffae7a; }
    `;
      style.textContent += `
  .grid.three-up .needs { opacity:.9; font-style:italic; }
`;
    document.head.appendChild(style);
  })();
}


    // ---------- Market selling ----------
function run_marketSelling(){
  (function(){
    'use strict';

    const MARKET_KEY = "Zed-market-data";
    let modalActive = false;

    function getMarketMap(){
      try { return JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); }
      catch { return {}; }
    }

    function fillSellPriceRespectfully(){
      const titleDiv = document.querySelector(".title div");
      if (!titleDiv || !/Create Listing/i.test(titleDiv.textContent || "")) return false;

      const nameEl = document.querySelector(".q-py-sm > div:nth-child(1)");
      if (!nameEl) return false;

      const itemName = (nameEl.textContent || "").trim();
      if (!itemName) return false;

      const market = getMarketMap();
      const price = Number(market[itemName]);
      if (!Number.isFinite(price)) return false;

      const moneyWrap = document.querySelector(".zed-money-input");
      const input = moneyWrap && moneyWrap.querySelector("input");
      if (!input) return false;

      // Respect the user: only prefill if empty or previously autofilled
      const alreadyAuto = input.dataset.autofilled === "1";
      const emptyField = (input.value || "").trim() === "";

      if (!alreadyAuto && !emptyField) return true; // user already typed something

      // Don’t change while the user is actively typing
      if (document.activeElement === input && !alreadyAuto) return true;

      input.value = String(price - 1);
      input.dataset.autofilled = "1";
      input.dispatchEvent(new Event("input", { bubbles: true }));

      // First user edit disables further autofill for this modal
      const onUserEdit = () => {
        delete input.dataset.autofilled;
        input.removeEventListener("input", onUserEdit);
      };
      input.addEventListener("input", onUserEdit);

      return true;
    }

    // Watch for the Create Listing modal appearing/disappearing
    const mo = new MutationObserver(() => {
      const isOpen = !!document.querySelector(".title div") &&
                     /Create Listing/i.test((document.querySelector(".title div")?.textContent || ""));

      if (isOpen && !modalActive) {
        modalActive = true;
        // Try a few times to catch late-drawn inputs, then stop
        let tries = 8;
        const tryFill = () => {
          if (fillSellPriceRespectfully() || --tries <= 0 || !modalActive) return;
          setTimeout(tryFill, 60);
        };
        tryFill();
      } else if (!isOpen && modalActive) {
        // Modal closed: reset state for next time
        modalActive = false;
      }
    });

    mo.observe(document.documentElement, { childList: true, subtree: true });

    // Also try once shortly in case the modal is already open
    setTimeout(() => { if (!modalActive) fillSellPriceRespectfully(); }, 200);
  })();
}



    // ---------- Store Remaining ----------
function run_storeRemainingAmounts() {
  if (window.__SRA_INIT__) return; window.__SRA_INIT__ = true;

  const ITEM_LIMIT = 360;
  let used = 0;
  const stock = Object.create(null);
  const inv = Object.create(null);

  function handleStore(resp){
    const total = +resp?.limits?.limit || 0;
    const left = +resp?.limits?.limit_left || 0;
    used = Math.max(0, total - left);

    for (const it of (resp?.storeItems || [])) stock[it.name] = +it.quantity || 0;
    for (const it of (resp?.userItems || [])) inv[it.name] = +it.quantity || 0;

    microTry(insertBadge);
    //microTry(autofillQty); -- BUG
  }

  function microTry(fn, tries=10){
    (function loop(){ if (fn() === true || --tries <= 0) return; setTimeout(loop, 50); })();
  }

  function insertBadge(){
    const host = document.querySelector('.text-h4');
    if (!host) return false;
    let badge = document.getElementById('zed-item-limit');
    if (!badge){ badge = document.createElement('div'); badge.id = 'zed-item-limit'; host.appendChild(badge); }
    badge.textContent = ` [ ${used} / ${ITEM_LIMIT} ]`;
    host.style.color = used < ITEM_LIMIT ? '#6fcf97' : '#e76f51';
    return true;
  }

  /*function autofillQty(){     ///TODO FIX BUG CAUSING MASS DELITION OF ITEMS BY ACCIDENT - NOT GOOD!! BAD CODE. Hello, if you are reading this :)
    const nameEl = document.querySelector('.small-modal > div:nth-child(1)');
    const qtyInput = document.querySelector('.small-modal div.grid-cont input');
    const actionEl = document.querySelector('div.text-center:nth-child(2) > button span span');
    if (!nameEl || !qtyInput || !actionEl) return false;

    const item = (nameEl.textContent || '').trim();
    const buying = /Buy/i.test(actionEl.textContent || '');
    let qty = 0;

    if (buying){
      const remaining = Math.max(0, ITEM_LIMIT - used);
      qty = Math.min(remaining, +stock[item] || 0);
    } else {
      qty = +inv[item] || 0;
    }

    qtyInput.value = String(qty);
    qtyInput.dispatchEvent(new Event('input', { bubbles:true }));
    return true;
  }*/

  // ---- Chain-safe XHR hook: tap current impl, don't clobber it ----
  (function hookXHROnce(){
    if (XMLHttpRequest.prototype.__sraHooked__) return;
    const prevOpen = XMLHttpRequest.prototype.open;
    const prevSend = XMLHttpRequest.prototype.send;

    XMLHttpRequest.prototype.open = function(method, url){
      this.__sra_url = url || '';
      return prevOpen.apply(this, arguments); // chain
    };
    XMLHttpRequest.prototype.send = function(body){
      // read after completion
      this.addEventListener('readystatechange', function(){
        if (this.readyState !== 4) return;
        try {
          const url = String(this.__sra_url || '');
          if (url.includes('store_id')) {
            const json = JSON.parse(this.responseText || 'null');
            if (json) handleStore(json);
          }
        } catch {}
      });
      return prevSend.apply(this, arguments); // chain
    };
    Object.defineProperty(XMLHttpRequest.prototype, '__sraHooked__', { value: true });
  })();

  // ---- Chain-safe fetch hook (tiny) ----
  (function hookFetchOnce(){
    if (window.__sraFetchHooked__) return;
    window.__sraFetchHooked__ = true;
    const prevFetch = window.fetch;
    window.fetch = async function(){
      const res = await prevFetch.apply(this, arguments);
      try {
        const req = arguments[0];
        const url = typeof req === 'string' ? req : (req && req.url) || '';
        if (url.includes('store_id')) {
          const clone = res.clone();
          const json = await clone.json();
          if (json) handleStore(json);
        }
      } catch {}
      return res;
    };
  })();

  // also try filling when user opens the modal ---- BUG
 // document.addEventListener('click', () => microTry(autofillQty), true);
}


// ===============================
// Rad Tracker (uses Zed-market-data + codename index)
// ===============================
function run_radTracker(){
  (function(){
    // --- keys from your market module
    const MARKET_KEY = "Zed-market-data";
    const CODEMAP_KEY = "Zed-market-codemap";
    const ROI_KEY = "Zed-explore-roi";

    // init ROI store
    if (!localStorage.getItem(ROI_KEY)) {
      localStorage.setItem(ROI_KEY, JSON.stringify({ trip:{ rads:0, value:0 }, lastSummaryAt:0 }));
    }

    // ---------- small storage helpers ----------
    const readROI = () => { try { return JSON.parse(localStorage.getItem(ROI_KEY)) || { trip:{rads:0,value:0}, lastSummaryAt:0 }; } catch { return { trip:{rads:0,value:0}, lastSummaryAt:0 }; } };
    const writeROI = (roi) => localStorage.setItem(ROI_KEY, JSON.stringify(roi));
    const readMarket = () => { try { return JSON.parse(localStorage.getItem(MARKET_KEY) || "{}"); } catch { return {}; } };
    const readCodeMap = () => { try { return JSON.parse(localStorage.getItem(CODEMAP_KEY) || "{}"); } catch { return {}; } };
    const writeCodeMap = (m) => localStorage.setItem(CODEMAP_KEY, JSON.stringify(m || {}));

    // ---------- UI helpers ----------
    function roiGet(){ const r = readROI(); return { ...r.trip, roi: r.trip.rads > 0 ? (r.trip.value / r.trip.rads) : 0 }; }
    function roiAddRads(n){ if (!Number.isFinite(+n) || !+n) return; const r=readROI(); r.trip.rads = Math.max(0,(r.trip.rads||0)+(+n)); writeROI(r); refreshRoiPanel(); }
    function roiAddValue(n){ if (!Number.isFinite(+n) || !+n) return; const r=readROI(); r.trip.value= Math.max(0,(r.trip.value||0)+(+n)); writeROI(r); refreshRoiPanel(); }
    function roiClearAll(){ writeROI({ trip:{rads:0,value:0}, lastSummaryAt:0 }); refreshRoiPanel(); }

    // ---------- build a codename→name/price index whenever getMarket is seen ----------
    // getMarket responses have items with { name, codename, market_price }.
    // We'll mirror them into CODEMAP_KEY for fast codename lookups.
    if (typeof addNetListener === "function") {
      addNetListener(/getMarket(?!User)/i, (_url, resp) => {
        try {
          const items = (resp && (resp.items || (resp.data && resp.data.items) || Array.isArray(resp) && resp)) || [];
          if (!Array.isArray(items) || !items.length) return;
          const m = readCodeMap();
          const now = Date.now();
          for (const it of items) {
            const code = it?.codename; const name = it?.name;
            const price = Number(it?.market_price);
            if (!code || !name) continue;
            const entry = m[code] || {};
            m[code] = {
              name,
              // prefer latest numeric price if present
              price: Number.isFinite(price) ? price : (Number.isFinite(entry.price) ? entry.price : undefined),
              ts: now
            };
          }
          writeCodeMap(m);
        } catch {}
      });
    }

    // ---------- pricing helper (codename → unit price) ----------
    function unitPriceForCodename(code, rewardVars){
      if (!code) return null;

      // 1) Try codemap direct (latest market_price we saw with this codename)
      const cm = readCodeMap()[code];
      if (cm && Number.isFinite(cm.price)) return cm.price;

      // 2) Try codemap name → market cache (name -> price in MARKET_KEY)
      //    This covers cases where our codemap has name but not price yet.
      if (cm && typeof cm.name === "string") {
        const named = readMarket()[cm.name];
        if (Number.isFinite(+named)) return +named;
      }

      // 3) Last resort: if reward payload has hints
      const fallbacks = [
        Number(rewardVars?.value),
        Number(rewardVars?.vars?.sell),
        Number(rewardVars?.vars?.buy)
      ];
      for (const v of fallbacks) if (Number.isFinite(v)) return v;

      // 4) Also try a naive name lookup if the reward included a readable name
      //    (some APIs send both name and codename)
      const nameFromReward = rewardVars?.name;
      if (nameFromReward) {
        const byName = readMarket()[nameFromReward];
        if (Number.isFinite(+byName)) return +byName;
      }

      return null;
    }

    // ---------- Start_job valuation ----------
    function valueStartJob(resp){
      const outcome = resp?.outcome || {};
      const job = resp?.job || {};
      const iters = Number.isFinite(outcome.iterations) ? outcome.iterations : 1;

      // Rads from requirement "rad" in job.items (req_qty * iterations)
      let radSpent = 0;
      if (job.items) {
        for (const k of Object.keys(job.items)) {
          const req = job.items[k];
          if (req?.codename === "rad") radSpent += (Number(req.req_qty) || 0) * iters;
        }
      }

      // Loot value = Σ (posted_qty × unitPriceForCodename)
      let valueAdd = 0;
      for (const r of Array.isArray(outcome.rewards) ? outcome.rewards : []) {
        const code = r.codename || r.name || "unknown";
        const qty = Number.isFinite(r.posted_qty) ? r.posted_qty : 0;
        if (qty <= 0) continue;

        const unit = unitPriceForCodename(code, r);
        if (Number.isFinite(+unit)) valueAdd += (+unit) * qty;
      }

      return { radSpent, valueAdd };
    }

    // ---------- scan network responses ----------
    function scanResponse(url, response){
      try{
        if (!/start_job|startJob/i.test(url)) return;
        if (!response?.outcome) return;

        const { radSpent, valueAdd } = valueStartJob(response);
        if (radSpent > 0) roiAddRads(radSpent);
        if (valueAdd > 0) roiAddValue(valueAdd);

        // console.debug("[radTracker] Δ", { radSpent, valueAdd });
      } catch {}
    }

    // ---------- wire to XHR/fetch (guarded) ----------
    (function (){
      const FLAG = '__radTrackerWrapped';
      if (XMLHttpRequest.prototype[FLAG]) return;
      const originalOpen = XMLHttpRequest.prototype.open;
      const originalSend = XMLHttpRequest.prototype.send;
      XMLHttpRequest.prototype.open = function (method, url) { this._url = url; return originalOpen.apply(this, arguments); };
      XMLHttpRequest.prototype.send = function (body) {
        this.addEventListener("readystatechange", function () {
          if (this.readyState === 4) {
            try {
              const text = this.responseText || "";
              const first = text[0];
              if (!text || (first !== '{' && first !== '[')) return;
              const response = JSON.parse(text);
              scanResponse(this._url || "", response);
            } catch {}
          }
        });
        return originalSend.apply(this, arguments);
      };
      Object.defineProperty(XMLHttpRequest.prototype, FLAG, { value: true, configurable: false });
    })();

    // ---------- mini UI ----------
    let roiPanel, roiBtn;
    function refreshRoiPanel(){
      if (!roiPanel) return;
      const m = roiGet();
      roiPanel.querySelector(".erp-rads").textContent = m.rads.toFixed(0);
      roiPanel.querySelector(".erp-value").textContent = m.value.toFixed(0);
      roiPanel.querySelector(".erp-vpr").textContent = (m.rads > 0 ? m.roi : 0).toFixed(2);
    }
    function toggleRoiPanel(){ if (!roiPanel) return; roiPanel.classList.toggle('open'); if (roiPanel.classList.contains('open')) refreshRoiPanel(); }
    function buildRoiPanel(){
      if (roiPanel) return roiPanel;
      roiPanel = document.createElement('div');
      roiPanel.className = 'explore-roi-panel';
      roiPanel.innerHTML = `
        <div class="erp-head"><strong>RAD ROI</strong><button class="erp-close" aria-label="Close">✕</button></div>
        <div class="erp-body">
          <div class="erp-row"><span>Rads spent:</span><b class="erp-rads">0</b></div>
          <div class="erp-row"><span>Loot value:</span><b class="erp-value">0</b></div>
          <div class="erp-row"><span>Value / Rad:</span><b class="erp-vpr">0</b></div>
        </div>
        <div class="erp-actions"><button class="erp-reset">Reset</button></div>
      `;
      roiPanel.querySelector(".erp-close").addEventListener("click", toggleRoiPanel);
      roiPanel.querySelector(".erp-reset").addEventListener("click", () => roiClearAll());
      document.body.appendChild(roiPanel);
      refreshRoiPanel();
      return roiPanel;
    }
    function ensureRoiButton(){
      if (roiBtn) return roiBtn;
      roiBtn = document.createElement('button');
      roiBtn.className = 'explore-roi-btn';
      roiBtn.type = 'button';
      roiBtn.title = 'Show ROI';
      roiBtn.textContent = 'ROI';
      roiBtn.addEventListener('click', () => { buildRoiPanel(); toggleRoiPanel(); });
      const gearLike = document.querySelector('.gear, .settings, .zed-gear, .zed-options, [data-gear], [data-zed-gear]');
      if (gearLike?.parentElement) gearLike.parentElement.appendChild(roiBtn);
      else document.body.appendChild(roiBtn);
      return roiBtn;
    }
const style = document.createElement('style');
style.textContent = `
  .explore-roi-btn {
    position:fixed; right:12px; bottom:58px; z-index:9999;
    padding:6px 12px; font-weight:600; font-size:12px; border-radius:8px;
    border:1px solid rgba(255,255,255,.15);
    background:rgba(20,20,28,.75); color:#fff; cursor:pointer;
    box-shadow: 0 3px 12px rgba(0,0,0,.4); backdrop-filter: blur(5px);
    transition: all .15s ease;
  }
  .explore-roi-btn:hover {
    background:rgba(32,34,44,.9);
    transform: translateY(-1px);
  }

  .explore-roi-panel {
    position:fixed; right:12px; bottom:110px; z-index:10000; width:240px;
    display:none; padding:12px; border-radius:12px;
    background:rgba(16,18,22,.95); color:#fff;
    border:1px solid rgba(255,255,255,.12);
    box-shadow:0 8px 28px rgba(0,0,0,.55); backdrop-filter: blur(8px);
    font-size:13px;
  }
  .explore-roi-panel.open { display:block; }

  .erp-head {
    display:flex; align-items:center; justify-content:space-between;
    margin-bottom:10px; font-weight:600; font-size:14px;
  }

  .erp-body {
    display:grid;
    grid-template-columns: 1fr auto;
    gap:8px 6px;
    margin-bottom:10px;
  }

  .erp-row {
    grid-column: 1 / -1;
    display:flex; align-items:center; justify-content:space-between;
    padding:6px 8px;
    border-radius:8px;
    background:rgba(255,255,255,.05);
    border:1px solid rgba(255,255,255,.08);
  }

  .erp-label { font-weight:500; color:#e2e2e2; }
  .erp-value { font-weight:600; color:#fff; font-variant-numeric: tabular-nums; }

  .erp-actions {
    display:flex; gap:6px; margin-top:8px;
  }
  .erp-actions button {
    flex:1 1 auto; padding:6px 10px; border-radius:6px;
    border:1px solid rgba(255,255,255,.18);
    background:rgba(32,36,42,.85); color:#fff;
    cursor:pointer; font-size:12px;
    transition: all .15s ease;
  }
  .erp-actions button:hover {
    background:rgba(45,50,60,.9);
  }
`;
document.head.appendChild(style);


    ensureRoiButton();
    buildRoiPanel();
  })();
}


    // ---------- Market Favs ----------
      function run_Durability(){
// Lite Durability/Condition annotator — FIXED (plain object)
(function(){
  const TIER_USES = {
    brittle:10,"very weak":25,poor:50,weak:100,moderate:150,tempered:200,
    resilient:250,durable:300,reinforced:350,robust:500,"long lasting":1000,
    enduring:2000,pristine:3000,embued:4000,imbued:4000,immaculate:5000
  };
  const TIERS = Object.keys(TIER_USES);
  const norm = s => String(s||'').toLowerCase().replace(/\s+/g,' ').trim();

  function readTierWord(el){
    const txt = norm(el.textContent||'');
    return TIERS.find(t => txt.includes(t)) || null;
  }
  function findConditionPercent(scope){
    const condEl = scope.querySelector('.stat-condition') || (() => {
      const blocks = scope.querySelectorAll('.stat-block');
      for (const b of blocks) {
        const l = b.querySelector('.stat-label');
        if (l && norm(l.textContent)==='condition') return b;
      }
      return null;
    })();
    if (!condEl) return null;
    const m = (condEl.textContent||'').match(/(\d{1,3})\s*%/);
    return m ? Math.min(100, Math.max(0, parseInt(m[1],10))) / 100 : null;
  }
  function update(root=document){
    root.querySelectorAll('.stat-block').forEach(block=>{
      const lbl = block.querySelector('.stat-label');
      if (!lbl || norm(lbl.textContent) !== 'durability') return;

      const val = block.querySelector('.stat-durability') || block.querySelector('.stat-value');
      if (!val) return;

      const tierWord = readTierWord(val);
      if (!tierWord) return;

      const max = TIER_USES[tierWord];
      if (!max) return;

      const grid = block.closest('.item-stats-grid') || document;
      const pct = findConditionPercent(grid);
      const text = (pct!=null) ? `${Math.round(max*pct)}/${max}` : `${max}`;

      let badge = val.querySelector('.zed-dur-uses-badge');
      if (!badge) {
        badge = document.createElement('span');
        badge.className = 'zed-dur-uses-badge';
        badge.style.cssText = 'margin-left:.5rem;font-size:12px;opacity:.9;';
        val.appendChild(badge);
      }
      badge.textContent = text;
    });
  }
  function scheduleBurst(){
    requestAnimationFrame(update);
    setTimeout(update, 80);
    setTimeout(update, 200);
    setTimeout(update, 450);
  }
  document.addEventListener('click', scheduleBurst, true);
  document.addEventListener('keyup', e => { if (e.key==='Enter'||e.key===' ') scheduleBurst(); }, true);
  new MutationObserver(m=>{ for (const x of m) for (const n of x.addedNodes||[]) if (n.nodeType===1 && n.querySelector?.('.stat-durability,.stat-condition,.item-stats-grid,.stat-block')) update(n); })
    .observe(document.body,{childList:true,subtree:true});
  setTimeout(update, 200);
})();

      }




// ===============================
// Market Buy Tracker
// ===============================
function run_marketBuyTracker(){
  (function(){
    const API_ITEMS = "https://api.zed.city/loadItems";
    const MARKET_KEY = "Zed-market-data";
    const MBT_LOG_KEY = "Zed-market-buys-log"; // array of entries to persist
    const POLL_MS = 30_000;

    let lastSnapshot = new Map();
    let lastSnapshotAt = 0;
    let pollTimer = null;
    let inFlightBuy = false;
    let panel, btn, list;

    const nowISO = () => new Date().toLocaleString();
    const readMarket = () => { try { return JSON.parse(localStorage.getItem(MARKET_KEY)||"{}"); } catch { return {}; } };
    const readLog = () => { try { return JSON.parse(localStorage.getItem(MBT_LOG_KEY)||"[]"); } catch { return []; } };
    const writeLog = (arr) => localStorage.setItem(MBT_LOG_KEY, JSON.stringify(arr.slice(-500))); // keep last 500

    function indexInv(json){
      const map = new Map();
      const items = Array.isArray(json?.items) ? json.items : [];
      for (const it of items){
        const key = it.codename || it.id || it.name;
        const qty = Number(it.quantity)||0;
        map.set(key, qty);
      }
      return map;
    }

    async function fetchInventory(){
      try{
        const r = await fetch(API_ITEMS, {credentials:"include"});
        if (!r.ok) return;
        const j = await r.json();
        lastSnapshot = indexInv(j);
        lastSnapshotAt = Date.now();
      }catch{}
    }

    function startPoll(){
      clearInterval(pollTimer);
      pollTimer = setInterval(fetchInventory, POLL_MS);
      fetchInventory();
    }

    function ensureStyles(){
      if (document.getElementById("mbt-style")) return;
      const css = document.createElement("style");
      css.id = "mbt-style";
      css.textContent = `
      .mbt-btn {
        position:fixed; right:12px; bottom:140px; z-index:9999;
        padding:6px 12px; font-weight:600; font-size:12px; border-radius:8px;
        border:1px solid rgba(255,255,255,.15);
        background:rgba(20,20,28,.75); color:#fff; cursor:pointer;
        box-shadow:0 3px 12px rgba(0,0,0,.4); backdrop-filter: blur(5px);
        transition:all .15s ease;
      }
      .mbt-btn:hover { background:rgba(32,34,44,.9); transform: translateY(-1px); }
      .mbt-panel {
        position:fixed; right:12px; bottom:164px; z-index:10000; width:300px;
        display:none; padding:12px; border-radius:12px;
        background:rgba(16,18,22,.95); color:#fff;
        border:1px solid rgba(255,255,255,.12);
        box-shadow:0 8px 28px rgba(0,0,0,.55); backdrop-filter: blur(8px);
        font-size:12px;
      }
      .mbt-panel.open { display:block; }
      .mbt-head { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; font-weight:700; }
      .mbt-list {
        max-height:260px; overflow:auto; display:flex; flex-direction:column; gap:6px;
        padding-right:2px;
      }
      .mbt-row {
        display:grid; grid-template-columns: 1fr auto; gap:4px 8px;
        padding:6px 8px; border-radius:8px;
        background:rgba(255,255,255,.05); border:1px solid rgba(255,255,255,.08);
      }
      .mbt-row .meta { grid-column:1 / -1; opacity:.85; font-size:11px; display:flex; gap:6px; flex-wrap:wrap; }
      .mbt-row .name { font-weight:600; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
      .mbt-row .qty { font-variant-numeric: tabular-nums; }
      .mbt-row .price { justify-self:end; font-variant-numeric: tabular-nums; }
      .mbt-actions { display:flex; gap:6px; margin-top:8px; }
      .mbt-actions button {
        flex:1 1 auto; padding:6px 10px; border-radius:6px;
        border:1px solid rgba(255,255,255,.18);
        background:rgba(32,36,42,.85); color:#fff;
        cursor:pointer; font-size:12px; transition:.15s ease;
      }
      .mbt-actions button:hover { background:rgba(45,50,60,.9); }
      `;
      document.head.appendChild(css);
    }
    function ensureUI(){
      ensureStyles();
      if (!btn){
        btn = document.createElement("button");
        btn.className = "mbt-btn";
        btn.textContent = "Buys";
        btn.title = "Show Market Buys";
        btn.addEventListener("click", () => { ensurePanel(); panel.classList.toggle("open"); });
        document.body.appendChild(btn);
      }
      ensurePanel();
    }
    function ensurePanel(){
      if (panel) return panel;
      panel = document.createElement("div");
      panel.className = "mbt-panel";
      panel.innerHTML = `
        <div class="mbt-head"><span>MARKET BUYS</span><button class="mbt-close" aria-label="Close">✕</button></div>
        <div class="mbt-list"></div>
        <div class="mbt-actions">
          <button class="mbt-clear">Clear</button>

        </div>
      `;
      panel.querySelector(".mbt-close").onclick = () => panel.classList.toggle("open");
      panel.querySelector(".mbt-clear").onclick = () => { writeLog([]); renderFromStorage(); };

      list = panel.querySelector(".mbt-list");
      document.body.appendChild(panel);
      renderFromStorage();
      return panel;
    }
    function renderFromStorage(){
      if (!list) return;
      const data = readLog().slice().reverse();
      list.innerHTML = "";
      for (const entry of data){
        const row = document.createElement("div");
        row.className = "mbt-row";
        const spend = Number(entry.cost)||0;
        const unitTxt = Number.isFinite(entry.unit) ? `$${Math.round(entry.unit).toLocaleString()}` : "—";
        row.innerHTML = `
          <div class="name">${entry.name} <span class="qty">×${entry.qty}</span></div>
          <div class="price">$${Math.round(spend).toLocaleString()}</div>
          <div class="meta">
            <span>${entry.time}</span>
            <span>Unit: ${unitTxt}</span>
          </div>
        `;
        list.appendChild(row);
      }
    }
    function pushLog(entry){
      const arr = readLog();
      arr.push(entry);
      writeLog(arr);
      ensureUI();
      const row = document.createElement("div");
      row.className = "mbt-row";
      const unitTxt = Number.isFinite(entry.unit) ? `$${Math.round(entry.unit).toLocaleString()}` : "—";
      row.innerHTML = `
        <div class="name">${entry.name} <span class="qty">×${entry.qty}</span></div>
        <div class="price">$${Math.round(entry.cost||0).toLocaleString()}</div>
        <div class="meta">
          <span>${entry.time}</span>
          <span>Unit: ${unitTxt}</span>
        </div>
      `;
      list?.insertBefore(row, list.firstChild || null);
    }

    function computeDiff(prev, next, nameByKey){
      const diffs = [];
      const keys = new Set([...prev.keys(), ...next.keys()]);
      for (const k of keys){
        const a = Number(prev.get(k)||0);
        const b = Number(next.get(k)||0);
        const d = b - a;
        if (d > 0){
          const name = nameByKey?.get(k) || String(k);
          diffs.push({ key:k, name, qty:d });
        }
      }
      return diffs;
    }

    function buildNameMap(json){
      const map = new Map();
      const items = Array.isArray(json?.items) ? json.items : [];
      for (const it of items){
        const key = it.codename || it.id || it.name;
        map.set(key, it.name || it.codename || String(it.id));
      }
      return map;
    }

    async function handleBuy(url, resp){
      if (inFlightBuy) return;
      inFlightBuy = true;
      try{
        const bag = Array.isArray(resp?.reactive_items_qty) ? resp.reactive_items_qty : [];
        if (!bag.length) { inFlightBuy = false; return; }

        clearInterval(pollTimer);

        const before = new Map(lastSnapshot);
        const r = await fetch(API_ITEMS, {credentials:"include"});
        if (!r.ok) { startPoll(); inFlightBuy = false; return; }
        const inv = await r.json();

        const after = indexInv(inv);
        const names = buildNameMap(inv);
        const changes = computeDiff(before, after, names);

        const unitPrices = readMarket();
        const time = nowISO();

        for (const ch of changes){
          const unit = Number(unitPrices[ch.name]);
          const cost = Number.isFinite(unit) ? unit * ch.qty : 0;
          pushLog({
            time, name: ch.name, qty: ch.qty,
            unit: Number.isFinite(unit) ? unit : null,
            cost, method: 'marketBuyItem'
          });
        }

        lastSnapshot = after;
        lastSnapshotAt = Date.now();

      } catch (e) {
      } finally {
        startPoll();
        inFlightBuy = false;
      }
    }

    if (typeof addNetListener === "function"){
      addNetListener(/marketBuyItem/i, handleBuy);
    }

    startPoll();
    document.addEventListener("visibilitychange", () => { if (!document.hidden) fetchInventory(); });
    ensureUI();
    renderFromStorage();
  })();
}


  // ---------- Boot toggles ----------
  if (RUN_MARKET) run_MarketFavs();
  if (RUN_PROFIT) run_ProfitHelper();
  if (RUN_NETWORTH) run_networth();
  if (RUN_TIMERS) run_Timers();
  if (RUN_EXPLORATION) run_ExplorationData();
  if (RUN_MARKETSELLING) run_marketSelling();
  if (RUN_STOREREMAININGAMOUNTS) run_storeRemainingAmounts();
  if (RUN_RADTRACKER) run_radTracker();
  if (RUN_DURABILITY) run_Durability();
  if (RUN_MARKETBUYTRACKER) run_marketBuyTracker();



    })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址