Asset Hunter

Search Ripper.Store for assets (DL detection, watchlist, LF post system, etc)

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Asset Hunter
// @namespace    https://github.com/xedinho/Asset-Hunter
// @version      6.2.0
// @description  Search Ripper.Store for assets (DL detection, watchlist, LF post system, etc)
// @author       Xedinho
// @license      MIT
// @match        *://booth.pm/*
// @match        *://*.booth.pm/*
// @match        *://gumroad.com/*
// @match        *://*.gumroad.com/*
// @match        *://jinxxy.com/*
// @match        *://*.jinxxy.com/*
// @match        *://payhip.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addValueChangeListener
// @connect      forum.ripper.store
// @connect      raw.githubusercontent.com
// @connect      api.github.com
// @run-at       document-idle
// ==/UserScript==

(function () {
  "use strict";

  // ─── Version & Changelog ──────────────────────────────────────────────────
  const CURRENT_VERSION = "6.2.0";

  const CHANGELOGS = [
    {
      version: "6.2.0",
      additions: [
        "changelog.add.bumpButton",
        "changelog.add.bumpAll",
        "changelog.add.postCooldown",
      ],
      info: [
        "changelog.info.bumpSetting",
      ],
      removals: [
        "changelog.remove.autoUpdate",
      ],
    },
    {
      version: "6.1.2",
      fixes: [
        "changelog.fix.nameParsingDash",
      ],
    },
    {
      version: "6.1.1",
      fixes: [
        "changelog.fix.boothSanitizationRemoved",
        "changelog.fix.previewBtnRemoved",
      ],
    },
    {
      version: "6.1.0",
      fixes: [
        "changelog.fix.boothLink",
      ],
      additions: [
        "changelog.add.changelogPopup",
        "changelog.add.changelogBtn",
      ],
    },
  ];

  // ─── API Endpoints ────────────────────────────────────────────────────────
  const API_URL        = "https://forum.ripper.store/api/search?term={query}&in=posts&matchWords=any&by=&categories=&searchChildren=false&hasTags=&replies=&repliesFilter=atleast&timeFilter=newer&timeRange=&sortBy=relevance&sortDirection=desc&showAs=topics";
  const TOPIC_API      = "https://forum.ripper.store/api/topic/{tid}";
  const POST_API       = "https://forum.ripper.store/api/v3/topics";
  const POST_REPLY_API = "https://forum.ripper.store/api/v3/topics/{tid}";
  const CONFIG_API     = "https://forum.ripper.store/api/config";
  const SITE_URL       = "https://forum.ripper.store";
  const DONORS_URL     = "https://api.github.com/repos/xedinho/Asset-Hunter/contents/donos.txt";

  // ─── Download Detection Patterns ─────────────────────────────────────────
  const DL_PATTERNS = [
    // Original hosts
    /mega\.nz/i, /mega\.io/i, /mediafire/i, /drive\.google/i, /gofile\.io/i,
    /pixeldrain/i, /anonfiles/i, /anonfile\.la/i, /workupload/i, /1fichier/i,
    /dropbox/i, /onedrive/i, /terabox/i, /bowfile/i,
    // New hosts from community list
    /1cloudfile\.com/i, /archive\.org\/download/i, /app\.bunkrr\.su/i,
    /buzzheavier\.com/i, /clicknupload\.click/i, /cyberfile\.me/i,
    /dailyuploads\.net/i, /datanodes\.to/i, /disk\.yandex\.com/i,
    /fastupload\.io/i, /filebin\.net/i, /fileditch\.com/i,
    /filepost\.io/i, /files\.fm/i, /filetransfer\.io/i,
    /fuckingfast\.net/i, /hexload\.com/i, /mixdrop\.ag/i,
    /send\.cm/i, /terminal\.lc/i, /transfer\.it/i,
    /uploadfile\.pl/i, /uploadhaven\.com/i, /uploadnow\.io/i,
    /wdho\.ru/i, /wetransfer\.com/i, /axfc\.net/i,
    /filemail\.com/i, /sendspace\.com/i, /swisstransfer\.com/i,
    /zippyshare\.day/i,
    // File extensions
    /\.zip\b/i, /\.rar\b/i, /\.7z\b/i,
    // Keywords
    /\bdownload\s*(?:link|here|now|file|this)\b/i,
    /(?:^|\s)dl\s*(?:link|here|:)/im,
    /baixar/i, /descargar/i,
    /\/hidelinks\/r\//i, /🔗[\s]*DL/,
  ];

  // ─── Platform detection ───────────────────────────────────────────────────
  const HOST = location.hostname.replace(/^www\./, "");

  // ─── Booth Adapter ────────────────────────────────────────────────────────
  const BOOTH = {
    getId: () => {
      const m = window.location.pathname.match(/items\/(\d+)/);
      return m ? m[1] : "";
    },
    getName: () => {
      let name = document.title.replace(/\s*[|]\s*BOOTH.*$/i, "").replace(/\s*[-–]\s*BOOTH.*$/i, "").trim();
      if (!name) {
        const el = document.querySelector("h1.u-tpg-title1") || document.querySelector("h1");
        name = el ? el.textContent.trim() : "";
      }
      return name;
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => /\/items\/\d+/.test(window.location.pathname),
  };

  // ─── Gumroad Adapter ──────────────────────────────────────────────────────
  const GUMROAD = {
    getId: () => {
      const m = window.location.pathname.match(/\/l\/([^/?#]+)/);
      return m ? m[1] : "";
    },
    getName: () => {
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").trim();
      }
      return document.title.replace(/\s*[|]\s*Gumroad.*$/i, "").replace(/\s*[-–]\s*Gumroad.*$/i, "").trim();
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => /\/l\/[^/?#]+/.test(window.location.pathname),
  };

  // ─── Jinxxy Adapter ───────────────────────────────────────────────────────
  const JINXXY = {
    getId: () => {
      const m = window.location.pathname.match(/^\/[^/]+\/([^/?#]+)/);
      return m ? m[1] : "";
    },
    getName: () => {
      const h1 = document.querySelector("h1");
      if (h1) return h1.textContent.trim();
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").replace(/\s+by\s+.+?\s+on\s+Jinxxy$/i, "").trim();
      }
      return document.title.replace(/\s*[|]\s*Jinxxy.*$/i, "").replace(/\s*[-–]\s*Jinxxy.*$/i, "").trim();
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => {
      const parts = window.location.pathname.replace(/^\/|\/$/g, "").split("/");
      if (parts.length !== 2) return false;
      const skip = ["market", "my", "about", "terms-of-service", "privacy-policy",
                    "refund-policy", "cart", "search"];
      if (skip.includes(parts[0])) return false;
      return true;
    },
  };

  // ─── Payhip Adapter ───────────────────────────────────────────────────────
  const PAYHIP = {
    getId: () => {
      const m = window.location.pathname.match(/\/b\/([^/?#]+)/);
      return m ? m[1] : "";
    },
    getName: () => {
      const h1 = document.querySelector("h1.font-section-product-name");
      if (h1) return h1.textContent.trim();
      const h1g = document.querySelector("h1");
      if (h1g) return h1g.textContent.trim();
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").trim();
      }
      return document.title.replace(/\s*[|]\s*Payhip.*$/i, "").replace(/\s*[-–]\s*Payhip.*$/i, "").trim();
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => /\/b\/[^/?#]+/.test(window.location.pathname),
  };

  // ─── Active adapter ───────────────────────────────────────────────────────
  function getAdapter() {
    if (HOST === "booth.pm" || HOST.endsWith(".booth.pm")) return BOOTH;
    if (HOST === "gumroad.com" || HOST.endsWith(".gumroad.com")) return GUMROAD;
    if (HOST === "jinxxy.com" || HOST.endsWith(".jinxxy.com")) return JINXXY;
    if (HOST === "payhip.com" || HOST.endsWith(".payhip.com")) return PAYHIP;
    return null;
  }

  // ─── Watermark ────────────────────────────────────────────────────────────
  const WATERMARK = "\n\n---\n# Posted via [Asset Hunter](https://forum.ripper.store/topic/108432/asset-hunter)";

  // ─── Category color map ───────────────────────────────────────────────────
  const CAT_COLORS = {
    "open":              { color: "#ff9540", bg: "rgba(255,149,64,.1)",  bd: "rgba(255,149,64,.25)"  },
    "solved":            { color: "#72f0a8", bg: "rgba(114,240,168,.1)", bd: "rgba(114,240,168,.25)" },
    "gifts":             { color: "#72f0a8", bg: "rgba(114,240,168,.1)", bd: "rgba(114,240,168,.25)" },
    "downloads":         { color: "#72f0a8", bg: "rgba(114,240,168,.1)", bd: "rgba(114,240,168,.25)" },
    "gifts / downloads": { color: "#72f0a8", bg: "rgba(114,240,168,.1)", bd: "rgba(114,240,168,.25)" },
    "looking for":       { color: "#ff9540", bg: "rgba(255,149,64,.1)",  bd: "rgba(255,149,64,.25)"  },
    "general assets":    { color: "#60c8ff", bg: "rgba(96,200,255,.1)",  bd: "rgba(96,200,255,.25)"  },
    "found avatars":     { color: "#72f0a8", bg: "rgba(114,240,168,.1)", bd: "rgba(114,240,168,.25)" },
    "booth avatars":     { color: "#ff6eb4", bg: "rgba(255,110,180,.1)", bd: "rgba(255,110,180,.25)" },
    "gumroad":           { color: "#ff8c69", bg: "rgba(255,140,105,.1)", bd: "rgba(255,140,105,.25)" },
    "payhip":            { color: "#ff8c69", bg: "rgba(255,140,105,.1)", bd: "rgba(255,140,105,.25)" },
    "furry":             { color: "#ffb347", bg: "rgba(255,179,71,.1)",  bd: "rgba(255,179,71,.25)"  },
    "nsfw":              { color: "#ff5577", bg: "rgba(255,85,119,.1)",  bd: "rgba(255,85,119,.25)"  },
    "scripts":           { color: "#a8e6cf", bg: "rgba(168,230,207,.1)", bd: "rgba(168,230,207,.25)" },
    "tools":             { color: "#a8e6cf", bg: "rgba(168,230,207,.1)", bd: "rgba(168,230,207,.25)" },
    "clothes":           { color: "#dda0dd", bg: "rgba(221,160,221,.1)", bd: "rgba(221,160,221,.25)" },
    "hair":              { color: "#f0e68c", bg: "rgba(240,230,140,.1)", bd: "rgba(240,230,140,.25)" },
    "textures":          { color: "#87ceeb", bg: "rgba(135,206,235,.1)", bd: "rgba(135,206,235,.25)" },
    "worlds":            { color: "#9b8ec4", bg: "rgba(155,142,196,.1)", bd: "rgba(155,142,196,.25)" },
    "live2d":            { color: "#ffaad4", bg: "rgba(255,170,212,.1)", bd: "rgba(255,170,212,.25)" },
    "general discussions":{ color: "#9898ff", bg: "rgba(152,152,255,.1)", bd: "rgba(152,152,255,.25)" },
    "uncategorized":     { color: "#6b6b80", bg: "rgba(107,107,128,.1)", bd: "rgba(107,107,128,.25)" },
    "other":             { color: "#6b6b80", bg: "rgba(107,107,128,.1)", bd: "rgba(107,107,128,.25)" },
    "accessories":       { color: "#ffd700", bg: "rgba(255,215,0,.1)",   bd: "rgba(255,215,0,.25)"   },
    "props":             { color: "#cd853f", bg: "rgba(205,133,63,.1)",  bd: "rgba(205,133,63,.25)"  },
    "shaders":           { color: "#00ced1", bg: "rgba(0,206,209,.1)",   bd: "rgba(0,206,209,.25)"   },
    "animations":        { color: "#ff7f50", bg: "rgba(255,127,80,.1)",  bd: "rgba(255,127,80,.25)"  },
  };

  const CAT_DEFAULT = { color: "#9898ff", bg: "rgba(152,152,255,.1)", bd: "rgba(152,152,255,.25)" };

  function getCatStyle(catName) {
    if (!catName) return CAT_DEFAULT;
    const lower = catName.toLowerCase();
    if (CAT_COLORS[lower]) return CAT_COLORS[lower];
    for (const [key, val] of Object.entries(CAT_COLORS)) {
      if (lower.includes(key)) return val;
    }
    return CAT_DEFAULT;
  }

  // ─── Settings Defaults & Helpers ─────────────────────────────────────────
  const DEFAULTS = {
    titleTpl:       "LF: {name}",
    bodyTpl:        "Looking for: **{name}**\n\n{url}\n\nPlease share if you have this item! 🙏",
    defaultTags:    "looking-for, lf, unsolved, asset-hunter",
    autoWatch:      true,
    autoSearch:     true,
    bumpMessage:    "bump",
  };
  // ─── Global post cooldown (10s after any successful post) ─────────────────
  const POST_COOLDOWN_MS = 10000;
  let _lastPostTime = parseInt(GM_getValue("ah-last-post-time", "0"), 10) || 0;

  function recordPost() {
    _lastPostTime = Date.now();
    GM_setValue("ah-last-post-time", String(_lastPostTime));
  }

  // Listen for changes made in other tabs
  GM_addValueChangeListener("ah-last-post-time", (_key, _oldVal, newVal) => {
    _lastPostTime = parseInt(newVal, 10) || 0;
    applyCooldownToAllButtons();
  });

  function cooldownRemaining() {
    return Math.max(0, POST_COOLDOWN_MS - (Date.now() - _lastPostTime));
  }

  function applyCooldownToAllButtons() {
    const remaining = cooldownRemaining();
    if (remaining <= 0) return;

    // Collect all post-action buttons anywhere in the document
    const candidates = Array.from(document.querySelectorAll(
      ".ah-bump-btn:not(.ah-bump-btn--done), #ah-wl-bump-all, #ah-lf-submit"
    ));

    candidates.forEach(btn => {
      if (btn.dataset.cooldownActive === "1") return;
      btn.dataset.cooldownActive = "1";
      const origText = btn.textContent;
      btn.disabled = true;
      const tick = () => {
        const r = cooldownRemaining();
        if (r <= 0) {
          btn.disabled = false;
          btn.textContent = origText;
          delete btn.dataset.cooldownActive;
        } else {
          btn.textContent = `${Math.ceil(r / 1000)}s`;
          setTimeout(tick, 250);
        }
      };
      tick();
    });
  }
  const WATCHLIST_RECHECK_DELAY_MS = 25;

  function getSetting(key) {
    const val = GM_getValue("ah-cfg-" + key, null);
    if (val === null) return DEFAULTS[key];
    if (key === "autoWatch" || key === "autoSearch") return val === "1";
    return val;
  }
  function setSetting(key, val) {
    if (key === "autoWatch" || key === "autoSearch") {
      GM_setValue("ah-cfg-" + key, val ? "1" : "0");
    } else {
      GM_setValue("ah-cfg-" + key, String(val));
    }
  }

  function buildTitle(name) {
    return getSetting("titleTpl").replace(/\{name\}/g, name);
  }
  function buildBody(name, url) {
    let body = getSetting("bodyTpl")
      .replace(/\{name\}/g, name)
      .replace(/\{url\}/g, url);
    return body + WATERMARK;
  }

  // ─── Localisation ─────────────────────────────────────────────────────────
  const STRINGS = {
    en: {
      title: "ASSET HUNTER", minimize: "Minimize", unknown: "Unknown", search: "Search",
      placeholder: "Search query...", noResult: "No results",
      hits: " hits", solved: "Solved", unsolved: "Open", untitled: "Untitled",
      errParse: "Failed to parse response", errNetwork: "Network error", errTimeout: "Request timed out",
      lfBtn: "Post LF Request", lfTitle: "LF Request", lfNotFound: "Not found on Ripper. Want to request it?",
      lfPost: "Post Request", lfPosting: "Posting...",
      lfSuccess: "Posted!", lfViewPost: "View post ->", lfLoginWarn: "You must be logged in at forum.ripper.store.",
      watchlist: "Watchlist", addWatch: "Add to Watchlist", inWatch: "Watching",
      noWatch: "No items in watchlist", recheck: "Re-check all", settings: "Settings",
      langLabel: "Language",
      secLfTemplates: "LF Post Templates", secBehaviour: "Behaviour", secDataMgmt: "Data Management",
      labelTitleTpl: "Title Template", hintTitleTpl: "Use <code>{name}</code> for the asset name",
      labelBodyTpl: "Body Template", hintBodyTpl: "Use <code>{name}</code> for the asset name, <code>{url}</code> for the item link",
      labelDefaultTags: "Default Tags", hintDefaultTags: "Comma separated -- the current platform tag (booth, gumroad, etc.) and smart content tags are appended automatically",
      labelInterval: "Update interval", intervalEvery: "Every", intervalMin: "minutes",
      labelAutoWatch: "Auto-watch posted topics", hintAutoWatch: "Automatically follow your LF posts so you get notified when someone replies",
      labelAutoUpdate: "Auto-update watchlist", hintAutoUpdate: "Automatically re-check all watchlist items on a timer",
      wmLabel: "Watermark -- always appended, not editable",
      btnSave: "Save Settings", btnExport: "Export Data", btnImport: "Import Data",
      btnResetDef: "Reset Defaults", btnDeleteData: "Delete Data", btnReset: "↺ Reset",
      savedMsg: "✓ Saved",
      lfLabelTitle: "Title", lfLabelCategory: "Category", lfLabelTags: "Tags",
      lfLabelTagsHint: "(comma separated)", lfLabelContent: "Content",
      lfLabelContentHint: "(Markdown -- watermark auto-appended)",
      btnCancel: "Cancel",
      importTitle: "Import Data", importDrop: "Drop your JSON here", importDropSub: "or click to browse",
      importOk: "✓ Imported successfully!", importErr: "Failed to parse JSON -- is this a valid export file?",
      importInvalid: "Please drop a valid .json file.",
      lfErrTitle: "Please enter a title.", lfErrContent: "Please enter content.",
      lfConnecting: "Connecting to Ripper.Store...",
      modalResetTitle: "Reset to Defaults", modalResetMsg: "This will reset all settings to their default values. Your watchlist will not be affected.",
      modalResetProceed: "Reset",
      modalDeleteTitle: "Delete Watchlist", modalDeleteMsg: "This will permanently delete all items in your watchlist. This cannot be undone.",
      modalDeleteProceed: "Delete",
      searching: "Searching...",
      openPost: "↗ Open post", openItem: "↗ Open item",
      warnNoName: "<strong>Title template</strong> is missing <code>{name}</code> -- the asset name won't appear in your post title.",
      warnBadUrl: "<strong>Body template</strong> has <code>{url}</code> on a line with other text -- Ripper.Store won't generate a link preview embed unless <code>{url}</code> is alone on its own line.",
      kofiCardMsg: "Enjoying Asset Hunter? Consider supporting!",
      kofiModalTitle: "Before you close this...",
      kofiModalBody: "Asset Hunter is free and took a lot of effort, time, and sanity to build. Donations are never necessary, but please consider supporting if it has helped you.",
      kofiKeepBtn: "I'll consider",
      kofiCloseBtn: "Close anyway",
      kofiDontAsk: "Don't ask again",
      labelAutoSearch: "Auto-search on page load", hintAutoSearch: "Automatically search Ripper.Store when you open an item page",
      manualSearchBtn: "Search Ripper.Store",
      supTitle: "Top Supporters",
      supSubtitle: "Donate any amount to have your name here",
      supEmpty: "No donations yet.",
      supEmptySub: "Be the first -- your name will appear right here.",
      supLoadErr: "Could not load supporter data.",
      // Bump
      bumpBtn: "Bump", bumpBtnSending: "Bumping...", bumpBtnDone: "Bumped!",
      bumpBtnErr: "Failed",
      bumpAllBtn: "Bump All Unsolved",
      labelBumpMessage: "Bump Message", hintBumpMessage: "The text posted when you click Bump on a result",
      // Changelog
      changelogBtn: "Changelogs",
      changelogTitle: "Asset Hunter updated!",
      changelogSubtitle: "here are the changes:",
      changelogSectionFixes: "Fixes",
      changelogSectionAdditions: "Additions",
      changelogSectionRemovals: "Removals",
      changelogSectionInfo: "Info",
      changelogClose: "Got it",
      changelogKofiTitle: "Enjoying Asset Hunter?",
      changelogKofiSub: "Donate on Ko-fi to support updates and suggest new features",
      // Changelog entries -- 6.2.0
      "changelog.info.bumpSetting": "The bump message can be customized in Settings, check the Settings tab to configure it",
      "changelog.add.bumpButton": "Added Bump buttons to search result cards and watchlist items, lets you post a reply directly from Asset Hunter",
      "changelog.add.bumpSetting": "Added a Bump Message setting, customize what text gets posted when you bump (default: \"bump\")",
      "changelog.add.bumpWatchlist": "Bump buttons now appear on watchlist items that have a linked forum topic and are not marked as downloaded",
      "changelog.add.bumpAll": "Added a Bump All button to the watchlist tab, queues up every eligible topic and bumps them one by one with an 11-second gap and a live progress indicator",
      "changelog.add.postCooldown": "Added a 10-second post cooldown shared across all open tabs, any successful post locks all post buttons everywhere until the cooldown expires",
      "changelog.add.crossTabCooldown": "The post cooldown is now shared across all open tabs in real time, posting in one tab immediately locks the buttons in every other tab",
      "changelog.remove.autoUpdate": "Removed the auto-update watchlist setting and its interval timer, it was not working correctly and may be re-added in a future update",
      // Changelog entries -- 6.1.2
      "changelog.fix.nameParsingDash": "Fixed asset names being cut off at dashes, products like \"potato - for whatever\" now correctly use their full name instead of just \"potato\"",
      // Changelog entries - 6.1.1
      "changelog.fix.boothSanitizationRemoved": "Removed the Booth URL sanitization feature introduced in 6.1.0. It did not work as intended and has been fully reverted",
      "changelog.fix.previewBtnRemoved": "Removed the Preview on Site button from the LF post modal, it was redundant",
      // Changelog entries - 6.1.0
      "changelog.fix.boothLink": "Booth subdomain links (like user.booth.pm/items/...) were automatically stripped to the clean canonical URL (booth.pm/items/...) when posting",
      "changelog.add.changelogPopup": "Added this changelog popup! It shows up once after each update, and you can re-open it from Settings anytime",
      "changelog.add.changelogBtn": "Added a Changelogs button in the Settings tab so you never lose track of what changed",
    },
    ja: {
      title: "ASSET HUNTER", minimize: "最小化", unknown: "不明", search: "検索",
      placeholder: "検索ワード...", noResult: "結果なし",
      hits: "件ヒット", solved: "解決済", unsolved: "未解決", untitled: "無題",
      errParse: "解析失敗", errNetwork: "通信エラー", errTimeout: "タイムアウト",
      lfBtn: "LFリクエスト投稿", lfTitle: "LFリクエスト", lfNotFound: "Ripperで見つかりません。リクエストしますか?",
      lfPost: "投稿する", lfPosting: "投稿中...",
      lfSuccess: "投稿成功!", lfViewPost: "投稿を見る ->", lfLoginWarn: "forum.ripper.store にログインが必要です。",
      watchlist: "ウォッチリスト", addWatch: "監視リストに追加", inWatch: "監視中",
      noWatch: "監視アイテムなし", recheck: "再チェック", settings: "設定",
      langLabel: "言語",
      secLfTemplates: "LFテンプレート", secBehaviour: "動作設定", secDataMgmt: "データ管理",
      labelTitleTpl: "タイトルテンプレート", hintTitleTpl: "<code>{name}</code> でアセット名を挿入",
      labelBodyTpl: "本文テンプレート", hintBodyTpl: "<code>{name}</code> でアセット名、<code>{url}</code> でリンクを挿入",
      labelDefaultTags: "デフォルトタグ", hintDefaultTags: "カンマ区切り -- プラットフォームタグ(booth, gumroadなど)とスマートタグが自動付与されます",
      labelInterval: "更新間隔", intervalEvery: "毎", intervalMin: "分",
      labelAutoWatch: "投稿を自動ウォッチ", hintAutoWatch: "LF投稿に返信があると通知を受け取るため自動フォロー",
      labelAutoUpdate: "ウォッチリスト自動更新", hintAutoUpdate: "タイマーでウォッチリストを自動再チェック",
      wmLabel: "ウォーターマーク -- 常に追記されます(編集不可)",
      btnSave: "設定を保存", btnExport: "データ書き出し", btnImport: "データ読み込み",
      btnResetDef: "デフォルトに戻す", btnDeleteData: "データ削除", btnReset: "↺ リセット",
      savedMsg: "✓ 保存",
      lfLabelTitle: "タイトル", lfLabelCategory: "カテゴリ", lfLabelTags: "タグ",
      lfLabelTagsHint: "(カンマ区切り)", lfLabelContent: "本文",
      lfLabelContentHint: "(Markdown -- ウォーターマーク自動付与)",
      btnCancel: "キャンセル",
      importTitle: "データ読み込み", importDrop: "JSONをここにドロップ", importDropSub: "またはクリックして選択",
      importOk: "✓ 読み込み完了!", importErr: "JSON解析失敗 -- 正しいエクスポートファイルですか?",
      importInvalid: "有効な .json ファイルをドロップしてください。",
      lfErrTitle: "タイトルを入力してください。", lfErrContent: "本文を入力してください。",
      lfConnecting: "Ripper.Store に接続中...",
      modalResetTitle: "デフォルトにリセット", modalResetMsg: "すべての設定がデフォルト値に戻ります。ウォッチリストはそのままです。",
      modalResetProceed: "リセット",
      modalDeleteTitle: "ウォッチリスト削除", modalDeleteMsg: "ウォッチリストのすべての項目が完全に削除されます。元に戻せません。",
      modalDeleteProceed: "削除",
      searching: "検索中...",
      openPost: "↗ 投稿を開く", openItem: "↗ アイテムを開く",
      warnNoName: "<strong>タイトルテンプレート</strong>に <code>{name}</code> がありません。",
      warnBadUrl: "<strong>本文テンプレート</strong>の <code>{url}</code> は単独行に配置してください。",
      kofiCardMsg: "Asset Hunterを楽しんでいますか? よければサポートをご検討ください!",
      kofiModalTitle: "閉じる前に...",
      kofiModalBody: "Asset Hunter は無料ですが、作成と維持には多くの時間・労力・正気が必要でした。寄付は必須ではありませんが、役に立ったならぜひご支援をご検討ください。",
      kofiKeepBtn: "検討します",
      kofiCloseBtn: "それでも閉じる",
      kofiDontAsk: "今後は表示しない",
      labelAutoSearch: "ページ読み込み時に自動検索", hintAutoSearch: "アイテムページを開いたときに自動的にRipper.Storeを検索",
      manualSearchBtn: "Ripper.Storeを検索",
      supTitle: "トップサポーター",
      supSubtitle: "金額問わず寄付するとここに名前が載ります",
      supEmpty: "まだ寄付はありません。",
      supEmptySub: "最初の支援者になりましょう -- お名前がここに表示されます。",
      supLoadErr: "サポーターデータを読み込めませんでした。",
      // Bump
      bumpBtn: "バンプ", bumpBtnSending: "送信中...", bumpBtnDone: "完了!",
      bumpBtnErr: "失敗",
      bumpAllBtn: "未解決を全バンプ",
      labelBumpMessage: "バンプメッセージ", hintBumpMessage: "バンプボタンを押したときに投稿されるテキスト",
      // Changelog
      changelogBtn: "更新履歴",
      changelogTitle: "Asset Hunter が更新されました!",
      changelogSubtitle: "変更内容はこちら:",
      changelogSectionFixes: "修正",
      changelogSectionAdditions: "追加",
      changelogSectionRemovals: "削除",
      changelogSectionInfo: "情報",
      changelogClose: "了解",
      changelogKofiTitle: "Asset Hunterを楽しんでいますか?",
      changelogKofiSub: "Ko-fiで支援して、更新や新機能のリクエストを後押ししましょう",
      // Changelog entries -- 6.2.0
      "changelog.info.bumpSetting": "バンプメッセージは設定からカスタマイズできます。設定タブを確認してみてください",
      "changelog.add.bumpButton": "検索結果カードとウォッチリストアイテムにバンプボタンを追加しました。Asset Hunterから直接返信を投稿できます",
      "changelog.add.bumpSetting": "バンプメッセージの設定を追加しました。バンプ時に投稿される内容をカスタマイズできます(デフォルト:「bump」)",
      "changelog.add.bumpWatchlist": "フォーラムトピックが紐付いており、ダウンロード済みでないウォッチリストアイテムにもバンプボタンを表示するようになりました",
      "changelog.add.bumpAll": "ウォッチリストタブに「全バンプ」ボタンを追加しました。対象トピックを11秒間隔で順番にバンプし、進捗状況をリアルタイム表示します",
      "changelog.add.postCooldown": "投稿クールダウンを追加しました。投稿が成功すると10秒間、すべての開いているタブの投稿ボタンがロックされます",
      "changelog.add.crossTabCooldown": "投稿クールダウンはすべての開いているタブでリアルタイム共有されます。あるタブで投稿すると、他のすべてのタブのボタンも即座にロックされます",
      "changelog.remove.autoUpdate": "ウォッチリスト自動更新の設定とタイマーを削除しました。正常に動作していなかったため、将来的に再追加される可能性があります",
      // Changelog entries -- 6.1.2
      "changelog.fix.nameParsingDash": "アセット名がダッシュで途切れるバグを修正しました。「potato - for whatever」のような名前が「potato」に短縮されず、正しく全体が使われるようになりました",
      // Changelog entries -- 6.1.1
      "changelog.fix.boothSanitizationRemoved": "6.1.0で導入したBoothのURL変換機能を削除しました。意図した通りに動作しなかったため、完全に元に戻しました",
      "changelog.fix.previewBtnRemoved": "LFリクエストモーダルの「サイトでプレビュー」ボタンを削除しました。不要なボタンだったため",
      // Changelog entries -- 6.1.0
      "changelog.fix.boothLink": "投稿時にBoothのサブドメインリンク(例:user.booth.pm/items/...)が自動的に正規URL(booth.pm/items/...)に変換されるようになりました",
      "changelog.add.changelogPopup": "この更新履歴ポップアップを追加しました!更新のたびに1回表示され、設定からいつでも再表示できます",
      "changelog.add.changelogBtn": "設定タブに「更新履歴」ボタンを追加しました。変更内容をいつでも確認できます",
    },
    ru: {
      title: "ASSET HUNTER", minimize: "Свернуть", unknown: "Неизвестно", search: "Поиск",
      placeholder: "Поисковый запрос...", noResult: "Нет результатов",
      hits: " совпадений", solved: "Решено", unsolved: "Открыто", untitled: "Без названия",
      errParse: "Ошибка разбора ответа", errNetwork: "Ошибка сети", errTimeout: "Время запроса истекло",
      lfBtn: "Создать LF запрос", lfTitle: "LF Запрос", lfNotFound: "Не найдено на Ripper. Хотите запросить?",
      lfPost: "Опубликовать", lfPosting: "Публикация...",
      lfSuccess: "Опубликовано!", lfViewPost: "Открыть пост ->", lfLoginWarn: "Необходимо войти на forum.ripper.store.",
      watchlist: "Список слежения", addWatch: "Добавить в список", inWatch: "Отслеживается",
      noWatch: "Список слежения пуст", recheck: "Проверить всё", settings: "Настройки",
      langLabel: "Язык",
      secLfTemplates: "Шаблоны LF постов", secBehaviour: "Поведение", secDataMgmt: "Управление данными",
      labelTitleTpl: "Шаблон заголовка", hintTitleTpl: "Используйте <code>{name}</code> для названия ассета",
      labelBodyTpl: "Шаблон текста", hintBodyTpl: "Используйте <code>{name}</code> для названия, <code>{url}</code> для ссылки",
      labelDefaultTags: "Теги по умолчанию", hintDefaultTags: "Через запятую -- тег платформы (booth, gumroad и др.) и умные теги добавляются автоматически",
      labelInterval: "Интервал обновления", intervalEvery: "Каждые", intervalMin: "минут",
      labelAutoWatch: "Авто-слежение за постами", hintAutoWatch: "Автоматически следить за LF постами для получения уведомлений",
      labelAutoUpdate: "Авто-обновление списка", hintAutoUpdate: "Автоматически перепроверять список слежения по таймеру",
      wmLabel: "Водяной знак -- всегда добавляется, не редактируется",
      btnSave: "Сохранить настройки", btnExport: "Экспорт данных", btnImport: "Импорт данных",
      btnResetDef: "Сброс по умолчанию", btnDeleteData: "Удалить данные", btnReset: "↺ Сброс",
      savedMsg: "✓ Сохранено",
      lfLabelTitle: "Заголовок", lfLabelCategory: "Категория", lfLabelTags: "Теги",
      lfLabelTagsHint: "(через запятую)", lfLabelContent: "Содержание",
      lfLabelContentHint: "(Markdown -- водяной знак добавляется автоматически)",
      btnCancel: "Отмена",
      importTitle: "Импорт данных", importDrop: "Перетащите JSON сюда", importDropSub: "или нажмите для выбора",
      importOk: "✓ Импорт выполнен!", importErr: "Ошибка разбора JSON -- это верный файл экспорта?",
      importInvalid: "Перетащите корректный .json файл.",
      lfErrTitle: "Введите заголовок.", lfErrContent: "Введите содержание.",
      lfConnecting: "Подключение к Ripper.Store...",
      modalResetTitle: "Сброс настроек", modalResetMsg: "Все настройки будут сброшены до значений по умолчанию. Список слежения не затронут.",
      modalResetProceed: "Сбросить",
      modalDeleteTitle: "Удалить список слежения", modalDeleteMsg: "Все элементы списка слежения будут удалены безвозвратно.",
      modalDeleteProceed: "Удалить",
      searching: "Поиск...",
      openPost: "↗ Открыть пост", openItem: "↗ Открыть элемент",
      warnNoName: "<strong>Шаблон заголовка</strong> не содержит <code>{name}</code>.",
      warnBadUrl: "<strong>Шаблон текста</strong>: <code>{url}</code> должен быть на отдельной строке.",
      kofiCardMsg: "Нравится Asset Hunter? Подумайте о поддержке!",
      kofiModalTitle: "Перед тем как закрыть...",
      kofiModalBody: "Asset Hunter бесплатный, но на его создание ушло очень много сил, времени и нервов. Донаты не обязательны, но, пожалуйста, подумайте о поддержке, если он вам помог.",
      kofiKeepBtn: "Я подумаю",
      kofiCloseBtn: "Все равно закрыть",
      kofiDontAsk: "Больше не спрашивать",
      labelAutoSearch: "Автопоиск при загрузке страницы", hintAutoSearch: "Автоматически искать на Ripper.Store при открытии страницы товара",
      manualSearchBtn: "Искать на Ripper.Store",
      supTitle: "Топ поддержавших",
      supSubtitle: "Задонатьте любую сумму -- ваше имя появится здесь",
      supEmpty: "Пока нет донатов.",
      supEmptySub: "Будьте первым -- ваше имя появится прямо здесь.",
      supLoadErr: "Не удалось загрузить данные о поддержавших.",
      // Bump
      bumpBtn: "Бамп", bumpBtnSending: "Отправка...", bumpBtnDone: "Отправлено!",
      bumpBtnErr: "Ошибка",
      bumpAllBtn: "Бамп всех нерешённых",
      labelBumpMessage: "Сообщение бампа", hintBumpMessage: "Текст, который публикуется при нажатии кнопки Бамп",
      // Changelog
      changelogBtn: "Список изменений",
      changelogTitle: "Asset Hunter обновлён!",
      changelogSubtitle: "вот что изменилось:",
      changelogSectionFixes: "Исправления",
      changelogSectionAdditions: "Добавлено",
      changelogSectionRemovals: "Удалено",
      changelogSectionInfo: "Информация",
      changelogClose: "Понял",
      changelogKofiTitle: "Нравится Asset Hunter?",
      changelogKofiSub: "Поддержите на Ko-fi, чтобы помочь с обновлениями и предложить новые функции",
      // Changelog entries -- 6.2.0
      "changelog.info.bumpSetting": "Сообщение бампа можно настроить в разделе Настроек, загляните туда",
      "changelog.add.bumpButton": "Добавлены кнопки Бамп на карточки результатов и элементы списка наблюдения, позволяет публиковать ответ прямо из Asset Hunter",
      "changelog.add.bumpSetting": "Добавлена настройка сообщения бампа, теперь можно изменить текст, который публикуется при бампе (по умолчанию: «bump»)",
      "changelog.add.bumpWatchlist": "Кнопки бампа теперь отображаются на элементах списка наблюдения, у которых есть связанная тема на форуме и которые не отмечены как загруженные",
      "changelog.add.bumpAll": "Добавлена кнопка «Бамп всех» на вкладке списка наблюдения, ставит все подходящие темы в очередь и бампит их по одной с интервалом 11 секунд и индикатором прогресса",
      "changelog.add.postCooldown": "Добавлен кулдаун публикации на 10 секунд, после успешной публикации все кнопки публикации во всех открытых вкладках блокируются до окончания кулдауна",
      "changelog.add.crossTabCooldown": "Кулдаун публикации теперь синхронизируется между всеми открытыми вкладками, публикация в одной вкладке мгновенно блокирует кнопки во всех остальных",
      "changelog.remove.autoUpdate": "Удалена настройка автообновления списка наблюдения и таймер, функция работала некорректно и может быть возвращена в будущем",
      // Changelog entries -- 6.1.2
      "changelog.fix.nameParsingDash": "Исправлено обрезание названий ассетов по дефису, теперь названия вроде «potato - for whatever» сохраняются полностью, а не урезаются до «potato»",
      // Changelog entries -- 6.1.1
      "changelog.fix.boothSanitizationRemoved": "Удалена функция нормализации URL Booth, добавленная в 6.1.0. Она работала некорректно и была полностью отменена",
      "changelog.fix.previewBtnRemoved": "Убрана кнопка «Предпросмотр на сайте» из окна LF-запроса, так как она была лишней",
      // Changelog entries -- 6.1.0
      "changelog.fix.boothLink": "Ссылки на Booth с поддоменом (например, user.booth.pm/items/...) теперь автоматически заменялись чистым каноническим URL (booth.pm/items/...) при публикации",
      "changelog.add.changelogPopup": "Добавлено это всплывающее окно со списком изменений! Оно появляется один раз после каждого обновления, и его можно снова открыть из Настроек",
      "changelog.add.changelogBtn": "Добавлена кнопка «Список изменений» на вкладке Настроек, чтобы всегда можно было посмотреть что изменилось",
    },
    "pt-BR": {
      title: "ASSET HUNTER", minimize: "Minimizar", unknown: "Desconhecido", search: "Buscar",
      placeholder: "Termo de busca...", noResult: "Sem resultados",
      hits: " resultados", solved: "Resolvido", unsolved: "Aberto", untitled: "Sem título",
      errParse: "Falha ao processar resposta", errNetwork: "Erro de rede", errTimeout: "Tempo de requisição esgotado",
      lfBtn: "Postar pedido LF", lfTitle: "Pedido LF", lfNotFound: "Não encontrado no Ripper. Deseja solicitar?",
      lfPost: "Publicar", lfPosting: "Publicando...",
      lfSuccess: "Publicado!", lfViewPost: "Ver post ->", lfLoginWarn: "Você precisa estar logado no forum.ripper.store.",
      watchlist: "Lista de observação", addWatch: "Adicionar à lista", inWatch: "Monitorando",
      noWatch: "Nenhum item na lista", recheck: "Verificar tudo", settings: "Configurações",
      langLabel: "Idioma",
      secLfTemplates: "Modelos de post LF", secBehaviour: "Comportamento", secDataMgmt: "Gerenciar dados",
      labelTitleTpl: "Modelo de título", hintTitleTpl: "Use <code>{name}</code> para o nome do asset",
      labelBodyTpl: "Modelo de corpo", hintBodyTpl: "Use <code>{name}</code> para o nome, <code>{url}</code> para o link",
      labelDefaultTags: "Tags padrão", hintDefaultTags: "Separadas por vírgula -- a tag da plataforma (booth, gumroad, etc.) e tags inteligentes são adicionadas automaticamente",
      labelInterval: "Intervalo de atualização", intervalEvery: "A cada", intervalMin: "minutos",
      labelAutoWatch: "Monitorar posts automaticamente", hintAutoWatch: "Seguir seus posts LF automaticamente para receber notificações de resposta",
      labelAutoUpdate: "Atualizar lista automaticamente", hintAutoUpdate: "Verificar todos os itens da lista por temporizador",
      wmLabel: "Marca d'água -- sempre adicionada, não editável",
      btnSave: "Salvar configurações", btnExport: "Exportar dados", btnImport: "Importar dados",
      btnResetDef: "Restaurar padrões", btnDeleteData: "Apagar dados", btnReset: "↺ Restaurar",
      savedMsg: "✓ Salvo",
      lfLabelTitle: "Título", lfLabelCategory: "Categoria", lfLabelTags: "Tags",
      lfLabelTagsHint: "(separadas por vírgula)", lfLabelContent: "Conteúdo",
      lfLabelContentHint: "(Markdown -- marca d'água adicionada automaticamente)",
      btnCancel: "Cancelar",
      importTitle: "Importar dados", importDrop: "Arraste seu JSON aqui", importDropSub: "ou clique para selecionar",
      importOk: "✓ Importado com sucesso!", importErr: "Falha ao analisar JSON -- é um arquivo de exportação válido?",
      importInvalid: "Arraste um arquivo .json válido.",
      lfErrTitle: "Por favor insira um título.", lfErrContent: "Por favor insira o conteúdo.",
      lfConnecting: "Conectando ao Ripper.Store...",
      modalResetTitle: "Restaurar padrões", modalResetMsg: "Todas as configurações serão restauradas. Sua lista de observação não será afetada.",
      modalResetProceed: "Restaurar",
      modalDeleteTitle: "Apagar lista de observação", modalDeleteMsg: "Todos os itens da lista serão apagados permanentemente. Isso não pode ser desfeito.",
      modalDeleteProceed: "Apagar",
      searching: "Buscando...",
      openPost: "↗ Abrir post", openItem: "↗ Abrir item",
      warnNoName: "<strong>Modelo de título</strong> sem <code>{name}</code> -- o nome do asset não aparecerá no título.",
      warnBadUrl: "<strong>Modelo de corpo</strong>: <code>{url}</code> deve estar em uma linha sozinho.",
      kofiCardMsg: "Gostando do Asset Hunter? Considere apoiar!",
      kofiModalTitle: "Antes de fechar isso...",
      kofiModalBody: "O Asset Hunter é gratuito e exigiu muito esforço, tempo e sanidade para ser feito. Doações nunca são necessárias, mas por favor considere apoiar se ele te ajudou.",
      kofiKeepBtn: "Vou considerar",
      kofiCloseBtn: "Fechar mesmo assim",
      kofiDontAsk: "Não perguntar novamente",
      labelAutoSearch: "Busca automática ao carregar a página", hintAutoSearch: "Buscar automaticamente no Ripper.Store ao abrir uma página de item",
      manualSearchBtn: "Buscar no Ripper.Store",
      supTitle: "Top Apoiadores",
      supSubtitle: "Doe qualquer valor para ter seu nome aqui",
      supEmpty: "Nenhuma doação ainda.",
      supEmptySub: "Seja o primeiro -- seu nome aparecerá bem aqui.",
      supLoadErr: "Não foi possível carregar os dados de apoiadores.",
      // Bump
      bumpBtn: "Bump", bumpBtnSending: "Enviando...", bumpBtnDone: "Enviado!",
      bumpBtnErr: "Falhou",
      bumpAllBtn: "Bump em todos não resolvidos",
      labelBumpMessage: "Mensagem de Bump", hintBumpMessage: "O texto postado quando você clica em Bump em um resultado",
      // Changelog
      changelogBtn: "Atualizações",
      changelogTitle: "Asset Hunter foi atualizado!",
      changelogSubtitle: "aqui estão as mudanças:",
      changelogSectionFixes: "Correções",
      changelogSectionAdditions: "Adições",
      changelogSectionRemovals: "Removidos",
      changelogSectionInfo: "Informação",
      changelogClose: "Entendi",
      changelogKofiTitle: "Gostando do Asset Hunter?",
      changelogKofiSub: "Doe no Ko-fi para apoiar as atualizações e sugerir novas funções",
      // Changelog entries -- 6.2.0
      "changelog.info.bumpSetting": "A mensagem de bump pode ser personalizada nas Configurações, dá uma olhada lá",
      "changelog.add.bumpButton": "Adicionados botoes Bump nos cards de resultado e itens da lista de observacao, permite postar uma resposta direto pelo Asset Hunter",
      "changelog.add.bumpSetting": "Adicionada a configuracao de Mensagem de Bump para personalizar o que e postado quando voce faz bump (padrao: \"bump\")",
      "changelog.add.bumpWatchlist": "Botoes de Bump agora aparecem nos itens da lista de observacao que tem um topico do forum vinculado e nao estao marcados como baixados",
      "changelog.add.bumpAll": "Adicionado o botao Bump em Todos na aba da lista de observacao, coloca todos os topicos elegiveis em fila e os bumpa um por um com 11 segundos de intervalo e indicador de progresso",
      "changelog.add.postCooldown": "Adicionado um cooldown de 10 segundos, apos qualquer postagem bem-sucedida todos os botoes de postagem em todas as abas ficam bloqueados ate o cooldown acabar",
      "changelog.add.crossTabCooldown": "O cooldown de postagem agora e compartilhado entre todas as abas abertas em tempo real, postar em uma aba bloqueia os botoes em todas as outras imediatamente",
      "changelog.remove.autoUpdate": "Removida a configuracao de atualizacao automatica da lista de observacao e seu temporizador, nao estava funcionando corretamente e pode ser re-adicionada no futuro",
      // Changelog entries -- 6.1.2
      "changelog.fix.nameParsingDash": "Corrigido o corte de nomes de assets com traço, produtos como \"potato - for whatever\" agora usam o nome completo em vez de só \"potato\"",
      // Changelog entries -- 6.1.1
      "changelog.fix.boothSanitizationRemoved": "Removida a função de sanitização de URL do Booth introduzida na 6.1.0. Ela não funcionou como planejado e foi completamente revertida",
      "changelog.fix.previewBtnRemoved": "Removido o botão Pré-visualizar no Site do modal de LF, ele era desnecessário",
      // Changelog entries -- 6.1.0
      "changelog.fix.boothLink": "Links do Booth com subdomínio (como user.booth.pm/items/...) eram convertidos automaticamente para a URL canônica limpa (booth.pm/items/...) ao postar",
      "changelog.add.changelogPopup": "Adicionado este popup de atualizações! Aparece uma vez após cada atualização, e você pode abrir de novo pela aba de Configurações",
      "changelog.add.changelogBtn": "Adicionado o botão de Atualizações na aba de Configurações, pra você nunca perder o que mudou",
    },
  };

  const LANG_OPTIONS = [
    { value: "en",    label: "English" },
    { value: "ja",    label: "日本語" },
    { value: "ru",    label: "Русский" },
    { value: "pt-BR", label: "Português (BR)" },
  ];

  let currentLang = (function() {
    const saved = GM_getValue("ah-cfg-lang", null);
    return (saved && STRINGS[saved]) ? saved : "en";
  })();
  function t(key) { return (STRINGS[currentLang] || STRINGS.en)[key] || key; }

  // ─── Migrate stale default cid ────────────────────────────────────────────
  (function migrateCid() {
    const saved = GM_getValue("bs-lf-cid", null);
    if (saved === null || saved === 2 || saved === 3) GM_setValue("bs-lf-cid", 42);
  })();

  // ─── Migrate old defaultTags ──────────────────────────────────────────────
  (function migrateDefaultTags() {
    const saved = GM_getValue("ah-cfg-defaultTags", null);
    if (saved === null) return;
    const tags = saved.split(",").map(s => s.trim().toLowerCase()).filter(Boolean);
    let changed = false;
    const withoutBooth = tags.filter(t => t !== "booth");
    if (withoutBooth.length !== tags.length) changed = true;
    if (!withoutBooth.includes("asset-hunter")) {
      withoutBooth.push("asset-hunter");
      changed = true;
    }
    if (changed) GM_setValue("ah-cfg-defaultTags", withoutBooth.join(", "));
  })();

  GM_registerMenuCommand("☠ LF Post Category ID", () => {
    const cur = GM_getValue("bs-lf-cid", 42);
    const input = prompt(
      "Enter the category ID for LF/Request posts on forum.ripper.store\n(Check the URL when browsing that category, e.g. /category/42)",
      cur
    );
    const n = parseInt(input, 10);
    if (!isNaN(n) && n > 0) { GM_setValue("bs-lf-cid", n); alert(`Category ID set to ${n}. Reload.`); }
  });

  // ─── Helpers ──────────────────────────────────────────────────────────────
  function esc(s) { const d = document.createElement("div"); d.textContent = String(s); return d.innerHTML; }
  function dec(s) { const d = document.createElement("textarea"); d.innerHTML = s; return d.value; }

  function stripHTML(html) {
    const tmp = document.createElement("div");
    tmp.innerHTML = html;
    return tmp.textContent || tmp.innerText || "";
  }

  // ─── Core API Calls ───────────────────────────────────────────────────────
  function doSearch(query, cb) {
    GM_xmlhttpRequest({
      method: "GET",
      url: API_URL.replace("{query}", encodeURIComponent(query)),
      responseType: "json",
      timeout: 12000,
      onload: (r) => {
        try {
          const d = typeof r.response === "string" ? JSON.parse(r.response) : r.response;
          cb(null, d);
        } catch(e) { cb(t("errParse")); }
      },
      onerror:  () => cb(t("errNetwork")),
      ontimeout: () => cb(t("errTimeout")),
    });
  }

  const URL_EXTRACT_PATTERNS = [
    /https?:\/\/mega\.nz\/[^\s"'<>)]+/gi,
    /https?:\/\/mega\.io\/[^\s"'<>)]+/gi,
    /https?:\/\/(?:www\.)?mediafire\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/drive\.google\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/gofile\.io\/[^\s"'<>)]+/gi,
    /https?:\/\/pixeldrain\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/workupload\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/1fichier\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/(?:www\.)?dropbox\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/onedrive\.live\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/terabox\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/bowfile\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/forum\.ripper\.store\/hidelinks\/r\/[^\s"'<>)]+/gi,
    /https?:\/\/forum\.ripper\.store\/[^\s"'<>)]*hidelinks[^\s"'<>)]*/gi,
    /https?:\/\/1cloudfile\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/archive\.org\/download\/[^\s"'<>)]+/gi,
    /https?:\/\/anonfile\.la\/[^\s"'<>)]+/gi,
    /https?:\/\/app\.bunkrr\.su\/[^\s"'<>)]+/gi,
    /https?:\/\/buzzheavier\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/clicknupload\.click\/[^\s"'<>)]+/gi,
    /https?:\/\/cyberfile\.me\/[^\s"'<>)]+/gi,
    /https?:\/\/dailyuploads\.net\/[^\s"'<>)]+/gi,
    /https?:\/\/datanodes\.to\/[^\s"'<>)]+/gi,
    /https?:\/\/disk\.yandex\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/fastupload\.io\/[^\s"'<>)]+/gi,
    /https?:\/\/filebin\.net\/[^\s"'<>)]+/gi,
    /https?:\/\/fileditch\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/filepost\.io\/[^\s"'<>)]+/gi,
    /https?:\/\/files\.fm\/[^\s"'<>)]+/gi,
    /https?:\/\/filetransfer\.io\/[^\s"'<>)]+/gi,
    /https?:\/\/fuckingfast\.net\/[^\s"'<>)]+/gi,
    /https?:\/\/hexload\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/mixdrop\.ag\/[^\s"'<>)]+/gi,
    /https?:\/\/send\.cm\/[^\s"'<>)]+/gi,
    /https?:\/\/terminal\.lc\/[^\s"'<>)]+/gi,
    /https?:\/\/transfer\.it\/[^\s"'<>)]+/gi,
    /https?:\/\/uploadfile\.pl\/[^\s"'<>)]+/gi,
    /https?:\/\/uploadhaven\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/uploadnow\.io\/[^\s"'<>)]+/gi,
    /https?:\/\/wdho\.ru\/[^\s"'<>)]+/gi,
    /https?:\/\/(?:www\.)?wetransfer\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/axfc\.net\/[^\s"'<>)]+/gi,
    /https?:\/\/filemail\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/(?:www\.)?sendspace\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/swisstransfer\.com\/[^\s"'<>)]+/gi,
    /https?:\/\/zippyshare\.day\/[^\s"'<>)]+/gi,
  ];

  function extractFirstURL(text) {
    for (const pat of URL_EXTRACT_PATTERNS) {
      pat.lastIndex = 0;
      const m = pat.exec(text);
      if (m) return m[0].replace(/[.,;!?)]+$/, "");
    }
    return null;
  }

  function checkDL(tid, cb) {
    GM_xmlhttpRequest({
      method: "GET",
      url: TOPIC_API.replace("{tid}", tid),
      responseType: "json",
      timeout: 8000,
      onload: (r) => {
        try {
          const d = typeof r.response === "string" ? JSON.parse(r.response) : r.response;
          for (const p of (d.posts || [])) {
            const htmlContent = p.content || "";
            const rawContent  = p.rawContent || "";
            if (DL_PATTERNS.some(x => x.test(rawContent))) return cb(true);
            if (htmlContent) {
              const plain = stripHTML(htmlContent);
              if (DL_PATTERNS.some(x => x.test(plain))) return cb(true);
              const tmp = document.createElement("div");
              tmp.innerHTML = htmlContent;
              const anchors = tmp.querySelectorAll("a[href]");
              for (const a of anchors) {
                const href = a.getAttribute("href") || "";
                if (DL_PATTERNS.some(x => x.test(href))) return cb(true);
              }
            }
            if (p.attachments && p.attachments.length) return cb(true);
          }
          cb(false);
        } catch(e) { cb(false); }
      },
      onerror:  () => cb(false),
      ontimeout: () => cb(false),
    });
  }

  // ─── LF Post API ──────────────────────────────────────────────────────────
  function getCSRFToken(cb) {
    GM_xmlhttpRequest({
      method: "GET",
      url: CONFIG_API,
      responseType: "json",
      timeout: 8000,
      onload: (r) => {
        try {
          const d = typeof r.response === "string" ? JSON.parse(r.response) : r.response;
          cb(null, d.csrf_token || d["csrf-token"] || d.csrfToken || "");
        } catch(e) { cb("Failed to get CSRF token"); }
      },
      onerror:  () => cb("Network error getting CSRF"),
      ontimeout: () => cb("Timeout getting CSRF"),
    });
  }

  function postLFTopic(title, content, tags, cid, cb) {
    getCSRFToken((err, csrf) => {
      if (err) return cb(err);
      GM_xmlhttpRequest({
        method: "POST",
        url: POST_API,
        headers: { "Content-Type": "application/json", "x-csrf-token": csrf },
        data: JSON.stringify({ cid: parseInt(cid, 10), title, content, tags }),
        responseType: "json",
        timeout: 20000,
        onload: (r) => {
          try {
            const d = typeof r.response === "string" ? JSON.parse(r.response) : r.response;
            if (r.status === 200 && d && (d.tid || (d.response && d.response.tid))) {
              cb(null, d);
            } else if (d && d.status && d.status.code === "ok") {
              cb(null, d);
            } else {
              cb((d && d.status && d.status.message) || (d && d.message) ||
                 `HTTP ${r.status}: Post failed. Make sure you are logged in.`);
            }
          } catch(e) { cb("Failed to parse post response"); }
        },
        onerror:  () => cb("Network error while posting"),
        ontimeout: () => cb("Post request timed out"),
      });
    });
  }

  function postReply(tid, content, cb) {
    getCSRFToken((err, csrf) => {
      if (err) return cb(err);
      GM_xmlhttpRequest({
        method: "POST",
        url: POST_REPLY_API.replace("{tid}", tid),
        headers: { "Content-Type": "application/json", "x-csrf-token": csrf },
        data: JSON.stringify({ content }),
        responseType: "json",
        timeout: 15000,
        onload: (r) => {
          try {
            const d = typeof r.response === "string" ? JSON.parse(r.response) : r.response;
            // NodeBB v3 API: success is indicated by status.code === "ok" OR a pid in the response
            const isOk = (d && d.status && d.status.code === "ok") ||
                         (r.status >= 200 && r.status < 300 && d && (
                           d.pid ||
                           (d.response && d.response.pid) ||
                           (d.payload && d.payload.pid)
                         ));
            if (isOk) {
              cb(null, d);
            } else {
              cb((d && d.status && d.status.message) || (d && d.message) ||
                 "HTTP " + r.status + ": Reply failed. Make sure you are logged in to forum.ripper.store.");
            }
          } catch(e) { cb("Failed to parse reply response"); }
        },
        onerror:  () => cb("Network error while posting reply"),
        ontimeout: () => cb("Reply request timed out"),
      });
    });
  }

  function watchTopic(tid, csrf) {
    GM_xmlhttpRequest({
      method: "PUT",
      url: `${SITE_URL}/api/v3/topics/${tid}/follow`,
      headers: { "Content-Type": "application/json", "x-csrf-token": csrf },
      data: JSON.stringify({}),
      responseType: "json",
      timeout: 8000,
      onload: (r) => {
        if (r.status !== 200) {
          GM_xmlhttpRequest({
            method: "POST",
            url: `${SITE_URL}/topic/${tid}/follow`,
            headers: { "Content-Type": "application/json", "x-csrf-token": csrf },
            data: JSON.stringify({ tid }),
            responseType: "json", timeout: 8000,
            onload: () => {}, onerror: () => {}, ontimeout: () => {},
          });
        }
      },
      onerror: () => {}, ontimeout: () => {},
    });
  }

  // ─── Forum Category Map ───────────────────────────────────────────────────
  const FORUM_CATEGORIES = [
    { cid: 20, name: "General Discussions",        depth: 0 },
    { cid: 25, name: "Assets",                     depth: 0 },
    { cid: 28, name: "Looking for...",             depth: 1 },
    { cid: 29, name: "General Assets",             depth: 2 },
    { cid: 31, name: "Found Avatars & Assets",     depth: 2 },
    { cid: 33, name: "Booth Avatars",              depth: 2 },
    { cid: 34, name: "Gumroad/Payhip Avatars",     depth: 2 },
    { cid: 35, name: "Furry Avatars",              depth: 2 },
    { cid: 36, name: "NSFW",                       depth: 2 },
    { cid: 37, name: "Scripts & Tools",            depth: 2 },
    { cid: 38, name: "Clothes",                    depth: 2 },
    { cid: 39, name: "Hair",                       depth: 2 },
    { cid: 40, name: "Textures",                   depth: 2 },
    { cid: 41, name: "Uncategorized",              depth: 2 },
    { cid: 42, name: "Other Assets",               depth: 2 },
    { cid: 43, name: "Worlds",                     depth: 2 },
    { cid: 47, name: "Live2D",                     depth: 2 },
    { cid: 44, name: "Gifts / Downloads",          depth: 1 },
  ];

  const GIFTS_CIDS = new Set([44]);

  function isGiftsCategory(category) {
    const catName = dec((category && category.name) || "").toLowerCase();
    return catName.includes("gifts") || catName.includes("downloads") || GIFTS_CIDS.has(category && category.cid);
  }

  function getAutoTags(name) {
    const base  = getSetting("defaultTags").split(",").map(s => s.trim().toLowerCase()).filter(Boolean);
    const n     = (name || "").toLowerCase();
    const extra = [];
    if (HOST === "booth.pm" || HOST.endsWith(".booth.pm"))       extra.push("booth");
    else if (HOST === "gumroad.com" || HOST.endsWith(".gumroad.com")) extra.push("gumroad");
    else if (HOST === "jinxxy.com" || HOST.endsWith(".jinxxy.com"))   extra.push("jinxxy");
    else if (HOST === "payhip.com" || HOST.endsWith(".payhip.com"))   extra.push("payhip");
    if (/avatar|アバター/.test(n))                   extra.push("avatar");
    if (/cloth|clothes|outfit|wear|衣装|服/.test(n)) extra.push("clothing");
    if (/hair|髪/.test(n))                            extra.push("hair");
    if (/access|アクセ/.test(n))                      extra.push("accessory");
    if (/shader|シェーダー/.test(n))                   extra.push("shader");
    if (/vrchat|vrc/.test(n))                         extra.push("vrchat");
    if (/unity/.test(n))                              extra.push("unity");
    if (/prop|武器|weapon/.test(n))                    extra.push("prop");
    return [...new Set([...base, ...extra])].slice(0, 8);
  }

  // ─── Watchlist Helpers ────────────────────────────────────────────────────
  function wlGet()       { try { return JSON.parse(GM_getValue("bs-watchlist", "[]")); } catch(e) { return []; } }
  function wlSave(list)  { GM_setValue("bs-watchlist", JSON.stringify(list)); }
  function wlAdd(item)   { const l = wlGet(); if (!l.find(x => x.url === item.url)) { l.push(item); wlSave(l); } }
  function wlRemove(url) { wlSave(wlGet().filter(x => x.url !== url)); }

  function timeAgo(ts) {
    if (!ts) return "";
    const s = Math.floor((Date.now() - ts) / 1000);
    if (s < 60)   return `${s}s ago`;
    if (s < 3600) return `${Math.floor(s / 60)}m ago`;
    return `${Math.floor(s / 3600)}h ago`;
  }

  // ─── Donors API ───────────────────────────────────────────────────────────
  function fetchDonors(cb) {
    GM_xmlhttpRequest({
      method: "GET",
      url: DONORS_URL,
      headers: {
        "Accept": "application/vnd.github.v3.raw",
        "Cache-Control": "no-cache",
      },
      timeout: 10000,
      onload: (r) => {
        try {
          const lines = r.responseText.split(/\r?\n/).map(l => l.trim()).filter(Boolean);
          const map = {};
          for (const line of lines) {
            const idx = line.lastIndexOf(":");
            if (idx === -1) continue;
            const name   = line.slice(0, idx).trim();
            const amount = parseFloat(line.slice(idx + 1).trim());
            if (!name || isNaN(amount) || amount <= 0) continue;
            const key = name.toLowerCase();
            if (!map[key]) map[key] = { name, total: 0 };
            map[key].total += amount;
          }
          const sorted = Object.values(map).sort((a, b) => b.total - a.total);
          cb(null, sorted);
        } catch(e) { cb("Failed to parse donors"); }
      },
      onerror:  () => cb("Network error"),
      ontimeout: () => cb("Timeout"),
    });
  }

  let _recheckFn = null;

  // ─── Changelog Modal ──────────────────────────────────────────────────────
  function showChangelogModal() {
    document.getElementById("ah-changelog-modal")?.remove();

    const modal = document.createElement("div");
    modal.id = "ah-changelog-modal";

    function renderVersionBlock(entry, isLatest) {
      const fixes     = (entry.fixes     || []).map(k => t(k)).filter(Boolean);
      const additions = (entry.additions || []).map(k => t(k)).filter(Boolean);
      const removals  = (entry.removals  || []).map(k => t(k)).filter(Boolean);
      const info      = (entry.info      || []).map(k => t(k)).filter(Boolean);

      let html = `<div class="ah-cl-version-block${isLatest ? " ah-cl-version-block--latest" : ""}">`;
      html += `<div class="ah-cl-version-tag">
        <span class="ah-cl-version-star">${isLatest ? "✦" : "·"}</span>
        v${esc(entry.version)}
        ${isLatest ? `<span class="ah-cl-version-new">NEW</span>` : ""}
      </div>`;

      if (additions.length) {
        html += `<div class="ah-cl-section-label ah-cl-section-label--add">
          <span class="ah-cl-bullet">&#9656;</span> ${t("changelogSectionAdditions")}
        </div>`;
        html += `<ul class="ah-cl-list">`;
        for (const item of additions) {
          html += `<li class="ah-cl-item ah-cl-item--add"><span class="ah-cl-dot">&#9670;</span><span>${esc(item)}</span></li>`;
        }
        html += `</ul>`;
      }

      if (fixes.length) {
        html += `<div class="ah-cl-section-label ah-cl-section-label--fix">
          <span class="ah-cl-bullet">&#9656;</span> ${t("changelogSectionFixes")}
        </div>`;
        html += `<ul class="ah-cl-list">`;
        for (const item of fixes) {
          html += `<li class="ah-cl-item ah-cl-item--fix"><span class="ah-cl-dot">&#9670;</span><span>${esc(item)}</span></li>`;
        }
        html += `</ul>`;
      }

      if (info.length) {
        html += `<div class="ah-cl-section-label ah-cl-section-label--info">
          <span class="ah-cl-bullet">&#9656;</span> ${t("changelogSectionInfo")}
        </div>`;
        html += `<ul class="ah-cl-list">`;
        for (const item of info) {
          html += `<li class="ah-cl-item ah-cl-item--info"><span class="ah-cl-dot">&#9670;</span><span>${esc(item)}</span></li>`;
        }
        html += `</ul>`;
      }

      if (removals.length) {
        html += `<div class="ah-cl-section-label ah-cl-section-label--rem">
          <span class="ah-cl-bullet">&#9656;</span> ${t("changelogSectionRemovals")}
        </div>`;
        html += `<ul class="ah-cl-list">`;
        for (const item of removals) {
          html += `<li class="ah-cl-item ah-cl-item--rem"><span class="ah-cl-dot">&#9670;</span><span>${esc(item)}</span></li>`;
        }
        html += `</ul>`;
      }

      html += `</div>`;
      return html;
    }

    const kofiHtml = `<a href="https://ko-fi.com/xedinho" target="_blank" rel="noopener" class="ah-cl-kofi-banner">
      <span class="ah-cl-kofi-icon">&#9749;</span>
      <span class="ah-cl-kofi-text">
        <span class="ah-cl-kofi-title">${t("changelogKofiTitle")}</span>
        <span class="ah-cl-kofi-sub">${t("changelogKofiSub")}</span>
      </span>
      <span class="ah-cl-kofi-arrow">&#8599;</span>
    </a>`;

    let bodyHtml = kofiHtml;
    CHANGELOGS.forEach((entry, idx) => {
      bodyHtml += renderVersionBlock(entry, idx === 0);
    });

    modal.innerHTML = `
      <div class="ah-cl-backdrop"></div>
      <div class="ah-cl-dialog" role="dialog" aria-modal="true">
        <div class="ah-cl-header">
          <div class="ah-cl-header-left">
            <svg class="ah-cl-logo" width="14" height="14" viewBox="0 0 16 16" fill="none">
              <path d="M8 1L9.5 6H15L10.5 9L12 14L8 11L4 14L5.5 9L1 6H6.5L8 1Z"
                stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
            </svg>
            <div class="ah-cl-title-block">
              <div class="ah-cl-title">${t("changelogTitle")}</div>
              <div class="ah-cl-subtitle">${t("changelogSubtitle")}</div>
            </div>
          </div>
          <button class="ah-cl-close" id="ah-cl-close-btn">&#10005;</button>
        </div>
        <div class="ah-cl-body">
          ${bodyHtml}
        </div>
        <div class="ah-cl-footer">
          <button class="ah-cl-ok-btn" id="ah-cl-ok-btn">${t("changelogClose")}</button>
        </div>
      </div>`;

    document.body.appendChild(modal);
    injectChangelogCSS();

    const close = () => modal.remove();
    modal.querySelector(".ah-cl-backdrop").addEventListener("click", close);
    modal.querySelector("#ah-cl-close-btn").addEventListener("click", close);
    modal.querySelector("#ah-cl-ok-btn").addEventListener("click", close);
  }

  // ─── Changelog first-run check ────────────────────────────────────────────
  function maybeShowChangelog() {
    const seenVersion = GM_getValue("ah-changelog-seen", "");
    if (seenVersion !== CURRENT_VERSION) {
      GM_setValue("ah-changelog-seen", CURRENT_VERSION);
      setTimeout(showChangelogModal, 1800);
    }
  }

  // ─── LF Modal ─────────────────────────────────────────────────────────────
  function showLFModal() {
    const adapter = getAdapter();
    const id   = adapter ? adapter.getId()   : "";
    const name = adapter ? adapter.getName() : "";
    const url  = window.location.href;
    const cid  = GM_getValue("bs-lf-cid", 42);

    document.getElementById("ah-lf-modal")?.remove();

    const modal = document.createElement("div");
    modal.id = "ah-lf-modal";
    modal.innerHTML = `
      <div class="ah-lf-backdrop"></div>
      <div class="ah-lf-dialog">
        <div class="ah-lf-dialog-header">
          <div class="ah-lf-dialog-title">
            <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
              <path d="M7 1L8.5 5H13L9.5 7.5L11 11.5L7 9L3 11.5L4.5 7.5L1 5H5.5L7 1Z"
                stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
            </svg>
            ${t("lfTitle")}
          </div>
          <button class="ah-lf-close">&#10005;</button>
        </div>
        <div class="ah-lf-dialog-body">
          <div class="ah-lf-field">
            <label class="ah-lf-label">${t("lfLabelTitle")}</label>
            <input id="ah-lf-title" type="text" value="${esc(buildTitle(name))}" />
          </div>
          <div class="ah-lf-row-2">
            <div class="ah-lf-field">
              <label class="ah-lf-label">${t("lfLabelCategory")}</label>
              <select id="ah-lf-cid"></select>
            </div>
            <div class="ah-lf-field">
              <label class="ah-lf-label">${t("lfLabelTags")} <span class="ah-lf-hint">${t("lfLabelTagsHint")}</span></label>
              <input id="ah-lf-tags" type="text" value="${esc(getAutoTags(name).join(", "))}" />
            </div>
          </div>
          <div class="ah-lf-field">
            <label class="ah-lf-label">${t("lfLabelContent")} <span class="ah-lf-hint">${t("lfLabelContentHint")}</span></label>
            <textarea id="ah-lf-content" rows="5">${esc(buildBody(name, url).replace(WATERMARK, "").trimEnd())}</textarea>
          </div>
          <div class="ah-lf-notice">
            <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
              <circle cx="6.5" cy="6.5" r="5.5" stroke="currentColor" stroke-width="1.2"/>
              <path d="M6.5 5.5V9.5M6.5 3.5H6.51" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/>
            </svg>
            ${t("lfLoginWarn")}
            <a href="${SITE_URL}" target="_blank" rel="noopener">Open Ripper.Store &#8599;</a>
          </div>
          <div class="ah-lf-actions">
            <button id="ah-lf-submit">${t("lfPost")}</button>
          </div>
          <div id="ah-lf-status"></div>
        </div>
      </div>`;
    document.body.appendChild(modal);
    injectModalCSS();
    applyCooldownToAllButtons();

    modal.querySelector("#ah-lf-cid").innerHTML = FORUM_CATEGORIES.map(c => {
      const pad      = "\u00a0\u00a0\u00a0".repeat(c.depth);
      const selected = c.cid === cid ? " selected" : "";
      return `<option value="${c.cid}"${selected}>${pad}${c.name}</option>`;
    }).join("");

    modal.querySelector(".ah-lf-close").addEventListener("click",   () => modal.remove());
    modal.querySelector(".ah-lf-backdrop").addEventListener("click", () => modal.remove());

    modal.querySelector("#ah-lf-submit").addEventListener("click", () => {
      const postTitle   = modal.querySelector("#ah-lf-title").value.trim();
      const rawContent  = modal.querySelector("#ah-lf-content").value.trim();
      const postContent = rawContent + WATERMARK;
      const postTags    = modal.querySelector("#ah-lf-tags").value
                            .split(",").map(s => s.trim().toLowerCase()).filter(Boolean);
      const postCid     = parseInt(modal.querySelector("#ah-lf-cid").value, 10) || cid;

      if (!postTitle)   { setStatus(t("lfErrTitle"),   "err"); return; }
      if (!postContent) { setStatus(t("lfErrContent"), "err"); return; }

      const btn = modal.querySelector("#ah-lf-submit");
      btn.disabled = true; btn.textContent = t("lfPosting");
      setStatus(t("lfConnecting"), "load");
      GM_setValue("bs-lf-cid", postCid);

      postLFTopic(postTitle, postContent, postTags, postCid, (err, result) => {
        btn.disabled = false; btn.textContent = t("lfPost");
        if (err) { setStatus(`&#10060; ${err}`, "err"); return; }

        const topicData = (result && result.response && result.response.topicData) ||
                          (result && result.topicData) ||
                          (result && result.response) || result || {};
        const slug     = topicData.slug || topicData.tid || null;
        const topicUrl = slug ? `${SITE_URL}/topic/${slug}` : SITE_URL;
        const tid      = topicData.tid;

        if (tid && getSetting("autoWatch")) {
          getCSRFToken((csrfErr, csrf) => { if (!csrfErr && csrf) watchTopic(tid, csrf); });
        }

        recordPost();
        applyCooldownToAllButtons();
        setStatus(`${t("lfSuccess")} <a href="${topicUrl}" target="_blank" rel="noopener">${t("lfViewPost")}</a>`, "ok");
        setTimeout(() => modal.remove(), 5000);
      });
    });

    function setStatus(html, type) {
      const el = modal.querySelector("#ah-lf-status");
      el.innerHTML = html; el.className = `ah-lf-st-${type}`;
    }
  }

  // ─── Import Modal ─────────────────────────────────────────────────────────
  function showImportModal(onImport) {
    document.getElementById("ah-import-modal")?.remove();

    const modal = document.createElement("div");
    modal.id = "ah-import-modal";
    modal.innerHTML = `
      <div class="ah-import-backdrop"></div>
      <div class="ah-import-dialog">
        <div class="ah-import-header">
          <div class="ah-import-title">
            <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
              <path d="M6.5 9V3M3.5 6l3 3 3-3M1 11h11" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            Import Data
          </div>
          <button class="ah-import-close">&#10005;</button>
        </div>
        <div class="ah-import-body">
          <div class="ah-import-drop" id="ah-import-drop">
            <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
              <path d="M14 4v14M7 11l7-7 7 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
              <path d="M4 22h20" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
            </svg>
            <span class="ah-import-drop-label">${t("importDrop")}</span>
            <span class="ah-import-drop-sub">${t("importDropSub")}</span>
            <input type="file" id="ah-import-file" accept=".json,application/json" style="display:none"/>
          </div>
          <div id="ah-import-status"></div>
        </div>
      </div>`;
    document.body.appendChild(modal);
    injectImportCSS();

    const close = () => modal.remove();
    modal.querySelector(".ah-import-backdrop").addEventListener("click", close);
    modal.querySelector(".ah-import-close").addEventListener("click", close);

    const drop    = modal.querySelector("#ah-import-drop");
    const fileInp = modal.querySelector("#ah-import-file");
    const status  = modal.querySelector("#ah-import-status");

    function setStatus(msg, type) {
      status.textContent = msg;
      status.className   = `ah-import-st ah-import-st--${type}`;
    }

    function processFile(file) {
      if (!file || !file.name.endsWith(".json")) {
        setStatus(t("importInvalid"), "err"); return;
      }
      const reader = new FileReader();
      reader.onload = (e) => {
        try {
          const data = JSON.parse(e.target.result);
          if (!data || (typeof data !== "object")) throw new Error("Invalid format");
          setStatus(t("importOk"), "ok");
          setTimeout(() => { modal.remove(); onImport(data); }, 900);
        } catch(err) {
          setStatus(t("importErr"), "err");
        }
      };
      reader.readAsText(file);
    }

    drop.addEventListener("click", () => fileInp.click());
    fileInp.addEventListener("change", () => {
      if (fileInp.files[0]) processFile(fileInp.files[0]);
    });

    drop.addEventListener("dragover", (e) => {
      e.preventDefault();
      drop.classList.add("ah-import-drop--over");
    });
    drop.addEventListener("dragleave", () => drop.classList.remove("ah-import-drop--over"));
    drop.addEventListener("drop", (e) => {
      e.preventDefault();
      drop.classList.remove("ah-import-drop--over");
      const file = e.dataTransfer.files[0];
      processFile(file);
    });
  }

  // ─── Confirm Modal ─────────────────────────────────────────────────────────
  function showConfirmModal({ title, message, proceedLabel, onProceed }) {
    document.getElementById("ah-confirm-modal")?.remove();
    const modal = document.createElement("div");
    modal.id = "ah-confirm-modal";
    modal.innerHTML = `
      <div class="ah-confirm-backdrop"></div>
      <div class="ah-confirm-dialog" role="alertdialog" aria-modal="true">
        <div class="ah-confirm-icon-row">
          <svg width="28" height="28" viewBox="0 0 28 28" fill="none">
            <path d="M14 3L26 24H2L14 3Z" stroke="#ff6680" stroke-width="1.6" stroke-linejoin="round" fill="rgba(255,102,128,.08)"/>
            <path d="M14 11V17" stroke="#ff6680" stroke-width="1.8" stroke-linecap="round"/>
            <circle cx="14" cy="21" r="1.1" fill="#ff6680"/>
          </svg>
          <span class="ah-confirm-title">${esc(title)}</span>
        </div>
        <div class="ah-confirm-body">${esc(message)}</div>
        <div class="ah-confirm-actions">
          <button class="ah-confirm-cancel" id="ah-confirm-cancel">${t("btnCancel")}</button>
          <button class="ah-confirm-proceed" id="ah-confirm-proceed">${esc(proceedLabel || "Proceed")}</button>
        </div>
      </div>`;
    document.body.appendChild(modal);
    injectConfirmCSS();

    const close = () => modal.remove();
    modal.querySelector(".ah-confirm-backdrop").addEventListener("click", close);
    modal.querySelector("#ah-confirm-cancel").addEventListener("click", close);
    modal.querySelector("#ah-confirm-proceed").addEventListener("click", () => {
      close();
      onProceed();
    });
  }

  // ─── Render Results ───────────────────────────────────────────────────────
  function renderResults(data, panel) {
    const posts = data.posts || [], count = data.matchCount || 0;

    if (!count || !posts.length) {
      return `<div class="ah-no-results">${t("noResult")}</div>
        <div class="ah-lf-prompt">
          <p>${t("lfNotFound")}</p>
          <button class="ah-btn-lf" id="ah-lf-open">${t("lfBtn")}</button>
        </div>`;
    }

    const dloc = { en: "en-US", ja: "ja-JP", ru: "ru-RU", "pt-BR": "pt-BR" }[currentLang] || "en-US";

    function buildCard(post, isDL) {
      const tp    = post.topic    || {};
      const cat   = post.category || {};
      const u     = post.user     || {};
      const tid   = tp.tid || "";
      const title = dec(tp.titleRaw || tp.title || t("untitled"));
      const href  = `${SITE_URL}/topic/${tp.slug || tid}`;
      const catN  = dec(cat.name || "");
      const solved = tp.isSolved === 1;
      const pc    = tp.postcount || 0;
      const vc    = tp.viewcount || 0;
      const date  = post.timestampISO ? new Date(post.timestampISO).toLocaleDateString(dloc) : "";
      const tags  = (tp.tags || []).map(g => dec(g.value));
      const user  = dec(u.displayname || u.username || "?");

      const catStyle = getCatStyle(catN);
      const catBadge = catN
        ? `<span class="ah-cat" style="color:${catStyle.color};background:${catStyle.bg};border-color:${catStyle.bd}">${esc(catN)}</span>`
        : "";

      const openStyle   = getCatStyle("open");
      const solvedStyle = getCatStyle("solved");
      const statusBadge = solved
        ? `<span class="ah-badge ah-badge--solved" style="color:${solvedStyle.color};background:${solvedStyle.bg};border-color:${solvedStyle.bd}">${t("solved")}</span>`
        : `<span class="ah-badge ah-badge--open" style="color:${openStyle.color};background:${openStyle.bg};border-color:${openStyle.bd}">${t("unsolved")}</span>`;

      const dlChip = isDL
        ? `<span class="ah-dl-chip">&#8595; DL</span>`
        : "";

      const bumpBtn = tid && !isDL
        ? `<button class="ah-bump-btn" data-tid="${esc(String(tid))}" title="${esc(t("bumpBtn"))}">${esc(t("bumpBtn"))}</button>`
        : "";

      return `<a class="ah-card ${isDL ? "ah-card--dl" : "ah-card--disc"}" data-tid="${esc(String(tid))}" data-slug="${esc(String(tp.slug || tid))}" href="${href}" target="_blank" rel="noopener">
        <div class="ah-card-top">
          ${statusBadge}
          ${catBadge}
          ${dlChip}
          ${bumpBtn ? `<span class="ah-card-bump-wrap">${bumpBtn}</span>` : ""}
        </div>
        <div class="ah-card-title">${esc(title)}</div>
        <div class="ah-card-meta">
          <span class="ah-card-user">${esc(user)}</span>
          <span>${date}</span><span>&#128172; ${pc}</span><span>&#128065; ${vc}</span>
        </div>
        ${tags.length ? `<div class="ah-tags">${tags.map(g => `<span class="ah-tag">${esc(g)}</span>`).join("")}</div>` : ""}
      </a>`;
    }

    let html = `<div class="ah-result-count">${count}${t("hits")}</div>`;
    html += `<div class="ah-section-label ah-section--dl" id="ah-dl-hd" style="display:none">&#8595; Download Found</div>`;
    html += `<div class="ah-list" id="ah-dl-list"></div>`;
    html += `<div class="ah-section-label ah-section--disc" id="ah-disc-hd" style="display:none">&#9711; Discussions</div>`;
    html += `<div class="ah-list" id="ah-disc-list">`;
    for (const post of posts) {
      const cat  = post.category || {};
      const isGifts = isGiftsCategory(cat);
      html += buildCard(post, isGifts);
    }
    html += `</div>`;
    html += `<div class="ah-bottom-actions">
      <button class="ah-btn-watch" id="ah-wl-add">${t("addWatch")}</button>
      <button class="ah-btn-lf" id="ah-lf-open">${t("lfBtn")}</button>
    </div>`;

    setTimeout(() => {
      const out      = panel.querySelector("#ah-out"); if (!out) return;
      const discHd   = out.querySelector("#ah-disc-hd");
      const dlHd     = out.querySelector("#ah-dl-hd");
      const dlList   = out.querySelector("#ah-dl-list");
      const discList = out.querySelector("#ah-disc-list");

      if (discList && dlList) {
        Array.from(discList.querySelectorAll(".ah-card--dl")).forEach(card => {
          dlList.appendChild(card);
          dlHd.style.display = "";
        });
        if (discList.children.length) discHd.style.display = "";
      }

      for (const post of posts) {
        const tid  = (post.topic || {}).tid; if (!tid) continue;
        const isGifts = isGiftsCategory(post.category || {});
        if (isGifts) continue;

        checkDL(tid, (found) => {
          if (!found) return;
          const card = discList && discList.querySelector(`[data-tid="${tid}"]`);
          if (card && dlList) {
            const clone = card.cloneNode(true);
            clone.className = "ah-card ah-card--dl";
            if (!clone.querySelector(".ah-dl-chip")) {
              const chip = document.createElement("span");
              chip.className = "ah-dl-chip";
              chip.textContent = "\u2193 DL";
              clone.querySelector(".ah-card-top").appendChild(chip);
            }
            // Remove bump button from dl cards
            const cloneBump = clone.querySelector(".ah-bump-btn");
            if (cloneBump) cloneBump.closest(".ah-card-bump-wrap")?.remove() || cloneBump.remove();
            dlList.appendChild(clone);
            card.remove();
            dlHd.style.display = "";
            if (discList.children.length === 0) discHd.style.display = "none";
          }
        });
      }

      // Wire bump buttons on all initially rendered cards
      out.querySelectorAll(".ah-bump-btn").forEach(btn => wireBumpBtn(btn));
    }, 150);

    return html;
  }

  // ─── Bump button handler ──────────────────────────────────────────────────
  function wireBumpBtn(btn) {
    // Prevent the anchor click from firing when the button is clicked
    btn.addEventListener("click", (e) => {
      e.preventDefault();
      e.stopPropagation();

      if (btn.dataset.bumped === "1") return;

      const tid = btn.dataset.tid;
      if (!tid) return;

      const remaining = cooldownRemaining();
      if (remaining > 0) {
        // Already shown via applyCooldownToAllButtons, but as safety net:
        return;
      }

      const msg = (getSetting("bumpMessage") || "bump").trim() || "bump";

      btn.disabled = true;
      btn.textContent = t("bumpBtnSending");
      btn.classList.add("ah-bump-btn--sending");

      postReply(tid, msg, (err) => {
        if (err) {
          btn.disabled = false;
          btn.classList.remove("ah-bump-btn--sending");
          btn.classList.add("ah-bump-btn--err");
          btn.title = String(err);
          btn.textContent = t("bumpBtnErr");
          console.error("[AssetHunter] Bump failed:", err);
          // reset after 5 seconds
          setTimeout(() => {
            btn.textContent = t("bumpBtn");
            btn.classList.remove("ah-bump-btn--err");
            btn.title = t("bumpBtn");
          }, 5000);
        } else {
          btn.dataset.bumped = "1";
          btn.textContent = t("bumpBtnDone");
          btn.classList.remove("ah-bump-btn--sending");
          btn.classList.add("ah-bump-btn--done");
          recordPost();
          applyCooldownToAllButtons();
        }
      });
    });
  }

  // ─── Main Panel ───────────────────────────────────────────────────────────
  function injectUI() {
    if (document.getElementById("ah-panel")) return;

    const adapter = getAdapter();
    if (!adapter) return;

    const id    = adapter.getId();
    const name  = adapter.getName();
    const query = adapter.buildQuery(id, name);
    if (!query) return;

    const platformLabel = (() => {
      if (HOST === "booth.pm" || HOST.endsWith(".booth.pm")) return "booth.pm";
      if (HOST === "gumroad.com" || HOST.endsWith(".gumroad.com")) return "gumroad";
      if (HOST === "jinxxy.com" || HOST.endsWith(".jinxxy.com")) return "jinxxy";
      if (HOST === "payhip.com" || HOST.endsWith(".payhip.com")) return "payhip";
      return HOST;
    })();

    const panel = document.createElement("div");
    panel.id = "ah-panel";
    panel.innerHTML = `
      <div id="ah-header">
        <div id="ah-header-left">
          <svg class="ah-logo-star" width="16" height="16" viewBox="0 0 16 16" fill="none">
            <path d="M8 1L9.5 6H15L10.5 9L12 14L8 11L4 14L5.5 9L1 6H6.5L8 1Z"
              stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>
          </svg>
          <span id="ah-title">${t("title")}</span>
        </div>
        <div id="ah-header-right">
          <span id="ah-booth-badge">${platformLabel}</span>
          <button id="ah-minimize" title="${t("minimize")}">
            <svg width="10" height="10" viewBox="0 0 10 10" fill="none">
              <path d="M2 5H8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
            </svg>
          </button>
        </div>
      </div>

      <div id="ah-collapsible">
        <div id="ah-tabs">
          <button class="ah-tab ah-tab--active" data-tab="search" id="ah-tab-search">${t("search")}</button>
          <button class="ah-tab" data-tab="watchlist" id="ah-tab-watchlist">${t("watchlist")} <span id="ah-wl-count"></span></button>
          <button class="ah-tab ah-tab--supporters" data-tab="supporters" id="ah-tab-supporters" title="Top Supporters">&#10022;</button>
          <button class="ah-tab ah-tab--icon" data-tab="settings" title="${t("settings")}">
            <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
              <circle cx="6" cy="6" r="1.8" stroke="currentColor" stroke-width="1.2"/>
              <path d="M6 1.5V2.5M6 9.5V10.5M1.5 6H2.5M9.5 6H10.5M2.9 2.9L3.6 3.6M8.4 8.4L9.1 9.1M2.9 9.1L3.6 8.4M8.4 3.6L9.1 2.9"
                stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/>
            </svg>
          </button>
        </div>

        <div id="ah-pane-search">
          <div id="ah-item-info">
            <div id="ah-item-name" title="${esc(name)}">${esc(name) || t("unknown")}</div>
            ${id ? `<div id="ah-item-id">ID ${id}</div>` : ""}
          </div>
          <button id="ah-manual-search-btn" style="display:none">${t("manualSearchBtn")}</button>
          <div id="ah-search-row">
            <input id="ah-input" type="text" value="${esc(query)}" placeholder="${t("placeholder")}" />
            <button id="ah-search-btn">${t("search")}</button>
          </div>
          <div id="ah-out"></div>
        </div>

        <div id="ah-pane-watchlist" style="display:none">
          <div id="ah-wl-body"></div>
        </div>

        <div id="ah-pane-supporters" style="display:none">
          <div id="ah-supporters-body"></div>
        </div>

        <div id="ah-pane-settings" style="display:none">
          <div id="ah-settings-body"></div>
        </div>
      </div>`;
    document.body.appendChild(panel);
    injectCSS();

    // ── Ko-fi donation card ──────────────────────────────────────────────────
    const KOFI_CLOSE_ANYWAY_COUNT_KEY = "ah-kofi-close-anyway-count";
    const KOFI_DONT_ASK_KEY = "ah-kofi-dont-ask";
    const card = document.createElement("div");
    card.id = "ah-kofi-card";
    let renderKofiCardContent = () => {};
    if (GM_getValue(KOFI_DONT_ASK_KEY, "0") === "1") {
      renderKofiCardContent = () => {};
    } else {
      renderKofiCardContent = () => {
        card.innerHTML = `<div id="ah-kofi-inner">
      <span id="ah-kofi-heart">&#9829;</span>
      <div id="ah-kofi-text">
        <span id="ah-kofi-msg">${t("kofiCardMsg")}</span>
        <a href="https://ko-fi.com/xedinho" target="_blank" rel="noopener">&#9749; ko-fi.com/xedinho</a>
      </div>
      <button id="ah-kofi-close">&#10005;</button>
    </div>`;
        card.querySelector("#ah-kofi-close").addEventListener("click", showKofiCloseModal);
      };
      renderKofiCardContent();
      document.body.appendChild(card);

    function positionKofiCard() {
      const pr = panel.getBoundingClientRect();
      card.style.right  = (window.innerWidth  - pr.right)  + "px";
      card.style.bottom = (window.innerHeight - pr.top + 5) + "px";
      card.style.width  = pr.width + "px";
    }

    requestAnimationFrame(() => requestAnimationFrame(positionKofiCard));
    window.addEventListener("resize", positionKofiCard);
    if (typeof ResizeObserver !== "undefined") {
      const kofiObserver = new ResizeObserver(positionKofiCard);
      kofiObserver.observe(panel);
    }

    function showKofiCloseModal() {
      document.getElementById("ah-kofi-confirm-modal")?.remove();
      const closeAnywayCount = parseInt(GM_getValue(KOFI_CLOSE_ANYWAY_COUNT_KEY, "0"), 10) || 0;
      const canShowDontAsk = closeAnywayCount > 5;
      const modal = document.createElement("div");
      modal.id = "ah-kofi-confirm-modal";
      modal.innerHTML = `
        <div class="ah-kofi-confirm-backdrop"></div>
        <div class="ah-kofi-confirm-dialog" role="alertdialog" aria-modal="true">
          <div class="ah-kofi-confirm-title">
            <span class="ah-kofi-confirm-heart">&#9829;</span>
            ${t("kofiModalTitle")}
          </div>
          <div class="ah-kofi-confirm-body">
            ${t("kofiModalBody")}
          </div>
          <a href="https://ko-fi.com/xedinho" target="_blank" rel="noopener" class="ah-kofi-confirm-link">&#9749; ko-fi.com/xedinho</a>
          ${canShowDontAsk ? `<label class="ah-kofi-confirm-optout"><input type="checkbox" id="ah-kofi-dont-ask"> ${t("kofiDontAsk")}</label>` : ""}
          <div class="ah-kofi-confirm-actions">
            <button id="ah-kofi-keep-btn">${t("kofiKeepBtn")}</button>
            <button id="ah-kofi-close-btn">${t("kofiCloseBtn")}</button>
          </div>
        </div>`;
      document.body.appendChild(modal);

      const dismiss = () => modal.remove();
      modal.querySelector(".ah-kofi-confirm-backdrop").addEventListener("click", dismiss);
      modal.querySelector("#ah-kofi-keep-btn").addEventListener("click", dismiss);
      modal.querySelector("#ah-kofi-close-btn").addEventListener("click", () => {
        const nextCount = closeAnywayCount + 1;
        GM_setValue(KOFI_CLOSE_ANYWAY_COUNT_KEY, String(nextCount));
        const dontAskEl = modal.querySelector("#ah-kofi-dont-ask");
        if (dontAskEl && dontAskEl.checked) {
          GM_setValue(KOFI_DONT_ASK_KEY, "1");
        }
        card.remove();
        dismiss();
      });
    }
    }

    const out     = panel.querySelector("#ah-out");
    const inp     = panel.querySelector("#ah-input");
    const wlBody  = panel.querySelector("#ah-wl-body");
    const supBody = panel.querySelector("#ah-supporters-body");
    const setBody = panel.querySelector("#ah-settings-body");
    let lastSearchData = null;

    function updateWlCount() {
      const el = panel.querySelector("#ah-wl-count");
      const n  = wlGet().length;
      el.textContent = n > 0 ? `(${n})` : "";
    }
    updateWlCount();

    // ── Tab switching ──
    const PANES = {
      search:     "#ah-pane-search",
      watchlist:  "#ah-pane-watchlist",
      supporters: "#ah-pane-supporters",
      settings:   "#ah-pane-settings",
    };
    panel.querySelectorAll(".ah-tab").forEach(btn => {
      btn.addEventListener("click", () => {
        panel.querySelectorAll(".ah-tab").forEach(b => b.classList.remove("ah-tab--active"));
        btn.classList.add("ah-tab--active");
        const tab = btn.dataset.tab;
        Object.entries(PANES).forEach(([k, sel]) => {
          panel.querySelector(sel).style.display = k === tab ? "" : "none";
        });
        if (tab === "watchlist")  renderWatchlist();
        if (tab === "supporters") renderSupporters();
        if (tab === "settings")   renderSettings();
      });
    });

    // ── Watchlist ──
    function renderWatchlist() {
      const list = wlGet();
      updateWlCount();
      if (!list.length) { wlBody.innerHTML = `<div class="ah-wl-empty">${t("noWatch")}</div>`; return; }

      const dlItems    = list.filter(x => x.status === "dl");
      const discItems  = list.filter(x => x.status === "found");
      const otherItems = list.filter(x => x.status !== "dl" && x.status !== "found");

      // Items eligible for bump = have a ripperTid and are NOT marked as dl (already downloaded)
      const bumpableItems = list.filter(x => x.ripperTid && x.status !== "dl");

      function itemHTML(item) {
        const hasTid     = item.ripperTid || item.ripperSlug;
        const topicRef   = item.ripperTid || item.ripperSlug;
        const isDLStatus = item.status === "dl";
        const isFoundStatus = item.status === "found";

        const badge =
          isDLStatus      ? `<span class="ah-wl-badge ah-wl-badge--dl">&#8595; DL Found</span>`     :
          isFoundStatus   ? `<span class="ah-wl-badge ah-wl-badge--disc">&#9711; Discussion</span>`  :
          item.status === "none"
                          ? `<span class="ah-wl-badge ah-wl-badge--none">&#10005; Not Found</span>`   :
                            `<span class="ah-wl-badge ah-wl-badge--pending">&#8987; Pending</span>`;

        const checked = item.lastChecked
          ? `<span class="ah-wl-ts" data-ts="${item.lastChecked}">${timeAgo(item.lastChecked)}</span>`
          : "";

        const ripperLink = (isDLStatus || isFoundStatus) && hasTid
          ? `<a href="${SITE_URL}/topic/${esc(String(topicRef))}" target="_blank" class="ah-wl-link ah-wl-link--ripper">${t("openPost")}</a>`
          : "";

        // Show bump button only for items that have a tid and are NOT solved (dl)
        const bumpBtn = hasTid && !isDLStatus
          ? `<button class="ah-bump-btn ah-wl-bump-btn" data-tid="${esc(String(item.ripperTid || topicRef))}" title="${esc(t("bumpBtn"))}">${esc(t("bumpBtn"))}</button>`
          : "";

        return `<div class="ah-wl-item ah-wl-item--${item.status || "pending"}" data-url="${esc(item.url)}">
          <div class="ah-wl-row1">
            ${badge}
            <button class="ah-wl-remove" data-url="${esc(item.url)}">&#10005;</button>
          </div>
          <div class="ah-wl-name">${esc(item.name)}</div>
          <div class="ah-wl-row2">
            <a href="${esc(item.url)}" target="_blank" class="ah-wl-link">${t("openItem")}</a>
            ${ripperLink}
            ${bumpBtn}
            ${checked}
          </div>
        </div>`;
      }

      // Bump All button + recheck + progress bar
      const recheckSVG = `<svg width="13" height="13" viewBox="0 0 13 13" fill="none"><path d="M11 6.5A4.5 4.5 0 1 1 9.2 3M11 1.5V4H8.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
      const bumpAllHTML = `<div class="ah-wl-bump-all-row">
          <div class="ah-wl-bump-all-btns">
            ${bumpableItems.length ? `<button class="ah-wl-bump-all-btn" id="ah-wl-bump-all">${t("bumpAllBtn")} (${bumpableItems.length})</button>` : ""}
            <button class="ah-wl-recheck-btn" id="ah-recheck-btn" title="${t("recheck")}">${recheckSVG}</button>
          </div>
          <div class="ah-wl-bump-progress" id="ah-wl-bump-progress" style="display:none">
            <div class="ah-wl-bump-progress-bar-wrap"><div class="ah-wl-bump-progress-bar" id="ah-wl-bump-progress-bar"></div></div>
            <span class="ah-wl-bump-progress-text" id="ah-wl-bump-progress-text"></span>
          </div>
         </div>`;

      let html = bumpAllHTML;
      if (dlItems.length)    html += `<div class="ah-section-label ah-section--dl">&#8595; Download Found</div>` + dlItems.map(itemHTML).join("");
      if (discItems.length)  html += `<div class="ah-section-label ah-section--disc">&#9711; Discussions</div>` + discItems.map(itemHTML).join("");
      if (otherItems.length) {
        if (dlItems.length || discItems.length) html += `<div class="ah-section-label ah-section--other">&#9675; Other</div>`;
        html += otherItems.map(itemHTML).join("");
      }

      wlBody.innerHTML = html;

      wlBody.querySelectorAll(".ah-wl-remove").forEach(btn => {
        btn.addEventListener("click", e => { e.preventDefault(); wlRemove(btn.dataset.url); renderWatchlist(); });
      });

      const recheckBtn = wlBody.querySelector("#ah-recheck-btn");
      if (recheckBtn) recheckBtn.addEventListener("click", runWatchlistCheck);

      // Wire individual bump buttons in watchlist
      wlBody.querySelectorAll(".ah-wl-bump-btn").forEach(btn => wireBumpBtn(btn));

      // Wire Bump All button
      const bumpAllBtn = wlBody.querySelector("#ah-wl-bump-all");
      if (bumpAllBtn) {
        bumpAllBtn.addEventListener("click", () => {
          const items = wlGet().filter(x => x.ripperTid && x.status !== "dl");
          if (!items.length) return;

          const BUMP_DELAY_MS = 11000;
          const totalMs = items.length * BUMP_DELAY_MS;
          const totalSecs = Math.ceil(totalMs / 1000);

          const progressWrap = wlBody.querySelector("#ah-wl-bump-progress");
          const progressBar  = wlBody.querySelector("#ah-wl-bump-progress-bar");
          const progressText = wlBody.querySelector("#ah-wl-bump-progress-text");

          bumpAllBtn.disabled = true;
          progressWrap.style.display = "";

          let bumped = 0;
          let failed = 0;

          function updateProgress() {
            const done = bumped + failed;
            const pct  = Math.round((done / items.length) * 100);
            progressBar.style.width = pct + "%";
            const secsLeft = Math.ceil(((items.length - done) * BUMP_DELAY_MS) / 1000);
            progressText.textContent = `${done}/${items.length} bumped${failed ? ` (${failed} failed)` : ""} · ~${secsLeft}s left`;
          }

          function bumpNext(idx) {
            if (idx >= items.length) {
              progressText.textContent = `✓ Done — ${bumped} bumped${failed ? `, ${failed} failed` : ""}`;
              progressBar.style.width = "100%";
              bumpAllBtn.disabled = false;
              bumpAllBtn.textContent = t("bumpAllBtn") + ` (${items.length})`;
              return;
            }

            const item = items[idx];
            const msg  = (getSetting("bumpMessage") || "bump").trim() || "bump";

            // Highlight the item being bumped
            const itemEl = wlBody.querySelector(`.ah-wl-item[data-url="${CSS.escape(item.url)}"]`);
            if (itemEl) itemEl.classList.add("ah-wl-item--bumping");

            // Wait for cooldown before posting
            const wait = Math.max(0, cooldownRemaining());
            setTimeout(() => {
              postReply(String(item.ripperTid), msg, (err) => {
                if (itemEl) itemEl.classList.remove("ah-wl-item--bumping");
                if (err) { failed++; } else { bumped++; recordPost(); applyCooldownToAllButtons(); }
                updateProgress();
                // Wait 11 seconds before next bump regardless
                setTimeout(() => bumpNext(idx + 1), BUMP_DELAY_MS);
              });
            }, wait);
          }

          updateProgress();
          bumpNext(0);
        });
      }

      if (wlBody._tick) clearInterval(wlBody._tick);
      wlBody._tick = setInterval(() => {
        wlBody.querySelectorAll(".ah-wl-ts").forEach(el => {
          const ts = parseInt(el.dataset.ts, 10); if (ts) el.textContent = timeAgo(ts);
        });
      }, 1000);
    }

    // ── Supporters Leaderboard ──
    function renderSupporters() {
      supBody.innerHTML = `<div class="ah-sup-loading"><span class="ah-spinner"></span></div>`;

      fetchDonors((err, donors) => {
        if (err || !donors) {
          supBody.innerHTML = `<div class="ah-sup-empty">${t("supLoadErr")}</div>`;
          return;
        }

        function getRankStyle(pos) {
          if (pos === 0) return { cls: "ah-sup-rank--1", medal: "&#10022;" };
          if (pos === 1) return { cls: "ah-sup-rank--2", medal: "&#10022;" };
          if (pos === 2) return { cls: "ah-sup-rank--3", medal: "&#10023;" };
          if (pos < 6)   return { cls: "ah-sup-rank--mid", medal: "&middot;" };
          return { cls: "ah-sup-rank--low", medal: "" };
        }

        let html = `
          <div class="ah-sup-header">
            <div class="ah-sup-title">${t("supTitle")}</div>
            <div class="ah-sup-subtitle">${t("supSubtitle")}</div>
            <a class="ah-sup-kofi-link" href="https://ko-fi.com/xedinho" target="_blank" rel="noopener">&#9749; ko-fi.com/xedinho</a>
          </div>`;

        if (!donors.length) {
          html += `<div class="ah-sup-empty">${t("supEmpty")}<br><span class="ah-sup-empty-sub">${t("supEmptySub")}</span></div>`;
        } else {
          html += `<div class="ah-sup-list">`;
          donors.forEach((donor, pos) => {
            const { cls, medal } = getRankStyle(pos);
            const rank = pos + 1;
            html += `
              <div class="ah-sup-entry ${cls}">
                <span class="ah-sup-pos">${medal || rank}</span>
                <span class="ah-sup-name">${esc(donor.name)}</span>
                <span class="ah-sup-amount">&#8364;${donor.total % 1 === 0 ? donor.total.toFixed(0) : donor.total.toFixed(2)}</span>
              </div>`;
          });
          html += `</div>`;
        }

        supBody.innerHTML = html;
      });
    }

    // ── Settings validation warn popup ──
    function showSettingsWarn(errors) {
      if (document.getElementById("ah-warn-modal")) return;

      const modal = document.createElement("div");
      modal.id = "ah-warn-modal";

      const itemsHTML = errors.map(function(e) {
        return '<div class="ah-warn-item">' + e + '</div>';
      }).join("");

      modal.innerHTML = [
        '<div class="ah-warn-backdrop"></div>',
        '<div class="ah-warn-dialog" role="alertdialog" aria-modal="true">',
          '<div class="ah-warn-icon-row">',
            '<svg width="28" height="28" viewBox="0 0 28 28" fill="none">',
              '<path d="M14 3L26 24H2L14 3Z" stroke="#ff6680" stroke-width="1.6" stroke-linejoin="round" fill="rgba(255,102,128,.08)"/>',
              '<path d="M14 11V17" stroke="#ff6680" stroke-width="1.8" stroke-linecap="round"/>',
              '<circle cx="14" cy="21" r="1.1" fill="#ff6680"/>',
            '</svg>',
            '<span class="ah-warn-title">Check your templates</span>',
          '</div>',
          '<div class="ah-warn-body">' + itemsHTML + '</div>',
          '<button class="ah-warn-btn" id="ah-warn-ok">Understood</button>',
        '</div>'
      ].join("");

      document.body.appendChild(modal);
      injectWarnCSS();

      const close = function() { modal.remove(); };
      modal.querySelector("#ah-warn-ok").addEventListener("click", close);
      modal.querySelector(".ah-warn-backdrop").addEventListener("click", close);
    }

    // ── Settings ──
    function renderSettings() {
      setBody.innerHTML = `
        <div class="ah-set-changelog-btn-wrap">
          <button class="ah-set-changelog-btn" id="ah-set-changelog-open">
            <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
              <path d="M5.5 1L6.5 4H10L7.5 5.8L8.5 9L5.5 7.2L2.5 9L3.5 5.8L1 4H4.5L5.5 1Z"
                stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
            </svg>
            ${t("changelogBtn")}
            <span class="ah-set-changelog-version">v${CURRENT_VERSION}</span>
          </button>
        </div>

        <div class="ah-set-section">${t("langLabel")}</div>

        <div class="ah-set-field">
          <select class="ah-set-input ah-set-input--single" id="ah-set-lang">
            ${LANG_OPTIONS.map(o => `<option value="${o.value}"${o.value === currentLang ? " selected" : ""}>${o.label}</option>`).join("")}
          </select>
        </div>

        <div class="ah-set-section">${t("secLfTemplates")}</div>

        <div class="ah-set-field">
          <div class="ah-set-field-hd">
            <label class="ah-set-label">${t("labelTitleTpl")}</label>
            <button class="ah-set-reset" data-key="titleTpl">${t("btnReset")}</button>
          </div>
          <div class="ah-set-hint">${t("hintTitleTpl")}</div>
          <textarea class="ah-set-input ah-set-input--single" id="ah-set-titleTpl" rows="1">${esc(getSetting("titleTpl"))}</textarea>
        </div>

        <div class="ah-set-field">
          <div class="ah-set-field-hd">
            <label class="ah-set-label">${t("labelBodyTpl")}</label>
            <button class="ah-set-reset" data-key="bodyTpl">${t("btnReset")}</button>
          </div>
          <div class="ah-set-hint">${t("hintBodyTpl")}</div>
          <textarea class="ah-set-input" id="ah-set-bodyTpl" rows="6">${esc(getSetting("bodyTpl"))}</textarea>
          <div class="ah-set-wm-box">
            <span class="ah-set-wm-label">${t("wmLabel")}</span>
            <pre class="ah-set-wm-pre">---\n# Posted via Asset Hunter</pre>
          </div>
        </div>

        <div class="ah-set-field">
          <div class="ah-set-field-hd">
            <label class="ah-set-label">${t("labelDefaultTags")}</label>
            <button class="ah-set-reset" data-key="defaultTags">${t("btnReset")}</button>
          </div>
          <div class="ah-set-hint">${t("hintDefaultTags")}</div>
          <textarea class="ah-set-input ah-set-input--single" id="ah-set-defaultTags" rows="1">${esc(getSetting("defaultTags"))}</textarea>
        </div>

        <div class="ah-set-field">
          <div class="ah-set-field-hd">
            <label class="ah-set-label">${t("labelBumpMessage")}</label>
            <button class="ah-set-reset" data-key="bumpMessage">${t("btnReset")}</button>
          </div>
          <div class="ah-set-hint">${t("hintBumpMessage")}</div>
          <textarea class="ah-set-input ah-set-input--single" id="ah-set-bumpMessage" rows="1">${esc(getSetting("bumpMessage"))}</textarea>
        </div>

        <div class="ah-set-section">${t("secBehaviour")}</div>

        <div class="ah-set-toggle-row">
          <div class="ah-set-toggle-info">
            <span class="ah-set-toggle-label">${t("labelAutoSearch")}</span>
            <span class="ah-set-toggle-hint">${t("hintAutoSearch")}</span>
          </div>
          <button class="ah-set-toggle ${getSetting("autoSearch") ? "ah-set-toggle--on" : ""}"
            id="ah-set-autoSearch" aria-pressed="${getSetting("autoSearch")}">
            <span class="ah-set-toggle-knob"></span>
          </button>
        </div>

        <div class="ah-set-toggle-row">
          <div class="ah-set-toggle-info">
            <span class="ah-set-toggle-label">${t("labelAutoWatch")}</span>
            <span class="ah-set-toggle-hint">${t("hintAutoWatch")}</span>
          </div>
          <button class="ah-set-toggle ${getSetting("autoWatch") ? "ah-set-toggle--on" : ""}"
            id="ah-set-autoWatch" aria-pressed="${getSetting("autoWatch")}">
            <span class="ah-set-toggle-knob"></span>
          </button>
        </div>

        <div class="ah-set-section ah-set-section--danger">${t("secDataMgmt")}</div>

        <div class="ah-set-data-actions">
          <button class="ah-set-data-btn ah-set-data-btn--export" id="ah-set-export">
            <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
              <path d="M5.5 1v6M2.5 5l3 3 3-3M1 9h9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            ${t("btnExport")}
          </button>
          <button class="ah-set-data-btn ah-set-data-btn--import" id="ah-set-import">
            <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
              <path d="M5.5 10V4M2.5 6l3-3 3 3M1 1h9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            ${t("btnImport")}
          </button>
          <button class="ah-set-data-btn ah-set-data-btn--reset" id="ah-set-reset-defaults">
            <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
              <path d="M9 5.5A3.5 3.5 0 1 1 7.2 2.5M9 1v2.5H6.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            ${t("btnResetDef")}
          </button>
          <button class="ah-set-data-btn ah-set-data-btn--delete" id="ah-set-delete-data">
            <svg width="11" height="11" viewBox="0 0 11 11" fill="none">
              <path d="M1.5 3h8M4 3V2h3v1M2.5 3l.5 6h5l.5-6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
            </svg>
            ${t("btnDeleteData")}
          </button>
        </div>`;

      // Changelog button in settings
      setBody.querySelector("#ah-set-changelog-open").addEventListener("click", showChangelogModal);

      function autoSaveTitleTpl() {
        const val = setBody.querySelector("#ah-set-titleTpl").value || DEFAULTS.titleTpl;
        setSetting("titleTpl", val);
      }
      function autoSaveBodyTpl() {
        const val = setBody.querySelector("#ah-set-bodyTpl").value || DEFAULTS.bodyTpl;
        setSetting("bodyTpl", val);
      }
      function autoSaveDefaultTags() {
        const val = setBody.querySelector("#ah-set-defaultTags").value || DEFAULTS.defaultTags;
        setSetting("defaultTags", val);
      }
      function autoSaveBumpMessage() {
        const val = setBody.querySelector("#ah-set-bumpMessage").value.trim() || DEFAULTS.bumpMessage;
        setSetting("bumpMessage", val);
      }

      setBody.querySelector("#ah-set-titleTpl").addEventListener("change", autoSaveTitleTpl);
      setBody.querySelector("#ah-set-bodyTpl").addEventListener("change", autoSaveBodyTpl);
      setBody.querySelector("#ah-set-defaultTags").addEventListener("change", autoSaveDefaultTags);
      setBody.querySelector("#ah-set-bumpMessage").addEventListener("change", autoSaveBumpMessage);

      setBody.querySelector("#ah-set-lang").addEventListener("change", function() {
        const chosen = this.value;
        if (chosen && STRINGS[chosen]) {
          GM_setValue("ah-cfg-lang", chosen);
          currentLang = chosen;
          const tabSearch = panel.querySelector("#ah-tab-search");
          const tabWl     = panel.querySelector("#ah-tab-watchlist");
          const wlCount   = panel.querySelector("#ah-wl-count");
          if (tabSearch) tabSearch.textContent = t("search");
          if (tabWl)     tabWl.innerHTML = `${t("watchlist")} <span id="ah-wl-count">${wlCount ? wlCount.textContent : ""}</span>`;
          if (document.getElementById("ah-kofi-card")) renderKofiCardContent();
          if (lastSearchData) {
            out.innerHTML = renderResults(lastSearchData, panel);
            wireSearchResults();
          }
          const manualBtn = panel.querySelector("#ah-manual-search-btn");
          if (manualBtn) manualBtn.textContent = t("manualSearchBtn");
          if (wlBody.closest("#ah-pane-watchlist").style.display !== "none") {
            renderWatchlist();
          }
          if (supBody.closest("#ah-pane-supporters").style.display !== "none") {
            renderSupporters();
          }
          renderSettings();
        }
      });

      setBody.querySelector("#ah-set-autoSearch").addEventListener("click", function() {
        const on = this.classList.toggle("ah-set-toggle--on");
        this.setAttribute("aria-pressed", on);
        setSetting("autoSearch", on);
        const manualBtn = panel.querySelector("#ah-manual-search-btn");
        const searchRow = panel.querySelector("#ah-search-row");
        if (!manualBtn || !searchRow) return;
        if (on) {
          manualBtn.style.display = "none";
          searchRow.style.display = "";
        } else {
          if (!lastSearchData) {
            manualBtn.style.display = "";
            searchRow.style.display = "none";
          }
        }
      });

      setBody.querySelector("#ah-set-autoWatch").addEventListener("click", function() {
        const on = this.classList.toggle("ah-set-toggle--on");
        this.setAttribute("aria-pressed", on);
        setSetting("autoWatch", on);
      });

      setBody.querySelectorAll(".ah-set-reset").forEach(btn => {
        btn.addEventListener("click", () => {
          const key = btn.dataset.key;
          const el  = setBody.querySelector(`#ah-set-${key}`);
          if (el) { el.value = DEFAULTS[key]; setSetting(key, DEFAULTS[key]); }
        });
      });

      setBody.querySelector("#ah-set-export").addEventListener("click", () => {
        const exportData = {
          version: CURRENT_VERSION,
          exported: new Date().toISOString(),
          settings: {
            lang:           currentLang,
            titleTpl:       getSetting("titleTpl"),
            bodyTpl:        getSetting("bodyTpl"),
            defaultTags:    getSetting("defaultTags"),
            autoWatch:      getSetting("autoWatch"),
            autoSearch:     getSetting("autoSearch"),
            bumpMessage:    getSetting("bumpMessage"),
          },
          watchlist: wlGet(),
        };
        const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: "application/json" });
        const url  = URL.createObjectURL(blob);
        const a    = document.createElement("a");
        a.href     = url;
        a.download = `asset-hunter-data-${Date.now()}.json`;
        a.click();
        URL.revokeObjectURL(url);
      });

      setBody.querySelector("#ah-set-import").addEventListener("click", () => {
        showImportModal((data) => {
          if (data.settings) {
            const s = data.settings;
            if (s.lang && STRINGS[s.lang]) { GM_setValue("ah-cfg-lang", s.lang); currentLang = s.lang; }
            if (s.titleTpl)       setSetting("titleTpl",       s.titleTpl);
            if (s.bodyTpl)        setSetting("bodyTpl",        s.bodyTpl);
            if (s.defaultTags)    setSetting("defaultTags",    s.defaultTags);
            if (s.autoWatch      !== undefined) setSetting("autoWatch",      s.autoWatch);
            if (s.autoSearch     !== undefined) setSetting("autoSearch",     s.autoSearch);
            if (s.bumpMessage    !== undefined) setSetting("bumpMessage",    s.bumpMessage);
          }
          if (Array.isArray(data.watchlist)) {
            wlSave(data.watchlist);
            updateWlCount();
          }
          renderSettings();
        });
      });

      setBody.querySelector("#ah-set-reset-defaults").addEventListener("click", () => {
        showConfirmModal({
          title: t("modalResetTitle"),
          message: t("modalResetMsg"),
          proceedLabel: t("modalResetProceed"),
          onProceed: () => {
            ["titleTpl","bodyTpl","defaultTags","autoWatch","autoSearch","bumpMessage"].forEach(k => {
              GM_setValue("ah-cfg-" + k, null);
            });
            GM_setValue("ah-cfg-lang", null);
            currentLang = "en";
            renderSettings();
          },
        });
      });

      setBody.querySelector("#ah-set-delete-data").addEventListener("click", () => {
        showConfirmModal({
          title: t("modalDeleteTitle"),
          message: t("modalDeleteMsg"),
          proceedLabel: t("modalDeleteProceed"),
          onProceed: () => {
            wlSave([]);
            updateWlCount();
            renderSettings();
          },
        });
      });
    }

    // ── Watchlist re-check ──
    function runWatchlistCheck() {
      panel.querySelectorAll(".ah-tab").forEach(b => b.classList.remove("ah-tab--active"));
      panel.querySelector('[data-tab="watchlist"]').classList.add("ah-tab--active");
      Object.entries(PANES).forEach(([k, sel]) => {
        panel.querySelector(sel).style.display = k === "watchlist" ? "" : "none";
      });

      const list = wlGet();
      if (!list.length) { renderWatchlist(); return; }
      list.forEach(item => { item.status = "pending"; });
      wlSave(list); renderWatchlist();

      const CONCURRENCY = 2;
      const ITEM_DELAY_MS = 1200;
      let idx = 0;
      let active = 0;

      function scheduleNext() {
        while (active < CONCURRENCY && idx < list.length) {
          active++;
          const item = list[idx++];
          processItem(item, () => {
            active--;
            scheduleNext();
          });
        }
      }

      function processItem(item, done) {
        setTimeout(() => {
          doSearch(item.id || item.name, (err, data) => {
            const cur   = wlGet();
            const entry = cur.find(x => x.url === item.url);
            if (!entry) { done(); return; }

            if (err || !data || !data.matchCount || !data.posts || !data.posts.length) {
              entry.status = "none"; entry.lastChecked = Date.now();
              wlSave(cur); renderWatchlist(); done(); return;
            }

            const giftsPost = data.posts.find(p => isGiftsCategory(p.category || {}));
            if (giftsPost) {
              const tp = giftsPost.topic || {};
              entry.status      = "dl";
              entry.lastChecked = Date.now();
              entry.ripperTid   = tp.tid || null;
              entry.ripperSlug  = tp.slug || tp.tid || null;
              wlSave(cur); renderWatchlist(); done(); return;
            }

            const topics = data.posts
              .map(p => ({ tid: (p.topic || {}).tid, slug: (p.topic || {}).slug }))
              .filter(x => x.tid);

            let foundDL = false;
            function checkNext(tidIdx) {
              if (tidIdx >= topics.length) {
                if (!foundDL) {
                  entry.status      = topics.length ? "found" : "none";
                  entry.lastChecked = Date.now();
                  if (topics.length) {
                    entry.ripperTid  = topics[0].tid;
                    entry.ripperSlug = topics[0].slug || topics[0].tid;
                  }
                  wlSave(cur); renderWatchlist();
                }
                done(); return;
              }
              checkDL(topics[tidIdx].tid, (confirmed) => {
                if (confirmed && !foundDL) {
                  foundDL           = true;
                  entry.status      = "dl";
                  entry.lastChecked = Date.now();
                  entry.ripperTid   = topics[tidIdx].tid;
                  entry.ripperSlug  = topics[tidIdx].slug || topics[tidIdx].tid;
                  wlSave(cur); renderWatchlist();
                  done();
                } else {
                  checkNext(tidIdx + 1);
                }
              });
            }
            checkNext(0);
          });
        }, (idx - 1) * ITEM_DELAY_MS);
      }

      scheduleNext();
    }

    _recheckFn = runWatchlistCheck;

    // ── Search ──
    function wireSearchResults() {
      const lfBtn = panel.querySelector("#ah-lf-open");
      if (lfBtn) lfBtn.addEventListener("click", () => showLFModal());

      const wlBtn = panel.querySelector("#ah-wl-add");
      if (wlBtn) {
        if (wlGet().find(x => x.url === window.location.href)) {
          wlBtn.textContent = `✓ ${t("inWatch")}`; wlBtn.disabled = true;
        }
        wlBtn.addEventListener("click", () => {
          wlAdd({ name, url: window.location.href, id: query, status: "pending" });
          wlBtn.textContent = `✓ ${t("inWatch")}`; wlBtn.disabled = true;
          updateWlCount();
        });
      }
    }

    function search(q) {
      if (!q) return;
      inp.value     = q;
      out.innerHTML = `<div class="ah-loading"><span class="ah-spinner"></span>${t("searching")}</div>`;
      doSearch(q, (err, data) => {
        lastSearchData = err ? null : data;
        out.innerHTML = err
          ? `<div class="ah-error">&#9888; ${esc(err)}</div>`
          : renderResults(data, panel);
        wireSearchResults();
      });
    }

    panel.querySelector("#ah-search-btn").addEventListener("click", () => search(inp.value.trim()));
    inp.addEventListener("keydown", e => { if (e.key === "Enter") search(inp.value.trim()); });

    const manualBtn = panel.querySelector("#ah-manual-search-btn");
    manualBtn.addEventListener("click", () => {
      manualBtn.style.display = "none";
      panel.querySelector("#ah-search-row").style.display = "";
      search(inp.value.trim());
    });

    // ── Minimize ──
    const collapsible = panel.querySelector("#ah-collapsible");
    const minBtn      = panel.querySelector("#ah-minimize");
    let collapsed     = false;
    minBtn.addEventListener("click", e => {
      e.stopPropagation();
      collapsed = !collapsed;
      collapsible.classList.toggle("ah-collapsed", collapsed);
      minBtn.querySelector("svg").style.transform = collapsed ? "rotate(45deg)" : "";
      minBtn.title = collapsed ? "Expand" : t("minimize");
    });

    if (getSetting("autoSearch")) {
      search(query);
    } else {
      manualBtn.style.display = "";
      panel.querySelector("#ah-search-row").style.display = "none";
    }
  }

  // ─── Panel CSS ────────────────────────────────────────────────────────────
  function injectCSS() {
    if (document.getElementById("ah-css")) return;
    const s = document.createElement("style");
    s.id = "ah-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Noto+Sans+JP:wght@400;500;700&display=swap');

#ah-panel {
  --ah-bg0: #0c0c0e;
  --ah-bg1: #111115;
  --ah-bg2: #16161b;
  --ah-bg3: #1c1c23;
  --ah-border: rgba(255,255,255,.07);
  --ah-border-h: rgba(255,255,255,.14);
  --ah-txt: #e8e8f0;
  --ah-txt2: #6b6b80;
  --ah-muted: #3a3a48;
  --ah-accent: #c8a8ff;
  --ah-accent-dim: rgba(200,168,255,.12);
  --ah-dl: #72f0a8;
  --ah-dl-bg: rgba(114,240,168,.06);
  --ah-dl-bd: rgba(114,240,168,.18);
  --ah-disc: #9898ff;
  --ah-disc-bg: rgba(152,152,255,.06);
  --ah-disc-bd: rgba(152,152,255,.18);
  --ah-r: 10px;
  --ah-r-sm: 6px;
  --ah-f: 'Space Mono','Noto Sans JP',monospace;
  --ah-gold:   #ffd700;
  --ah-silver: #c0c8d8;
  --ah-bronze: #cd8a4a;
}

#ah-panel {
  position:fixed;bottom:24px;right:22px;z-index:999999;
  width:352px;
  max-height:600px;
  background:var(--ah-bg0);border:1px solid var(--ah-border);border-radius:var(--ah-r);
  box-shadow:0 0 0 1px rgba(255,255,255,.03),0 4px 6px rgba(0,0,0,.4),
             0 20px 60px rgba(0,0,0,.8),inset 0 1px 0 rgba(255,255,255,.04);
  font-family:var(--ah-f);color:var(--ah-txt);
  display:flex;flex-direction:column;overflow:hidden;font-size:11px;
}

/* Header */
#ah-header {
  display:flex;align-items:center;justify-content:space-between;
  padding:11px 14px;background:var(--ah-bg1);
  border-bottom:1px solid var(--ah-border);flex-shrink:0;user-select:none;
}
#ah-header-left { display:flex;align-items:center;gap:7px; }
.ah-logo-star   { color:var(--ah-accent);flex-shrink:0;opacity:.85; }
#ah-title       { font-size:9.5px;font-weight:700;letter-spacing:4px;text-transform:uppercase;font-style:italic; }
#ah-header-right { display:flex;align-items:center;gap:8px; }
#ah-booth-badge {
  font-size:8px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
  color:#ff1428;padding:2px 7px;border-radius:3px;
  background:rgba(255,20,40,.08);border:1px solid rgba(255,20,40,.2);
}
#ah-minimize {
  background:none;border:none;color:var(--ah-muted);cursor:pointer;
  padding:3px;display:flex;align-items:center;border-radius:4px;
  transition:color .15s,background .15s;
}
#ah-minimize:hover { color:var(--ah-txt);background:var(--ah-bg3); }
#ah-minimize svg  { transition:transform .25s ease; }

/* Collapse */
#ah-collapsible {
  display:flex;flex-direction:column;
  overflow:hidden;flex:1;min-height:0;
  max-height:10000px;
  transition:max-height .3s cubic-bezier(.4,0,.2,1),opacity .25s ease;
  opacity:1;
}
#ah-collapsible.ah-collapsed { max-height:0!important;opacity:0;pointer-events:none; }

/* Tabs */
#ah-tabs {
  display:flex;align-items:center;background:var(--ah-bg1);
  border-bottom:1px solid var(--ah-border);flex-shrink:0;padding:0 2px;
}
.ah-tab {
  flex:1;padding:8px 0;background:none;border:none;border-bottom:2px solid transparent;
  color:var(--ah-muted);font-family:var(--ah-f);font-size:8.5px;font-weight:700;
  letter-spacing:2px;text-transform:uppercase;cursor:pointer;
  transition:color .15s,border-color .15s;margin-bottom:-1px;
  display:flex;align-items:center;justify-content:center;gap:4px;
}
.ah-tab:hover      { color:var(--ah-txt2); }
.ah-tab--active    { color:var(--ah-txt)!important;border-bottom-color:var(--ah-accent)!important; }
.ah-tab--icon      { flex:0 0 34px; }
#ah-wl-count       { font-size:8px;opacity:.6; }
.ah-wl-bump-all-btns { display:flex;gap:6px;align-items:center; }
#ah-recheck-btn {
  background:none;border:1px solid rgba(200,168,255,.2);color:var(--ah-muted);cursor:pointer;
  padding:6px 8px;display:flex;align-items:center;border-radius:var(--ah-r-sm);
  transition:color .15s, border-color .15s;flex-shrink:0;
}
#ah-recheck-btn:hover { color:var(--ah-txt);border-color:rgba(200,168,255,.4); }

.ah-tab--supporters {
  flex:0 0 30px;
  font-size:13px;
  letter-spacing:0;
  text-transform:none;
  color:#8a7020;
  text-shadow: none;
  transition: color .2s, text-shadow .2s;
  animation: ah-sup-tab-pulse 3s ease-in-out infinite;
}
.ah-tab--supporters:hover,
.ah-tab--supporters.ah-tab--active {
  color: var(--ah-gold) !important;
  text-shadow:
    0 0 6px rgba(255,215,0,.9),
    0 0 14px rgba(255,215,0,.6),
    0 0 28px rgba(255,180,0,.4);
  border-bottom-color: var(--ah-gold) !important;
}
@keyframes ah-sup-tab-pulse {
  0%,100% { color:#8a7020; text-shadow:none; }
  50% {
    color:#d4a800;
    text-shadow: 0 0 8px rgba(255,215,0,.7), 0 0 18px rgba(255,180,0,.4);
  }
}

/* Panes */
#ah-pane-search,#ah-pane-watchlist,#ah-pane-supporters,#ah-pane-settings {
  padding:12px 13px;overflow-y:auto;flex:1;min-height:0;
}
#ah-pane-search::-webkit-scrollbar,
#ah-pane-watchlist::-webkit-scrollbar,
#ah-pane-supporters::-webkit-scrollbar,
#ah-pane-settings::-webkit-scrollbar  { width:2px; }
#ah-pane-search::-webkit-scrollbar-thumb,
#ah-pane-watchlist::-webkit-scrollbar-thumb,
#ah-pane-supporters::-webkit-scrollbar-thumb,
#ah-pane-settings::-webkit-scrollbar-thumb { background:var(--ah-bg3);border-radius:2px; }

/* Item info */
#ah-item-info {
  margin-bottom:10px;padding:8px 10px;
  background:var(--ah-bg2);border-radius:var(--ah-r-sm);border:1px solid var(--ah-border);
}
#ah-item-name {
  font-size:11px;font-weight:700;color:var(--ah-txt);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;line-height:1.4;
}
#ah-item-id { font-size:9px;color:var(--ah-muted);margin-top:2px;letter-spacing:1px; }

/* Manual search trigger button */
#ah-manual-search-btn {
  display:flex;align-items:center;justify-content:center;gap:8px;
  width:100%;padding:14px 16px;margin-bottom:12px;
  background:var(--ah-accent-dim);
  border:1px solid rgba(200,168,255,.25);border-radius:var(--ah-r-sm);
  color:var(--ah-accent);font-family:var(--ah-f);
  font-size:10px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
  cursor:pointer;
  transition:background .15s,border-color .15s,box-shadow .15s;
  box-sizing:border-box;
}
#ah-manual-search-btn:hover {
  background:rgba(200,168,255,.2);border-color:rgba(200,168,255,.45);
  box-shadow:0 0 0 1px rgba(200,168,255,.12),0 4px 16px rgba(200,168,255,.08);
}

/* Search row */
#ah-search-row { display:flex;gap:6px;margin-bottom:12px; }
#ah-input {
  flex:1;padding:7px 10px;
  background:#16161b !important;
  border:1px solid var(--ah-border);border-radius:var(--ah-r-sm);
  color:var(--ah-txt) !important;font-family:var(--ah-f);font-size:10.5px;
  outline:none !important;box-shadow:none !important;-webkit-appearance:none;
  transition:border-color .15s;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
  -webkit-text-fill-color:var(--ah-txt) !important;
  caret-color:var(--ah-accent);
}
#ah-input:focus {
  outline:none !important;box-shadow:none !important;
  border-color:rgba(200,168,255,.35);
  background:#16161b !important;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
}
#ah-input::placeholder { color:var(--ah-muted); }
#ah-search-btn {
  padding:7px 12px;background:var(--ah-accent-dim);
  border:1px solid rgba(200,168,255,.22);border-radius:var(--ah-r-sm);
  color:var(--ah-accent);font-family:var(--ah-f);font-size:9px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;white-space:nowrap;
  transition:background .15s,border-color .15s;
}
#ah-search-btn:hover { background:rgba(200,168,255,.2);border-color:rgba(200,168,255,.4); }

/* Loading/error */
.ah-loading {
  display:flex;align-items:center;gap:8px;padding:20px 0;justify-content:center;
  color:var(--ah-txt2);font-size:9.5px;letter-spacing:2px;text-transform:uppercase;
}
.ah-spinner {
  width:12px;height:12px;border:1.5px solid var(--ah-bg3);
  border-top-color:var(--ah-accent);border-radius:50%;
  animation:ah-spin .7s linear infinite;flex-shrink:0;
}
@keyframes ah-spin { to { transform:rotate(360deg); } }
.ah-error      { color:#ff6680;font-size:10.5px;padding:12px 0;text-align:center; }
.ah-no-results { color:var(--ah-muted);font-size:10px;padding:20px 0 6px;text-align:center;letter-spacing:1.5px;text-transform:uppercase; }
.ah-result-count { font-size:8.5px;color:var(--ah-muted);letter-spacing:2.5px;text-transform:uppercase;margin-bottom:10px; }

/* Section labels */
.ah-section-label {
  font-size:8px;font-weight:700;letter-spacing:3px;text-transform:uppercase;
  padding:4px 8px;border-radius:4px;margin:10px 0 6px;display:flex;align-items:center;gap:5px;
}
.ah-section--dl    { color:var(--ah-dl);  background:var(--ah-dl-bg);  border:1px solid var(--ah-dl-bd); }
.ah-section--disc  { color:var(--ah-disc);background:var(--ah-disc-bg);border:1px solid var(--ah-disc-bd); }
.ah-section--other { color:var(--ah-muted);background:var(--ah-bg2);border:1px solid var(--ah-border); }

/* Cards */
.ah-list  { display:flex;flex-direction:column;gap:5px; }
.ah-card  {
  display:block;padding:9px 11px;border-radius:var(--ah-r-sm);text-decoration:none;color:inherit;
  border:1px solid var(--ah-border);background:var(--ah-bg2);
  transition:border-color .15s,background .15s;
}
.ah-card:hover        { border-color:var(--ah-border-h);background:var(--ah-bg3); }
.ah-card--dl          { background:var(--ah-dl-bg);border-color:var(--ah-dl-bd); }
.ah-card--dl:hover    { border-color:rgba(114,240,168,.35);background:rgba(114,240,168,.09); }
.ah-card--disc        { background:var(--ah-disc-bg);border-color:var(--ah-disc-bd); }
.ah-card--disc:hover  { border-color:rgba(152,152,255,.35);background:rgba(152,152,255,.09); }
.ah-card-top          { display:flex;align-items:center;gap:5px;margin-bottom:5px;flex-wrap:wrap; }
.ah-card-bump-wrap    { margin-left:auto; }
.ah-badge             { font-size:7.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase;padding:2px 6px;border-radius:3px;border:1px solid transparent; }
.ah-cat               { font-size:7.5px;font-weight:700;padding:2px 6px;border-radius:3px;border:1px solid transparent;max-width:130px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.ah-dl-chip {
  font-size:7.5px;font-weight:700;letter-spacing:1px;
  color:var(--ah-dl);padding:2px 7px;border-radius:3px;
  background:rgba(114,240,168,.1);border:1px solid rgba(114,240,168,.25);
  margin-left:auto;
  animation:ah-dl-pulse 2.5s ease-in-out infinite;
}
@keyframes ah-dl-pulse { 0%,100%{opacity:1} 50%{opacity:.6} }
.ah-card-title        { font-size:11px;font-weight:700;color:var(--ah-txt);margin-bottom:5px;line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden; }
.ah-card--dl   .ah-card-title { color:var(--ah-dl); }
.ah-card--disc .ah-card-title { color:var(--ah-disc); }
.ah-card-meta         { display:flex;gap:8px;font-size:9px;color:var(--ah-muted);flex-wrap:wrap; }
.ah-card-user         { color:var(--ah-txt2); }
.ah-tags              { display:flex;flex-wrap:wrap;gap:3px;margin-top:6px; }
.ah-tag               { font-size:8px;padding:1px 5px;border-radius:3px;background:var(--ah-bg3);color:var(--ah-muted);border:1px solid var(--ah-border); }
.ah-card--dl   .ah-tag { background:rgba(114,240,168,.06);color:rgba(114,240,168,.5);border-color:rgba(114,240,168,.1); }
.ah-card--disc .ah-tag { background:rgba(152,152,255,.06);color:rgba(152,152,255,.5);border-color:rgba(152,152,255,.1); }

/* Bump button row */
.ah-bump-btn {
  display:inline-flex;align-items:center;justify-content:center;
  padding:4px 10px;
  font-family:var(--ah-f);font-size:8px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
  color:rgba(200,168,255,.7);
  background:rgba(200,168,255,.08);
  border:1px solid rgba(200,168,255,.2);
  border-radius:4px;
  cursor:pointer;
  transition:background .15s,border-color .15s,color .15s,opacity .15s;
  user-select:none;
}
.ah-bump-btn:hover:not(:disabled):not([data-bumped="1"]) {
  background:rgba(200,168,255,.16);
  border-color:rgba(200,168,255,.4);
  color:var(--ah-accent);
}
.ah-bump-btn:disabled { opacity:.5;cursor:default; }
.ah-bump-btn--sending {
  color:var(--ah-muted);
  background:rgba(255,255,255,.04);
  border-color:rgba(255,255,255,.08);
  animation:ah-bump-sending .8s ease-in-out infinite;
}
@keyframes ah-bump-sending { 0%,100%{opacity:.5} 50%{opacity:1} }
.ah-bump-btn--done {
  color:var(--ah-dl);
  background:rgba(114,240,168,.08);
  border-color:rgba(114,240,168,.2);
  cursor:default;
}
.ah-bump-btn--err {
  color:#ff6680;
  background:rgba(255,102,128,.08);
  border-color:rgba(255,102,128,.2);
}

/* Bottom actions */
.ah-bottom-actions { display:flex;gap:6px;margin-top:12px; }
.ah-btn-watch {
  flex:1;padding:8px;background:var(--ah-bg2);border:1px solid var(--ah-border);
  border-radius:var(--ah-r-sm);color:var(--ah-txt2);font-family:var(--ah-f);
  font-size:8.5px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
  cursor:pointer;transition:all .15s;
}
.ah-btn-watch:hover:not(:disabled) { border-color:var(--ah-border-h);color:var(--ah-txt); }
.ah-btn-watch:disabled { opacity:.4;cursor:default; }
.ah-btn-lf {
  flex:1;padding:8px;background:var(--ah-accent-dim);
  border:1px solid rgba(200,168,255,.2);border-radius:var(--ah-r-sm);
  color:var(--ah-accent);font-family:var(--ah-f);font-size:8.5px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s;
}
.ah-btn-lf:hover { background:rgba(200,168,255,.2);border-color:rgba(200,168,255,.4); }

/* LF prompt */
.ah-lf-prompt  { margin-top:8px;padding:12px;background:var(--ah-bg2);border:1px solid var(--ah-border);border-radius:var(--ah-r-sm);text-align:center; }
.ah-lf-prompt p { font-size:10px;color:var(--ah-txt2);margin:0 0 10px;line-height:1.5; }

/* Watchlist */
.ah-wl-empty { color:var(--ah-muted);font-size:9.5px;letter-spacing:1px;text-align:center;padding:24px 0;text-transform:uppercase; }
.ah-wl-item  { padding:9px 10px;margin-bottom:5px;background:var(--ah-bg2);border:1px solid var(--ah-border);border-radius:var(--ah-r-sm);transition:border-color .15s; }
.ah-wl-item--dl    { background:var(--ah-dl-bg);  border-color:var(--ah-dl-bd); }
.ah-wl-item--found { background:var(--ah-disc-bg);border-color:var(--ah-disc-bd); }
.ah-wl-row1  { display:flex;align-items:center;gap:6px;margin-bottom:4px; }
.ah-wl-badge { font-size:7.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase;padding:2px 7px;border-radius:3px; }
.ah-wl-badge--dl      { color:var(--ah-dl);  background:rgba(114,240,168,.1);border:1px solid rgba(114,240,168,.2); }
.ah-wl-badge--disc    { color:var(--ah-disc);background:rgba(152,152,255,.1);border:1px solid rgba(152,152,255,.2); }
.ah-wl-badge--none    { color:var(--ah-muted);background:var(--ah-bg3);border:1px solid var(--ah-border); }
.ah-wl-badge--pending { color:var(--ah-muted);background:var(--ah-bg3);border:1px solid var(--ah-border); }
.ah-wl-remove {
  background:none;border:none;color:var(--ah-muted);font-size:10px;cursor:pointer;
  padding:1px 4px;border-radius:3px;transition:color .12s;line-height:1;
  margin-left:auto;
}
.ah-wl-remove:hover { color:#ff6680; }
.ah-wl-name  { font-size:10.5px;font-weight:700;color:var(--ah-txt);margin-bottom:4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.ah-wl-item.ah-wl-item--dl    .ah-wl-name { color:var(--ah-dl); }
.ah-wl-item.ah-wl-item--found .ah-wl-name { color:var(--ah-disc); }
.ah-wl-row2  { display:flex;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:6px; }
.ah-wl-ts    { font-size:8.5px;color:var(--ah-muted);margin-left:auto; }
.ah-wl-link  { font-size:9px;color:var(--ah-muted);text-decoration:none;transition:color .12s; }
.ah-wl-link:hover { color:var(--ah-txt); }
.ah-wl-link--ripper { color:var(--ah-dl) !important;opacity:.8; }
.ah-wl-link--ripper:hover { opacity:1 !important; }
.ah-wl-item--found .ah-wl-link--ripper { color:var(--ah-disc) !important; }

/* Watchlist bump all */
.ah-wl-bump-all-row {
  margin-bottom:10px;
  display:flex;flex-direction:column;gap:6px;
}
.ah-wl-bump-all-btn {
  width:100%;padding:8px;
  background:rgba(200,168,255,.08);border:1px solid rgba(200,168,255,.2);
  border-radius:var(--ah-r-sm);color:var(--ah-accent);
  font-family:var(--ah-f);font-size:8.5px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s;
}
.ah-wl-bump-all-btn:hover:not(:disabled) { background:rgba(200,168,255,.15);border-color:rgba(200,168,255,.4); }
.ah-wl-bump-all-btn:disabled { opacity:.5;cursor:default; }
.ah-wl-bump-progress {
  width:100%;
}
.ah-wl-bump-progress-bar-wrap {
  width:100%;height:4px;background:var(--ah-bg3);border-radius:2px;overflow:hidden;margin-bottom:4px;
}
.ah-wl-bump-progress-bar {
  height:4px;background:var(--ah-accent);border-radius:2px;
  width:0%;transition:width .4s ease;
}
.ah-wl-bump-progress-text {
  display:block;font-size:8.5px;color:var(--ah-txt2);letter-spacing:.5px;
}
.ah-wl-item--bumping {
  border-color:rgba(200,168,255,.4) !important;
  background:rgba(200,168,255,.05) !important;
  animation:ah-wl-bumping-pulse 1s ease-in-out infinite;
}
@keyframes ah-wl-bumping-pulse {
  0%,100%{opacity:.7} 50%{opacity:1}
}

/* Supporters pane */
.ah-sup-loading {
  display:flex;align-items:center;justify-content:center;padding:32px 0;
}
.ah-sup-header {
  text-align:center;margin-bottom:16px;padding-bottom:14px;
  border-bottom:1px solid rgba(255,215,0,.1);
}
.ah-sup-title {
  font-size:11px;font-weight:700;letter-spacing:3px;text-transform:uppercase;
  color:var(--ah-gold);
  text-shadow:0 0 10px rgba(255,215,0,.5),0 0 24px rgba(255,180,0,.3);
  margin-bottom:5px;
}
.ah-sup-subtitle {
  font-size:8.5px;color:var(--ah-txt2);letter-spacing:.5px;line-height:1.5;
  margin-bottom:8px;
}
.ah-sup-kofi-link {
  display:inline-block;font-size:9px;font-weight:700;letter-spacing:1px;
  color:#ff5e5b;text-decoration:none;
  transition:color .15s,text-shadow .15s;
}
.ah-sup-kofi-link:hover {
  color:#ff8f8d;
  text-shadow:0 0 8px rgba(255,94,91,.5);
}
.ah-sup-empty {
  text-align:center;padding:28px 12px;
  font-size:10px;color:rgba(255,255,255,.12);
  line-height:1.8;letter-spacing:.3px;
}
.ah-sup-empty-sub {
  display:block;font-size:9px;color:rgba(255,255,255,.08);margin-top:4px;
}
.ah-sup-list { display:flex;flex-direction:column;gap:3px; }
.ah-sup-entry {
  display:flex;align-items:center;gap:0;
  padding:6px 10px;border-radius:var(--ah-r-sm);
  border:1px solid transparent;
  transition:background .15s,border-color .15s;
  position:relative;overflow:hidden;
}
.ah-sup-pos { flex:0 0 22px;text-align:center;font-size:11px;line-height:1; }
.ah-sup-name { flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding: 0 10px; }
.ah-sup-amount { flex-shrink:0;font-size:10px;font-weight:700;letter-spacing:.5px; }
.ah-sup-rank--1 {
  background: linear-gradient(90deg, rgba(255,215,0,.1) 0%, rgba(255,180,0,.04) 100%);
  border-color: rgba(255,215,0,.25);
}
.ah-sup-rank--1 .ah-sup-pos { font-size:14px;color:var(--ah-gold);text-shadow:0 0 6px rgba(255,215,0,1),0 0 14px rgba(255,200,0,.8),0 0 28px rgba(255,160,0,.5);animation:ah-gold-pulse 2.2s ease-in-out infinite; }
.ah-sup-rank--1 .ah-sup-name { font-size:13px;font-weight:700;color:var(--ah-gold);text-shadow:0 0 8px rgba(255,215,0,.7),0 0 20px rgba(255,180,0,.4);letter-spacing:.5px; }
.ah-sup-rank--1 .ah-sup-amount { font-size:12px;color:var(--ah-gold);text-shadow:0 0 8px rgba(255,215,0,.6); }
@keyframes ah-gold-pulse {
  0%,100% { text-shadow:0 0 6px rgba(255,215,0,1),0 0 14px rgba(255,200,0,.8),0 0 28px rgba(255,160,0,.5); }
  50%      { text-shadow:0 0 10px rgba(255,230,0,1),0 0 24px rgba(255,210,0,1),0 0 40px rgba(255,170,0,.7); }
}
.ah-sup-rank--2 { background:linear-gradient(90deg,rgba(192,200,216,.08) 0%,rgba(192,200,216,.02) 100%);border-color:rgba(192,200,216,.18); }
.ah-sup-rank--2 .ah-sup-pos { font-size:13px;color:var(--ah-silver);text-shadow:0 0 6px rgba(200,210,230,.6),0 0 14px rgba(180,190,210,.3); }
.ah-sup-rank--2 .ah-sup-name { font-size:12px;font-weight:700;color:var(--ah-silver);text-shadow:0 0 6px rgba(200,210,230,.4);letter-spacing:.3px; }
.ah-sup-rank--2 .ah-sup-amount { font-size:11px;color:var(--ah-silver);text-shadow:0 0 5px rgba(200,210,230,.4); }
.ah-sup-rank--3 { background:linear-gradient(90deg,rgba(205,138,74,.08) 0%,rgba(205,138,74,.02) 100%);border-color:rgba(205,138,74,.18); }
.ah-sup-rank--3 .ah-sup-pos { font-size:12px;color:var(--ah-bronze);text-shadow:0 0 5px rgba(205,138,74,.6),0 0 12px rgba(180,110,50,.3); }
.ah-sup-rank--3 .ah-sup-name { font-size:11px;font-weight:700;color:var(--ah-bronze);text-shadow:0 0 5px rgba(205,138,74,.4); }
.ah-sup-rank--3 .ah-sup-amount { font-size:10.5px;color:var(--ah-bronze);text-shadow:0 0 4px rgba(205,138,74,.4); }
.ah-sup-rank--mid { background:transparent;border-color:rgba(152,152,255,.08); }
.ah-sup-rank--mid .ah-sup-pos { font-size:10px;color:rgba(152,152,255,.45); }
.ah-sup-rank--mid .ah-sup-name { font-size:10.5px;font-weight:400;color:rgba(232,232,240,.55); }
.ah-sup-rank--mid .ah-sup-amount { font-size:10px;color:rgba(232,232,240,.4); }
.ah-sup-rank--low { background:transparent;border-color:transparent; }
.ah-sup-rank--low .ah-sup-pos { font-size:9px;color:var(--ah-muted); }
.ah-sup-rank--low .ah-sup-name { font-size:10px;font-weight:400;color:var(--ah-muted); }
.ah-sup-rank--low .ah-sup-amount { font-size:9.5px;color:rgba(107,107,128,.6); }

/* Settings pane */
.ah-set-section {
  font-size:8px;font-weight:700;letter-spacing:3px;text-transform:uppercase;
  color:var(--ah-muted);margin-bottom:12px;padding-bottom:6px;
  border-bottom:1px solid var(--ah-border);
}
.ah-set-section--danger {
  color:rgba(255,102,128,.5);border-color:rgba(255,102,128,.15);margin-top:8px;
}
.ah-set-field       { margin-bottom:14px; }
.ah-set-field-hd    { display:flex;align-items:center;justify-content:space-between;margin-bottom:3px; }
.ah-set-label       { font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:var(--ah-txt2); }
.ah-set-hint        { font-size:8.5px;color:var(--ah-muted);margin-bottom:5px;line-height:1.5; }
.ah-set-hint code   { font-family:var(--ah-f);font-size:8.5px;color:var(--ah-accent);background:var(--ah-accent-dim);padding:1px 4px;border-radius:3px; }
.ah-set-input {
  display:block;width:100%;box-sizing:border-box;
  padding:8px 10px;background:#16161b !important;
  border:1px solid rgba(255,255,255,.07);border-radius:var(--ah-r-sm);
  color:var(--ah-txt) !important;font-family:var(--ah-f);font-size:10.5px;line-height:1.6;
  outline:none !important;box-shadow:none !important;-webkit-appearance:none;appearance:none;
  transition:border-color .15s;resize:vertical;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
  -webkit-text-fill-color:var(--ah-txt) !important;caret-color:var(--ah-accent);
}
textarea.ah-set-input.ah-set-input--single {
  min-height:unset;height:36px;resize:none;overflow:hidden;white-space:nowrap;
}
textarea.ah-set-input:not(.ah-set-input--single) { min-height:100px;resize:vertical; }
.ah-set-input:focus {
  outline:none !important;box-shadow:none !important;
  border-color:rgba(200,168,255,.4);background:#16161b !important;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
}
.ah-set-input:-webkit-autofill,.ah-set-input:-webkit-autofill:focus {
  outline:none !important;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
  -webkit-text-fill-color:var(--ah-txt) !important;
}
.ah-set-reset {
  background:none;border:1px solid var(--ah-border);border-radius:4px;
  color:var(--ah-muted);font-family:var(--ah-f);font-size:8px;letter-spacing:.5px;
  padding:2px 7px;cursor:pointer;transition:all .14s;white-space:nowrap;
}
.ah-set-reset:hover { color:var(--ah-txt);border-color:var(--ah-border-h); }
.ah-set-wm-box  { margin-top:6px;padding:7px 10px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.04);border-radius:var(--ah-r-sm); }
.ah-set-wm-label { display:block;font-size:7.5px;letter-spacing:1.5px;text-transform:uppercase;color:var(--ah-muted);margin-bottom:4px; }
.ah-set-wm-pre  { margin:0;font-family:var(--ah-f);font-size:9.5px;color:rgba(255,255,255,.18);white-space:pre-wrap;line-height:1.6; }
.ah-set-toggle-row {
  display:flex;align-items:flex-start;justify-content:space-between;gap:14px;
  padding:10px 12px;margin-bottom:10px;
  background:var(--ah-bg2);border:1px solid var(--ah-border);border-radius:var(--ah-r-sm);
}
.ah-set-toggle-info  { display:flex;flex-direction:column;gap:3px; }
.ah-set-toggle-label { font-size:10px;font-weight:700;color:var(--ah-txt); }
.ah-set-toggle-hint  { font-size:8.5px;color:var(--ah-muted);line-height:1.5;max-width:220px; }
.ah-set-toggle {
  flex-shrink:0;width:34px;height:18px;border-radius:9px;
  background:var(--ah-bg3);border:1px solid var(--ah-border);
  cursor:pointer;position:relative;transition:background .2s,border-color .2s;padding:0;
}
.ah-set-toggle--on   { background:rgba(200,168,255,.28);border-color:rgba(200,168,255,.45); }
.ah-set-toggle-knob  {
  position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;
  background:var(--ah-muted);transition:transform .2s,background .2s;pointer-events:none;
}
.ah-set-toggle--on .ah-set-toggle-knob { transform:translateX(16px);background:var(--ah-accent); }
.ah-set-data-actions { display:flex;gap:6px;flex-wrap:wrap; }
.ah-set-data-btn {
  flex:1;min-width:80px;padding:8px 6px;border-radius:var(--ah-r-sm);
  font-family:var(--ah-f);font-size:8px;font-weight:700;letter-spacing:1px;
  text-transform:uppercase;cursor:pointer;transition:all .15s;
  display:flex;align-items:center;justify-content:center;gap:5px;
}
.ah-set-data-btn--export { background:rgba(96,200,255,.08);border:1px solid rgba(96,200,255,.2);color:#60c8ff; }
.ah-set-data-btn--export:hover { background:rgba(96,200,255,.15);border-color:rgba(96,200,255,.4); }
.ah-set-data-btn--import { background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:rgba(255,255,255,.35); }
.ah-set-data-btn--import:hover { background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.2);color:rgba(255,255,255,.6); }
.ah-set-data-btn--reset  { background:rgba(255,179,71,.08);border:1px solid rgba(255,179,71,.2);color:#ffb347; }
.ah-set-data-btn--reset:hover { background:rgba(255,179,71,.15);border-color:rgba(255,179,71,.4); }
.ah-set-data-btn--delete { background:rgba(255,86,128,.08);border:1px solid rgba(255,86,128,.2);color:#ff5680; }
.ah-set-data-btn--delete:hover { background:rgba(255,86,128,.15);border-color:rgba(255,86,128,.4); }

/* Changelog button in settings */
.ah-set-changelog-btn-wrap {
  margin-bottom:14px;
}
.ah-set-changelog-btn {
  display:flex;align-items:center;justify-content:center;gap:7px;
  width:100%;padding:9px 12px;
  background:rgba(200,168,255,.06);
  border:1px solid rgba(200,168,255,.18);
  border-radius:var(--ah-r-sm);
  color:rgba(200,168,255,.6);
  font-family:var(--ah-f);font-size:8.5px;font-weight:700;
  letter-spacing:2px;text-transform:uppercase;
  cursor:pointer;transition:all .15s;
  box-sizing:border-box;
}
.ah-set-changelog-btn:hover {
  background:rgba(200,168,255,.12);
  border-color:rgba(200,168,255,.32);
  color:var(--ah-accent);
}
.ah-set-changelog-version {
  font-size:7.5px;letter-spacing:1px;
  color:rgba(200,168,255,.35);
  background:rgba(200,168,255,.08);
  padding:1px 5px;border-radius:3px;
  border:1px solid rgba(200,168,255,.12);
}
.ah-set-changelog-btn:hover .ah-set-changelog-version {
  color:rgba(200,168,255,.6);
  border-color:rgba(200,168,255,.2);
}

/* Ko-fi card */
#ah-kofi-card {
  position:fixed;z-index:9999998;
  font-family:'Space Mono','Noto Sans JP',monospace;
  box-sizing:border-box;
}
#ah-kofi-inner {
  display:flex;align-items:center;gap:9px;
  padding:9px 12px;
  background:#0c0c0e;
  border:1px solid rgba(255,255,255,.08);
  border-left:3px solid #ff5e5b;
  border-radius:10px;
  box-shadow:0 0 0 1px rgba(255,255,255,.02),0 4px 6px rgba(0,0,0,.4),0 20px 60px rgba(0,0,0,.8),inset 0 1px 0 rgba(255,255,255,.04);
  font-size:10px;color:#9898a8;
}
#ah-kofi-heart { color:#ff5e5b;font-size:12px;line-height:1;animation:ah-kofi-pulse 1.35s ease-in-out infinite; }
#ah-kofi-text { display:flex;flex-direction:column;gap:2px;min-width:0; }
#ah-kofi-msg { color:#9898a8;font-size:9px;line-height:1.3; }
#ah-kofi-inner a { color:#ff5e5b;text-decoration:none;font-weight:700;font-size:10px;line-height:1.2; }
#ah-kofi-inner a:hover { text-decoration:underline; }
#ah-kofi-close {
  background:none;border:none;color:#3a3a48;cursor:pointer;
  font-size:10px;padding:2px 4px;line-height:1;margin-left:auto;
  font-family:'Space Mono',monospace;transition:color .15s;
}
#ah-kofi-close:hover { color:#9898a8; }
@keyframes ah-kofi-pulse {
  0%,100% { transform:scale(1); opacity:.9; }
  50% { transform:scale(1.16); opacity:1; }
}
#ah-kofi-confirm-modal {
  position:fixed;inset:0;z-index:99999999;
  display:flex;align-items:center;justify-content:center;pointer-events:all;
  font-family:'Space Mono','Noto Sans JP',monospace;
}
.ah-kofi-confirm-backdrop { position:absolute;inset:0;background:rgba(0,0,0,.7);backdrop-filter:blur(5px); }
.ah-kofi-confirm-dialog {
  position:relative;z-index:1;
  width:430px;max-width:92vw;
  background:#0c0c0e;
  border:1px solid rgba(255,255,255,.08);
  border-left:3px solid #ff5e5b;
  border-radius:10px;
  box-shadow:0 0 0 1px rgba(255,255,255,.02),0 8px 16px rgba(0,0,0,.5),0 32px 80px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,255,255,.04);
  padding:16px;
  animation:ah-kofi-confirm-in .18s cubic-bezier(.34,1.4,.64,1) both;
}
@keyframes ah-kofi-confirm-in { from{opacity:0;transform:scale(.9) translateY(8px)} to{opacity:1;transform:scale(1) translateY(0)} }
.ah-kofi-confirm-title { display:flex;align-items:center;gap:8px;color:#ffb7b6;font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;margin-bottom:10px; }
.ah-kofi-confirm-heart { color:#ff5e5b;font-size:12px;line-height:1;animation:ah-kofi-pulse 1.35s ease-in-out infinite; }
.ah-kofi-confirm-body { color:#9898a8;font-size:10px;line-height:1.6;background:#111115;border:1px solid rgba(255,255,255,.06);border-radius:8px;padding:10px 11px;margin-bottom:10px; }
.ah-kofi-confirm-link { display:inline-block;color:#ff5e5b;text-decoration:none;font-weight:700;font-size:10px;margin-bottom:12px; }
.ah-kofi-confirm-link:hover { text-decoration:underline; }
.ah-kofi-confirm-optout { display:flex;align-items:center;gap:7px;color:#9898a8;font-size:9px;line-height:1.2;margin-bottom:11px;user-select:none; }
.ah-kofi-confirm-optout input { accent-color:#ff5e5b;width:12px;height:12px;cursor:pointer; }
.ah-kofi-confirm-actions { display:flex;gap:8px; }
#ah-kofi-keep-btn,#ah-kofi-close-btn {
  flex:1;padding:9px 10px;border-radius:6px;cursor:pointer;
  font-family:'Space Mono','Noto Sans JP',monospace;
  font-size:8.5px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;
  transition:all .15s;
}
#ah-kofi-keep-btn { background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);color:#9898a8; }
#ah-kofi-keep-btn:hover { background:rgba(255,255,255,.08);color:#d0d0e4; }
#ah-kofi-close-btn { background:rgba(255,94,91,.12);border:1px solid rgba(255,94,91,.35);color:#ff8f8d; }
#ah-kofi-close-btn:hover { background:rgba(255,94,91,.2);border-color:rgba(255,94,91,.55); }
`;
    document.head.appendChild(s);
  }

  // ─── Modal CSS ────────────────────────────────────────────────────────────
  function injectModalCSS() {
    if (document.getElementById("ah-lf-css")) return;
    const s = document.createElement("style");
    s.id = "ah-lf-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Noto+Sans+JP:wght@400;700&display=swap');
#ah-lf-modal {
  position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999999;
  display:flex;align-items:center;justify-content:center;
}
.ah-lf-backdrop  { position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(6px); }
.ah-lf-dialog {
  position:relative;z-index:1;width:480px;max-width:95vw;max-height:90vh;overflow-y:auto;
  background:#0c0c0e;border:1px solid rgba(255,255,255,.08);border-radius:12px;
  box-shadow:0 0 0 1px rgba(255,255,255,.02),0 8px 16px rgba(0,0,0,.5),0 32px 80px rgba(0,0,0,.9);
  font-family:'Space Mono','Noto Sans JP',monospace;color:#c0c0d0;font-size:11px;
}
.ah-lf-dialog::-webkit-scrollbar       { width:2px; }
.ah-lf-dialog::-webkit-scrollbar-thumb { background:rgba(255,255,255,.08); }
.ah-lf-dialog-header {
  display:flex;align-items:center;justify-content:space-between;
  padding:13px 16px;background:#111115;border-bottom:1px solid rgba(255,255,255,.07);
  position:sticky;top:0;z-index:2;
}
.ah-lf-dialog-title { display:flex;align-items:center;gap:7px;font-size:9.5px;font-weight:700;letter-spacing:3.5px;text-transform:uppercase;color:#c8a8ff; }
.ah-lf-close { background:none;border:none;color:rgba(255,255,255,.25);font-size:15px;cursor:pointer;padding:2px 6px;border-radius:4px;transition:color .15s;line-height:1; }
.ah-lf-close:hover { color:rgba(255,255,255,.75); }
.ah-lf-dialog-body { padding:16px 18px; }
.ah-lf-field       { margin-bottom:11px; }
.ah-lf-row-2       { display:flex;gap:12px;margin-bottom:11px; }
.ah-lf-row-2 .ah-lf-field { flex:1;margin-bottom:0; }
.ah-lf-label { display:block;font-size:8px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:rgba(255,255,255,.25);margin-bottom:5px; }
.ah-lf-hint { font-size:7.5px;text-transform:none;letter-spacing:.5px;opacity:.6; }
.ah-lf-dialog-body input,
.ah-lf-dialog-body textarea,
.ah-lf-dialog-body select {
  width:100%;box-sizing:border-box;padding:8px 10px;
  background:#16161b !important;border:1px solid rgba(255,255,255,.07);border-radius:6px;
  color:#d0d0e4 !important;font-family:'Space Mono',monospace;font-size:10.5px;
  outline:none !important;box-shadow:none !important;-webkit-appearance:none;
  transition:border-color .15s;resize:vertical;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
  -webkit-text-fill-color:#d0d0e4 !important;caret-color:#c8a8ff;
}
.ah-lf-dialog-body select { cursor:pointer;resize:none; }
.ah-lf-dialog-body select option { background:#111115;color:#d0d0e4; }
.ah-lf-dialog-body input:focus,
.ah-lf-dialog-body textarea:focus,
.ah-lf-dialog-body select:focus {
  outline:none !important;box-shadow:none !important;
  border-color:rgba(200,168,255,.35);background:#16161b !important;
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
}
.ah-lf-dialog-body input:-webkit-autofill,
.ah-lf-dialog-body input:-webkit-autofill:focus {
  -webkit-box-shadow:0 0 0 1000px #16161b inset !important;
  -webkit-text-fill-color:#d0d0e4 !important;
}
.ah-lf-dialog-body input::placeholder,
.ah-lf-dialog-body textarea::placeholder { color:rgba(255,255,255,.12); }
.ah-lf-notice { display:flex;align-items:flex-start;gap:7px;padding:9px 11px;background:rgba(255,255,255,.02);border:1px solid rgba(255,255,255,.05);border-radius:6px;margin:12px 0;font-size:9.5px;color:rgba(255,255,255,.3);line-height:1.5; }
.ah-lf-notice svg { flex-shrink:0;margin-top:1px;color:rgba(255,255,255,.2); }
.ah-lf-notice a { color:#c8a8ff;text-decoration:none; }
.ah-lf-notice a:hover { text-decoration:underline; }
.ah-lf-actions { display:flex;gap:8px; }
#ah-lf-submit { flex:1;padding:10px 16px;background:rgba(200,168,255,.1);border:1px solid rgba(200,168,255,.25);border-radius:6px;color:#c8a8ff;font-family:'Space Mono',monospace;font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s; }
#ah-lf-submit:hover:not(:disabled) { background:rgba(200,168,255,.18);border-color:rgba(200,168,255,.5); }
#ah-lf-submit:disabled { opacity:.4;cursor:not-allowed; }
#ah-lf-status { font-size:10px;letter-spacing:.3px; }
#ah-lf-status:not(:empty) { margin-top:10px; }
#ah-lf-status a { text-decoration:none; }
#ah-lf-status a:hover { text-decoration:underline; }
.ah-lf-st-load { color:rgba(255,255,255,.3); }
.ah-lf-st-err  { color:#ff7090; }
.ah-lf-st-ok   { color:#72f0a8; }
.ah-lf-st-ok a { color:#72f0a8; }
`;
    document.head.appendChild(s);
  }

  // ─── Changelog CSS ────────────────────────────────────────────────────────
  function injectChangelogCSS() {
    if (document.getElementById("ah-cl-css")) return;
    const s = document.createElement("style");
    s.id = "ah-cl-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:ital,wght@0,400;0,700;1,400&family=Noto+Sans+JP:wght@400;700&display=swap');

#ah-changelog-modal {
  position:fixed;inset:0;z-index:99999999;
  display:flex;align-items:center;justify-content:center;pointer-events:all;
  font-family:'Space Mono','Noto Sans JP',monospace;
}
.ah-cl-backdrop {
  position:absolute;inset:0;
  background:rgba(0,0,0,.78);
  backdrop-filter:blur(8px);
  cursor:default;
}
.ah-cl-dialog {
  position:relative;z-index:1;
  width:480px;max-width:94vw;
  max-height:85vh;
  display:flex;flex-direction:column;
  background:#0c0c0e;
  border:1px solid rgba(200,168,255,.14);
  border-radius:12px;
  box-shadow:
    0 0 0 1px rgba(200,168,255,.04),
    0 8px 16px rgba(0,0,0,.5),
    0 32px 80px rgba(0,0,0,.95),
    inset 0 1px 0 rgba(200,168,255,.06);
  animation:ah-cl-in .22s cubic-bezier(.34,1.36,.64,1) both;
  overflow:hidden;
}
@keyframes ah-cl-in {
  from { opacity:0; transform:scale(.9) translateY(12px); }
  to   { opacity:1; transform:scale(1) translateY(0); }
}

/* Header */
.ah-cl-header {
  display:flex;align-items:center;justify-content:space-between;
  padding:14px 18px;
  background:#111115;
  border-bottom:1px solid rgba(200,168,255,.1);
  flex-shrink:0;
}
.ah-cl-header-left {
  display:flex;align-items:center;gap:10px;
}
.ah-cl-logo {
  color:#c8a8ff;opacity:.8;flex-shrink:0;
}
.ah-cl-title-block {
  display:flex;flex-direction:column;gap:2px;
}
.ah-cl-title {
  font-size:10.5px;font-weight:700;letter-spacing:3px;text-transform:uppercase;
  font-style:italic;color:#c8a8ff;
  text-shadow:0 0 18px rgba(200,168,255,.35);
}
.ah-cl-subtitle {
  font-size:8px;letter-spacing:2px;text-transform:uppercase;
  color:rgba(200,168,255,.4);
}
.ah-cl-close {
  background:none;border:none;color:rgba(255,255,255,.2);
  font-size:15px;cursor:pointer;padding:3px 7px;border-radius:5px;
  transition:color .15s,background .15s;line-height:1;
}
.ah-cl-close:hover { color:rgba(255,255,255,.7);background:rgba(255,255,255,.05); }

/* Body */
.ah-cl-body {
  flex:1;overflow-y:auto;
  padding:18px 20px;
  display:flex;flex-direction:column;gap:0;
}
.ah-cl-body::-webkit-scrollbar { width:2px; }
.ah-cl-body::-webkit-scrollbar-thumb { background:rgba(200,168,255,.15);border-radius:2px; }

/* Version block */
.ah-cl-version-block {
  padding:14px 0;
  border-bottom:1px solid rgba(255,255,255,.05);
}
.ah-cl-version-block:last-child { border-bottom:none;padding-bottom:4px; }
.ah-cl-version-block--latest { padding-top:0; }

.ah-cl-version-tag {
  display:flex;align-items:center;gap:8px;
  font-size:9px;font-weight:700;letter-spacing:3px;text-transform:uppercase;
  color:rgba(255,255,255,.2);
  margin-bottom:12px;
}
.ah-cl-version-star {
  color:rgba(200,168,255,.5);
  font-size:11px;
}
.ah-cl-version-block--latest .ah-cl-version-tag {
  color:rgba(200,168,255,.6);
}
.ah-cl-version-block--latest .ah-cl-version-star {
  color:#c8a8ff;
  font-size:13px;
  text-shadow:0 0 10px rgba(200,168,255,.7),0 0 22px rgba(200,168,255,.35);
  animation:ah-cl-star-pulse 2.4s ease-in-out infinite;
}
@keyframes ah-cl-star-pulse {
  0%,100% { text-shadow:0 0 10px rgba(200,168,255,.7),0 0 22px rgba(200,168,255,.35); }
  50%      { text-shadow:0 0 16px rgba(200,168,255,1),0 0 36px rgba(200,168,255,.6); }
}
.ah-cl-version-new {
  font-size:7px;font-weight:700;letter-spacing:2px;
  color:#c8a8ff;
  background:rgba(200,168,255,.12);
  border:1px solid rgba(200,168,255,.25);
  padding:1px 6px;border-radius:3px;
  text-shadow:0 0 8px rgba(200,168,255,.5);
}

/* Section labels */
.ah-cl-section-label {
  display:flex;align-items:center;gap:6px;
  font-size:8px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;
  margin:10px 0 6px;padding:4px 8px;
  border-radius:4px;
}
.ah-cl-section-label--fix {
  color:#60c8ff;
  background:rgba(96,200,255,.06);
  border:1px solid rgba(96,200,255,.15);
}
.ah-cl-section-label--add {
  color:#72f0a8;
  background:rgba(114,240,168,.06);
  border:1px solid rgba(114,240,168,.15);
}
.ah-cl-bullet { font-size:9px;opacity:.7; }

/* List items */
.ah-cl-list {
  list-style:none;margin:0;padding:0;
  display:flex;flex-direction:column;gap:5px;
}
.ah-cl-item {
  display:flex;align-items:flex-start;gap:8px;
  font-size:10px;line-height:1.6;
  padding:7px 10px;border-radius:5px;
  color:rgba(232,232,240,.65);
}
.ah-cl-item--fix {
  background:rgba(96,200,255,.04);
  border:1px solid rgba(96,200,255,.1);
}
.ah-cl-item--add {
  background:rgba(114,240,168,.04);
  border:1px solid rgba(114,240,168,.1);
}
.ah-cl-dot {
  flex-shrink:0;margin-top:3px;font-size:6px;opacity:.5;
}
.ah-cl-item--fix .ah-cl-dot { color:#60c8ff; }
.ah-cl-item--add .ah-cl-dot { color:#72f0a8; }
.ah-cl-section-label--rem { color:#ff6b6b; }
.ah-cl-item--rem {
  background:rgba(255,107,107,.04);
  border:1px solid rgba(255,107,107,.1);
}
.ah-cl-item--rem .ah-cl-dot { color:#ff6b6b; opacity:1; }

/* Info section */
.ah-cl-section-label--info {
  color:#ffd700;
  background:rgba(255,215,0,.06);
  border:1px solid rgba(255,215,0,.15);
}
.ah-cl-item--info {
  background:rgba(255,215,0,.03);
  border:1px solid rgba(255,215,0,.1);
  color:rgba(232,232,240,.65);
}
.ah-cl-item--info .ah-cl-dot { color:#ffd700; opacity:.7; }

/* Ko-fi banner in changelog */
.ah-cl-kofi-banner {
  display:flex;align-items:center;gap:10px;
  padding:10px 13px;margin-bottom:16px;
  background:#0c0c0e;
  border:1px solid rgba(255,255,255,.08);
  border-left:3px solid #ff5e5b;
  border-radius:8px;
  box-shadow:0 0 0 1px rgba(255,255,255,.02),0 4px 6px rgba(0,0,0,.3),inset 0 1px 0 rgba(255,255,255,.03);
  text-decoration:none;
  transition:background .15s,border-color .15s,box-shadow .15s;
}
.ah-cl-kofi-banner:hover {
  background:#111115;
  border-color:rgba(255,94,91,.35);
  box-shadow:0 0 0 1px rgba(255,94,91,.06),0 4px 12px rgba(0,0,0,.4);
}
.ah-cl-kofi-icon {
  font-size:18px;line-height:1;flex-shrink:0;
  animation:ah-kofi-pulse 1.35s ease-in-out infinite;
}
.ah-cl-kofi-text {
  display:flex;flex-direction:column;gap:2px;min-width:0;flex:1;
}
.ah-cl-kofi-title {
  font-size:10px;font-weight:700;letter-spacing:.5px;
  color:#ffb7b6;
}
.ah-cl-kofi-sub {
  font-size:8.5px;color:#9898a8;letter-spacing:.3px;line-height:1.4;
}
.ah-cl-kofi-arrow {
  font-size:14px;color:#ff5e5b;flex-shrink:0;opacity:.7;
  transition:opacity .15s,transform .15s;
}
.ah-cl-kofi-banner:hover .ah-cl-kofi-arrow {
  opacity:1;transform:translate(2px,-2px);
}

/* Footer */
.ah-cl-footer {
  padding:12px 20px 14px;
  background:#111115;
  border-top:1px solid rgba(200,168,255,.08);
  flex-shrink:0;
}
.ah-cl-ok-btn {
  display:block;width:100%;
  padding:10px 0;
  background:rgba(200,168,255,.1);
  border:1px solid rgba(200,168,255,.25);
  border-radius:7px;
  color:#c8a8ff;
  font-family:'Space Mono','Noto Sans JP',monospace;
  font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
  cursor:pointer;
  transition:background .15s,border-color .15s,box-shadow .15s;
  text-shadow:0 0 10px rgba(200,168,255,.3);
}
.ah-cl-ok-btn:hover {
  background:rgba(200,168,255,.18);
  border-color:rgba(200,168,255,.45);
  box-shadow:0 0 0 1px rgba(200,168,255,.08),0 4px 16px rgba(200,168,255,.1);
}
`;
    document.head.appendChild(s);
  }

  // ─── Warn popup CSS ───────────────────────────────────────────────────────
  function injectWarnCSS() {
    if (document.getElementById("ah-warn-css")) return;
    const s = document.createElement("style");
    s.id = "ah-warn-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
#ah-warn-modal {
  position:fixed;inset:0;z-index:99999999;
  display:flex;align-items:center;justify-content:center;pointer-events:all;
}
.ah-warn-backdrop { position:absolute;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(3px);cursor:default; }
.ah-warn-dialog {
  position:relative;z-index:1;
  width:fit-content;
  min-width:280px;max-width:min(440px,92vw);
  background:#0f0a0a;border:1px solid rgba(255,102,128,.25);border-radius:10px;
  box-shadow:0 0 0 1px rgba(255,102,128,.08),0 8px 24px rgba(0,0,0,.6),0 24px 64px rgba(0,0,0,.9),inset 0 1px 0 rgba(255,102,128,.08);
  font-family:'Space Mono',monospace;padding:22px 22px 18px;
  animation:ah-warn-in .18s cubic-bezier(.34,1.4,.64,1) both;
}
@keyframes ah-warn-in { from{opacity:0;transform:scale(.88) translateY(8px)} to{opacity:1;transform:scale(1) translateY(0)} }
.ah-warn-icon-row { display:flex;align-items:center;gap:10px;margin-bottom:14px; }
.ah-warn-title { font-size:11px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:#ff8898; }
.ah-warn-body { display:flex;flex-direction:column;gap:9px;margin-bottom:18px; }
.ah-warn-item { font-size:10.5px;color:rgba(255,255,255,.55);line-height:1.6;padding:9px 11px;background:rgba(255,102,128,.05);border:1px solid rgba(255,102,128,.12);border-radius:6px; }
.ah-warn-item strong { color:#ff8898;font-weight:700; }
.ah-warn-item code { font-family:'Space Mono',monospace;font-size:9.5px;color:#c8a8ff;background:rgba(200,168,255,.12);padding:1px 5px;border-radius:3px; }
.ah-warn-btn { display:block;width:100%;padding:10px 0;background:rgba(255,102,128,.1);border:1px solid rgba(255,102,128,.3);border-radius:6px;color:#ff8898;font-family:'Space Mono',monospace;font-size:9px;font-weight:700;letter-spacing:2px;text-transform:uppercase;cursor:pointer;transition:background .15s,border-color .15s; }
.ah-warn-btn:hover { background:rgba(255,102,128,.18);border-color:rgba(255,102,128,.55); }
`;
    document.head.appendChild(s);
  }

  // ─── Import popup CSS ─────────────────────────────────────────────────────
  function injectImportCSS() {
    if (document.getElementById("ah-import-css")) return;
    const s = document.createElement("style");
    s.id = "ah-import-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
#ah-import-modal {
  position:fixed;inset:0;z-index:99999999;
  display:flex;align-items:center;justify-content:center;pointer-events:all;
}
.ah-import-backdrop { position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(6px);cursor:default; }
.ah-import-dialog {
  position:relative;z-index:1;width:360px;max-width:92vw;
  background:#0c0c0e;border:1px solid rgba(255,255,255,.08);border-radius:12px;
  box-shadow:0 0 0 1px rgba(255,255,255,.02),0 8px 16px rgba(0,0,0,.5),0 32px 80px rgba(0,0,0,.9);
  font-family:'Space Mono',monospace;color:#c0c0d0;font-size:11px;
  animation:ah-import-in .18s cubic-bezier(.34,1.4,.64,1) both;
}
@keyframes ah-import-in { from{opacity:0;transform:scale(.9) translateY(8px)} to{opacity:1;transform:scale(1) translateY(0)} }
.ah-import-header { display:flex;align-items:center;justify-content:space-between;padding:13px 16px;background:#111115;border-bottom:1px solid rgba(255,255,255,.07);border-radius:12px 12px 0 0; }
.ah-import-title { display:flex;align-items:center;gap:7px;font-size:9.5px;font-weight:700;letter-spacing:3.5px;text-transform:uppercase;color:#c8a8ff; }
.ah-import-close { background:none;border:none;color:rgba(255,255,255,.25);font-size:15px;cursor:pointer;padding:2px 6px;border-radius:4px;transition:color .15s;line-height:1; }
.ah-import-close:hover { color:rgba(255,255,255,.75); }
.ah-import-body { padding:18px; }
.ah-import-drop { display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:32px 20px;border:1.5px dashed rgba(255,255,255,.1);border-radius:8px;background:rgba(255,255,255,.02);cursor:pointer;transition:border-color .15s,background .15s;color:rgba(255,255,255,.25); }
.ah-import-drop:hover,.ah-import-drop--over { border-color:rgba(200,168,255,.4);background:rgba(200,168,255,.04);color:rgba(200,168,255,.6); }
.ah-import-drop svg { opacity:.5;transition:opacity .15s; }
.ah-import-drop:hover svg,.ah-import-drop--over svg { opacity:1; }
.ah-import-drop-label { font-size:10.5px;font-weight:700;letter-spacing:1px; }
.ah-import-drop-sub   { font-size:8.5px;letter-spacing:.5px;opacity:.5; }
.ah-import-st { display:block;margin-top:12px;font-size:9.5px;text-align:center;letter-spacing:.5px;min-height:16px; }
.ah-import-st--err { color:#ff7090; }
.ah-import-st--ok  { color:#72f0a8; }
`;
    document.head.appendChild(s);
  }

  // ─── Confirm popup CSS ────────────────────────────────────────────────────
  function injectConfirmCSS() {
    if (document.getElementById("ah-confirm-css")) return;
    const s = document.createElement("style");
    s.id = "ah-confirm-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
#ah-confirm-modal {
  position:fixed;inset:0;z-index:99999999;
  display:flex;align-items:center;justify-content:center;pointer-events:all;
}
.ah-confirm-backdrop { position:absolute;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(3px);cursor:default; }
.ah-confirm-dialog {
  position:relative;z-index:1;
  min-width:280px;max-width:min(400px,92vw);
  background:#0f0a0a;border:1px solid rgba(255,102,128,.25);border-radius:10px;
  box-shadow:0 0 0 1px rgba(255,102,128,.08),0 8px 24px rgba(0,0,0,.6),0 24px 64px rgba(0,0,0,.9);
  font-family:'Space Mono',monospace;padding:22px 22px 18px;
  animation:ah-confirm-in .18s cubic-bezier(.34,1.4,.64,1) both;
}
@keyframes ah-confirm-in { from{opacity:0;transform:scale(.88) translateY(8px)} to{opacity:1;transform:scale(1) translateY(0)} }
.ah-confirm-icon-row { display:flex;align-items:center;gap:10px;margin-bottom:14px; }
.ah-confirm-title { font-size:11px;font-weight:700;letter-spacing:2.5px;text-transform:uppercase;color:#ff8898; }
.ah-confirm-body { font-size:10.5px;color:rgba(255,255,255,.5);line-height:1.6;padding:10px 12px;background:rgba(255,102,128,.04);border:1px solid rgba(255,102,128,.1);border-radius:6px;margin-bottom:18px; }
.ah-confirm-actions { display:flex;gap:8px; }
.ah-confirm-cancel { flex:1;padding:9px 0;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;color:rgba(255,255,255,.4);font-family:'Space Mono',monospace;font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s; }
.ah-confirm-cancel:hover { background:rgba(255,255,255,.08);color:rgba(255,255,255,.7); }
.ah-confirm-proceed { flex:1;padding:9px 0;background:rgba(255,102,128,.12);border:1px solid rgba(255,102,128,.3);border-radius:6px;color:#ff8898;font-family:'Space Mono',monospace;font-size:9px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s; }
.ah-confirm-proceed:hover { background:rgba(255,102,128,.22);border-color:rgba(255,102,128,.6); }
`;
    document.head.appendChild(s);
  }

  // ─── Boot ─────────────────────────────────────────────────────────────────
  function boot() {
    const adapter = getAdapter();
    if (!adapter) return;
    if (!adapter.isItemPage()) return;
    injectUI();
    maybeShowChangelog();
  }

  setTimeout(boot, 1200);

  let _lastHref = location.href;
  const _observer = new MutationObserver(() => {
    if (location.href !== _lastHref) {
      _lastHref = location.href;
      document.getElementById("ah-panel")?.remove();
      document.getElementById("ah-kofi-card")?.remove();
      _recheckFn = null;
      setTimeout(boot, 1500);
    }
  });
  _observer.observe(document.body, { childList: true, subtree: true });

})();