-Fortie- Faction : Armory Loan Scanner

Scan faction armory pages, save loaned item data locally, show member loan popups, and recommend recipients in armory tooltips. Includes PDA scan support.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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

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

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

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

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

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

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         -Fortie- Faction : Armory Loan Scanner
// @namespace    https://torn.com/
// @version      1.2.1
// @description  Scan faction armory pages, save loaned item data locally, show member loan popups, and recommend recipients in armory tooltips. Includes PDA scan support.
// @author       -Fortie-
// @match        https://www.torn.com/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(() => {
  "use strict";

  const STYLE_ID = "fortie-Armory-loan-style";
  const SCAN_BTN_ID = "fortie-Armory-scan-btn";
  const PDA_SCAN_BTN_ID = "fortie-Armory-scan-pda-btn";
  const STORAGE_KEY = "fortie_Armory_scan_data_v10";
  const POPUP_ID = "fortie-loaned-items-popup";

  let observer = null;
  let updateTimer = null;
  let hoverTimer = null;
  let lastPageSignature = "";
  let tooltipBindDone = false;
  let uiHeartbeat = null;

  let cachedStoredItems = null;
  let cachedMemberSummary = null;
  let recommendationCache = new Map();

  const log = (...a) => console.log("[Fortie Armory Loan Scanner]", ...a);
  const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

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

  function escapeHtml(str) {
    return String(str || "")
      .replace(/&/g, "&")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;")
      .replace(/'/g, "&#039;");
  }

  function xidFromHref(href) {
    const m = String(href || "").match(/[?&]XID=(\d+)/i);
    return m ? m[1] : null;
  }

  function itemIdFromSrc(src) {
    const m = String(src || "").match(/\/images\/items\/(\d+)\//i);
    return m ? m[1] : null;
  }

  function readLS(key, fallback = null) {
    try {
      const raw = localStorage.getItem(key);
      return raw ? JSON.parse(raw) : fallback;
    } catch {
      return fallback;
    }
  }

  function writeLS(key, value) {
    localStorage.setItem(key, JSON.stringify(value));
  }

  function invalidateCaches() {
    cachedStoredItems = null;
    cachedMemberSummary = null;
    recommendationCache.clear();
  }

  function applyFortieThemeBridge() {
    const root = document.documentElement;
    const styles = getComputedStyle(root);

    const getVar = (name, fallback) => {
      const v = styles.getPropertyValue(name).trim();
      return v || fallback;
    };

    root.style.setProperty("--fls-bg", getVar("--fortie-bg", "#0f141c"));
    root.style.setProperty("--fls-bg-soft", getVar("--fortie-bgSoft", "#151c26"));
    root.style.setProperty("--fls-panel", getVar("--fortie-panel", "#18212d"));
    root.style.setProperty("--fls-panel2", getVar("--fortie-panel2", "#1d2836"));
    root.style.setProperty("--fls-elevated", getVar("--fortie-elevated", "#223041"));
    root.style.setProperty("--fls-border", getVar("--fortie-border", "#314154"));
    root.style.setProperty("--fls-border-strong", getVar("--fortie-borderStrong", "#4a6079"));

    root.style.setProperty("--fls-accent", getVar("--fortie-faction", getVar("--fortie-blue2", "#4ea0ff")));
    root.style.setProperty("--fls-success", getVar("--fortie-success", getVar("--fortie-green2", "#3ecf6c")));
    root.style.setProperty("--fls-warning", getVar("--fortie-warning", getVar("--fortie-yellow2", "#f0c94d")));
    root.style.setProperty("--fls-danger", getVar("--fortie-danger", getVar("--fortie-red2", "#e04848")));
    root.style.setProperty("--fls-text", getVar("--fortie-text", "#e7edf5"));
    root.style.setProperty("--fls-text-soft", getVar("--fortie-textSoft", "#a9b7c8"));
    root.style.setProperty("--fls-text-muted", getVar("--fortie-textMuted", "#6b7f91"));

    root.style.setProperty("--fls-weapons", getVar("--fortie-faction", getVar("--fortie-danger", "#d85b6a")));
    root.style.setProperty("--fls-armour", getVar("--fortie-info", getVar("--fortie-blue2", "#4ea0ff")));
    root.style.setProperty("--fls-temporary", getVar("--fortie-success", getVar("--fortie-green2", "#3ecf6c")));
  }

  window.addEventListener("fortie:theme-updated", applyFortieThemeBridge);

  function isFactionPage() {
    const u = location.href.toLowerCase();
    return u.includes("factions.php") && u.includes("step=your");
  }

  function getHashValue(key) {
    const hash = String(location.hash || "");
    const m = hash.match(new RegExp(`[?&#/]?${key}=([^&#]+)`, "i"));
    return m ? m[1] ? decodeURIComponent(m[1]).toLowerCase() : "" : "";
  }

  function isFactionInfoPage() {
    if (!isFactionPage()) return false;
    return getHashValue("tab") === "info";
  }

  function getArmorySubTab() {
    if (!isFactionPage()) return null;

    const tab = getHashValue("tab");
    if (tab !== "armoury" && tab !== "armory") return null;

    const sub = getHashValue("sub");

    if (sub === "weapons") return "weapons";
    if (sub === "armour" || sub === "armor") return "armour";
    if (sub === "temporary") return "temporary";

    return null;
  }

  function isSupportedArmoryPage() {
    return ["weapons", "armour", "temporary"].includes(getArmorySubTab());
  }

  function getStartValue() {
    const hash = String(location.hash || "");
    const match = hash.match(/(?:^|[#/&?])start=([^&#/]+)/i);
    const raw = match ? decodeURIComponent(match[1]) : "0";

    if (!raw || raw === "undefined" || raw === "null") return "0";
    if (/^\d+$/.test(raw)) return raw;

    return "0";
  }

  function toNum(v) {
    const n = parseFloat(String(v || "").replace(/[^\d.]/g, ""));
    return Number.isFinite(n) ? n : 0;
  }

  function normalizeText(v) {
    return cleanText(v || "").toLowerCase();
  }

  function readStoredData() {
    return readLS(STORAGE_KEY, {});
  }

  function saveStoredData(subTab, pageStart, items) {
    const existing = readStoredData();
    const section = existing[subTab] || {};
    const pageKey = String(pageStart || "0");

    const payload = {
      ...existing,
      [subTab]: {
        ...section,
        [pageKey]: {
          subTab,
          pageStart: pageKey,
          scannedAt: Date.now(),
          url: location.href,
          count: items.length,
          items
        }
      }
    };

    writeLS(STORAGE_KEY, payload);
    invalidateCaches();
    return payload;
  }

  function flattenStoredItems() {
    if (cachedStoredItems) return cachedStoredItems;

    const data = readStoredData();
    const out = [];

    for (const subTab of Object.keys(data)) {
      const pages = data[subTab] || {};
      for (const pageStart of Object.keys(pages)) {
        const page = pages[pageStart];
        const items = Array.isArray(page?.items) ? page.items : [];
        for (const item of items) out.push(item);
      }
    }

    cachedStoredItems = out;
    return out;
  }

  function getMemberKey(item) {
    return String(item.loanedToXid || item.loanedTo || "").trim();
  }

  function buildMemberLoanSummary() {
    const items = flattenStoredItems().filter(i => getMemberKey(i));
    const byMember = new Map();

    for (const item of items) {
      const key = getMemberKey(item);
      if (!key) continue;

      if (!byMember.has(key)) {
        byMember.set(key, {
          xid: item.loanedToXid || key,
          key,
          name: item.loanedTo || `Member ${key}`,
          weapons: [],
          armour: [],
          temporary: []
        });
      }

      const member = byMember.get(key);

      if (item.category === "weapons") member.weapons.push(item);
      if (item.category === "armour") member.armour.push(item);
      if (item.category === "temporary") member.temporary.push(item);
    }

    return Array.from(byMember.values());
  }

  function getMemberLoanSummary() {
    if (!cachedMemberSummary) cachedMemberSummary = buildMemberLoanSummary();
    return cachedMemberSummary;
  }

  function getItemsForMember(memberKey) {
    const target = String(memberKey || "").trim();
    if (!target) return [];

    const items = flattenStoredItems().filter(item => getMemberKey(item) === target);
    const seen = new Set();

    return items.filter(item => {
      const key = [
        item.category || "",
        item.armoryId || "",
        item.itemId || "",
        item.itemName || "",
        item.weaponType || "",
        item.weaponClass || "",
        item.armorType || "",
        item.damage || "",
        item.accuracy || "",
        item.armorValue || "",
        item.quantity || "",
        getMemberKey(item)
      ].join("|");

      if (seen.has(key)) return false;
      seen.add(key);
      return true;
    });
  }

  function addStyles() {
    if (document.getElementById(STYLE_ID)) return;

    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
      :root {
        --fls-bg: #0f141c;
        --fls-bg-soft: #151c26;
        --fls-panel: #18212d;
        --fls-panel2: #1d2836;
        --fls-elevated: #223041;
        --fls-border: #314154;
        --fls-border-strong: #4a6079;
        --fls-accent: #4ea0ff;
        --fls-success: #3ecf6c;
        --fls-warning: #f0c94d;
        --fls-danger: #e04848;
        --fls-text: #e7edf5;
        --fls-text-soft: #a9b7c8;
        --fls-text-muted: #6b7f91;
        --fls-weapons: #d85b6a;
        --fls-armour: #4ea0ff;
        --fls-temporary: #3ecf6c;
      }

      li:has(> a[href="#faction-info"]) {
        background: var(--fls-bg-soft) !important;
        border-color: var(--fls-border) !important;
      }

      li:has(> a[href="#faction-info"]):hover {
        background: var(--fls-panel) !important;
        border-color: var(--fls-border-strong) !important;
      }

      li:has(> a[href="#faction-info"]).ui-tabs-active,
      li:has(> a[href="#faction-info"]).ui-state-active {
        background: linear-gradient(180deg, var(--fls-panel), var(--fls-bg)) !important;
        border-color: var(--fls-accent) !important;
        border-bottom-color: transparent !important;
      }

      li:has(> a[href="#faction-info"]) .tab-name {
        color: var(--fls-text-soft) !important;
      }

      li:has(> a[href="#faction-info"]).ui-tabs-active .tab-name,
      li:has(> a[href="#faction-info"]).ui-state-active .tab-name {
        color: var(--fls-text) !important;
      }

      li:has(> a[href="#faction-info"]) svg path {
        fill: var(--fls-text-muted) !important;
      }

      li:has(> a[href="#faction-info"]).ui-tabs-active svg path,
      li:has(> a[href="#faction-info"]).ui-state-active svg path {
        fill: var(--fls-accent) !important;
      }

      li.weapons,
      li.armor,
      li.armour,
      li.temporary {
        background: var(--fls-bg-soft) !important;
        border-color: var(--fls-border) !important;
      }

      li.weapons,
      li.weapons a,
      li.weapons .ui-tabs-anchor {
        color: var(--fls-weapons) !important;
      }

      li.armor,
      li.armour,
      li.armor a,
      li.armour a,
      li.armor .ui-tabs-anchor,
      li.armour .ui-tabs-anchor {
        color: var(--fls-armour) !important;
      }

      li.temporary,
      li.temporary a,
      li.temporary .ui-tabs-anchor {
        color: var(--fls-temporary) !important;
      }

      li.weapons:hover {
        background: color-mix(in srgb, var(--fls-weapons) 16%, var(--fls-panel)) !important;
        border-color: var(--fls-weapons) !important;
      }

      li.armor:hover,
      li.armour:hover {
        background: color-mix(in srgb, var(--fls-armour) 16%, var(--fls-panel)) !important;
        border-color: var(--fls-armour) !important;
      }

      li.temporary:hover {
        background: color-mix(in srgb, var(--fls-temporary) 16%, var(--fls-panel)) !important;
        border-color: var(--fls-temporary) !important;
      }

      li.weapons.ui-tabs-active,
      li.weapons.ui-state-active {
        background: linear-gradient(180deg, color-mix(in srgb, var(--fls-weapons) 18%, var(--fls-panel)), var(--fls-bg)) !important;
        border-color: var(--fls-weapons) !important;
        border-bottom-color: transparent !important;
      }

      li.armor.ui-tabs-active,
      li.armor.ui-state-active,
      li.armour.ui-tabs-active,
      li.armour.ui-state-active {
        background: linear-gradient(180deg, color-mix(in srgb, var(--fls-armour) 18%, var(--fls-panel)), var(--fls-bg)) !important;
        border-color: var(--fls-armour) !important;
        border-bottom-color: transparent !important;
      }

      li.temporary.ui-tabs-active,
      li.temporary.ui-state-active {
        background: linear-gradient(180deg, color-mix(in srgb, var(--fls-temporary) 18%, var(--fls-panel)), var(--fls-bg)) !important;
        border-color: var(--fls-temporary) !important;
        border-bottom-color: transparent !important;
      }

      li.weapons.ui-tabs-active a,
      li.weapons.ui-state-active a,
      li.weapons.ui-tabs-active .ui-tabs-anchor,
      li.weapons.ui-state-active .ui-tabs-anchor {
        color: var(--fls-weapons) !important;
        font-weight: 800 !important;
      }

      li.armor.ui-tabs-active a,
      li.armor.ui-state-active a,
      li.armour.ui-tabs-active a,
      li.armour.ui-state-active a,
      li.armor.ui-tabs-active .ui-tabs-anchor,
      li.armor.ui-state-active .ui-tabs-anchor,
      li.armour.ui-tabs-active .ui-tabs-anchor,
      li.armour.ui-state-active .ui-tabs-anchor {
        color: var(--fls-armour) !important;
        font-weight: 800 !important;
      }

      li.temporary.ui-tabs-active a,
      li.temporary.ui-state-active a,
      li.temporary.ui-tabs-active .ui-tabs-anchor,
      li.temporary.ui-state-active .ui-tabs-anchor {
        color: var(--fls-temporary) !important;
        font-weight: 800 !important;
      }

      #${SCAN_BTN_ID},
      #${PDA_SCAN_BTN_ID} {
        display: inline-flex !important;
        align-items: center !important;
        justify-content: center !important;
        gap: 6px !important;
        margin-left: 10px !important;
        padding: 6px 10px !important;
        border-radius: 6px !important;
        border: 1px solid var(--fls-border-strong) !important;
        background: linear-gradient(180deg, var(--fls-panel2), var(--fls-elevated)) !important;
        color: var(--fls-text-soft) !important;
        cursor: pointer !important;
        font: 700 12px Arial, sans-serif !important;
        transition: filter .15s ease, border-color .15s ease, color .15s ease !important;
        box-shadow: inset 0 1px 0 rgba(255,255,255,.08), 0 2px 8px rgba(0,0,0,.35) !important;
        white-space: nowrap !important;
      }

      #${SCAN_BTN_ID}:hover,
      #${PDA_SCAN_BTN_ID}:hover {
        filter: brightness(1.15) !important;
        border-color: var(--fls-accent) !important;
        color: var(--fls-text) !important;
      }

      #${SCAN_BTN_ID}:disabled,
      #${PDA_SCAN_BTN_ID}:disabled {
        opacity: .65 !important;
        cursor: not-allowed !important;
      }

      #${SCAN_BTN_ID} svg,
      #${PDA_SCAN_BTN_ID} svg {
        width: 14px !important;
        height: 14px !important;
        fill: currentColor !important;
        flex-shrink: 0 !important;
      }

      #${PDA_SCAN_BTN_ID} {
        float: right !important;
        min-height: 28px !important;
      }

      .fortie-loan-country-slot {
        display: inline-flex !important;
        align-items: center !important;
        margin-left: 6px !important;
        vertical-align: middle !important;
        position: relative !important;
        z-index: 20 !important;
      }

      .fortie-loan-country-btn {
        display: inline-flex !important;
        align-items: center !important;
        justify-content: center !important;
        width: 18px !important;
        height: 18px !important;
        min-width: 18px !important;
        border-radius: 3px !important;
        border: 1px solid rgba(255,255,255,.15) !important;
        background: var(--fls-danger) !important;
        color: #fff !important;
        font: 700 9px/1 Arial, sans-serif !important;
        cursor: pointer !important;
        padding: 0 !important;
        box-shadow: 0 1px 4px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.12) !important;
      }

      #${POPUP_ID} {
        position: fixed;
        inset: 0;
        z-index: 2147483646;
        background: rgba(5, 10, 18, 0.75);
        backdrop-filter: blur(4px);
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .fls-card {
        width: min(640px, 94vw);
        max-height: 84vh;
        display: flex;
        flex-direction: column;
        background: var(--fls-bg);
        border: 1px solid var(--fls-border);
        border-radius: 12px;
        overflow: hidden;
        box-shadow: 0 0 0 1px rgba(78,160,255,.06), 0 32px 80px rgba(0,0,0,.72), 0 8px 24px rgba(0,0,0,.4);
        font-family: Arial, sans-serif;
      }

      .fls-accent {
        height: 3px;
        flex-shrink: 0;
        background: linear-gradient(90deg, var(--fls-weapons) 0%, var(--fls-armour) 50%, var(--fls-temporary) 100%);
      }

      .fls-header {
        display: flex;
        align-items: center;
        gap: 12px;
        padding: 13px 15px;
        background: var(--fls-bg-soft);
        border-bottom: 1px solid var(--fls-border);
        flex-shrink: 0;
      }

      .fls-header-icon {
        width: 36px;
        height: 36px;
        border-radius: 8px;
        background: linear-gradient(135deg, var(--fls-danger), color-mix(in srgb, var(--fls-danger) 55%, #000));
        border: 1px solid rgba(255,255,255,.1);
        box-shadow: 0 2px 8px rgba(0,0,0,.35);
        display: flex;
        align-items: center;
        justify-content: center;
        font: 700 11px/1 Arial, sans-serif;
        color: #fff;
        flex-shrink: 0;
      }

      .fls-header-text { flex: 1; min-width: 0; }

      .fls-header-title {
        font-size: 14px;
        font-weight: 700;
        color: var(--fls-text);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .fls-header-sub {
        margin-top: 2px;
        font-size: 11px;
        color: var(--fls-text-muted);
      }

      .fls-header-sub b {
        color: var(--fls-text-soft);
        font-weight: 600;
      }

      .fls-close {
        display: flex;
        align-items: center;
        justify-content: center;
        width: 28px;
        height: 28px;
        border-radius: 6px;
        border: 1px solid transparent;
        background: transparent;
        color: var(--fls-text-muted);
        cursor: pointer;
        font-size: 14px;
      }

      .fls-close:hover {
        color: var(--fls-text);
        border-color: var(--fls-border);
        background: var(--fls-elevated);
      }

      .fls-body {
        flex: 1;
        overflow-y: auto;
        padding: 14px 13px 16px;
        display: flex;
        flex-direction: column;
        gap: 16px;
      }

      .fls-empty {
        padding: 36px 20px;
        text-align: center;
        border-radius: 8px;
        border: 1px dashed var(--fls-border);
        color: var(--fls-text-muted);
        font-size: 13px;
        line-height: 1.6;
      }

      .fls-section-header {
        display: flex;
        align-items: center;
        gap: 8px;
        margin-bottom: 7px;
      }

      .fls-section-label {
        font-size: 10px;
        font-weight: 700;
        letter-spacing: .08em;
        text-transform: uppercase;
        display: flex;
        align-items: center;
        gap: 5px;
        white-space: nowrap;
      }

      .fls-section-label.weapons { color: var(--fls-weapons); }
      .fls-section-label.armour { color: var(--fls-armour); }
      .fls-section-label.temporary { color: var(--fls-temporary); }

      .fls-section-rule {
        flex: 1;
        height: 1px;
        background: var(--fls-border);
      }

      .fls-section-count {
        font-size: 10px;
        font-weight: 700;
        padding: 2px 7px;
        border-radius: 20px;
        background: var(--fls-elevated);
        color: var(--fls-text-muted);
        border: 1px solid var(--fls-border);
      }

      .fls-items {
        display: flex;
        flex-direction: column;
        gap: 8px;
      }

      .fls-item {
        display: flex;
        align-items: stretch;
        border-radius: 10px;
        overflow: hidden;
        background: #24364b;
        border: 1px solid #31465e;
      }

      .fls-item-accent { width: 4px; flex-shrink: 0; }
      .fls-item.weapons .fls-item-accent { background: var(--fls-weapons); }
      .fls-item.armour .fls-item-accent { background: var(--fls-armour); }
      .fls-item.temporary .fls-item-accent { background: var(--fls-temporary); }

      .fls-item-body {
        flex: 1;
        padding: 10px 12px;
        min-width: 0;
      }

      .fls-item-name-row {
        display: flex;
        align-items: center;
        gap: 8px;
        flex-wrap: wrap;
      }

      .fls-item-name {
        font-size: 13px;
        font-weight: 700;
        color: var(--fls-text);
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .fls-item-stats {
        margin-top: 4px;
        display: flex;
        gap: 12px;
        flex-wrap: wrap;
      }

      .fls-item-stat {
        display: flex;
        align-items: baseline;
        gap: 4px;
        font-size: 11px;
        color: var(--fls-warning);
      }

      .fls-item-stat-label {
        font-size: 9px;
        font-weight: 700;
        color: var(--fls-text-muted);
        text-transform: uppercase;
        letter-spacing: .05em;
      }

      .fls-item-bonuses {
        margin-top: 6px;
        display: flex;
        gap: 5px;
        flex-wrap: wrap;
      }

      .fls-bonus-pill {
        display: inline-flex;
        align-items: center;
        padding: 2px 8px;
        border-radius: 20px;
        font-size: 10px;
        font-weight: 600;
        background: color-mix(in srgb, var(--fls-accent) 13%, transparent);
        border: 1px solid color-mix(in srgb, var(--fls-accent) 25%, transparent);
        color: var(--fls-accent);
        white-space: nowrap;
      }

      .fortie-rec-wrap {
        margin-top: 10px !important;
        padding: 10px !important;
        border-radius: 10px !important;
        border: 1px solid color-mix(in srgb, var(--fls-accent) 55%, transparent) !important;
        background: linear-gradient(180deg, var(--fls-panel2), var(--fls-bg)) !important;
        box-shadow: 0 8px 20px rgba(0,0,0,.5), inset 0 1px 0 rgba(255,255,255,.05) !important;
        color: var(--fls-text) !important;
        max-height: 430px !important;
        overflow: hidden !important;
      }

      .fortie-rec-head {
        display: flex !important;
        justify-content: space-between !important;
        align-items: center !important;
        margin-bottom: 8px !important;
        padding-bottom: 7px !important;
        border-bottom: 1px solid rgba(255,255,255,.08) !important;
      }

      .fortie-rec-title {
        font: 800 12px Arial, sans-serif !important;
        letter-spacing: .10em !important;
        color: var(--fls-text) !important;
        text-transform: uppercase !important;
      }

      .fortie-rec-subtitle {
        font: 900 11px Arial, sans-serif !important;
        color: var(--fls-accent) !important;
        text-shadow: 0 0 8px color-mix(in srgb, var(--fls-accent) 35%, transparent) !important;
      }

      .fortie-rec-list {
        display: flex !important;
        flex-direction: column !important;
        gap: 8px !important;
        max-height: 350px !important;
        overflow-y: auto !important;
        padding-right: 3px !important;
        scroll-behavior: smooth !important;
      }

      .fortie-rec-list::-webkit-scrollbar {
        width: 4px !important;
      }

      .fortie-rec-list::-webkit-scrollbar-thumb {
        background: color-mix(in srgb, var(--fls-accent) 55%, transparent) !important;
        border-radius: 999px !important;
      }

      .fortie-rec-row {
        display: flex !important;
        align-items: center !important;
        justify-content: space-between !important;
        gap: 10px !important;
        padding: 10px !important;
        border-radius: 8px !important;
        background: rgba(255,255,255,.045) !important;
        border: 1px solid rgba(255,255,255,.075) !important;
      }

      .fortie-rec-row:first-child {
        border-color: color-mix(in srgb, var(--fls-accent) 55%, transparent) !important;
        background:
          linear-gradient(180deg,
            color-mix(in srgb, var(--fls-accent) 13%, var(--fls-panel2)),
            var(--fls-panel2)
          ) !important;
        box-shadow:
          0 0 0 1px color-mix(in srgb, var(--fls-accent) 25%, transparent),
          0 8px 22px rgba(0,0,0,.45) !important;
      }

      .fortie-rec-left {
        display: flex !important;
        align-items: center !important;
        gap: 8px !important;
        min-width: 0 !important;
      }

      .fortie-rank {
        width: 24px !important;
        min-width: 24px !important;
        height: 24px !important;
        display: inline-flex !important;
        align-items: center !important;
        justify-content: center !important;
        border-radius: 999px !important;
        background: rgba(255,255,255,.08) !important;
        color: #cbd7e8 !important;
        font: 900 11px Arial, sans-serif !important;
      }

      .fortie-rank.gold {
        color: var(--fls-warning) !important;
        background: color-mix(in srgb, var(--fls-warning) 15%, transparent) !important;
        border: 1px solid color-mix(in srgb, var(--fls-warning) 35%, transparent) !important;
      }

      .fortie-rank.silver {
        color: var(--fls-text-soft) !important;
        background: rgba(255,255,255,.10) !important;
        border: 1px solid rgba(255,255,255,.22) !important;
      }

      .fortie-rank.bronze {
        color: #ff9f43 !important;
        background: rgba(255,159,67,.12) !important;
        border: 1px solid rgba(255,159,67,.28) !important;
      }

      .fortie-rec-main {
        min-width: 0 !important;
      }

      .fortie-rec-name {
        font: 800 12px Arial, sans-serif !important;
        color: var(--fls-text) !important;
        line-height: 1.2 !important;
        white-space: nowrap !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        max-width: 170px !important;
      }

      .fortie-rec-reason {
        margin-top: 2px !important;
        font: 700 10px Arial, sans-serif !important;
        color: var(--fls-text-soft) !important;
        line-height: 1.2 !important;
      }

      .fortie-rec-current {
        margin-top: 3px !important;
        font: 600 9px Arial, sans-serif !important;
        color: var(--fls-text-muted) !important;
        line-height: 1.25 !important;
        max-width: 210px !important;
      }

      .fortie-rec-current.no-match {
        color: var(--fls-warning) !important;
        font-weight: 800 !important;
      }

      .fortie-tag {
        flex-shrink: 0 !important;
        padding: 4px 7px !important;
        border-radius: 999px !important;
        font: 900 9px Arial, sans-serif !important;
        letter-spacing: .04em !important;
        text-transform: uppercase !important;
        min-width: 58px !important;
        text-align: center !important;
      }

      .fortie-tag.best {
        background: color-mix(in srgb, var(--fls-accent) 18%, transparent) !important;
        color: var(--fls-accent) !important;
        border: 1px solid color-mix(in srgb, var(--fls-accent) 45%, transparent) !important;
      }

      .fortie-tag.strong {
        background: color-mix(in srgb, var(--fls-success) 18%, transparent) !important;
        color: var(--fls-success) !important;
        border: 1px solid color-mix(in srgb, var(--fls-success) 45%, transparent) !important;
      }

      .fortie-tag.good {
        background: color-mix(in srgb, var(--fls-warning) 18%, transparent) !important;
        color: var(--fls-warning) !important;
        border: 1px solid color-mix(in srgb, var(--fls-warning) 45%, transparent) !important;
      }

      .fortie-rec-empty {
        text-align: center !important;
        padding: 12px !important;
        font: 700 11px Arial, sans-serif !important;
        color: var(--fls-text-muted) !important;
      }
    `;
    document.head.appendChild(style);
  }

  function getItemBlock(row) {
    const parts = [row];

    let next = row.nextElementSibling;
    while (next && !next.querySelector?.(".img-wrap")) {
      parts.push(next);
      next = next.nextElementSibling;
    }

    return parts;
  }

  function getItemBlockText(row) {
    return getItemBlock(row)
      .map(el => cleanText(el.textContent || ""))
      .join(" ");
  }

  function isPdaExpanded(row) {
    return /Loaned:\s*/i.test(getItemBlockText(row));
  }

  function getPdaExpandButton(row) {
    const candidates = [
      row.querySelector('[class*="settings"]'),
      row.querySelector('[class*="gear"]'),
      row.querySelector('[class*="manage"]'),
      row.querySelector('button[aria-label*="manage" i]'),
      row.querySelector('button[aria-label*="settings" i]'),
      row.querySelector('i[class*="settings"]'),
      row.querySelector('i[class*="gear"]')
    ].filter(Boolean);

    if (candidates.length) return candidates[0];

    return Array.from(row.querySelectorAll("button, a, i, span, div"))
      .filter(el => {
        if (!(el instanceof HTMLElement)) return false;
        const r = el.getBoundingClientRect();
        if (r.width < 18 || r.height < 18) return false;

        const txt = cleanText(el.textContent).toLowerCase();
        const cls = String(el.className || "").toLowerCase();

        return cls.includes("settings") || cls.includes("gear") || txt === "⚙" || txt === "";
      })
      .sort((a, b) => b.getBoundingClientRect().left - a.getBoundingClientRect().left)[0] || null;
  }

  async function autoExpandPdaRows() {
    if (!isSupportedArmoryPage()) return;

    const rows = getAllCandidateRows().filter(row => row.offsetParent !== null);
    let expanded = 0;

    for (const row of rows) {
      if (isPdaExpanded(row)) continue;

      const btn = getPdaExpandButton(row);
      if (!btn) continue;

      try {
        btn.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true, view: window }));
        btn.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true, view: window }));
        btn.click();
        expanded++;
        await sleep(140);
      } catch {}
    }

    if (expanded) await sleep(350);
  }

  function getAllCandidateRows() {
    return Array.from(document.querySelectorAll("li"))
      .filter(li => {
        const hasImg = li.querySelector(".img-wrap[data-armoryid][data-itemid], .img-wrap img, .img-wrap");
        const hasName = li.querySelector(".name") || cleanText(li.textContent).length > 2;
        return hasImg && hasName;
      });
  }

  function getBonusSpans(row) {
    return Array.from(row.querySelectorAll("ul.bonuses > li > span"));
  }

  function detectWeaponSlot(row) {
    const txt = ` ${getItemBlockText(row).toLowerCase()} `;
    const typeText = ` ${cleanText(row.querySelector(".type")?.textContent || "").toLowerCase()} `;

    if (txt.includes(" primary ") || typeText.includes(" primary ")) return "Primary";
    if (txt.includes(" secondary ") || typeText.includes(" secondary ")) return "Secondary";
    if (txt.includes(" melee ") || typeText.includes(" melee ")) return "Melee";

    return null;
  }

  function detectWeaponClass(row) {
    const hay = ` ${getItemBlockText(row).toLowerCase()} ${cleanText(row.querySelector(".type")?.textContent || "").toLowerCase()} `;

    const classMap = [
      ["machine gun", "Machine Gun"],
      ["heavy artillery", "Heavy Artillery"],
      ["sub machine gun", "SMG"],
      ["smg", "SMG"],
      ["shotgun", "Shotgun"],
      ["rifle", "Rifle"],
      ["pistol", "Pistol"],
      ["revolver", "Pistol"],
      ["launcher", "Launcher"],
      ["flamethrower", "Flamethrower"],
      ["bow", "Bow"],
      ["crossbow", "Crossbow"],
      ["clubbing", "Clubbing"],
      ["slashing", "Slashing"],
      ["piercing", "Piercing"],
      ["mechanical", "Mechanical"]
    ];

    for (const [needle, label] of classMap) {
      if (hay.includes(` ${needle} `)) return label;
    }

    return null;
  }

  function inferRowCategory(row) {
    const typeText = cleanText(row.querySelector(".type")?.textContent || "").toLowerCase();
    const bonusSpans = getBonusSpans(row);

    if (typeText === "temporary") return "temporary";
    if (typeText === "defensive") return "armour";

    if (detectWeaponSlot(row)) return "weapons";
    if (bonusSpans.length >= 2) return "weapons";

    if (typeText.includes("primary") || typeText.includes("secondary") || typeText.includes("melee")) return "weapons";

    return getArmorySubTab() || "unknown";
  }

  function getRowsForSubTab(subTab) {
    const currentSubTab = getArmorySubTab();
    const visibleRows = getAllCandidateRows().filter(row => row.offsetParent !== null);

    if (currentSubTab === subTab && visibleRows.length) return visibleRows;

    return getAllCandidateRows().filter(row => inferRowCategory(row) === subTab);
  }

  function extractBonusLabel(icon) {
    if (!icon) return null;

    const classes = Array.from(icon.classList || []);
    if (classes.some(c => c.includes("blank"))) return null;
    if (classes.some(c => /damage|accuracy|attack|defence|defense|armour|armor/i.test(c))) return null;

    const title = icon.getAttribute("title");
    if (!title) return null;

    const boldMatch = title.match(/<b>(.*?)<\/b>/i);
    if (boldMatch?.[1]) {
      const label = cleanText(boldMatch[1]);
      if (!/^(damage|accuracy|attack|defence|defense|armour|armor)$/i.test(label)) return label;
    }

    const stripped = title
      .replace(/<br\s*\/?>/gi, " ")
      .replace(/<[^>]+>/g, " ")
      .replace(/\s+/g, " ")
      .trim();

    if (!stripped) return null;
    if (/^(damage|accuracy|attack|defence|defense|armour|armor)$/i.test(stripped)) return null;

    return stripped;
  }

  function extractBonusLabels(row) {
    const seen = new Set();

    return Array.from(row.querySelectorAll("ul.bonuses i[title]"))
      .map(extractBonusLabel)
      .filter(Boolean)
      .filter(label => {
        const key = label.toLowerCase();
        if (seen.has(key)) return false;
        seen.add(key);
        return true;
      });
  }

  function detectWeaponTier(row) {
    const img = row.querySelector(".img-wrap img");
    const cls = String(img?.className || "").toLowerCase();

    if (cls.includes("glow-red")) return "red";
    if (cls.includes("glow-orange")) return "orange";
    if (cls.includes("glow-yellow")) return "yellow";
    return "grey";
  }

  function extractLoanedTo(row) {
    const loaned = row.querySelector(".loaned");

    if (loaned) {
      const link = loaned.querySelector('a[href*="XID="]');
      if (link) {
        return {
          status: "loaned",
          name: cleanText(link.textContent),
          xid: xidFromHref(link.getAttribute("href"))
        };
      }

      const txt = cleanText(loaned.textContent).replace(/^Loaned:\s*/i, "");
      if (/available/i.test(txt)) return { status: "available", name: null, xid: null };
      return { status: txt ? "loaned" : "unknown", name: txt || null, xid: null };
    }

    const text = getItemBlockText(row);
    const match = text.match(/Loaned:\s*(.*?)(?=\s+(Loan|Give|Buy:|Sell:|Value:|Circ:|Damage:|Accuracy:|Rate of Fire:|Stealth:|Caliber:|Ammo:|Bonus:|Quality:|$))/i);

    if (!match) return { status: "unknown", name: null, xid: null };

    const value = cleanText(match[1]);

    if (!value || /available/i.test(value)) {
      return { status: "available", name: null, xid: null };
    }

    const link = row.querySelector('a[href*="profiles.php?XID="], a[href*="XID="]');
    const xid = xidFromHref(link?.getAttribute("href"));

    return {
      status: "loaned",
      name: value,
      xid: xid || null
    };
  }

  function extractBaseRowData(row) {
    const blockText = getItemBlockText(row);

    const wrap = row.querySelector(".img-wrap[data-armoryid][data-itemid]");
    const nameEl = row.querySelector(".name");
    const typeEl = row.querySelector(".type");
    const img = row.querySelector(".img-wrap img");

    let itemName = cleanText(nameEl?.cloneNode(true)?.textContent || "");

    if (!itemName) {
      const possible = row.querySelector('[class*="name"]');
      itemName = cleanText(possible?.textContent || "");
    }

    if (!itemName) {
      const m = blockText.match(/^([A-Za-z0-9 .'\-]+?)\s+(Loaned:|The\s)/i);
      itemName = cleanText(m?.[1] || "");
    }

    return {
      armoryId: wrap?.dataset.armoryid || null,
      itemId: wrap?.dataset.itemid || itemIdFromSrc(img?.getAttribute("src")) || null,
      itemName,
      type: cleanText(typeEl?.textContent || "")
    };
  }

  function extractWeaponStats(row) {
    const bonusSpans = getBonusSpans(row);
    const bonuses = extractBonusLabels(row);
    const text = getItemBlockText(row);

    const dmg = cleanText(bonusSpans[0]?.textContent || "") || cleanText(text.match(/Damage:\s*([\d.]+)/i)?.[1] || "");
    const acc = cleanText(bonusSpans[1]?.textContent || "") || cleanText(text.match(/Accuracy:\s*([\d.]+)/i)?.[1] || "");
    const bonusText = cleanText(text.match(/Bonus:\s*([^|]+?)(?=\s+Quality:|$)/i)?.[1] || "");

    return {
      damage: dmg || null,
      accuracy: acc || null,
      bonus1: bonuses[0] || bonusText || null,
      bonus2: bonuses[1] || null
    };
  }

  function extractArmorStats(row) {
    const bonusSpans = getBonusSpans(row);
    const bonuses = extractBonusLabels(row);
    const text = getItemBlockText(row);

    return {
      armorValue: cleanText(bonusSpans[0]?.textContent || "") || cleanText(text.match(/Armor:\s*([\d.]+)/i)?.[1] || "") || null,
      bonus1: bonuses[0] || null,
      bonus2: bonuses[1] || null
    };
  }

  function extractTemporaryStats(row) {
    const qtyEl = row.querySelector(".name .qty");
    const qty = cleanText(qtyEl?.textContent || "");

    let name = cleanText(row.querySelector(".name")?.textContent || "");
    name = name.replace(/x\s*\d+\s*$/i, "").trim();

    return {
      itemName: name || null,
      quantity: qty || null
    };
  }

  function parseWeaponsPage() {
    return getRowsForSubTab("weapons").map(row => {
      const base = extractBaseRowData(row);
      const loaned = extractLoanedTo(row);
      const stats = extractWeaponStats(row);
      const weaponType = detectWeaponSlot(row) || null;
      const weaponClass = detectWeaponClass(row) || null;

      return {
        category: "weapons",
        armoryId: base.armoryId,
        itemId: base.itemId,
        itemName: base.itemName,
        weaponType,
        weaponClass,
        rawTypeText: base.type || null,
        damage: stats.damage,
        accuracy: stats.accuracy,
        bonus1: stats.bonus1,
        bonus2: stats.bonus2,
        loanColor: detectWeaponTier(row),
        loanedStatus: loaned.status,
        loanedTo: loaned.name,
        loanedToXid: loaned.xid,
        scannedAt: Date.now()
      };
    });
  }

  function parseArmourPage() {
    return getRowsForSubTab("armour").map(row => {
      const base = extractBaseRowData(row);
      const loaned = extractLoanedTo(row);
      const stats = extractArmorStats(row);

      return {
        category: "armour",
        armoryId: base.armoryId,
        itemId: base.itemId,
        itemName: base.itemName,
        armorType: base.type || null,
        armorValue: stats.armorValue,
        bonus1: stats.bonus1,
        bonus2: stats.bonus2,
        loanColor: "grey",
        loanedStatus: loaned.status,
        loanedTo: loaned.name,
        loanedToXid: loaned.xid,
        scannedAt: Date.now()
      };
    });
  }

  function parseTemporaryPage() {
    return getRowsForSubTab("temporary").map(row => {
      const base = extractBaseRowData(row);
      const loaned = extractLoanedTo(row);
      const temp = extractTemporaryStats(row);

      return {
        category: "temporary",
        armoryId: base.armoryId,
        itemId: base.itemId,
        itemName: temp.itemName || base.itemName,
        quantity: temp.quantity,
        loanColor: "grey",
        loanedStatus: loaned.status,
        loanedTo: loaned.name,
        loanedToXid: loaned.xid,
        scannedAt: Date.now()
      };
    });
  }

  function scanCurrentPage() {
    const subTab = getArmorySubTab();
    if (subTab === "weapons") return parseWeaponsPage();
    if (subTab === "armour") return parseArmourPage();
    if (subTab === "temporary") return parseTemporaryPage();
    return [];
  }

  function refreshDerivedCaches() {
    cachedStoredItems = null;
    cachedMemberSummary = null;
    recommendationCache.clear();

    cachedStoredItems = flattenStoredItems();
    cachedMemberSummary = buildMemberLoanSummary();
  }

  async function onScanClick(e) {
    const btn = e.currentTarget;
    const label = btn.querySelector("span");
    const original = btn.innerHTML;

    btn.disabled = true;
    if (label) label.textContent = "Expanding...";
    else btn.innerHTML = "Expanding...";

    try {
      const subTab = getArmorySubTab();
      const start = String(getStartValue());

      if (!subTab) throw new Error(`Could not determine Armory sub-tab from hash: ${location.hash}`);

      await autoExpandPdaRows();

      if (label) label.textContent = "Scanning...";
      else btn.innerHTML = "Scanning...";

      const items = scanCurrentPage();

      log("Scan target:", {
        href: location.href,
        hash: location.hash,
        subTab,
        start,
        candidateRows: getAllCandidateRows().length,
        matchedRows: getRowsForSubTab(subTab).length,
        count: items.length,
        loaned: items.filter(i => i.loanedTo || i.loanedToXid).length
      });

      saveStoredData(subTab, start, items);
      refreshDerivedCaches();

      if (label) label.textContent = `Saved ${items.length}`;
      else btn.innerHTML = `Saved ${items.length}`;

      setTimeout(() => {
        btn.innerHTML = original;
        btn.disabled = false;
      }, 2000);
    } catch (err) {
      console.error("[Fortie Armory Loan Scanner] Scan failed:", err);

      if (label) label.textContent = "Failed";
      else btn.innerHTML = "Failed";

      setTimeout(() => {
        btn.innerHTML = original;
        btn.disabled = false;
      }, 2000);
    }
  }

  function getWeaponStrength(item) {
    return toNum(item.damage) + toNum(item.accuracy);
  }

  function getArmourStrength(item) {
    return toNum(item.armorValue);
  }

  function inferArmourSlot(itemName) {
    const n = normalizeText(itemName);
    if (n.includes("boot")) return "boots";
    if (n.includes("helmet")) return "helmet";
    if (n.includes("vest")) return "vest";
    if (n.includes("glove")) return "gloves";
    if (n.includes("pant") || n.includes("trouser")) return "pants";
    return "defensive";
  }

  function formatWeaponShort(item) {
    if (!item) return "";

    const parts = [];
    if (item.itemName) parts.push(item.itemName);
    if (item.damage || item.accuracy) parts.push(`DMG ${item.damage || "?"} / ACC ${item.accuracy || "?"}`);
    if (item.bonus1) parts.push(item.bonus1);
    if (item.bonus2) parts.push(item.bonus2);

    return parts.join(" · ");
  }

  function formatArmourShort(item) {
    if (!item) return "";

    const parts = [];
    if (item.itemName) parts.push(item.itemName);
    if (item.armorValue) parts.push(`ARM ${item.armorValue}`);
    if (item.bonus1) parts.push(item.bonus1);
    if (item.bonus2) parts.push(item.bonus2);

    return parts.join(" · ");
  }

  function makeRecommendationCacheKey(item) {
    return [
      item.category || "",
      item.itemName || "",
      item.weaponType || "",
      item.weaponClass || "",
      item.damage || "",
      item.accuracy || "",
      item.armorType || "",
      item.armorValue || "",
      item.bonus1 || "",
      item.bonus2 || "",
      item.quantity || ""
    ].join("|");
  }

  function recommendWeaponRecipients(item, members) {
    const wantedType = normalizeText(item.weaponType);
    const newStrength = getWeaponStrength(item);

    return members.map(member => {
      const sameSlot = member.weapons.filter(w => normalizeText(w.weaponType) === wantedType);
      const sameSlotWithBonus = sameSlot.filter(w => w.bonus1 || w.bonus2);

      if (!sameSlot.length) {
        return {
          xid: member.xid,
          key: member.key,
          name: member.name,
          score: 1400 + newStrength,
          tag: "missing",
          reason: `No ${item.weaponType || "weapon"} loaned`,
          current: "No matching weapon"
        };
      }

      const bestOwned = (sameSlotWithBonus.length ? sameSlotWithBonus : sameSlot)
        .map(w => ({ item: w, strength: getWeaponStrength(w) }))
        .sort((a, b) => b.strength - a.strength)[0];

      const diff = newStrength - bestOwned.strength;

      if (!sameSlotWithBonus.length) {
        return {
          xid: member.xid,
          key: member.key,
          name: member.name,
          score: 1100 + newStrength,
          tag: "missing",
          reason: `No bonused ${item.weaponType || "weapon"} loaned`,
          current: formatWeaponShort(bestOwned.item)
        };
      }

      if (diff > 5) {
        return {
          xid: member.xid,
          key: member.key,
          name: member.name,
          score: 500 + diff,
          tag: "upgrade",
          reason: `Upgrade over ${bestOwned.item.itemName}`,
          current: formatWeaponShort(bestOwned.item)
        };
      }

      if (diff >= -5) {
        return {
          xid: member.xid,
          key: member.key,
          name: member.name,
          score: 100 + diff,
          tag: "sidegrade",
          reason: `Similar to ${bestOwned.item.itemName}`,
          current: formatWeaponShort(bestOwned.item)
        };
      }

      return {
        xid: member.xid,
        key: member.key,
        name: member.name,
        score: diff,
        tag: "better",
        reason: `Already has better ${item.weaponType || "weapon"}`,
        current: formatWeaponShort(bestOwned.item)
      };
    }).sort((a, b) => b.score - a.score);
  }

  function recommendArmourRecipients(item, members) {
    const wantedSlot = inferArmourSlot(item.itemName);
    const newStrength = getArmourStrength(item);

    return members.map(member => {
      const sameSlot = member.armour.filter(a => inferArmourSlot(a.itemName) === wantedSlot);

      if (!sameSlot.length) {
        return {
          xid: member.xid,
          key: member.key,
          name: member.name,
          score: 1000 + newStrength,
          tag: "missing",
          reason: `Missing ${wantedSlot}`,
          current: "No matching armour"
        };
      }

      const bestOwned = sameSlot
        .map(a => ({ item: a, strength: getArmourStrength(a) }))
        .sort((a, b) => b.strength - a.strength)[0];

      const diff = newStrength - bestOwned.strength;

      return {
        xid: member.xid,
        key: member.key,
        name: member.name,
        score: diff > 3 ? 500 + diff : diff >= -3 ? 100 + diff : diff,
        tag: diff > 3 ? "upgrade" : diff >= -3 ? "sidegrade" : "better",
        reason: diff > 3 ? `Upgrade over ${bestOwned.item.itemName}` : diff >= -3 ? `Similar to ${bestOwned.item.itemName}` : `Already has better ${wantedSlot}`,
        current: formatArmourShort(bestOwned.item)
      };
    }).sort((a, b) => b.score - a.score);
  }

  function getRecommendationsForTooltipItem(item) {
    if (!item) return [];

    const key = makeRecommendationCacheKey(item);
    if (recommendationCache.has(key)) return recommendationCache.get(key);

    const members = getMemberLoanSummary();

    if (!members.length) {
      recommendationCache.set(key, []);
      return [];
    }

    let recs = [];
    if (item.category === "weapons") recs = recommendWeaponRecipients(item, members);
    if (item.category === "armour") recs = recommendArmourRecipients(item, members);

    recommendationCache.set(key, recs);
    return recs;
  }

  function renderRecommendationPanel(item, recs) {
    const top = recs.slice(0, 10);

    if (!top.length) {
      return `
        <div class="fortie-rec-wrap" data-fortie-rec="1">
          <div class="fortie-rec-head">
            <div class="fortie-rec-title">Suggested Members</div>
          </div>
          <div class="fortie-rec-empty">Scan the armoury to generate suggestions.</div>
        </div>
      `;
    }

    return `
      <div class="fortie-rec-wrap" data-fortie-rec="1">
        <div class="fortie-rec-head">
          <div class="fortie-rec-title">Suggested Members</div>
          <div class="fortie-rec-subtitle">${escapeHtml(item.itemName || "")}</div>
        </div>

        <div class="fortie-rec-list">
          ${top.map((rec, i) => {
            const rankClass = i === 0 ? "gold" : i === 1 ? "silver" : i === 2 ? "bronze" : "";
            const rankText = i === 0 ? "👑" : String(i + 1);

            const tagClass = i === 0 ? "best" : i === 1 ? "strong" : i === 2 ? "good" : "";
            const tagLabel = i === 0 ? "Best" : i === 1 ? "Strong" : i === 2 ? "Good" : "";

            const currentClass = /no matching/i.test(rec.current || "") ? "no-match" : "";
            const currentText = /no matching/i.test(rec.current || "") ? `⚠ ${rec.current}` : rec.current;

            return `
              <div class="fortie-rec-row">
                <div class="fortie-rec-left">
                  <div class="fortie-rank ${rankClass}">${rankText}</div>
                  <div class="fortie-rec-main">
                    <div class="fortie-rec-name">${escapeHtml(rec.name)}</div>
                    <div class="fortie-rec-reason">${escapeHtml(rec.reason)}</div>
                    ${rec.current ? `<div class="fortie-rec-current ${currentClass}">${escapeHtml(currentText)}</div>` : ""}
                  </div>
                </div>
                ${tagClass ? `<div class="fortie-tag ${tagClass}">${tagLabel}</div>` : ""}
              </div>
            `;
          }).join("")}
        </div>
      </div>
    `;
  }

  function getOpenTooltipRoot() {
    const candidates = Array.from(document.querySelectorAll(".view-item-info, .tooltip4, .item-info-content, .ui-tooltip"))
      .filter(el => {
        const txt = cleanText(el.textContent || "");
        return txt.length > 20 && el.offsetParent !== null;
      });

    return candidates.sort((a, b) => (b.textContent || "").length - (a.textContent || "").length)[0] || null;
  }

  function findTooltipSourceRow(tooltipRoot) {
    if (!tooltipRoot) return null;

    const row = tooltipRoot.closest("li");
    if (row?.querySelector(".img-wrap")) return row;

    const action = tooltipRoot.closest("[data-armoryid]");

    if (action) {
      const armoryId = action.getAttribute("data-armoryid");

      if (armoryId) {
        const wrap = document.querySelector(`.img-wrap[data-armoryid="${armoryId}"]`);
        return wrap?.closest("li") || null;
      }
    }

    const hovered = document.querySelector("li:hover");
    if (hovered?.querySelector(".img-wrap")) return hovered;

    return null;
  }

  function getTooltipItemContext() {
    const tooltipRoot = getOpenTooltipRoot();
    const row = findTooltipSourceRow(tooltipRoot);
    if (!row) return null;

    const category = inferRowCategory(row);
    const base = extractBaseRowData(row);

    if (category === "weapons") {
      const stats = extractWeaponStats(row);

      return {
        category,
        itemName: base.itemName,
        weaponType: detectWeaponSlot(row) || "Primary",
        weaponClass: detectWeaponClass(row) || null,
        damage: stats.damage,
        accuracy: stats.accuracy,
        bonus1: stats.bonus1,
        bonus2: stats.bonus2
      };
    }

    if (category === "armour") {
      const stats = extractArmorStats(row);

      return {
        category,
        itemName: base.itemName,
        armorType: base.type || "Defensive",
        armorValue: stats.armorValue,
        bonus1: stats.bonus1,
        bonus2: stats.bonus2
      };
    }

    if (category === "temporary") {
      const temp = extractTemporaryStats(row);

      return {
        category,
        itemName: temp.itemName || base.itemName,
        quantity: temp.quantity
      };
    }

    return null;
  }

  function injectTooltipRecommendations() {
    const tooltipRoot = getOpenTooltipRoot();
    if (!tooltipRoot) return;
    if (tooltipRoot.querySelector('[data-fortie-rec="1"]')) return;

    const item = getTooltipItemContext();
    if (!item) return;
    if (item.category === "temporary") return;

    const recs = getRecommendationsForTooltipItem(item);
    tooltipRoot.insertAdjacentHTML("beforeend", renderRecommendationPanel(item, recs));
  }

  function scheduleTooltipInjection(delay = 180) {
    clearTimeout(hoverTimer);

    hoverTimer = setTimeout(() => {
      try {
        injectTooltipRecommendations();
      } catch (err) {
        console.error("[Fortie Armory Loan Scanner] Tooltip inject failed:", err);
      }
    }, delay);
  }

  function groupItems(items) {
    return {
      weapons: items.filter(i => i.category === "weapons"),
      armour: items.filter(i => i.category === "armour"),
      temporary: items.filter(i => i.category === "temporary")
    };
  }

  function renderBonusPills(item) {
    const bonuses = [item.bonus1, item.bonus2].filter(Boolean);
    if (!bonuses.length) return "";

    return `<div class="fls-item-bonuses">${bonuses.map(b => `<span class="fls-bonus-pill">${escapeHtml(b)}</span>`).join("")}</div>`;
  }

  function renderWeaponRow(item) {
    const stats = [
      item.weaponType ? `<span class="fls-item-stat"><span class="fls-item-stat-label">Type</span>${escapeHtml(item.weaponType)}</span>` : "",
      item.weaponClass ? `<span class="fls-item-stat"><span class="fls-item-stat-label">Class</span>${escapeHtml(item.weaponClass)}</span>` : "",
      item.damage ? `<span class="fls-item-stat"><span class="fls-item-stat-label">DMG</span>${escapeHtml(item.damage)}</span>` : "",
      item.accuracy ? `<span class="fls-item-stat"><span class="fls-item-stat-label">ACC</span>${escapeHtml(item.accuracy)}</span>` : ""
    ].filter(Boolean).join("");

    return `
      <div class="fls-item weapons">
        <div class="fls-item-accent"></div>
        <div class="fls-item-body">
          <div class="fls-item-name-row">
            <div class="fls-item-name">${escapeHtml(item.itemName || "Unknown weapon")}</div>
          </div>
          ${stats ? `<div class="fls-item-stats">${stats}</div>` : ""}
          ${renderBonusPills(item)}
        </div>
      </div>
    `;
  }

  function renderArmourRow(item) {
    const stats = [
      item.armorType ? `<span class="fls-item-stat"><span class="fls-item-stat-label">Type</span>${escapeHtml(item.armorType)}</span>` : "",
      item.armorValue ? `<span class="fls-item-stat"><span class="fls-item-stat-label">ARM</span>${escapeHtml(item.armorValue)}</span>` : ""
    ].filter(Boolean).join("");

    return `
      <div class="fls-item armour">
        <div class="fls-item-accent"></div>
        <div class="fls-item-body">
          <div class="fls-item-name-row">
            <div class="fls-item-name">${escapeHtml(item.itemName || "Unknown armour")}</div>
          </div>
          ${stats ? `<div class="fls-item-stats">${stats}</div>` : ""}
          ${renderBonusPills(item)}
        </div>
      </div>
    `;
  }

  function renderTemporaryRow(item) {
    const stats = item.quantity
      ? `<div class="fls-item-stats"><span class="fls-item-stat"><span class="fls-item-stat-label">QTY</span>${escapeHtml(item.quantity)}</span></div>`
      : "";

    return `
      <div class="fls-item temporary">
        <div class="fls-item-accent"></div>
        <div class="fls-item-body">
          <div class="fls-item-name-row">
            <div class="fls-item-name">${escapeHtml(item.itemName || "Unknown item")}</div>
          </div>
          ${stats}
        </div>
      </div>
    `;
  }

  const SECTION_META = {
    weapons: { label: "Weapons", icon: "🔫" },
    armour: { label: "Armour", icon: "🦺" },
    temporary: { label: "Temporary", icon: "💣" }
  };

  function renderSection(key, items, renderer) {
    if (!items.length) return "";

    const { label, icon } = SECTION_META[key];

    return `
      <div class="fls-section">
        <div class="fls-section-header">
          <div class="fls-section-label ${key}">${icon} ${escapeHtml(label)}</div>
          <div class="fls-section-rule"></div>
          <div class="fls-section-count">${items.length}</div>
        </div>
        <div class="fls-items">${items.map(renderer).join("")}</div>
      </div>
    `;
  }

  function closeLoanedPopup() {
    document.getElementById(POPUP_ID)?.remove();
    document.removeEventListener("keydown", onPopupEsc, true);
  }

  function onPopupEsc(e) {
    if (e.key === "Escape") closeLoanedPopup();
  }

  function showLoanedPopup(memberName, memberKey) {
    closeLoanedPopup();

    const items = getItemsForMember(memberKey);
    const grouped = groupItems(items);
    const total = items.length;

    const bodyContent = total
      ? `${renderSection("weapons", grouped.weapons, renderWeaponRow)}
         ${renderSection("armour", grouped.armour, renderArmourRow)}
         ${renderSection("temporary", grouped.temporary, renderTemporaryRow)}`
      : `<div class="fls-empty">
           <div class="fls-empty-icon">📦</div>
           No saved loaned items found for this member.<br>Scan the Armory pages first.
         </div>`;

    const popup = document.createElement("div");
    popup.id = POPUP_ID;
    popup.innerHTML = `
      <div class="fls-card">
        <div class="fls-accent"></div>
        <div class="fls-header">
          <div class="fls-header-icon">AR</div>
          <div class="fls-header-text">
            <div class="fls-header-title">${escapeHtml(memberName)}</div>
            <div class="fls-header-sub"><b>${escapeHtml(memberKey)}</b> &nbsp;·&nbsp; ${total} loaned item${total !== 1 ? "s" : ""}</div>
          </div>
          <button type="button" class="fls-close" aria-label="Close">✕</button>
        </div>
        <div class="fls-body">${bodyContent}</div>
      </div>
    `;

    document.body.appendChild(popup);

    popup.addEventListener("click", e => {
      if (e.target === popup) closeLoanedPopup();
    });

    popup.querySelector(".fls-close")?.addEventListener("click", closeLoanedPopup);
    document.addEventListener("keydown", onPopupEsc, true);
  }

  function getMemberRows() {
    return Array.from(document.querySelectorAll(`
      li.table-row,
      ul.members-list > li,
      .members-list > li,
      .f-war-list > li,
      .user-info-list > li,
      [class*="member"] li.table-row
    `)).filter(row => row.querySelector('a[href*="profiles.php?XID="]'));
  }

  function removeLoanedItemButtons() {
    document.querySelectorAll('[data-fortie-loan-button="1"]').forEach(el => el.remove());
  }

  function ensureLoanedItemButtons() {
    if (!isFactionInfoPage()) {
      removeLoanedItemButtons();
      return;
    }

    for (const row of getMemberRows()) {
      if (!(row instanceof HTMLElement)) continue;
      if (row.querySelector('[data-fortie-loan-button="1"]')) continue;

      const profileLink = row.querySelector('a[href*="profiles.php?XID="]');
      const memberXid = xidFromHref(profileLink?.getAttribute("href"));
      if (!memberXid) continue;

      const memberName = cleanText(
        row.querySelector(".honor-text")?.textContent ||
        row.querySelector(".name")?.textContent ||
        row.querySelector('[class*="userName"]')?.textContent ||
        row.querySelector('[class*="name"]')?.textContent ||
        profileLink?.textContent ||
        `Member ${memberXid}`
      );

      const target =
        row.querySelector("li.table-cell.member-icons") ||
        row.querySelector(".table-cell.member-icons") ||
        profileLink?.parentElement;

      if (!target) continue;

      const slot = document.createElement("span");
      slot.dataset.fortieLoanButton = "1";
      slot.className = "fortie-loan-country-slot";
      slot.title = `Loaned Items — ${memberName}`;
      slot.innerHTML = `<button type="button" class="fortie-loan-country-btn" aria-label="Loaned items for ${escapeHtml(memberName)}">AR</button>`;

      slot.querySelector("button")?.addEventListener("click", ev => {
        ev.preventDefault();
        ev.stopPropagation();
        showLoanedPopup(memberName, memberXid);
      });

      target.appendChild(slot);
    }
  }

  function scanButtonHtml() {
    return `
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M10.5 4a6.5 6.5 0 1 0 4.06 11.58l3.43 3.43 1.41-1.41-3.43-3.43A6.5 6.5 0 0 0 10.5 4Zm0 2a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Z"/>
      </svg>
      <span>Scan</span>
    `;
  }

  function getItemTitleHeader() {
    return Array.from(document.querySelectorAll("li.item-title"))
      .find(el => cleanText(el.textContent).toLowerCase() === "item") || null;
  }

  function getPdaArmoryHeader() {
    const exact = "manage armories and deposit to faction";

    return Array.from(document.querySelectorAll("h1, h2, h3, h4, div, span, li"))
      .filter(el => {
        if (!(el instanceof HTMLElement)) return false;
        if (!el.offsetParent) return false;
        if (el.querySelector(`#${PDA_SCAN_BTN_ID}`)) return false;

        const directText = cleanText(
          Array.from(el.childNodes)
            .filter(n => n.nodeType === Node.TEXT_NODE)
            .map(n => n.textContent)
            .join(" ")
        ).toLowerCase();

        return directText === exact;
      })
      .sort((a, b) => {
        const ar = a.getBoundingClientRect();
        const br = b.getBoundingClientRect();
        return (ar.width * ar.height) - (br.width * br.height);
      })[0] || null;
  }

  function removeOrphanScanButtons() {
    document.querySelectorAll(`#${SCAN_BTN_ID}`).forEach(btn => {
      const parent = btn.parentElement;
      if (!parent || !parent.matches("li.item-title")) btn.remove();
    });

    if (!isSupportedArmoryPage()) {
      document.querySelectorAll(`#${PDA_SCAN_BTN_ID}`).forEach(btn => btn.remove());
    }
  }

  function ensureScanButton() {
    removeOrphanScanButtons();
    if (!isSupportedArmoryPage()) return;

    const itemTitle = getItemTitleHeader();

    if (itemTitle && !itemTitle.querySelector(`#${SCAN_BTN_ID}`)) {
      itemTitle.style.display = "flex";
      itemTitle.style.alignItems = "center";
      itemTitle.style.gap = "10px";

      const btn = document.createElement("button");
      btn.id = SCAN_BTN_ID;
      btn.type = "button";
      btn.title = `Scan this ${getArmorySubTab()} page`;
      btn.innerHTML = scanButtonHtml();
      btn.addEventListener("click", onScanClick);

      itemTitle.appendChild(btn);
    }

    ensurePdaScanButton();
  }

  function ensurePdaScanButton() {
    if (!isSupportedArmoryPage()) return;
    if (document.getElementById(PDA_SCAN_BTN_ID)) return;

    const header = getPdaArmoryHeader();
    if (!header) return;

    header.style.display = "flex";
    header.style.alignItems = "center";
    header.style.justifyContent = "space-between";
    header.style.gap = "10px";

    const btn = document.createElement("button");
    btn.id = PDA_SCAN_BTN_ID;
    btn.type = "button";
    btn.title = `Scan this ${getArmorySubTab()} page`;
    btn.innerHTML = scanButtonHtml();
    btn.addEventListener("click", onScanClick);

    header.appendChild(btn);
  }

  function getPageSignature() {
    return [
      location.pathname,
      location.search,
      location.hash,
      isFactionInfoPage() ? "info" : "",
      getArmorySubTab() || ""
    ].join("|");
  }

  function bindLightTooltipTriggers() {
    if (tooltipBindDone) return;
    tooltipBindDone = true;

    const handler = e => {
      const target = e.target;
      if (!(target instanceof Element)) return;

      const row = target.closest("li");
      if (!row) return;
      if (!row.querySelector(".img-wrap")) return;
      if (!isSupportedArmoryPage()) return;

      scheduleTooltipInjection(220);
      scheduleTooltipInjection(420);
    };

    document.addEventListener("mouseover", handler, true);
    document.addEventListener("focusin", handler, true);
    document.addEventListener("click", handler, true);
  }

  function onPageUpdate(force = false) {
    applyFortieThemeBridge();
    addStyles();
    bindLightTooltipTriggers();

    const sig = getPageSignature();
    const pageChanged = sig !== lastPageSignature;

    if (pageChanged || force) lastPageSignature = sig;

    if (isSupportedArmoryPage()) ensureScanButton();
    else removeOrphanScanButtons();

    ensureLoanedItemButtons();
  }

  function startObserver() {
    if (observer) observer.disconnect();

    observer = new MutationObserver(mutations => {
      const onlyTooltipChanges = mutations.every(m => {
        const target = m.target instanceof Element ? m.target : null;
        return target?.closest?.(".view-item-info, .tooltip4, .item-info-content, .ui-tooltip");
      });

      if (onlyTooltipChanges) return;

      clearTimeout(updateTimer);
      updateTimer = setTimeout(() => onPageUpdate(false), 180);
    });

    observer.observe(document.body, { childList: true, subtree: true });

    window.addEventListener("hashchange", () => setTimeout(() => onPageUpdate(true), 300));
    window.addEventListener("popstate", () => setTimeout(() => onPageUpdate(true), 300));

    document.addEventListener("visibilitychange", () => {
      if (!document.hidden) onPageUpdate(true);
    });

    if (uiHeartbeat) clearInterval(uiHeartbeat);

    uiHeartbeat = setInterval(() => {
      try {
        applyFortieThemeBridge();

        if (!isFactionPage()) return;

        if (isSupportedArmoryPage()) {
          ensureScanButton();
          ensurePdaScanButton();
        } else {
          removeOrphanScanButtons();
        }

        ensureLoanedItemButtons();
      } catch (err) {
        console.error("[Fortie Armory Loan Scanner] UI heartbeat failed:", err);
      }
    }, 1200);

    onPageUpdate(true);
  }

  function init() {
    if (!document.body) {
      requestAnimationFrame(init);
      return;
    }

    applyFortieThemeBridge();
    refreshDerivedCaches();
    startObserver();
    log("Loaded");
  }

  init();
})();