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.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Asset Hunter
// @namespace    https://github.com/xedinho/Asset-Hunter
// @version      6.5.1
// @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/*
// @match        *://itch.io/*
// @match        *://*.itch.io/*
// @match        *://builtbybit.com/*
// @match        *://*.builtbybit.com/*
// @match        *://assetstore.unity.com/*
// @match        *://etsy.com/*
// @match        *://*.etsy.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.5.1";

  const CHANGELOGS = [
    {
      version: "6.5.1",
      additions: [
        "changelog.add.chineseLang",
      ],
    },
    {
      version: "6.5.0",
      additions: [
        "changelog.add.postImage",
        "changelog.add.builtbybit",
        "changelog.add.unityAssetStore",
        "changelog.add.etsy",
        "changelog.add.watchlistIgnore",
        "changelog.add.watchlistWait",
        "changelog.add.dailyAutoCheck",
      ],
      fixes: [
        "changelog.fix.watchlistCheckReliability",
      ],
    },
    {
      version: "6.4.1",
      fixes: [
        "changelog.fix.frenchTranslations",
      ],
    },
    {
      version: "6.4.0",
      additions: [
        "changelog.add.frenchLang",
        "changelog.add.itchio",
        "changelog.add.siteFilter",
        "changelog.add.startMinimized",
        "changelog.add.urlSearchMode",
        "changelog.add.minimizeHotkey",
        "changelog.add.matchWordsToggle",
      ],
      tips: [
        "changelog.tip.searchWorkaround",
      ],
    },
    {
      version: "6.3.0",
      additions: [
        "changelog.add.bumpTopicPopup",
        "changelog.add.kofiRandomPopup",
      ],
      fixes: [
        "changelog.fix.kofiThreshold",
      ],
    },
    {
      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={matchWords}&by=&categories=&searchChildren=false&hasTags=&replies=&repliesFilter=atleast&timeFilter=newer&timeRange=&sortBy=relevance&sortDirection=desc&showAs=topics";
  // A plain, language-independent marker so callers can tell "we got rate limited"
  // apart from other errors, without depending on a translated string's exact text.
  const RATE_LIMITED_ERR = "RATE_LIMITED";
  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 POST_VOTE_API  = "https://forum.ripper.store/api/v3/posts/{pid}/vote";
  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";
  const AH_TOPIC_TID   = 108432; // Main Asset Hunter topic, upvoted via the update prompt

  // ─── 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\./, "");

  // ─── Page image helper ────────────────────────────────────────────────────
  // Best-effort "main image" for the current item page, used to attach a
  // preview to LF posts so they don't rely solely on the forum's link embed.
  function getPageImage() {
    const og = document.querySelector('meta[property="og:image"]') ||
               document.querySelector('meta[name="og:image"]');
    if (og && og.getAttribute("content")) return og.getAttribute("content");
    const tw = document.querySelector('meta[name="twitter:image"]') ||
               document.querySelector('meta[property="twitter:image"]');
    if (tw && tw.getAttribute("content")) return tw.getAttribute("content");
    return "";
  }

  // ─── 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),
    getImage: getPageImage,
  };

  // ─── 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),
    getImage: getPageImage,
  };

  // ─── 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;
    },
    getImage: getPageImage,
  };

  // ─── 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),
    getImage: getPageImage,
  };

  // ─── itch.io Adapter ──────────────────────────────────────────────────────
  const ITCH = {
    getId: () => {
      const m = window.location.pathname.match(/^\/([^/?#]+)/);
      return m ? m[1] : "";
    },
    getName: () => {
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").replace(/\s+by\s+.+?\s+on\s+itch\.io\s*$/i, "").trim();
      }
      const h1 = document.querySelector("h1.game_title") || document.querySelector(".game_title h1") || document.querySelector("h1");
      if (h1) return h1.textContent.trim();
      return document.title.replace(/\s*[|]\s*itch\.io.*$/i, "").replace(/\s+on itch\.io.*$/i, "").trim();
    },
    buildQuery: (id, name) => name || id,
    isItemPage: () => {
      if (!HOST.endsWith(".itch.io")) return false;
      const m = window.location.pathname.match(/^\/([^/?#]+)/);
      if (!m || !m[1]) return false;
      const skip = ["login", "register", "profile", "dashboard", "search", "tags", "game", "games",
                    "updates", "feed", "purchases", "bundles", "download", "devlog", "community"];
      return !skip.includes(m[1]);
    },
    getImage: getPageImage,
  };

  // ─── BuiltByBit Adapter ───────────────────────────────────────────────────
  // Resource pages look like builtbybit.com/resources/<slug>.<id>/
  const BUILTBYBIT = {
    getId: () => {
      const m = window.location.pathname.match(/\/resources\/[^/]*\.(\d+)\/?/);
      return m ? m[1] : "";
    },
    getName: () => {
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").replace(/\s*[|]\s*BuiltByBit.*$/i, "").trim();
      }
      const h1 = document.querySelector("h1");
      if (h1) return h1.textContent.trim();
      return document.title.replace(/\s*[|]\s*BuiltByBit.*$/i, "").trim();
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => /\/resources\/[^/]*\.\d+\/?/.test(window.location.pathname),
    getImage: getPageImage,
  };

  // ─── Unity Asset Store Adapter ────────────────────────────────────────────
  // Package pages look like assetstore.unity.com/packages/<cat>/.../<slug>-<id>
  const UNITY = {
    getId: () => {
      const m = window.location.pathname.match(/\/packages\/.+-(\d+)\/?$/);
      return m ? m[1] : "";
    },
    getName: () => {
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").replace(/\s*[|]\s*Unity Asset Store.*$/i, "").trim();
      }
      const h1 = document.querySelector("h1");
      if (h1) return h1.textContent.trim();
      return document.title.replace(/\s*[|]\s*Unity Asset Store.*$/i, "").trim();
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => /\/packages\/.+-\d+\/?$/.test(window.location.pathname),
    getImage: getPageImage,
  };

  // ─── Etsy Adapter ─────────────────────────────────────────────────────────
  // Listing pages look like etsy.com/listing/<id>/<slug>
  const ETSY = {
    getId: () => {
      const m = window.location.pathname.match(/\/listing\/(\d+)/);
      return m ? m[1] : "";
    },
    getName: () => {
      const og = document.querySelector('meta[property="og:title"]');
      if (og && og.getAttribute("content")) {
        return og.getAttribute("content").replace(/\s*[|]\s*Etsy.*$/i, "").trim();
      }
      const h1 = document.querySelector("h1");
      if (h1) return h1.textContent.trim();
      return document.title.replace(/\s*[|]\s*Etsy.*$/i, "").trim();
    },
    buildQuery: (id, name) => id || name,
    isItemPage: () => /\/listing\/\d+/.test(window.location.pathname),
    getImage: getPageImage,
  };

  // ─── Site key helpers ─────────────────────────────────────────────────────
  const SITE_DEFS = [
    { key: "booth",       label: "Booth" },
    { key: "gumroad",     label: "Gumroad" },
    { key: "jinxxy",      label: "Jinxxy" },
    { key: "payhip",      label: "Payhip" },
    { key: "itch",        label: "itch.io" },
    { key: "builtbybit",  label: "BuiltByBit" },
    { key: "unity",       label: "Unity Asset Store" },
    { key: "etsy",        label: "Etsy" },
  ];

  function getCurrentSiteKey() {
    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";
    if (HOST === "itch.io"    || HOST.endsWith(".itch.io"))    return "itch";
    if (HOST === "builtbybit.com" || HOST.endsWith(".builtbybit.com")) return "builtbybit";
    if (HOST === "assetstore.unity.com") return "unity";
    if (HOST === "etsy.com"   || HOST.endsWith(".etsy.com"))   return "etsy";
    return null;
  }

  function isSiteEnabled(siteKey) {
    const key = siteKey != null ? siteKey : getCurrentSiteKey();
    if (!key) return true;
    return GM_getValue("ah-site-enabled-" + key, "1") !== "0";
  }

  // ─── 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;
    if (HOST === "itch.io" || HOST.endsWith(".itch.io")) return ITCH;
    if (HOST === "builtbybit.com" || HOST.endsWith(".builtbybit.com")) return BUILTBYBIT;
    if (HOST === "assetstore.unity.com") return UNITY;
    if (HOST === "etsy.com" || HOST.endsWith(".etsy.com")) return ETSY;
    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{img}\n{url}\n\nPlease share if you have this item! 🙏",
    defaultTags:       "looking-for, lf, unsolved, asset-hunter",
    autoWatch:         true,
    autoSearch:        true,
    bumpMessage:       "bump",
    startMinimized:    false,
    defaultSearchMode: "name",
    minimizeHotkey:    "",
  };
  // The body template default before {img} existed - used to detect and
  // migrate old saved templates (see getSetting below).
  const OLD_DEFAULT_BODY_TPL = "Looking for: **{name}**\n\n{url}\n\nPlease share if you have this item! 🙏";
  // ─── 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) {
    let val = GM_getValue("ah-cfg-" + key, null);
    if (val === null) return DEFAULTS[key];
    if (key === "autoWatch" || key === "autoSearch" || key === "startMinimized") return val === "1";

    if (key === "bodyTpl" && !val.includes("{img}")) {
      // Saved before {img} existed - drop it back in exactly where the script
      // used to force-place the image automatically: one line above {url} for
      // an untouched default template, or at the bottom for a customized one.
      val = (val === OLD_DEFAULT_BODY_TPL)
        ? DEFAULTS.bodyTpl
        : val.replace(/\s+$/, "") + "\n\n{img}";
      GM_setValue("ah-cfg-" + key, val);
    }

    return val;
  }
  function setSetting(key, val) {
    if (key === "autoWatch" || key === "autoSearch" || key === "startMinimized") {
      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, imageUrl) {
    const imgLine = imageUrl ? `![preview](${imageUrl})` : "";
    const rawTpl  = getSetting("bodyTpl");

    let tpl;
    if (imgLine) {
      tpl = rawTpl.replace(/\{img\}/g, imgLine);
    } else {
      // No image available for this item - drop {img}. If it was alone on its
      // own line (as recommended), drop that whole line so there's no gap left behind.
      tpl = rawTpl
        .split("\n")
        .filter(line => line.trim() !== "{img}")
        .join("\n")
        .replace(/\{img\}/g, "");
    }

    const body = tpl
      .replace(/\{name\}/g, name)
      .replace(/\{url\}/g, url);

    return body + WATERMARK;
  }

  // Checks that a body template has both {url} and {img}, each alone on its
  // own line - {url} so Ripper.Store generates a link preview, {img} so the
  // image renders as an actual image rather than sitting inline with text.
  // Returns an array of translated warning strings (empty if all good).
  function validateBodyTpl(val) {
    const errors = [];
    const lines = val.split("\n").map(l => l.trim());

    if (!val.includes("{url}")) errors.push(t("warnMissingUrl"));
    else if (!lines.includes("{url}")) errors.push(t("warnBadUrl"));

    if (!val.includes("{img}")) errors.push(t("warnMissingImg"));
    else if (!lines.includes("{img}")) errors.push(t("warnBadImg"));

    return errors;
  }

  // ─── Localisation ─────────────────────────────────────────────────────────
  const STRINGS = {
    en: {
      title: "ASSET HUNTER", minimize: "Minimize", expand: "Expand", unknown: "Unknown", search: "Search",
      placeholder: "Search query...", noResult: "No results",
      hits: " hits", solved: "Solved", unsolved: "Open", untitled: "Untitled",
      dlFound: "Download Found", secDiscussions: "Discussions", secOther: "Other",
      errParse: "Failed to parse response", errNetwork: "Network error", errTimeout: "Request timed out",
      errRateLimit: "Rate limited by the forum - please wait a moment and try again",
      errPostFailed: "HTTP {status}: Post failed. Make sure you are logged in.",
      errReplyFailed: "HTTP {status}: Reply failed. Make sure you are logged in to forum.ripper.store.",
      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.",
      openRipper: "Open 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, <code>{img}</code> for the preview image",
      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.",
      warnMissingUrl: "<strong>Body template</strong> is missing <code>{url}</code> - add it so people know where to find the item.",
      warnMissingImg: "<strong>Body template</strong> is missing <code>{img}</code> - add it so your posts include a preview image.",
      warnBadImg: "<strong>Body template</strong> has <code>{img}</code> on a line with other text - put <code>{img}</code> alone on its own line so the image renders properly.",
      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",
      bumpProgressRunning: "{done}/{total} bumped{failed} · ~{secs}s left",
      bumpProgressFailed: " ({n} failed)",
      bumpProgressDone: "Done - {bumped} bumped{failed}",
      bumpProgressDoneFailed: ", {n} failed",
      warnModalTitle: "Check your templates", warnModalOk: "Understood",
      changelogBadgeNew: "NEW",
      // Changelog
      changelogBtn: "Changelogs",
      changelogTitle: "Asset Hunter updated!",
      changelogSubtitle: "here are the changes:",
      changelogSectionFixes: "Fixes",
      changelogSectionAdditions: "Additions",
      changelogSectionRemovals: "Removals",
      changelogSectionInfo: "Info",
      changelogSectionTips: "Tip",
      changelogClose: "Got it",
      changelogKofiTitle: "Enjoying Asset Hunter?",
      changelogKofiSub: "Donate on Ko-fi to support updates and suggest new features",
      // Bump topic popup
      bumpTopicTitle: "Consider upvoting the main post!",
      bumpTopicBody: "It helps more people find Asset Hunter.",
      bumpTopicSub: "Clicking \"Sure!\" will upvote it automatically.",
      bumpTopicYes: "Sure!",
      bumpTopicNo: "No thanks",
      bumpTopicSending: "Upvoting...",
      bumpTopicDone: "Upvoted! Thanks!",
      bumpTopicErr: "Failed to upvote",
      // Kofi random popup
      kofiPopupTitle: "Enjoying Asset Hunter?",
      kofiPopupBody: "Asset Hunter is free and takes real effort to keep updated. If it's been useful to you, consider buying me a coffee, it means a lot and helps keep the tool alive!",
      kofiPopupBtn: "&#9749; Support on Ko-fi",
      kofiPopupDismiss: "Maybe later",
      // New settings strings
      labelStartMinimized: "Start Minimized", hintStartMinimized: "Open the panel minimized by default",
      secActiveSites: "Active Platforms", hintActiveSites: "Uncheck to disable the script entirely on that platform",
      labelDefaultSearchMode: "Default Search Mode", hintDefaultSearchMode: "Whether searches use the detected name or the full page URL by default",
      searchModeToggle_name: "Mode: Name", searchModeToggle_url: "Mode: URL",
      matchModeToggle_any: "Match: Any", matchModeToggle_all: "Match: All",
      labelMinimizeHotkey: "Minimize Hotkey", hintMinimizeHotkey: "Key combo to toggle the panel open and closed. Example: Alt+H",
      hotkeyPlaceholder: "e.g. Alt+H",
      // Watchlist ignore / wait-for-new
      wlBadgeIgnored: "Ignored", wlBadgeWaiting: "Waiting for New",
      wlIgnoreBtn: "Ignore Thread", wlWaitBtn: "Wait for New", wlUnignoreBtn: "Stop Ignoring",
      secIgnored: "Ignored",
      // Changelog entries - 6.5.1
      "changelog.add.chineseLang": "Added Simplified Chinese as a language option.",
      // Changelog entries - 6.5.0
      "changelog.add.postImage": "Added a new <code>{img}</code> placeholder for the body template, right alongside <code>{name}</code> and <code>{url}</code> - it's replaced with the asset's main image (pulled from the item page) wherever you put it, so a preview still shows up even when the forum's link embed fails to load. It's required just like <code>{url}</code>: if either is missing from a custom template, or not on its own line, you'll get a warning when you save.",
      "changelog.add.builtbybit": "Added BuiltByBit support.",
      "changelog.add.unityAssetStore": "Added Unity Asset Store support.",
      "changelog.add.etsy": "Added Etsy support.",
      "changelog.add.watchlistIgnore": "Added an \"Ignore Thread\" button to DL Found watchlist items, for when the matched thread is a mega-thread or otherwise not actually about your item. Ignoring only blacklists that specific thread for that item - it keeps searching normally and will still flag a genuine match on a different thread. If the ignored thread is the only thing that matches, the item moves to its own muted section at the bottom, with a Stop Ignoring button to bring it back.",
      "changelog.add.watchlistWait": "Added a \"Wait for New\" button to DL Found watchlist items, for when the only downloads currently posted don't apply to you. The item keeps watching that same thread and only flags again once something new is actually posted.",
      "changelog.add.dailyAutoCheck": "The watchlist now automatically re-checks itself once a day, the first time you load the script that day, no need to remember to hit Re-check.",
      "changelog.fix.watchlistCheckReliability": "Cleaned up the watchlist checking logic and fixed a bug where the delay between checks could grow unnecessarily on larger watchlists.",
      // Changelog entries - 6.4.1
      "changelog.fix.frenchTranslations": "Fixed minor French translation errors.",
      // Changelog entries - 6.4.0
      "changelog.add.frenchLang": "Added French language support.",
      "changelog.add.itchio": "Added itch.io support.",
      "changelog.add.siteFilter": "Added per-platform toggles in settings to completely disable the script on specific sites.",
      "changelog.add.startMinimized": "Added a setting to start the panel minimized by default. The Ko-fi banner also hides when minimized.",
      "changelog.add.urlSearchMode": "Added a URL search mode button to search by the full page URL instead of the detected name. A default mode setting is available in Settings.",
      "changelog.add.minimizeHotkey": "Added a customizable hotkey to toggle the panel open and closed.",
      "changelog.add.matchWordsToggle": "Added a Match: Any / Match: All toggle next to the search mode button. Flip to All to require every word in your query to appear in a post, which cuts out a lot of unrelated results.",
      "changelog.tip.searchWorkaround": "Getting a flood of random results? Two small buttons above the search bar can help. Switch to Match: All to require every word to show up in a post, or flip to Mode: URL to search by the full page address instead of the product name. These are workarounds for a fundamental limitation of Ripper.Store's built-in keyword search - there is no perfect fix, but they usually clean things up a lot.",
      // Changelog entries - 6.3.0
      "changelog.add.bumpTopicPopup": "Added a bump prompt that appears after the changelogs.",
      "changelog.add.kofiRandomPopup": "Added a small donation reminder popup with a 1 in 10 chance of appearing on page load.",
      "changelog.fix.kofiThreshold": "The \"don't show again\" checkbox on the Ko-fi banner now appears after 2 closures instead of 5",
      // 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: "最小化", expand: "展開", unknown: "不明", search: "検索",
      placeholder: "検索ワード...", noResult: "結果なし",
      hits: "件ヒット", solved: "解決済", unsolved: "未解決", untitled: "無題",
      dlFound: "ダウンロード発見", secDiscussions: "ディスカッション", secOther: "その他",
      errParse: "解析失敗", errNetwork: "通信エラー", errTimeout: "タイムアウト",
      errRateLimit: "フォーラム側のレート制限にかかりました。しばらく待ってから再試行してください",
      errPostFailed: "HTTP {status}: 投稿に失敗しました。ログインしているか確認してください。",
      errReplyFailed: "HTTP {status}: 返信に失敗しました。forum.ripper.storeにログインしているか確認してください。",
      lfBtn: "LFリクエスト投稿", lfTitle: "LFリクエスト", lfNotFound: "Ripperで見つかりません。リクエストしますか?",
      lfPost: "投稿する", lfPosting: "投稿中...",
      lfSuccess: "投稿成功!", lfViewPost: "投稿を見る ->", lfLoginWarn: "forum.ripper.store にログインが必要です。",
      openRipper: "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> でリンク、<code>{img}</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> は単独行に配置してください。",
      warnMissingUrl: "<strong>本文テンプレート</strong>に <code>{url}</code> がありません。アイテムの場所がわかるよう追加してください。",
      warnMissingImg: "<strong>本文テンプレート</strong>に <code>{img}</code> がありません。投稿にプレビュー画像が含まれるよう追加してください。",
      warnBadImg: "<strong>本文テンプレート</strong>の <code>{img}</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: "バンプボタンを押したときに投稿されるテキスト",
      bumpProgressRunning: "{done}/{total} バンプ済{failed} · 残り約{secs}秒",
      bumpProgressFailed: " ({n}件失敗)",
      bumpProgressDone: "完了 - {bumped}件バンプ済{failed}",
      bumpProgressDoneFailed: "、{n}件失敗",
      warnModalTitle: "テンプレートを確認してください", warnModalOk: "了解しました",
      changelogBadgeNew: "NEW",
      // Changelog
      changelogBtn: "更新履歴",
      changelogTitle: "Asset Hunter が更新されました!",
      changelogSubtitle: "変更内容はこちら:",
      changelogSectionFixes: "修正",
      changelogSectionAdditions: "追加",
      changelogSectionRemovals: "削除",
      changelogSectionInfo: "情報",
      changelogSectionTips: "ヒント",
      changelogClose: "了解",
      changelogKofiTitle: "Asset Hunterを楽しんでいますか?",
      changelogKofiSub: "Ko-fiで支援して、更新や新機能のリクエストを後押ししましょう",
      // Bump topic popup
      bumpTopicTitle: "メイン投稿へのいいねをご検討ください!",
      bumpTopicBody: "Asset Hunterがもっと見つけてもらいやすくなります。",
      bumpTopicSub: "「もちろん!」をクリックすると自動でいいねされます。",
      bumpTopicYes: "もちろん!",
      bumpTopicNo: "いいえ、結構です",
      bumpTopicSending: "いいね中...",
      bumpTopicDone: "いいねしました!ありがとう!",
      bumpTopicErr: "いいねに失敗しました",
      // Kofi random popup
      kofiPopupTitle: "Asset Hunterを楽しんでいますか?",
      kofiPopupBody: "Asset Hunterは無料ですが、維持するには本当の努力が必要です。役に立っていると感じたら、コーヒー一杯の支援をご検討ください!",
      kofiPopupBtn: "&#9749; Ko-fiでサポート",
      kofiPopupDismiss: "また今度",
      // 設定の新しい文字列
      labelStartMinimized: "最初から最小化", hintStartMinimized: "デフォルトでパネルを最小化して開く",
      secActiveSites: "対応プラットフォーム", hintActiveSites: "チェックを外すと特定のサイトでスクリプトを無効化",
      labelDefaultSearchMode: "デフォルト検索モード", hintDefaultSearchMode: "デフォルトで名前またはURLで検索するかを選択",
      searchModeToggle_name: "モード:名前", searchModeToggle_url: "モード:URL",
      matchModeToggle_any: "一致:いずれか", matchModeToggle_all: "一致:すべて",
      labelMinimizeHotkey: "最小化ホットキー", hintMinimizeHotkey: "パネルの開閉に使うキー。例:Alt+H",
      hotkeyPlaceholder: "例:Alt+H",
      // Watchlist ignore / wait-for-new
      wlBadgeIgnored: "無視中", wlBadgeWaiting: "新着待ち",
      wlIgnoreBtn: "スレッドを無視", wlWaitBtn: "新着を待つ", wlUnignoreBtn: "無視を解除",
      secIgnored: "無視済み",
      // Changelog entries - 6.5.1
      "changelog.add.chineseLang": "簡体字中国語を言語オプションとして追加しました。",
      // Changelog entries - 6.5.0
      "changelog.add.postImage": "本文テンプレートに、<code>{name}</code> や <code>{url}</code> と同じように使える新しいプレースホルダー <code>{img}</code> を追加しました。アイテムページから取得したメイン画像に置き換えられ、テンプレート内のどこに置いても反映されるので、フォーラムのリンク埋め込みが読み込まれない場合でもプレビューが表示されます。<code>{url}</code> と同様に必須項目で、カスタムテンプレートでどちらかが欠けていたり単独行になっていない場合、保存時に警告が表示されます。",
      "changelog.add.builtbybit": "BuiltByBitのサポートを追加しました。",
      "changelog.add.unityAssetStore": "Unity Asset Storeのサポートを追加しました。",
      "changelog.add.etsy": "Etsyのサポートを追加しました。",
      "changelog.add.watchlistIgnore": "ウォッチリストの「DL Found」アイテムに「スレッドを無視」ボタンを追加しました。マッチしたスレッドがメガスレッドなどで実際には関係ない場合に使えます。無視はそのアイテムに対してその特定のスレッドのみをブラックリストに入れるもので、検索自体は通常どおり続行され、別のスレッドで本当にマッチした場合はきちんとフラグが立ちます。無視したスレッドしかマッチしない場合のみ、アイテムは一番下の専用セクションに移動し、いつでも「無視を解除」ボタンで元に戻せます。",
      "changelog.add.watchlistWait": "ウォッチリストの「DL Found」アイテムに「新着を待つ」ボタンを追加しました。現在投稿されているダウンロードが自分の状況に合わない場合に使えます。同じスレッドの監視を続け、実際に新しい投稿があった場合のみ再度フラグが立ちます。",
      "changelog.add.dailyAutoCheck": "ウォッチリストがその日最初にスクリプトを読み込んだ際に、1日1回自動的に再チェックされるようになりました。再チェックボタンを押し忘れる心配がありません。",
      "changelog.fix.watchlistCheckReliability": "ウォッチリストのチェック処理を整理し、大きなウォッチリストでチェック間隔が不必要に長くなってしまうバグを修正しました。",
      // Changelog entries - 6.4.1
      "changelog.fix.frenchTranslations": "フランス語の翻訳の一部を修正しました。",
      // Changelog entries - 6.4.0
      "changelog.add.frenchLang": "フランス語サポートを追加しました。",
      "changelog.add.itchio": "itch.ioのサポートを追加しました。",
      "changelog.add.siteFilter": "設定に各プラットフォームの有効・無効の切り替えを追加しました。",
      "changelog.add.startMinimized": "デフォルトでパネルを最小化して開始する設定を追加しました。最小化時はKo-fiバナーも非表示になります。",
      "changelog.add.urlSearchMode": "名前ではなくURLで検索するURLモードボタンを追加しました。デフォルトモードの設定も利用可能です。",
      "changelog.add.minimizeHotkey": "パネルの開閉に使えるカスタムホットキーを追加しました。",
      "changelog.add.matchWordsToggle": "検索モードボタンの横に「一致:いずれか / 一致:すべて」トグルを追加しました。「すべて」に切り替えると、クエリのすべての単語が投稿に含まれていないと表示されません。",
      "changelog.tip.searchWorkaround": "関係のない結果が大量に表示される場合、検索バー上の2つのボタンが役立ちます。「一致:すべて」に切り替えるとすべての単語を含む投稿だけが表示され、「モード:URL」に切り替えると製品名の代わりにページのURLで検索します。これはRipper.Storeの検索エンジンの根本的な制限に対する回避策です。完璧な解決策はありませんが、通常は大幅に改善されます。",
      // Changelog entries - 6.3.0
      "changelog.add.bumpTopicPopup": "更新履歴の後に表示されるバンプ促進ポップアップを追加しました。",
      "changelog.add.kofiRandomPopup": "ページ読み込み時に10分の1の確率で表示される寄付リマインダーポップアップを追加しました。",
      "changelog.fix.kofiThreshold": "Ko-fiバナーの「今後は表示しない」チェックボックスが5回の閉鎖後ではなく2回後に表示されるようになりました",
      // 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: "Свернуть", expand: "Развернуть", unknown: "Неизвестно", search: "Поиск",
      placeholder: "Поисковый запрос...", noResult: "Нет результатов",
      hits: " совпадений", solved: "Решено", unsolved: "Открыто", untitled: "Без названия",
      dlFound: "Загрузка найдена", secDiscussions: "Обсуждения", secOther: "Другое",
      errParse: "Ошибка разбора ответа", errNetwork: "Ошибка сети", errTimeout: "Время запроса истекло",
      errRateLimit: "Форум ограничил частоту запросов - подождите немного и попробуйте снова",
      errPostFailed: "HTTP {status}: Не удалось опубликовать. Убедитесь, что вы авторизованы.",
      errReplyFailed: "HTTP {status}: Ответ не отправлен. Убедитесь, что вы авторизованы на forum.ripper.store.",
      lfBtn: "Создать LF запрос", lfTitle: "LF Запрос", lfNotFound: "Не найдено на Ripper. Хотите запросить?",
      lfPost: "Опубликовать", lfPosting: "Публикация...",
      lfSuccess: "Опубликовано!", lfViewPost: "Открыть пост ->", lfLoginWarn: "Необходимо войти на forum.ripper.store.",
      openRipper: "Открыть 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> для ссылки, <code>{img}</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> должен быть на отдельной строке.",
      warnMissingUrl: "<strong>Шаблон текста</strong>: отсутствует <code>{url}</code> — добавьте его, чтобы люди могли найти предмет.",
      warnMissingImg: "<strong>Шаблон текста</strong>: отсутствует <code>{img}</code> — добавьте его, чтобы в посте была превью-картинка.",
      warnBadImg: "<strong>Шаблон текста</strong>: <code>{img}</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: "Текст, который публикуется при нажатии кнопки Бамп",
      bumpProgressRunning: "{done}/{total} забампено{failed} · ~{secs}с осталось",
      bumpProgressFailed: " ({n} не удалось)",
      bumpProgressDone: "Готово - {bumped} забампено{failed}",
      bumpProgressDoneFailed: ", {n} не удалось",
      warnModalTitle: "Проверьте шаблоны", warnModalOk: "Понятно",
      changelogBadgeNew: "NEW",
      // Changelog
      changelogBtn: "Список изменений",
      changelogTitle: "Asset Hunter обновлён!",
      changelogSubtitle: "вот что изменилось:",
      changelogSectionFixes: "Исправления",
      changelogSectionAdditions: "Добавлено",
      changelogSectionRemovals: "Удалено",
      changelogSectionInfo: "Информация",
      changelogSectionTips: "Совет",
      changelogClose: "Понял",
      changelogKofiTitle: "Нравится Asset Hunter?",
      changelogKofiSub: "Поддержите на Ko-fi, чтобы помочь с обновлениями и предложить новые функции",
      // Bump topic popup
      bumpTopicTitle: "Поставьте лайк главному посту!",
      bumpTopicBody: "Это поможет большему числу людей найти Asset Hunter.",
      bumpTopicSub: "Нажав «Конечно!», вы автоматически поставите лайк.",
      bumpTopicYes: "Конечно!",
      bumpTopicNo: "Нет, спасибо",
      bumpTopicSending: "Ставим лайк...",
      bumpTopicDone: "Лайк поставлен! Спасибо!",
      bumpTopicErr: "Не удалось поставить лайк",
      // Kofi random popup
      kofiPopupTitle: "Нравится Asset Hunter?",
      kofiPopupBody: "Asset Hunter бесплатный, но поддерживать его требует реальных усилий. Если он оказался полезным, подумайте о небольшой поддержке, это очень важно!",
      kofiPopupBtn: "&#9749; Поддержать на Ko-fi",
      kofiPopupDismiss: "Может, позже",
      // Новые строки настроек
      labelStartMinimized: "Запуск свёрнутым", hintStartMinimized: "Открывать панель свёрнутой по умолчанию",
      secActiveSites: "Активные платформы", hintActiveSites: "Снимите флажок, чтобы отключить скрипт на конкретной платформе",
      labelDefaultSearchMode: "Режим поиска по умолчанию", hintDefaultSearchMode: "Поиск по имени или по полному URL по умолчанию",
      searchModeToggle_name: "Режим: Имя", searchModeToggle_url: "Режим: URL",
      matchModeToggle_any: "Слова: любые", matchModeToggle_all: "Слова: все",
      labelMinimizeHotkey: "Горячая клавиша", hintMinimizeHotkey: "Сочетание клавиш для сворачивания/разворачивания панели. Пример: Alt+H",
      hotkeyPlaceholder: "напр. Alt+H",
      // Watchlist ignore / wait-for-new
      wlBadgeIgnored: "Игнорируется", wlBadgeWaiting: "Ожидание новых",
      wlIgnoreBtn: "Игнорировать тред", wlWaitBtn: "Ждать новое", wlUnignoreBtn: "Не игнорировать",
      secIgnored: "Игнорируемые",
      // Changelog entries - 6.5.1
      "changelog.add.chineseLang": "Добавлен упрощённый китайский язык в качестве варианта интерфейса.",
      // Changelog entries - 6.5.0
      "changelog.add.postImage": "В шаблон текста добавлен новый плейсхолдер <code>{img}</code>, наравне с <code>{name}</code> и <code>{url}</code> — он заменяется главным изображением товара (взятым со страницы товара) там, где вы его разместите, так что превью показывается, даже если встраивание ссылки форумом не сработало. Он обязателен, как и <code>{url}</code>: если в кастомном шаблоне отсутствует любой из них или он не на отдельной строке, при сохранении появится предупреждение.",
      "changelog.add.builtbybit": "Добавлена поддержка BuiltByBit.",
      "changelog.add.unityAssetStore": "Добавлена поддержка Unity Asset Store.",
      "changelog.add.etsy": "Добавлена поддержка Etsy.",
      "changelog.add.watchlistIgnore": "Добавлена кнопка «Игнорировать тред» для элементов списка отслеживания со статусом DL Found — на случай, если найденный тред оказался мега-тредом или вообще не о вашем предмете. Игнорирование заносит в чёрный список только этот конкретный тред для этого элемента — поиск продолжается как обычно и по-прежнему сработает при настоящем совпадении в другом треде. Если единственное совпадение — игнорируемый тред, элемент переносится в отдельный раздел внизу списка, откуда его можно вернуть кнопкой «Не игнорировать».",
      "changelog.add.watchlistWait": "Добавлена кнопка «Ждать новое» для элементов списка отслеживания со статусом DL Found — на случай, если размещённые сейчас файлы вам не подходят. Элемент продолжает следить за тем же тредом и снова помечается только когда там появляется что-то новое.",
      "changelog.add.dailyAutoCheck": "Список отслеживания теперь автоматически проверяется раз в день, при первой загрузке скрипта за этот день — больше не нужно помнить о ручной перепроверке.",
      "changelog.fix.watchlistCheckReliability": "Логика проверки списка отслеживания приведена в порядок, исправлена ошибка, из-за которой задержка между проверками могла неоправданно расти на больших списках.",
      // Changelog entries - 6.4.1
      "changelog.fix.frenchTranslations": "Исправлены небольшие ошибки во французском переводе.",
      // Changelog entries - 6.4.0
      "changelog.add.frenchLang": "Добавлена поддержка французского языка.",
      "changelog.add.itchio": "Добавлена поддержка itch.io.",
      "changelog.add.siteFilter": "Добавлены переключатели в настройках для полного отключения скрипта на конкретных платформах.",
      "changelog.add.startMinimized": "Добавлена настройка запуска панели свёрнутой по умолчанию. Баннер Ko-fi также скрывается при сворачивании.",
      "changelog.add.urlSearchMode": "Добавлена кнопка режима URL для поиска по полному URL страницы вместо определённого имени. Доступна настройка режима по умолчанию.",
      "changelog.add.minimizeHotkey": "Добавлена настраиваемая горячая клавиша для сворачивания/разворачивания панели.",
      "changelog.add.matchWordsToggle": "Добавлена кнопка «Слова: любые / Слова: все» рядом с переключателем режима поиска. В режиме «все» результаты будут содержать только посты со всеми словами запроса.",
      "changelog.tip.searchWorkaround": "Слишком много лишних результатов? Два переключателя над строкой поиска помогут. «Слова: все» требует, чтобы все слова запроса присутствовали в посте. «Режим: URL» ищет по полному адресу страницы вместо названия товара. Это обходные решения для фундаментального ограничения поиска Ripper.Store - идеального решения нет, но обычно становится значительно лучше.",
      // Changelog entries - 6.3.0
      "changelog.add.bumpTopicPopup": "Добавлен запрос бампа после чейнджлогов.",
      "changelog.add.kofiRandomPopup": "Добавлено небольшое напоминание о донате с вероятностью 1 из 10 при загрузке страницы.",
      "changelog.fix.kofiThreshold": "Чекбокс «Больше не спрашивать» на баннере Ko-fi теперь появляется после 2 закрытий вместо 5",
      // 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", expand: "Expandir", unknown: "Desconhecido", search: "Buscar",
      placeholder: "Termo de busca...", noResult: "Sem resultados",
      hits: " resultados", solved: "Resolvido", unsolved: "Aberto", untitled: "Sem título",
      dlFound: "Download encontrado", secDiscussions: "Discussões", secOther: "Outros",
      errParse: "Falha ao processar resposta", errNetwork: "Erro de rede", errTimeout: "Tempo de requisição esgotado",
      errRateLimit: "Limite de requisições do fórum atingido - espere um pouco e tente novamente",
      errPostFailed: "HTTP {status}: Falha ao publicar. Verifique se você está logado.",
      errReplyFailed: "HTTP {status}: Falha ao responder. Verifique se você está logado em forum.ripper.store.",
      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.",
      openRipper: "Abrir 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, <code>{img}</code> para a imagem de prévia",
      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.",
      warnMissingUrl: "<strong>Modelo de corpo</strong>: falta <code>{url}</code> - adicione para que as pessoas saibam onde encontrar o item.",
      warnMissingImg: "<strong>Modelo de corpo</strong>: falta <code>{img}</code> - adicione para que suas postagens incluam uma imagem de prévia.",
      warnBadImg: "<strong>Modelo de corpo</strong>: <code>{img}</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",
      bumpProgressRunning: "{done}/{total} bumpados{failed} · ~{secs}s restantes",
      bumpProgressFailed: " ({n} falhou)",
      bumpProgressDone: "Feito - {bumped} bumpados{failed}",
      bumpProgressDoneFailed: ", {n} falhou",
      warnModalTitle: "Verifique seus modelos", warnModalOk: "Entendido",
      changelogBadgeNew: "NOVO",
      // 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",
      changelogSectionTips: "Dica",
      changelogClose: "Entendi",
      changelogKofiTitle: "Gostando do Asset Hunter?",
      changelogKofiSub: "Doe no Ko-fi para apoiar as atualizações e sugerir novas funções",
      // Bump topic popup
      bumpTopicTitle: "Considere dar upvote no post principal!",
      bumpTopicBody: "Isso ajuda mais gente a encontrar o Asset Hunter.",
      bumpTopicSub: "Clicar em \"Claro!\" vai dar upvote automaticamente.",
      bumpTopicYes: "Claro!",
      bumpTopicNo: "Não, obrigado",
      bumpTopicSending: "Dando upvote...",
      bumpTopicDone: "Upvote dado! Valeu!",
      bumpTopicErr: "Falha ao dar upvote",
      // Kofi random popup
      kofiPopupTitle: "Gostando do Asset Hunter?",
      kofiPopupBody: "O Asset Hunter é gratuito, mas manter ele atualizado dá trabalho de verdade. Se foi útil pra você, considere me pagar um café, significa muito e ajuda a manter a ferramenta viva!",
      kofiPopupBtn: "&#9749; Apoiar no Ko-fi",
      kofiPopupDismiss: "Quem sabe depois",
      // Novas strings de configuração
      labelStartMinimized: "Iniciar minimizado", hintStartMinimized: "Abrir o painel minimizado por padrão",
      secActiveSites: "Plataformas ativas", hintActiveSites: "Desmarque para desativar o script em uma plataforma específica",
      labelDefaultSearchMode: "Modo de busca padrão", hintDefaultSearchMode: "Escolha se a busca usa o nome ou a URL completa por padrão",
      searchModeToggle_name: "Modo: Nome", searchModeToggle_url: "Modo: URL",
      matchModeToggle_any: "Palavras: qualquer", matchModeToggle_all: "Palavras: todas",
      labelMinimizeHotkey: "Atalho de teclado", hintMinimizeHotkey: "Teclas para abrir/fechar o painel. Exemplo: Alt+H",
      hotkeyPlaceholder: "ex. Alt+H",
      // Watchlist ignore / wait-for-new
      wlBadgeIgnored: "Ignorado", wlBadgeWaiting: "Aguardando novidade",
      wlIgnoreBtn: "Ignorar tópico", wlWaitBtn: "Aguardar novidade", wlUnignoreBtn: "Parar de ignorar",
      secIgnored: "Ignorados",
      // Changelog entries - 6.5.1
      "changelog.add.chineseLang": "Adicionado o chinês simplificado como opção de idioma.",
      // Changelog entries - 6.5.0
      "changelog.add.postImage": "Adicionado um novo espaço reservado <code>{img}</code> para o modelo de corpo, ao lado de <code>{name}</code> e <code>{url}</code> - ele é substituído pela imagem principal do item (retirada da página do item) onde você o colocar, então uma prévia continua aparecendo mesmo se a incorporação do link do fórum falhar. É obrigatório assim como <code>{url}</code>: se algum dos dois estiver faltando em um modelo personalizado, ou não estiver em uma linha sozinho, você verá um aviso ao salvar.",
      "changelog.add.builtbybit": "Adicionado suporte ao BuiltByBit.",
      "changelog.add.unityAssetStore": "Adicionado suporte à Unity Asset Store.",
      "changelog.add.etsy": "Adicionado suporte à Etsy.",
      "changelog.add.watchlistIgnore": "Adicionado o botão \"Ignorar tópico\" aos itens com status DL Found na lista de observação, para quando o tópico encontrado é um mega-tópico ou não é realmente sobre o seu item. Ignorar coloca apenas aquele tópico específico na lista negra para aquele item - a busca continua normalmente e ainda vai sinalizar uma correspondência real em outro tópico. Se o tópico ignorado for a única coisa que corresponder, o item vai para uma seção própria no final da lista, com um botão Parar de Ignorar para trazê-lo de volta.",
      "changelog.add.watchlistWait": "Adicionado o botão \"Aguardar novidade\" aos itens com status DL Found na lista de observação, para quando os downloads postados no momento não servem para você. O item continua observando o mesmo tópico e só é sinalizado novamente quando algo realmente novo é postado.",
      "changelog.add.dailyAutoCheck": "A lista de observação agora se atualiza automaticamente uma vez por dia, na primeira vez que o script carrega naquele dia, sem precisar lembrar de clicar em Reverificar.",
      "changelog.fix.watchlistCheckReliability": "A lógica de verificação da lista de observação foi organizada, e foi corrigido um bug em que o intervalo entre verificações podia crescer sem necessidade em listas maiores.",
      // Changelog entries - 6.4.1
      "changelog.fix.frenchTranslations": "Corrigidos pequenos erros na tradução francesa.",
      // Changelog entries - 6.4.0
      "changelog.add.frenchLang": "Adicionado suporte ao idioma francês.",
      "changelog.add.itchio": "Adicionado suporte ao itch.io.",
      "changelog.add.siteFilter": "Adicionados controles por plataforma nas configurações para desativar o script em sites específicos.",
      "changelog.add.startMinimized": "Adicionada configuração para iniciar o painel minimizado por padrão. O banner Ko-fi também se oculta quando minimizado.",
      "changelog.add.urlSearchMode": "Adicionado botão de modo URL para buscar pela URL completa em vez do nome detectado. Configuração de modo padrão disponível.",
      "changelog.add.minimizeHotkey": "Adicionado atalho de teclado personalizável para abrir/fechar o painel.",
      "changelog.add.matchWordsToggle": "Adicionado o botão Palavras: qualquer / Palavras: todas ao lado do botão de modo. Com Todas, a busca exige que todos os termos estejam no post, filtrando resultados sem relação.",
      "changelog.tip.searchWorkaround": "Apareceram muitos resultados sem relação? Os dois novos botões acima da barra de busca podem ajudar. Mude para Palavras: todas para exigir todos os termos no post, ou use Modo: URL para buscar pelo endereço completo da página em vez do nome do produto. Sao contornos de uma limitacao fundamental do sistema de busca do Ripper.Store - nao existe solucao perfeita, mas geralmente melhora bastante.",
      // Changelog entries - 6.3.0
      "changelog.add.bumpTopicPopup": "Adicionado um prompt de bump que aparece após os changelogs.",
      "changelog.add.kofiRandomPopup": "Adicionado um popup de doação com chance de 1 em 10 de aparecer ao carregar a página.",
      "changelog.fix.kofiThreshold": "O checkbox \"não perguntar novamente\" no banner do Ko-fi agora aparece após 2 fechamentos em vez de 5",
      // 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",
    },
    fr: {
      title: "ASSET HUNTER", minimize: "Réduire", expand: "Agrandir", unknown: "Inconnu", search: "Rechercher",
      placeholder: "Recherche...", noResult: "Aucun résultat",
      hits: " résultats", solved: "Résolu", unsolved: "Ouvert", untitled: "Sans titre",
      dlFound: "Téléchargement trouvé", secDiscussions: "Discussions", secOther: "Autres",
      errParse: "Échec de l'analyse", errNetwork: "Erreur réseau", errTimeout: "Délai dépassé",
      errRateLimit: "Limite de requêtes atteinte sur le forum - veuillez patienter puis réessayer",
      errPostFailed: "HTTP {status} : Échec de la publication. Vérifiez que vous êtes connecté.",
      errReplyFailed: "HTTP {status} : Échec de la réponse. Vérifiez que vous êtes connecté à forum.ripper.store.",
      lfBtn: "Poster une demande LF", lfTitle: "Demande LF", lfNotFound: "Introuvable sur Ripper. Faire une demande ?",
      lfPost: "Publier", lfPosting: "Publication...",
      lfSuccess: "Publié !", lfViewPost: "Voir le post ->", lfLoginWarn: "Vous devez être connecté sur forum.ripper.store.",
      openRipper: "Ouvrir Ripper.Store",
      watchlist: "Suivi", addWatch: "Ajouter au suivi", inWatch: "Suivi",
      noWatch: "Aucun élément dans la liste", recheck: "Tout revérifier", settings: "Paramètres",
      langLabel: "Langue",
      secLfTemplates: "Modèles de posts LF", secBehaviour: "Comportement", secDataMgmt: "Gestion des données",
      labelTitleTpl: "Titre du modèle", hintTitleTpl: "Utilisez <code>{name}</code> pour le nom de l'asset",
      labelBodyTpl: "Corps du modèle", hintBodyTpl: "Utilisez <code>{name}</code> pour le nom, <code>{url}</code> pour le lien, <code>{img}</code> pour l'image d'aperçu",
      labelDefaultTags: "Tags par défaut", hintDefaultTags: "Séparés par des virgules - le tag de la plateforme (booth, gumroad, etc.) et les tags intelligents sont ajoutés automatiquement",
      labelInterval: "Intervalle", intervalEvery: "Toutes les", intervalMin: "minutes",
      labelAutoWatch: "Suivre les posts automatiquement", hintAutoWatch: "Suivre vos posts LF automatiquement pour être notifié des réponses",
      labelAutoUpdate: "Mise à jour automatique", hintAutoUpdate: "Revérifier automatiquement tous les éléments de la liste",
      wmLabel: "Filigrane - toujours ajouté, non modifiable",
      btnSave: "Enregistrer", btnExport: "Exporter les données", btnImport: "Importer les données",
      btnResetDef: "Rétablir les valeurs par défaut", btnDeleteData: "Supprimer les données", btnReset: "↺ Réinitialiser",
      savedMsg: "✓ Enregistré",
      lfLabelTitle: "Titre", lfLabelCategory: "Catégorie", lfLabelTags: "Tags",
      lfLabelTagsHint: "(séparés par des virgules)", lfLabelContent: "Contenu",
      lfLabelContentHint: "(Markdown - filigrane ajouté automatiquement)",
      btnCancel: "Annuler",
      importTitle: "Importer les données", importDrop: "Déposez votre fichier JSON ici", importDropSub: "ou cliquez pour parcourir",
      importOk: "✓ Importé avec succès !", importErr: "Échec de l'analyse JSON - fichier d'export valide ?",
      importInvalid: "Veuillez déposer un fichier .json valide.",
      lfErrTitle: "Veuillez entrer un titre.", lfErrContent: "Veuillez entrer un contenu.",
      lfConnecting: "Connexion à Ripper.Store...",
      modalResetTitle: "Rétablir les valeurs par défaut", modalResetMsg: "Tous les paramètres seront réinitialisés. Votre liste de suivi ne sera pas affectée.",
      modalResetProceed: "Réinitialiser",
      modalDeleteTitle: "Supprimer la liste de suivi", modalDeleteMsg: "Tous les éléments de la liste seront définitivement supprimés. Cette action est irréversible.",
      modalDeleteProceed: "Supprimer",
      searching: "Recherche...",
      openPost: "↗ Ouvrir le post", openItem: "↗ Ouvrir l'élément",
      warnNoName: "<strong>Titre du modèle</strong> sans <code>{name}</code> - le nom de l'asset n'apparaîtra pas dans le titre.",
      warnBadUrl: "<strong>Corps du modèle</strong> : <code>{url}</code> doit être seul sur sa ligne.",
      warnMissingUrl: "<strong>Corps du modèle</strong> : <code>{url}</code> est manquant - ajoutez-le pour que les gens sachent où trouver l'objet.",
      warnMissingImg: "<strong>Corps du modèle</strong> : <code>{img}</code> est manquant - ajoutez-le pour que vos publications incluent une image d'aperçu.",
      warnBadImg: "<strong>Corps du modèle</strong> : <code>{img}</code> doit être seul sur sa ligne.",
      kofiCardMsg: "Vous aimez Asset Hunter ? Pensez à soutenir !",
      kofiModalTitle: "Avant de fermer...",
      kofiModalBody: "Asset Hunter est gratuit et a demandé beaucoup de travail. Les dons ne sont pas obligatoires, mais pensez à soutenir si l'outil vous a aidé.",
      kofiKeepBtn: "Je vais y réfléchir",
      kofiCloseBtn: "Fermer quand même",
      kofiDontAsk: "Ne plus demander",
      labelAutoSearch: "Recherche automatique au chargement", hintAutoSearch: "Rechercher automatiquement sur Ripper.Store à l'ouverture d'une page d'article",
      manualSearchBtn: "Rechercher sur Ripper.Store",
      supTitle: "Meilleurs soutiens",
      supSubtitle: "Faites un don de n'importe quel montant pour voir votre nom ici",
      supEmpty: "Aucun don pour le moment.",
      supEmptySub: "Soyez le premier - votre nom apparaîtra ici.",
      supLoadErr: "Impossible de charger les données des soutiens.",
      // Bump
      bumpBtn: "Bump", bumpBtnSending: "Envoi...", bumpBtnDone: "Envoyé !",
      bumpBtnErr: "Échec",
      bumpAllBtn: "Bumper tous les non résolus",
      labelBumpMessage: "Message de bump", hintBumpMessage: "Le texte publié quand vous cliquez sur Bump",
      bumpProgressRunning: "{done}/{total} bumpé(s){failed} · ~{secs}s restantes",
      bumpProgressFailed: " ({n} échoué(s))",
      bumpProgressDone: "Terminé - {bumped} bumpé(s){failed}",
      bumpProgressDoneFailed: ", {n} échoué(s)",
      warnModalTitle: "Vérifiez vos modèles", warnModalOk: "Compris",
      changelogBadgeNew: "NOUVEAU",
      // Changelog
      changelogBtn: "Historique",
      changelogTitle: "Asset Hunter a été mis à jour !",
      changelogSubtitle: "voici les changements :",
      changelogSectionFixes: "Corrections",
      changelogSectionAdditions: "Ajouts",
      changelogSectionRemovals: "Suppressions",
      changelogSectionInfo: "Informations",
      changelogSectionTips: "Conseil",
      changelogClose: "Compris",
      changelogKofiTitle: "Vous aimez Asset Hunter ?",
      changelogKofiSub: "Soutenez sur Ko-fi pour contribuer aux mises à jour et suggérer de nouvelles fonctions",
      // Bump topic popup
      bumpTopicTitle: "Pensez à mettre un upvote sur le post principal !",
      bumpTopicBody: "Ça aide plus de gens à découvrir Asset Hunter.",
      bumpTopicSub: "Cliquer sur \"Bien sûr !\" mettra un upvote automatiquement.",
      bumpTopicYes: "Bien sûr !",
      bumpTopicNo: "Non merci",
      bumpTopicSending: "Envoi du vote...",
      bumpTopicDone: "Voté ! Merci !",
      bumpTopicErr: "Échec du vote",
      // Kofi random popup
      kofiPopupTitle: "Vous aimez Asset Hunter ?",
      kofiPopupBody: "Asset Hunter est gratuit, mais le maintenir demande de vrais efforts. Si l'outil vous a été utile, pensez à me payer un café, ça aide vraiment !",
      kofiPopupBtn: "&#9749; Soutenir sur Ko-fi",
      kofiPopupDismiss: "Peut-être plus tard",
      // Paramètres
      labelStartMinimized: "Démarrer minimisé", hintStartMinimized: "Ouvrir le panneau minimisé par défaut",
      secActiveSites: "Plateformes actives", hintActiveSites: "Décochez pour désactiver le script sur une plateforme",
      labelDefaultSearchMode: "Mode de recherche par défaut", hintDefaultSearchMode: "Choisir si la recherche utilise le nom ou l'URL complète par défaut",
      searchModeToggle_name: "Mode : Nom", searchModeToggle_url: "Mode : URL",
      matchModeToggle_any: "Mots : partiel", matchModeToggle_all: "Mots : tous",
      labelMinimizeHotkey: "Raccourci clavier", hintMinimizeHotkey: "Combinaison de touches pour réduire/agrandir le panneau. Exemple : Alt+H",
      hotkeyPlaceholder: "ex. Alt+H",
      // Watchlist ignore / wait-for-new
      wlBadgeIgnored: "Ignoré", wlBadgeWaiting: "En attente de nouveauté",
      wlIgnoreBtn: "Ignorer le sujet", wlWaitBtn: "Attendre du nouveau", wlUnignoreBtn: "Ne plus ignorer",
      secIgnored: "Ignorés",
      // Changelog entries - 6.5.1
      "changelog.add.chineseLang": "Ajout du chinois simplifié comme option de langue.",
      // Changelog entries - 6.5.0
      "changelog.add.postImage": "Ajout d'un nouveau paramètre <code>{img}</code> pour le corps du modèle, aux côtés de <code>{name}</code> et <code>{url}</code> - il est remplacé par l'image principale de l'objet (récupérée depuis sa page) là où vous le placez, afin qu'un aperçu reste visible même si l'intégration du lien par le forum échoue. Il est obligatoire comme <code>{url}</code> : si l'un des deux manque dans un modèle personnalisé, ou n'est pas seul sur sa ligne, un avertissement s'affichera à l'enregistrement.",
      "changelog.add.builtbybit": "Ajout du support de BuiltByBit.",
      "changelog.add.unityAssetStore": "Ajout du support de l'Unity Asset Store.",
      "changelog.add.etsy": "Ajout du support d'Etsy.",
      "changelog.add.watchlistIgnore": "Ajout d'un bouton « Ignorer le sujet » sur les éléments DL Found de la liste de suivi, pour les cas où le sujet trouvé est un méga-sujet ou ne concerne pas réellement votre objet. Ignorer met seulement ce sujet précis sur liste noire pour cet élément - la recherche continue normalement et signalera toujours une vraie correspondance sur un autre sujet. Si le sujet ignoré est la seule chose qui correspond, l'élément passe dans sa propre section en bas de la liste, avec un bouton Ne plus ignorer pour le faire revenir.",
      "changelog.add.watchlistWait": "Ajout d'un bouton « Attendre du nouveau » sur les éléments DL Found de la liste de suivi, pour les cas où les téléchargements actuellement postés ne vous conviennent pas. L'élément continue de surveiller le même sujet et n'est signalé à nouveau que lorsque quelque chose de vraiment nouveau est posté.",
      "changelog.add.dailyAutoCheck": "La liste de suivi se revérifie désormais automatiquement une fois par jour, dès le premier chargement du script ce jour-là, plus besoin de penser à cliquer sur Revérifier.",
      "changelog.fix.watchlistCheckReliability": "La logique de vérification de la liste de suivi a été clarifiée, et un bug a été corrigé : le délai entre les vérifications pouvait augmenter inutilement sur les grandes listes de suivi.",
      // Changelog entries - 6.4.1
      "changelog.fix.frenchTranslations": "Correction de petites erreurs de traduction.",
      // Changelog entries - 6.4.0
      "changelog.add.frenchLang": "Ajout de la langue française.",
      "changelog.add.itchio": "Ajout de la prise en charge d'itch.io.",
      "changelog.add.siteFilter": "Ajout d'une option dans les paramètres pour désactiver le script sur des plateformes spécifiques.",
      "changelog.add.startMinimized": "Ajout d'un paramètre pour démarrer le panneau minimisé par défaut. Le bandeau Ko-fi se masque aussi en mode minimisé.",
      "changelog.add.urlSearchMode": "Ajout d'un bouton de mode URL pour rechercher par URL complète plutôt que par nom. Paramètre de mode par défaut disponible dans les paramètres.",
      "changelog.add.minimizeHotkey": "Ajout d'un raccourci clavier personnalisable pour réduire/agrandir le panneau.",
      "changelog.add.matchWordsToggle": "Ajout d'un bouton Mots : partiel / Mots : tous à côté du bouton de mode. Passer à Tous exige que chaque mot de la recherche apparaisse dans le post, réduisant les résultats sans rapport.",
      "changelog.tip.searchWorkaround": "Des résultats sans rapport envahissent la liste ? Les deux nouveaux boutons au-dessus de la barre de recherche peuvent aider. Passez à Mots : tous pour exiger chaque mot dans les posts, ou utilisez Mode : URL pour chercher par l'adresse complète de la page plutôt que par le nom du produit. Ce sont des contournements d'une limitation fondamentale du moteur de recherche de Ripper.Store - il n'y a pas de solution parfaite, mais ca ameliore generalement beaucoup les resultats.",
      // Changelog entries - 6.3.0
      "changelog.add.bumpTopicPopup": "Ajout d'une invite de bump qui apparaît après les changelogs.",
      "changelog.add.kofiRandomPopup": "Ajout d'un rappel de don avec 1 chance sur 10 d'apparaître au chargement de la page.",
      "changelog.fix.kofiThreshold": "La case \"Ne plus demander\" sur le bandeau Ko-fi apparaît maintenant après 2 fermetures au lieu de 5.",
      // Changelog entries - 6.2.0
      "changelog.info.bumpSetting": "Le message de bump est personnalisable dans les paramètres.",
      "changelog.add.bumpButton": "Ajout de boutons Bump sur les cartes de résultats et les éléments de la liste de suivi.",
      "changelog.add.bumpSetting": "Ajout d'un paramètre de message de bump pour personnaliser le texte (par défaut : \"bump\").",
      "changelog.add.bumpWatchlist": "Les boutons Bump apparaissent sur les éléments de la liste avec un sujet lié et non marqués comme téléchargés.",
      "changelog.add.bumpAll": "Ajout d'un bouton Bumper tous dans l'onglet liste de suivi, avec indicateur de progression.",
      "changelog.add.postCooldown": "Ajout d'un délai de 10 secondes après chaque publication, partagé entre tous les onglets ouverts.",
      "changelog.add.crossTabCooldown": "Le délai de publication est synchronisé entre tous les onglets en temps réel.",
      "changelog.remove.autoUpdate": "Suppression du paramètre de mise à jour automatique de la liste de suivi.",
      // Changelog entries - 6.1.2
      "changelog.fix.nameParsingDash": "Correction du découpage des noms d'assets au niveau des tirets.",
      // Changelog entries - 6.1.1
      "changelog.fix.boothSanitizationRemoved": "Suppression de la normalisation des URL Booth introduite en 6.1.0.",
      "changelog.fix.previewBtnRemoved": "Suppression du bouton Aperçu du modal de post LF.",
      // Changelog entries - 6.1.0
      "changelog.fix.boothLink": "Les liens Booth avec sous-domaine étaient automatiquement convertis en URL canonique lors de la publication.",
      "changelog.add.changelogPopup": "Ajout de ce popup d'historique des changements.",
      "changelog.add.changelogBtn": "Ajout d'un bouton Historique dans l'onglet Paramètres.",
    },
    zh: {
      title: "ASSET HUNTER", minimize: "最小化", expand: "展开", unknown: "未知", search: "搜索",
      placeholder: "输入搜索关键词...", noResult: "没有结果",
      hits: " 条结果", solved: "已解决", unsolved: "进行中", untitled: "无标题",
      dlFound: "已找到下载", secDiscussions: "讨论", secOther: "其他",
      errParse: "解析响应失败", errNetwork: "网络错误", errTimeout: "请求超时",
      errRateLimit: "被论坛限流了 - 请稍等片刻再试",
      errPostFailed: "HTTP {status}:发布失败,请确认你已登录。",
      errReplyFailed: "HTTP {status}:回复失败,请确认你已登录 forum.ripper.store。",
      lfBtn: "发布求助帖", lfTitle: "求助请求", lfNotFound: "Ripper 上没有找到,要发布求助帖吗?",
      lfPost: "发布请求", lfPosting: "发布中...",
      lfSuccess: "已发布!", lfViewPost: "查看帖子 ->", lfLoginWarn: "你必须先登录 forum.ripper.store。",
      openRipper: "打开 Ripper.Store",
      watchlist: "监视列表", addWatch: "加入监视列表", inWatch: "监视中",
      noWatch: "监视列表为空", recheck: "重新检查全部", settings: "设置",
      langLabel: "语言",
      secLfTemplates: "求助帖模板", secBehaviour: "行为设置", secDataMgmt: "数据管理",
      labelTitleTpl: "标题模板", hintTitleTpl: "使用 <code>{name}</code> 表示素材名称",
      labelBodyTpl: "正文模板", hintBodyTpl: "使用 <code>{name}</code> 表示素材名称,<code>{url}</code> 表示商品链接,<code>{img}</code> 表示预览图",
      labelDefaultTags: "默认标签", hintDefaultTags: "用逗号分隔 - 当前平台标签(booth、gumroad 等)和智能内容标签会自动追加",
      labelInterval: "更新间隔", intervalEvery: "每", intervalMin: "分钟",
      labelAutoWatch: "自动监视已发布的帖子", hintAutoWatch: "自动关注你发布的求助帖,有人回复时会收到提醒",
      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> 和其他文字在同一行 - 除非 <code>{url}</code> 单独占一行,否则 Ripper.Store 不会生成链接预览。",
      warnMissingUrl: "<strong>正文模板</strong>缺少 <code>{url}</code> - 请添加它,好让别人知道去哪找这个商品。",
      warnMissingImg: "<strong>正文模板</strong>缺少 <code>{img}</code> - 请添加它,好让帖子附带预览图。",
      warnBadImg: "<strong>正文模板</strong>中的 <code>{img}</code> 和其他文字在同一行 - 请让 <code>{img}</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: "点击顶帖按钮时发布的文字内容",
      bumpProgressRunning: "已顶 {done}/{total}{failed} · 剩余约 {secs} 秒",
      bumpProgressFailed: "({n} 个失败)",
      bumpProgressDone: "完成 - 已顶帖 {bumped} 个{failed}",
      bumpProgressDoneFailed: ",{n} 个失败",
      warnModalTitle: "请检查你的模板", warnModalOk: "知道了",
      changelogBadgeNew: "新",
      // Changelog
      changelogBtn: "更新日志",
      changelogTitle: "Asset Hunter 已更新!",
      changelogSubtitle: "以下是本次更新内容:",
      changelogSectionFixes: "修复",
      changelogSectionAdditions: "新增",
      changelogSectionRemovals: "移除",
      changelogSectionInfo: "说明",
      changelogSectionTips: "小贴士",
      changelogClose: "知道了",
      changelogKofiTitle: "喜欢 Asset Hunter 吗?",
      changelogKofiSub: "在 Ko-fi 上捐赠即可支持后续更新并提出新功能建议",
      // Bump topic popup
      bumpTopicTitle: "考虑给主帖点个赞吧!",
      bumpTopicBody: "这能帮助更多人发现 Asset Hunter。",
      bumpTopicSub: "点击“好的!”会自动为主帖点赞。",
      bumpTopicYes: "好的!",
      bumpTopicNo: "不用了",
      bumpTopicSending: "点赞中...",
      bumpTopicDone: "已点赞!谢谢!",
      bumpTopicErr: "点赞失败",
      // Kofi random popup
      kofiPopupTitle: "喜欢 Asset Hunter 吗?",
      kofiPopupBody: "Asset Hunter 是免费的,但持续更新需要不少精力。如果它对你有帮助,欢迎请我喝杯咖啡,这对我意义重大,也能帮助这个工具持续维护下去!",
      kofiPopupBtn: "&#9749; 在 Ko-fi 上支持",
      kofiPopupDismiss: "以后再说",
      // New settings strings
      labelStartMinimized: "启动时最小化", hintStartMinimized: "默认以最小化状态打开面板",
      secActiveSites: "启用的平台", hintActiveSites: "取消勾选可在该平台完全禁用此脚本",
      labelDefaultSearchMode: "默认搜索模式", hintDefaultSearchMode: "搜索时默认使用识别到的名称还是完整页面网址",
      searchModeToggle_name: "模式:名称", searchModeToggle_url: "模式:网址",
      matchModeToggle_any: "匹配:任意词", matchModeToggle_all: "匹配:全部词",
      labelMinimizeHotkey: "最小化快捷键", hintMinimizeHotkey: "用于切换面板显示/隐藏的组合键,例如:Alt+H",
      hotkeyPlaceholder: "例如 Alt+H",
      // Watchlist ignore / wait-for-new
      wlBadgeIgnored: "已忽略", wlBadgeWaiting: "等待新内容",
      wlIgnoreBtn: "忽略此帖", wlWaitBtn: "等待新内容", wlUnignoreBtn: "取消忽略",
      secIgnored: "已忽略",
      // Changelog entries - 6.5.1
      "changelog.add.chineseLang": "新增简体中文作为语言选项。",
      // Changelog entries - 6.5.0
      "changelog.add.postImage": "正文模板新增了 <code>{img}</code> 占位符,与 <code>{name}</code> 和 <code>{url}</code> 并列使用 - 无论放在哪里,它都会被替换成素材的主图(从商品页面获取),这样即使论坛的链接预览加载失败,帖子里也依然会显示预览图。它和 <code>{url}</code> 一样是必填项:自定义模板中如果缺少其中任意一个,或者没有单独占一行,保存时都会弹出提示。",
      "changelog.add.builtbybit": "新增 BuiltByBit 支持。",
      "changelog.add.unityAssetStore": "新增 Unity Asset Store 支持。",
      "changelog.add.etsy": "新增 Etsy 支持。",
      "changelog.add.watchlistIgnore": "为监视列表中已找到下载的项目新增了“忽略此帖”按钮,适用于匹配到的帖子其实是大杂烩帖、或根本不是你要找的东西的情况。忽略操作只会把那一个帖子加入该项目的黑名单 - 搜索会照常进行,如果其他帖子确实匹配,仍然会被标记。如果匹配到的只有被忽略的帖子,该项目会移动到列表底部的单独区域,并附带“取消忽略”按钮可以随时恢复。",
      "changelog.add.watchlistWait": "为监视列表中已找到下载的项目新增了“等待新内容”按钮,适用于目前发布的下载链接并不适用于你的情况。该项目会继续关注同一个帖子,只有当真的有新内容发布时才会重新标记。",
      "changelog.add.dailyAutoCheck": "监视列表现在每天首次加载脚本时会自动重新检查一次,不用再记得手动点击“重新检查”。",
      "changelog.fix.watchlistCheckReliability": "整理了监视列表的检查逻辑,并修复了一个问题:在较大的监视列表中,检查之间的间隔可能会不必要地不断增长。",
      // Changelog entries - 6.4.1
      "changelog.fix.frenchTranslations": "修复了法语翻译中的一些小错误。",
      // Changelog entries - 6.4.0
      "changelog.add.frenchLang": "新增法语支持。",
      "changelog.add.itchio": "新增 itch.io 支持。",
      "changelog.add.siteFilter": "在设置中新增了按平台的开关,可以在特定网站上完全禁用此脚本。",
      "changelog.add.startMinimized": "新增了默认以最小化状态启动面板的设置,最小化时 Ko-fi 横幅也会一并隐藏。",
      "changelog.add.urlSearchMode": "新增了网址搜索模式按钮,可以用完整页面网址代替识别到的名称进行搜索,默认模式可在设置中配置。",
      "changelog.add.minimizeHotkey": "新增了可自定义的快捷键,用于切换面板的显示/隐藏。",
      "changelog.add.matchWordsToggle": "在搜索模式按钮旁新增了“匹配:任意词 / 匹配:全部词”切换开关。切换到“全部词”会要求搜索词中的每一个词都出现在帖子中,能过滤掉不少无关结果。",
      "changelog.tip.searchWorkaround": "搜索结果一堆无关内容?搜索框上方的两个小按钮能帮上忙。切换到“匹配:全部词”可以要求每个词都出现在帖子中,或者切换到“模式:网址”改用完整页面地址而不是商品名称搜索。这些都是针对 Ripper.Store 内置关键词搜索本身局限性的变通方法 - 没有完美的解决方案,但通常能让结果干净不少。",
      // Changelog entries - 6.3.0
      "changelog.add.bumpTopicPopup": "新增了在更新日志之后出现的顶帖提示。",
      "changelog.add.kofiRandomPopup": "新增了一个小型捐赠提醒弹窗,页面加载时有十分之一的概率出现。",
      "changelog.fix.kofiThreshold": "Ko-fi 横幅上的“不再显示”复选框现在在关闭 2 次后就会出现,而不是 5 次",
      // 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 网址净化功能,该功能未能按预期工作,已被完全回退",
      "changelog.fix.previewBtnRemoved": "从求助帖弹窗中移除了“在网站上预览”按钮,该功能是多余的",
      // Changelog entries - 6.1.0
      "changelog.fix.boothLink": "发帖时,Booth 子域名链接(如 user.booth.pm/items/...)会自动精简为标准网址(booth.pm/items/...)",
      "changelog.add.changelogPopup": "新增了这个更新日志弹窗!每次更新后会出现一次,你也可以随时从设置中重新打开它",
      "changelog.add.changelogBtn": "在设置标签页新增了“更新日志”按钮,让你随时了解有哪些变化",
    },
  };

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

  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 || "";
  }

  // ─── Active match mode (toggled per session) ─────────────────────────────
  let _matchWords = "any";

  // ─── Core API Calls ───────────────────────────────────────────────────────
  // retriesLeft defaults to 2 (so up to 3 attempts total) - only ever passed
  // explicitly by doSearch itself when retrying after a 429.
  function doSearch(query, cb, retriesLeft) {
    if (retriesLeft === undefined) retriesLeft = 2;
    GM_xmlhttpRequest({
      method: "GET",
      url: API_URL.replace("{query}", encodeURIComponent(query)).replace("{matchWords}", _matchWords),
      responseType: "json",
      timeout: 12000,
      onload: (r) => {
        if (r.status === 429) {
          if (retriesLeft > 0) {
            // Cloudflare's search rate limit - back off and try again rather
            // than treating this like the item genuinely has no matches.
            const backoffMs = (3 - retriesLeft) * 5000 + 5000; // 5s, then 10s
            setTimeout(() => doSearch(query, cb, retriesLeft - 1), backoffMs);
          } else {
            cb(RATE_LIMITED_ERR);
          }
          return;
        }
        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;
  }

  // Fetches the raw posts array for a forum topic. Used by checkDL (has this
  // thread ever had a download posted?) and checkDLSince (has a download been
  // posted since a given point?) so the actual fetch + parsing only lives here.
  function fetchTopicPosts(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;
          cb(null, d.posts || []);
        } catch(e) { cb(e, []); }
      },
      onerror:   () => cb(new Error("network"), []),
      ontimeout: () => cb(new Error("timeout"), []),
    });
  }

  // True if a single post looks like it contains a download (raw content,
  // rendered content, a link, or an attachment matching DL_PATTERNS).
  function postMatchesDL(p) {
    const htmlContent = p.content || "";
    const rawContent  = p.rawContent || "";
    if (DL_PATTERNS.some(x => x.test(rawContent))) return true;
    if (htmlContent) {
      const plain = stripHTML(htmlContent);
      if (DL_PATTERNS.some(x => x.test(plain))) return 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 true;
      }
    }
    if (p.attachments && p.attachments.length) return true;
    return false;
  }

  function checkDL(tid, cb) {
    fetchTopicPosts(tid, (err, posts) => {
      if (err) return cb(false);
      cb(posts.some(postMatchesDL));
    });
  }

  // Like checkDL, but only looks at posts after sincePostCount (the number of
  // posts the thread had the last time we looked). Used for watchlist items
  // in the "waiting for new" state, so an old, already-seen download doesn't
  // keep re-triggering the DL Found flag. Returns the matched flag plus the
  // thread's current total post count, so the caller can move the checkpoint
  // forward regardless of whether a match was found this time.
  function checkDLSince(tid, sincePostCount, cb) {
    fetchTopicPosts(tid, (err, posts) => {
      if (err) return cb(false, sincePostCount);
      const newPosts = posts.slice(Math.max(0, sincePostCount || 0));
      cb(newPosts.some(postMatchesDL), posts.length);
    });
  }

  // Grabs just the current post count for a thread, used to set the initial
  // checkpoint when a watchlist item is switched into "waiting for new".
  function fetchTopicPostCount(tid, cb) {
    fetchTopicPosts(tid, (err, posts) => cb(err ? -1 : posts.length));
  }

  // ─── 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) ||
                 t("errPostFailed").replace("{status}", r.status));
            }
          } 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) ||
                 t("errReplyFailed").replace("{status}", r.status));
            }
          } catch(e) { cb("Failed to parse reply response"); }
        },
        onerror:  () => cb("Network error while posting reply"),
        ontimeout: () => cb("Reply request timed out"),
      });
    });
  }

  function upvotePost(pid, cb) {
    getCSRFToken((err, csrf) => {
      if (err) return cb(err);
      GM_xmlhttpRequest({
        method: "PUT",
        url: POST_VOTE_API.replace("{pid}", pid),
        headers: { "Content-Type": "application/json; charset=utf-8", "x-csrf-token": csrf },
        data: JSON.stringify({ delta: 1 }),
        responseType: "json",
        timeout: 15000,
        onload: (r) => {
          try {
            const d = typeof r.response === "string" ? JSON.parse(r.response) : r.response;
            const isOk = (d && d.status && d.status.code === "ok") || (r.status >= 200 && r.status < 300);
            if (isOk) cb(null, d);
            else cb((d && d.status && d.status.message) || (d && d.message) || `HTTP ${r.status}`);
          } catch(e) { cb("Failed to parse vote response"); }
        },
        onerror:  () => cb("Network error while voting"),
        ontimeout: () => cb("Vote request timed out"),
      });
    });
  }

  // Resolves the pid of a topic's original post - the topic API returns the
  // full post list with the original post first, so we just fetch and take posts[0].
  function getTopicMainPid(tid, cb) {
    fetchTopicPosts(tid, (err, posts) => {
      if (err || !posts.length) return cb(err || "No posts found");
      cb(null, posts[0].pid);
    });
  }

  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");
    else if (HOST === "itch.io"    || HOST.endsWith(".itch.io"))      extra.push("itch");
    else if (HOST === "builtbybit.com" || HOST.endsWith(".builtbybit.com")) extra.push("builtbybit");
    else if (HOST === "assetstore.unity.com") extra.push("unity");
    else if (HOST === "etsy.com" || HOST.endsWith(".etsy.com")) extra.push("etsy");
    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 {
      const list = JSON.parse(GM_getValue("bs-watchlist", "[]"));
      // Migrate items ignored under the old whole-item lock to the new per-thread blacklist.
      list.forEach(item => {
        if (item.status === "ignored" && !item.ignoredTids && item.ripperTid) {
          item.ignoredTids = [item.ripperTid];
        }
      });
      return list;
    } 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(isForced) {
    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);
      const tips      = (entry.tips      || []).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">${t("changelogBadgeNew")}</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>${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>${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>${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>${item}</span></li>`;
        }
        html += `</ul>`;
      }

      if (tips.length) {
        for (const tip of tips) {
          html += `<div class="ah-cl-tip-card">
            <div class="ah-cl-tip-label">&#9654; ${t("changelogSectionTips")}</div>
            <div class="ah-cl-tip-text">${esc(tip)}</div>
          </div>`;
        }
      }

      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();
      if (isForced && GM_getValue("ah-upvoted-main-post", "0") !== "1") showBumpTopicModal();
    };
    modal.querySelector("#ah-cl-close-btn").addEventListener("click", close);
    modal.querySelector("#ah-cl-ok-btn").addEventListener("click", close);
  }

  // ─── Bump Topic Popup ─────────────────────────────────────────────────────
  function showBumpTopicModal() {
    document.getElementById("ah-bumptopic-modal")?.remove();

    const modal = document.createElement("div");
    modal.id = "ah-bumptopic-modal";
    modal.innerHTML = `
      <div class="ah-bt-backdrop"></div>
      <div class="ah-bt-dialog" role="dialog" aria-modal="true">
        <div class="ah-bt-icon-row">
          <svg width="22" height="22" viewBox="0 0 22 22" fill="none">
            <path d="M11 2L13 8H20L14.5 11.5L16.5 18L11 14.5L5.5 18L7.5 11.5L2 8H9L11 2Z"
              stroke="#c8a8ff" stroke-width="1.4" stroke-linejoin="round" fill="rgba(200,168,255,.08)"/>
          </svg>
          <span class="ah-bt-title">${t("bumpTopicTitle")}</span>
        </div>
        <div class="ah-bt-body">${esc(t("bumpTopicBody"))}</div>
        <div class="ah-bt-sub">${esc(t("bumpTopicSub"))}</div>
        <div class="ah-bt-actions">
          <button class="ah-bt-no" id="ah-bt-no">${t("bumpTopicNo")}</button>
          <button class="ah-bt-yes" id="ah-bt-yes">${t("bumpTopicYes")}</button>
        </div>
        <div class="ah-bt-status" id="ah-bt-status"></div>
      </div>`;

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

    const close = () => modal.remove();
    modal.querySelector("#ah-bt-no").addEventListener("click", close);

    modal.querySelector("#ah-bt-yes").addEventListener("click", () => {
      const yesBtn = modal.querySelector("#ah-bt-yes");
      const noBtn  = modal.querySelector("#ah-bt-no");
      const status = modal.querySelector("#ah-bt-status");
      yesBtn.disabled = true;
      noBtn.disabled  = true;
      yesBtn.textContent = t("bumpTopicSending");
      getTopicMainPid(AH_TOPIC_TID, (pidErr, pid) => {
        if (pidErr) {
          status.textContent = t("bumpTopicErr");
          status.className   = "ah-bt-status ah-bt-status--err";
          yesBtn.disabled = false;
          noBtn.disabled  = false;
          yesBtn.textContent = t("bumpTopicYes");
          return;
        }
        upvotePost(pid, (err) => {
          if (err) {
            status.textContent = t("bumpTopicErr");
            status.className   = "ah-bt-status ah-bt-status--err";
            yesBtn.disabled = false;
            noBtn.disabled  = false;
            yesBtn.textContent = t("bumpTopicYes");
          } else {
            GM_setValue("ah-upvoted-main-post", "1");
            status.textContent = t("bumpTopicDone");
            status.className   = "ah-bt-status ah-bt-status--ok";
            yesBtn.textContent = "✓";
            setTimeout(close, 2200);
          }
        });
      });
    });
  }

  // ─── Random Ko-fi Popup ───────────────────────────────────────────────────
  const KOFI_RANDOM_SEEN_COUNT_KEY = "ah-kofi-random-seen-count";
  function maybeShowKofiRandomPopup() {
    if (GM_getValue("ah-kofi-dont-ask", "0") === "1") return;
    if (Math.random() > 1 / 10) return;
    document.getElementById("ah-kofi-random-modal")?.remove();

    const seenCount = (parseInt(GM_getValue(KOFI_RANDOM_SEEN_COUNT_KEY, "0"), 10) || 0) + 1;
    GM_setValue(KOFI_RANDOM_SEEN_COUNT_KEY, String(seenCount));
    const canShowDontAsk = seenCount >= 10;

    const modal = document.createElement("div");
    modal.id = "ah-kofi-random-modal";
    modal.innerHTML = `
      <div class="ah-kr-backdrop"></div>
      <div class="ah-kr-dialog" role="dialog" aria-modal="true">
        <div class="ah-kr-header">
          <span class="ah-kr-heart">&#9829;</span>
          <span class="ah-kr-title">${t("kofiPopupTitle")}</span>
          <button class="ah-kr-x" id="ah-kr-x">&#10005;</button>
        </div>
        <div class="ah-kr-body">${esc(t("kofiPopupBody"))}</div>
        ${canShowDontAsk ? `<label class="ah-kr-optout"><input type="checkbox" id="ah-kr-dont-ask"> ${t("kofiDontAsk")}</label>` : ""}
        <div class="ah-kr-actions">
          <button class="ah-kr-dismiss" id="ah-kr-dismiss">${t("kofiPopupDismiss")}</button>
          <a class="ah-kr-kofi-btn" href="https://ko-fi.com/xedinho" target="_blank" rel="noopener">${t("kofiPopupBtn")}</a>
        </div>
      </div>`;

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

    const close = () => {
      const dontAskEl = modal.querySelector("#ah-kr-dont-ask");
      if (dontAskEl && dontAskEl.checked) GM_setValue("ah-kofi-dont-ask", "1");
      modal.remove();
    };
    modal.querySelector(".ah-kr-backdrop").addEventListener("click", close);
    modal.querySelector("#ah-kr-x").addEventListener("click", close);
    modal.querySelector("#ah-kr-dismiss").addEventListener("click", close);
    modal.querySelector(".ah-kr-kofi-btn").addEventListener("click", () => setTimeout(close, 300));
  }

  // ─── 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(true), 1800);
    }
  }

  // ─── LF Modal ─────────────────────────────────────────────────────────────
  function showLFModal() {
    const adapter = getAdapter();
    const id   = adapter ? adapter.getId()   : "";
    const name = adapter ? adapter.getName() : "";
    const url  = window.location.href;
    const imageUrl = adapter && adapter.getImage ? (adapter.getImage() || "") : "";
    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, imageUrl).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">${t("openRipper")} &#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", fr: "fr-FR", zh: "zh-CN" }[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; ${t("dlFound")}</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; ${t("secDiscussions")}</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";
      if (HOST === "itch.io" || HOST.endsWith(".itch.io")) return "itch.io";
      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="${t('supTitle')}">&#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>
          <div id="ah-search-type-row">
            <button id="ah-search-mode-btn" class="ah-search-mode-btn" data-mode="${getSetting("defaultSearchMode") || "name"}">
              ${(getSetting("defaultSearchMode") || "name") === "url" ? t("searchModeToggle_url") : t("searchModeToggle_name")}
            </button>
            <button id="ah-match-mode-btn" class="ah-search-mode-btn" data-match="any">${t("matchModeToggle_any")}</button>
          </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);
      // Hide immediately if panel starts minimized
      if (getSetting("startMinimized")) card.style.display = "none";

    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 > 2;
      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 ignoredItems = list.filter(x => x.status === "ignored");
      const otherItems   = list.filter(x => x.status !== "dl" && x.status !== "found" && x.status !== "ignored");

      // Items eligible for bump = have a ripperTid, are NOT marked as dl (already
      // downloaded), and are NOT ignored (the matched thread was decided to be irrelevant).
      const bumpableItems = list.filter(x => x.ripperTid && x.status !== "dl" && x.status !== "ignored");

      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 isIgnored  = item.status === "ignored";
        const isWaiting  = item.status === "waiting";

        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>`  :
          isIgnored       ? `<span class="ah-wl-badge ah-wl-badge--ignored">&#8856; ${esc(t("wlBadgeIgnored"))}</span>` :
          isWaiting       ? `<span class="ah-wl-badge ah-wl-badge--waiting">&#8635; ${esc(t("wlBadgeWaiting"))}</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 || isIgnored || isWaiting) && 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) or ignored
        const bumpBtn = hasTid && !isDLStatus && !isIgnored
          ? `<button class="ah-bump-btn ah-wl-bump-btn" data-tid="${esc(String(item.ripperTid || topicRef))}" title="${esc(t("bumpBtn"))}">${esc(t("bumpBtn"))}</button>`
          : "";

        // DL Found items get a way to say "that thread isn't actually right" -
        // either park it (ignore) or keep watching the same thread for something new.
        const dlActionsRow = isDLStatus
          ? `<div class="ah-wl-row3">
              <button class="ah-wl-action-btn ah-wl-ignore-btn" data-url="${esc(item.url)}">${esc(t("wlIgnoreBtn"))}</button>
              <button class="ah-wl-action-btn ah-wl-wait-btn" data-url="${esc(item.url)}">${esc(t("wlWaitBtn"))}</button>
            </div>`
          : "";

        const unignoreRow = isIgnored
          ? `<div class="ah-wl-row3">
              <button class="ah-wl-action-btn ah-wl-unignore-btn" data-url="${esc(item.url)}">${esc(t("wlUnignoreBtn"))}</button>
            </div>`
          : "";

        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>
          ${dlActionsRow}
          ${unignoreRow}
        </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; ${t("dlFound")}</div>` + dlItems.map(itemHTML).join("");
      if (discItems.length)  html += `<div class="ah-section-label ah-section--disc">&#9711; ${t("secDiscussions")}</div>` + discItems.map(itemHTML).join("");
      if (otherItems.length) {
        if (dlItems.length || discItems.length) html += `<div class="ah-section-label ah-section--other">&#9675; ${t("secOther")}</div>`;
        html += otherItems.map(itemHTML).join("");
      }
      if (ignoredItems.length) html += `<div class="ah-section-label ah-section--ignored">&#8856; ${t("secIgnored")}</div>` + ignoredItems.map(itemHTML).join("");

      wlBody.innerHTML = html;

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

      // "Ignore Thread" - this specific matched thread isn't actually relevant
      // (e.g. a mega-thread). Blacklist just that thread ID for this item and
      // re-check right away, so a different, genuinely relevant thread can
      // surface immediately instead of waiting for the next full recheck.
      wlBody.querySelectorAll(".ah-wl-ignore-btn").forEach(btn => {
        btn.addEventListener("click", e => {
          e.preventDefault();
          const cur = wlGet();
          const entry = cur.find(x => x.url === btn.dataset.url);
          if (!entry || !entry.ripperTid) return;
          entry.ignoredTids = entry.ignoredTids || [];
          if (!entry.ignoredTids.includes(entry.ripperTid)) entry.ignoredTids.push(entry.ripperTid);
          delete entry.waitFromCount;
          entry.status = "pending";
          wlSave(cur);
          renderWatchlist();
          processItem(entry, () => {});
        });
      });

      // "Wait for New" - the downloads currently in this thread don't apply to
      // you (e.g. incompatible version), keep watching the same thread but only
      // flag it again once something genuinely new gets posted.
      wlBody.querySelectorAll(".ah-wl-wait-btn").forEach(btn => {
        btn.addEventListener("click", e => {
          e.preventDefault();
          const url = btn.dataset.url;
          const cur = wlGet();
          const entry = cur.find(x => x.url === url);
          if (!entry || !entry.ripperTid) return;
          fetchTopicPostCount(entry.ripperTid, (count) => {
            if (count < 0) return; // network hiccup - leave the item as-is, user can just click Wait again
            const cur2 = wlGet();
            const entry2 = cur2.find(x => x.url === url);
            if (!entry2) return;
            entry2.status = "waiting";
            entry2.waitFromCount = count;
            wlSave(cur2);
            renderWatchlist();
          });
        });
      });

      // "Stop Ignoring" - clear this item's ignored-thread list and re-check
      // right away.
      wlBody.querySelectorAll(".ah-wl-unignore-btn").forEach(btn => {
        btn.addEventListener("click", e => {
          e.preventDefault();
          const cur = wlGet();
          const entry = cur.find(x => x.url === btn.dataset.url);
          if (!entry) return;
          entry.ignoredTids = [];
          entry.status = "pending";
          wlSave(cur);
          renderWatchlist();
          processItem(entry, () => {});
        });
      });

      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" && x.status !== "ignored");
          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 failedPartRun  = failed ? t("bumpProgressFailed").replace("{n}", failed) : "";
            const secsLeft = Math.ceil(((items.length - done) * BUMP_DELAY_MS) / 1000);
            progressText.textContent = t("bumpProgressRunning")
              .replace("{done}", done).replace("{total}", items.length)
              .replace("{failed}", failedPartRun).replace("{secs}", secsLeft);
          }

          function bumpNext(idx) {
            if (idx >= items.length) {
              const failedPartDone = failed ? t("bumpProgressDoneFailed").replace("{n}", failed) : "";
              progressText.textContent = t("bumpProgressDone")
                .replace("{bumped}", bumped).replace("{failed}", failedPartDone);
              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">' + t("warnModalTitle") + '</span>',
          '</div>',
          '<div class="ah-warn-body">' + itemsHTML + '</div>',
          '<button class="ah-warn-btn" id="ah-warn-ok">' + t("warnModalOk") + '</button>',
        '</div>'
      ].join("");

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

      const close = function() { modal.remove(); };
      modal.querySelector("#ah-warn-ok").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">
          <div class="ah-lang-picker" id="ah-lang-picker">
            <button class="ah-lang-picker-btn" id="ah-lang-picker-btn" type="button">
              <span>${esc(LANG_OPTIONS.find(o => o.value === currentLang)?.label || currentLang)}</span>
              <svg width="9" height="9" viewBox="0 0 9 9" fill="none">
                <path d="M1.5 3L4.5 6L7.5 3" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/>
              </svg>
            </button>
            <div class="ah-lang-picker-list" id="ah-lang-picker-list">
              ${LANG_OPTIONS.map(o => `<button class="ah-lang-picker-opt${o.value === currentLang ? " ah-lang-picker-opt--active" : ""}" type="button" data-value="${o.value}">${o.label}</button>`).join("")}
            </div>
          </div>
        </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-toggle-row">
          <div class="ah-set-toggle-info">
            <span class="ah-set-toggle-label">${t("labelStartMinimized")}</span>
            <span class="ah-set-toggle-hint">${t("hintStartMinimized")}</span>
          </div>
          <button class="ah-set-toggle ${getSetting("startMinimized") ? "ah-set-toggle--on" : ""}"
            id="ah-set-startMinimized" aria-pressed="${getSetting("startMinimized")}">
            <span class="ah-set-toggle-knob"></span>
          </button>
        </div>

        <div class="ah-set-field">
          <div class="ah-set-field-hd">
            <label class="ah-set-label">${t("labelDefaultSearchMode")}</label>
          </div>
          <div class="ah-set-hint">${t("hintDefaultSearchMode")}</div>
          <div class="ah-set-search-mode-row">
            <button class="ah-set-mode-opt ${(getSetting("defaultSearchMode") || "name") === "name" ? "ah-set-mode-opt--active" : ""}" id="ah-set-mode-name" data-mode="name">${t("searchModeToggle_name")}</button>
            <button class="ah-set-mode-opt ${(getSetting("defaultSearchMode") || "name") === "url"  ? "ah-set-mode-opt--active" : ""}" id="ah-set-mode-url"  data-mode="url">${t("searchModeToggle_url")}</button>
          </div>
        </div>

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

        <div class="ah-set-section">${t("secActiveSites")}</div>
        <div class="ah-set-hint" style="margin-bottom:10px">${t("hintActiveSites")}</div>
        <div class="ah-set-sites-grid">
          ${SITE_DEFS.map(s => `
            <label class="ah-set-site-row">
              <input type="checkbox" class="ah-set-site-cb" data-site="${s.key}" ${isSiteEnabled(s.key) ? "checked" : ""}>
              <span class="ah-set-site-name">${s.label}</span>
            </label>
          `).join("")}
        </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(false));

      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);
        const errors = validateBodyTpl(val);
        if (errors.length) showSettingsWarn(errors);
      }
      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);

      // ── Language custom picker ──
      const langPickerBtn  = setBody.querySelector("#ah-lang-picker-btn");
      const langPickerList = setBody.querySelector("#ah-lang-picker-list");

      langPickerBtn.addEventListener("click", (e) => {
        e.stopPropagation();
        const isOpen = langPickerList.classList.toggle("ah-lang-picker-list--open");
        langPickerBtn.classList.toggle("ah-lang-picker-btn--open", isOpen);
        if (isOpen) {
          setTimeout(() => {
            const onOutside = (ev) => {
              if (!setBody.querySelector("#ah-lang-picker")?.contains(ev.target)) {
                langPickerList.classList.remove("ah-lang-picker-list--open");
                langPickerBtn.classList.remove("ah-lang-picker-btn--open");
                document.removeEventListener("click", onOutside, true);
              }
            };
            document.addEventListener("click", onOutside, true);
          }, 0);
        }
      });

      setBody.querySelectorAll(".ah-lang-picker-opt").forEach(btn => {
        btn.addEventListener("click", () => {
          const chosen = btn.dataset.value;
          langPickerList.classList.remove("ah-lang-picker-list--open");
          langPickerBtn.classList.remove("ah-lang-picker-btn--open");
          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");
            const smBtn = panel.querySelector("#ah-search-mode-btn");
            if (smBtn) smBtn.textContent = smBtn.dataset.mode === "url" ? t("searchModeToggle_url") : t("searchModeToggle_name");
            const mmBtn = panel.querySelector("#ah-match-mode-btn");
            if (mmBtn) mmBtn.textContent = _matchWords === "all" ? t("matchModeToggle_all") : t("matchModeToggle_any");
            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.querySelector("#ah-set-startMinimized").addEventListener("click", function() {
        const on = this.classList.toggle("ah-set-toggle--on");
        this.setAttribute("aria-pressed", on);
        setSetting("startMinimized", on);
      });

      setBody.querySelectorAll(".ah-set-mode-opt").forEach(btn => {
        btn.addEventListener("click", () => {
          setBody.querySelectorAll(".ah-set-mode-opt").forEach(b => b.classList.remove("ah-set-mode-opt--active"));
          btn.classList.add("ah-set-mode-opt--active");
          setSetting("defaultSearchMode", btn.dataset.mode);
          // Update the live toggle button in the search pane too
          const modeBtn = panel.querySelector("#ah-search-mode-btn");
          if (modeBtn) {
            modeBtn.dataset.mode = btn.dataset.mode;
            modeBtn.textContent = btn.dataset.mode === "url" ? t("searchModeToggle_url") : t("searchModeToggle_name");
          }
        });
      });

      function autoSaveMinimizeHotkey() {
        const val = setBody.querySelector("#ah-set-minimizeHotkey").value.trim();
        setSetting("minimizeHotkey", val);
      }
      setBody.querySelector("#ah-set-minimizeHotkey").addEventListener("change", autoSaveMinimizeHotkey);

      setBody.querySelectorAll(".ah-set-site-cb").forEach(cb => {
        cb.addEventListener("change", function() {
          GM_setValue("ah-site-enabled-" + this.dataset.site, this.checked ? "1" : "0");
        });
      });

      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"),
            startMinimized:    getSetting("startMinimized"),
            defaultSearchMode: getSetting("defaultSearchMode"),
            minimizeHotkey:    getSetting("minimizeHotkey"),
          },
          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 (s.startMinimized !== undefined) setSetting("startMinimized",    s.startMinimized);
            if (s.defaultSearchMode)            setSetting("defaultSearchMode", s.defaultSearchMode);
            if (s.minimizeHotkey !== undefined) setSetting("minimizeHotkey",    s.minimizeHotkey);
          }
          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",
             "startMinimized","defaultSearchMode","minimizeHotkey"].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();
          },
        });
      });
    }

    // Checks a single watchlist item and updates its status: searches Ripper.Store,
    // then figures out dl / found / none - skipping any thread ID listed in the
    // item's ignoredTids (threads the user has explicitly said aren't relevant,
    // e.g. mega-threads). An ignored thread never blocks a genuine match elsewhere;
    // it's only if literally everything that matched is on the ignore list that the
    // item falls into the "ignored" bucket instead of "none".
    function processItem(item, done) {
      // Items in "waiting for new" only re-check their own known thread for
      // posts newer than the checkpoint saved when the user hit "Wait for New" -
      // they skip the normal search entirely.
      if (item.status === "waiting" && item.ripperTid) {
        checkDLSince(item.ripperTid, item.waitFromCount || 0, (matched, totalCount) => {
          const cur   = wlGet();
          const entry = cur.find(x => x.url === item.url);
          if (!entry) { done(); return; }
          entry.lastChecked = Date.now();
          if (matched) {
            entry.status = "dl";
            delete entry.waitFromCount;
          } else {
            entry.waitFromCount = totalCount;
          }
          wlSave(cur); renderWatchlist(); done();
        });
        return;
      }

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

        if (err === RATE_LIMITED_ERR) {
          // Couldn't check this one, not "nothing found" - leave status and
          // lastChecked alone so it gets a clean attempt on the next check.
          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 allPosts        = data.posts;
        const nonIgnoredPosts = allPosts.filter(p => !ignoredTids.includes((p.topic || {}).tid));

        const giftsPost = nonIgnoredPosts.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 = nonIgnoredPosts
          .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) {
              if (topics.length) {
                entry.status      = "found";
                entry.ripperTid   = topics[0].tid;
                entry.ripperSlug  = topics[0].slug || topics[0].tid;
              } else if (ignoredTids.length) {
                // Everything that matched is on the ignore list - nothing new to show,
                // but keep pointing at the ignored thread so "Open Post" still works.
                const ip  = allPosts.find(p => ignoredTids.includes((p.topic || {}).tid));
                const itp = ip ? (ip.topic || {}) : {};
                entry.status     = "ignored";
                entry.ripperTid  = itp.tid   || entry.ripperTid  || null;
                entry.ripperSlug = itp.slug  || itp.tid || entry.ripperSlug || null;
              } else {
                entry.status = "none";
              }
              entry.lastChecked = Date.now();
              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);
      });
    }

    // ── Watchlist re-check ──
    // Runs processItem over every watchlist item: searches Ripper.Store for each
    // one and updates its status. No tab-switching or other UI side effects here,
    // so this is safe to call quietly in the background (see maybeAutoCheckWatchlist
    // below) as well as from the manual recheck button.
    function checkAllWatchlistItems(onDone) {
      const fullList = wlGet();
      if (!fullList.length) { renderWatchlist(); if (onDone) onDone(); return; }

      // Waiting items keep their own status while we look for genuinely new posts
      // in their thread, so they're skipped by this reset. Ignored threads are
      // tracked per-thread (ignoredTids), not per-item, so items with an ignored
      // thread still get checked normally - a different, non-ignored match can
      // still surface and take over. DL Found items are left alone entirely (see
      // queue filter below) - a download's already confirmed there, no need to
      // keep spending search requests re-confirming it.
      fullList.forEach(item => {
        if (item.status !== "waiting" && item.status !== "dl") item.status = "pending";
      });
      wlSave(fullList); renderWatchlist();

      // DL Found items are cached and skipped - only waiting/ignored/everything
      // else actually gets checked. This also meaningfully cuts down on request
      // volume against the forum's search rate limit.
      const queue = fullList.filter(item => item.status !== "dl");

      // Tuned against the forum's actual search rate limit: back-to-back
      // requests started failing (429) around the 5th one (~3.4s in), while
      // 2s spacing with no concurrency ran clean. CONCURRENCY=1 keeps this to
      // one search in flight at a time; ITEM_DELAY_MS adds real margin above
      // the 2s that tested clean, since checkNext's own topic-check requests
      // (which tested fine unthrottled) still land in the same window.
      const CONCURRENCY = 1;
      const ITEM_DELAY_MS = 3000;
      let cursor = 0;
      let active = 0;
      let nextDispatchAt = 0;

      function pump() {
        while (active < CONCURRENCY && cursor < queue.length) {
          const item = queue[cursor++];
          active++;
          const now  = Date.now();
          const wait = Math.max(0, nextDispatchAt - now);
          nextDispatchAt = Math.max(now, nextDispatchAt) + ITEM_DELAY_MS;
          setTimeout(() => {
            processItem(item, () => {
              active--;
              pump();
            });
          }, wait);
        }
        if (active === 0 && cursor >= queue.length && onDone) onDone();
      }

      pump();
    }

    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";
      });
      checkAllWatchlistItems();
    }

    // Once-a-day, tab-quiet watchlist check: runs the first time the script
    // loads on a given calendar day, without switching tabs or otherwise
    // disturbing whatever the user is doing.
    const AUTO_CHECK_DATE_KEY = "ah-wl-last-autocheck-date";
    function todayKey() {
      const d = new Date();
      return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
    }
    function maybeAutoCheckWatchlist() {
      const today = todayKey();
      if (GM_getValue(AUTO_CHECK_DATE_KEY, "") === today) return;
      GM_setValue(AUTO_CHECK_DATE_KEY, today); // claim the day first so other tabs/sites don't also kick one off
      if (!wlGet().length) return;
      setTimeout(() => { checkAllWatchlistItems(); }, 4000);
    }

    _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;
        const errMsg = err === RATE_LIMITED_ERR ? t("errRateLimit") : err;
        out.innerHTML = err
          ? `<div class="ah-error">&#9888; ${esc(errMsg)}</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 ? t("expand") : t("minimize");
      const kofiCard = document.getElementById("ah-kofi-card");
      if (kofiCard) kofiCard.style.display = collapsed ? "none" : "";
    });

    // ── Match mode toggle ──
    const matchModeBtn = panel.querySelector("#ah-match-mode-btn");
    if (matchModeBtn) {
      matchModeBtn.addEventListener("click", () => {
        _matchWords = _matchWords === "any" ? "all" : "any";
        matchModeBtn.dataset.match = _matchWords;
        matchModeBtn.textContent = _matchWords === "all" ? t("matchModeToggle_all") : t("matchModeToggle_any");
      });
    }

    // ── Start minimized ──
    if (getSetting("startMinimized")) {
      collapsed = true;
      collapsible.classList.add("ah-collapsed");
      minBtn.querySelector("svg").style.transform = "rotate(45deg)";
      minBtn.title = t("expand");
      // Ko-fi card isn't in DOM yet at this point; it gets hidden after it's appended below
    }

    // ── Search mode toggle ──
    let currentSearchMode = getSetting("defaultSearchMode") || "name";
    const searchModeBtn = panel.querySelector("#ah-search-mode-btn");
    if (searchModeBtn) {
      function applySearchMode(mode) {
        currentSearchMode = mode;
        searchModeBtn.dataset.mode = mode;
        searchModeBtn.textContent = mode === "url" ? t("searchModeToggle_url") : t("searchModeToggle_name");
        inp.value = mode === "url" ? window.location.href : query;
      }
      searchModeBtn.addEventListener("click", () => {
        applySearchMode(currentSearchMode === "url" ? "name" : "url");
      });
    }

    // ── Hotkey listener ──
    function matchesHotkey(e, hotkeyStr) {
      if (!hotkeyStr) return false;
      const parts = hotkeyStr.split("+").map(s => s.trim().toLowerCase());
      const key = parts[parts.length - 1];
      const needCtrl  = parts.includes("ctrl");
      const needAlt   = parts.includes("alt");
      const needShift = parts.includes("shift");
      if (needCtrl  !== e.ctrlKey)  return false;
      if (needAlt   !== e.altKey)   return false;
      if (needShift !== e.shiftKey) return false;
      return e.key.toLowerCase() === key || e.code.toLowerCase() === ("key" + key) || e.code.toLowerCase() === key;
    }
    document.addEventListener("keydown", function hotkeyHandler(e) {
      if (!document.getElementById("ah-panel")) {
        document.removeEventListener("keydown", hotkeyHandler, true);
        return;
      }
      const hotkey = getSetting("minimizeHotkey");
      if (!hotkey) return;
      const tag = (document.activeElement || {}).tagName;
      if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
      if (!matchesHotkey(e, hotkey)) return;
      e.preventDefault();
      minBtn.click();
    }, true);

    if (getSetting("autoSearch")) {
      const initialQuery = currentSearchMode === "url" ? window.location.href : query;
      search(initialQuery);
    } else {
      if (currentSearchMode === "url") inp.value = window.location.href;
      manualBtn.style.display = "";
      panel.querySelector("#ah-search-row").style.display = "none";
    }

    maybeAutoCheckWatchlist();
  }

  // ─── 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-ignored: #6b6b80;
  --ah-ignored-bg: rgba(107,107,128,.06);
  --ah-ignored-bd: rgba(107,107,128,.18);
  --ah-wait: #ffb347;
  --ah-wait-bg: rgba(255,179,71,.06);
  --ah-wait-bd: rgba(255,179,71,.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 mode toggle row */
#ah-search-type-row {
  display:flex;gap:5px;margin-bottom:8px;
}
.ah-search-mode-btn {
  padding:5px 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,.55);
  font-family:var(--ah-f);font-size:8px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;
  cursor:pointer;transition:background .15s,border-color .15s,color .15s;
}
.ah-search-mode-btn:hover {
  background:rgba(200,168,255,.12);
  border-color:rgba(200,168,255,.32);
  color:var(--ah-accent);
}
.ah-search-mode-btn[data-mode="url"] {
  background:rgba(114,240,168,.06);
  border-color:rgba(114,240,168,.22);
  color:rgba(114,240,168,.7);
}
.ah-search-mode-btn[data-mode="url"]:hover {
  background:rgba(114,240,168,.12);
  border-color:rgba(114,240,168,.4);
  color:var(--ah-dl);
}

.ah-search-mode-btn[data-match="all"] {
  background:rgba(255,215,0,.06);
  border-color:rgba(255,215,0,.22);
  color:rgba(255,215,0,.7);
}
.ah-search-mode-btn[data-match="all"]:hover {
  background:rgba(255,215,0,.12);
  border-color:rgba(255,215,0,.4);
  color:var(--ah-gold);
}

/* 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); }
.ah-section--ignored { color:var(--ah-ignored);background:var(--ah-ignored-bg);border:1px solid var(--ah-ignored-bd); }

/* 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-item--waiting { background:var(--ah-wait-bg); border-color:var(--ah-wait-bd); }
.ah-wl-item--ignored { background:var(--ah-ignored-bg); border-color:var(--ah-ignored-bd); opacity:.7; }
.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-badge--waiting  { color:var(--ah-wait);    background:rgba(255,179,71,.1); border:1px solid rgba(255,179,71,.2); }
.ah-wl-badge--ignored  { color:var(--ah-ignored); background:rgba(107,107,128,.1);border:1px solid rgba(107,107,128,.2); }
.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-item.ah-wl-item--waiting .ah-wl-name { color:var(--ah-wait); }
.ah-wl-item.ah-wl-item--ignored .ah-wl-name { color:var(--ah-ignored); }
.ah-wl-row2  { display:flex;align-items:center;justify-content:flex-start;flex-wrap:wrap;gap:6px; }
.ah-wl-row3  { display:flex;align-items:center;gap:6px;margin-top:6px; }
.ah-wl-action-btn {
  flex:1;padding:4px 0;
  font-family:var(--ah-f);font-size:7.5px;font-weight:700;letter-spacing:1px;text-transform:uppercase;
  color:var(--ah-muted);background:var(--ah-bg3);border:1px solid var(--ah-border);
  border-radius:4px;cursor:pointer;transition:all .15s;
}
.ah-wl-action-btn:hover { color:var(--ah-txt);border-color:var(--ah-border-h); }
.ah-wl-unignore-btn { flex:none;padding:4px 10px; }
.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; }
.ah-wl-item--waiting .ah-wl-link--ripper { color:var(--ah-wait) !important; }
.ah-wl-item--ignored .ah-wl-link--ripper { color:var(--ah-ignored) !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;
}

/* Custom language picker (replaces native select) */
.ah-lang-picker { position:relative; }
.ah-lang-picker-btn {
  display:flex;align-items:center;justify-content:space-between;
  width:100%;box-sizing:border-box;
  padding:8px 10px;
  background:#16161b;border:1px solid rgba(255,255,255,.07);border-radius:var(--ah-r-sm);
  color:var(--ah-txt);
  font-family:system-ui,-apple-system,'Noto Sans JP','Noto Sans','DejaVu Sans',sans-serif;
  font-size:10.5px;cursor:pointer;text-align:left;transition:border-color .15s;
}
.ah-lang-picker-btn:hover,.ah-lang-picker-btn--open {
  border-color:rgba(200,168,255,.4);
}
.ah-lang-picker-btn svg { flex-shrink:0;color:var(--ah-muted);transition:transform .2s; }
.ah-lang-picker-btn--open svg { transform:rotate(180deg); }
.ah-lang-picker-list {
  display:none;position:absolute;left:0;right:0;top:calc(100% + 3px);z-index:9999;
  background:#111115;border:1px solid rgba(200,168,255,.18);border-radius:var(--ah-r-sm);
  box-shadow:0 4px 16px rgba(0,0,0,.55),0 8px 28px rgba(0,0,0,.4);overflow:hidden;
}
.ah-lang-picker-list--open { display:block; }
.ah-lang-picker-opt {
  display:block;width:100%;padding:8px 12px;
  background:none;border:none;border-bottom:1px solid rgba(255,255,255,.04);
  color:var(--ah-txt2);text-align:left;cursor:pointer;
  font-family:system-ui,-apple-system,'Noto Sans JP','Noto Sans','DejaVu Sans',sans-serif;
  font-size:10.5px;transition:background .12s,color .12s;
}
.ah-lang-picker-opt:last-child { border-bottom:none; }
.ah-lang-picker-opt:hover { background:rgba(255,255,255,.06);color:var(--ah-txt); }
.ah-lang-picker-opt--active { color:var(--ah-accent);background:var(--ah-accent-dim); }
.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); }

/* Search mode option buttons in settings */
.ah-set-search-mode-row { display:flex;gap:6px; }
.ah-set-mode-opt {
  flex:1;padding:7px 0;border-radius:var(--ah-r-sm);
  font-family:var(--ah-f);font-size:8.5px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
  cursor:pointer;transition:all .15s;
  background:var(--ah-bg3);border:1px solid var(--ah-border);color:var(--ah-muted);
}
.ah-set-mode-opt:hover { border-color:var(--ah-border-h);color:var(--ah-txt2); }
.ah-set-mode-opt--active {
  background:rgba(200,168,255,.1);border-color:rgba(200,168,255,.3);color:var(--ah-accent);
}

/* Site enable grid in settings */
.ah-set-sites-grid {
  display:flex;flex-wrap:wrap;gap:6px;margin-bottom:14px;
}
.ah-set-site-row {
  display:flex;align-items:center;gap:7px;
  padding:6px 10px;border-radius:var(--ah-r-sm);
  background:var(--ah-bg2);border:1px solid var(--ah-border);
  cursor:pointer;transition:border-color .15s;user-select:none;
}
.ah-set-site-row:hover { border-color:var(--ah-border-h); }
.ah-set-site-row input[type="checkbox"] {
  accent-color:var(--ah-accent);width:12px;height:12px;cursor:pointer;margin:0;flex-shrink:0;
}
.ah-set-site-name { font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:var(--ah-txt2); }
.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; }

/* Tip card */
.ah-cl-tip-card {
  margin:12px 0 4px;
  padding:10px 13px;
  border-radius:5px;
  background:rgba(255,179,71,.04);
  border:1px solid rgba(255,179,71,.12);
  border-left:3px solid rgba(255,179,71,.45);
}
.ah-cl-tip-label {
  font-size:7.5px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
  color:rgba(255,179,71,.65);margin-bottom:6px;
}
.ah-cl-tip-text {
  font-size:9.5px;line-height:1.65;color:rgba(232,232,240,.6);
}

/* 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);
  }

  // ─── Bump Topic popup CSS ─────────────────────────────────────────────────
  function injectBumpTopicCSS() {
    if (document.getElementById("ah-bt-css")) return;
    const s = document.createElement("style");
    s.id = "ah-bt-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
#ah-bumptopic-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-bt-backdrop {
  position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(7px);cursor:default;
}
.ah-bt-dialog {
  position:relative;z-index:1;
  width:420px;max-width:92vw;
  background:#0c0c0e;
  border:1px solid rgba(200,168,255,.18);
  border-radius:12px;
  box-shadow:0 0 0 1px rgba(200,168,255,.05),0 8px 20px rgba(0,0,0,.5),0 32px 80px rgba(0,0,0,.95),inset 0 1px 0 rgba(200,168,255,.07);
  padding:20px 22px 18px;
  animation:ah-bt-in .2s cubic-bezier(.34,1.36,.64,1) both;
}
@keyframes ah-bt-in { from{opacity:0;transform:scale(.9) translateY(10px)} to{opacity:1;transform:scale(1) translateY(0)} }
.ah-bt-icon-row {
  display:flex;align-items:center;gap:9px;margin-bottom:12px;
}
.ah-bt-title {
  font-size:10.5px;font-weight:700;letter-spacing:2px;text-transform:uppercase;
  color:#c8a8ff;text-shadow:0 0 14px rgba(200,168,255,.35);
}
.ah-bt-body {
  font-size:10.5px;color:rgba(232,232,240,.85);line-height:1.65;
  margin-bottom:10px;
}
.ah-bt-sub {
  font-size:9px;color:rgba(200,168,255,.4);line-height:1.6;
  padding:8px 10px;background:rgba(200,168,255,.04);
  border:1px solid rgba(200,168,255,.1);border-radius:6px;
  margin-bottom:16px;
}
.ah-bt-actions { display:flex;gap:8px; }
.ah-bt-no {
  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,.35);
  font-family:'Space Mono',monospace;font-size:8.5px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s;
}
.ah-bt-no:hover:not(:disabled) { background:rgba(255,255,255,.08);color:rgba(255,255,255,.65); }
.ah-bt-no:disabled { opacity:.4;cursor:default; }
.ah-bt-yes {
  flex:1;padding:9px 0;background:rgba(200,168,255,.12);border:1px solid rgba(200,168,255,.3);
  border-radius:6px;color:#c8a8ff;
  font-family:'Space Mono',monospace;font-size:8.5px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s;
}
.ah-bt-yes:hover:not(:disabled) { background:rgba(200,168,255,.22);border-color:rgba(200,168,255,.55); }
.ah-bt-yes:disabled { opacity:.6;cursor:default; }
.ah-bt-status { min-height:16px;margin-top:10px;font-size:9.5px;text-align:center;letter-spacing:.3px; }
.ah-bt-status--ok  { color:#72f0a8; }
.ah-bt-status--err { color:#ff6680; }
`;
    document.head.appendChild(s);
  }

  // ─── Ko-fi random popup CSS ───────────────────────────────────────────────
  function injectKofiRandomCSS() {
    if (document.getElementById("ah-kr-css")) return;
    const s = document.createElement("style");
    s.id = "ah-kr-css";
    s.textContent = `
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');
#ah-kofi-random-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-kr-backdrop {
  position:absolute;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(7px);cursor:default;
}
.ah-kr-dialog {
  position:relative;z-index:1;
  width:420px;max-width:92vw;
  background:#0c0c0e;
  border:1px solid rgba(255,255,255,.08);
  border-left:3px solid #ff5e5b;
  border-radius:12px;
  box-shadow:0 0 0 1px rgba(255,255,255,.02),0 8px 20px rgba(0,0,0,.5),0 32px 80px rgba(0,0,0,.95),inset 0 1px 0 rgba(255,255,255,.04);
  padding:18px 20px 16px;
  animation:ah-kr-in .2s cubic-bezier(.34,1.36,.64,1) both;
}
@keyframes ah-kr-in { from{opacity:0;transform:scale(.9) translateY(10px)} to{opacity:1;transform:scale(1) translateY(0)} }
.ah-kr-header {
  display:flex;align-items:center;gap:8px;margin-bottom:12px;
}
.ah-kr-heart {
  color:#ff5e5b;font-size:14px;line-height:1;
  animation:ah-kofi-pulse 1.35s ease-in-out infinite;
  flex-shrink:0;
}
.ah-kr-title {
  flex:1;font-size:10px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;
  color:#ffb7b6;
}
.ah-kr-x {
  background:none;border:none;color:rgba(255,255,255,.2);font-size:14px;
  cursor:pointer;padding:2px 5px;border-radius:4px;line-height:1;
  transition:color .15s;font-family:'Space Mono',monospace;
}
.ah-kr-x:hover { color:rgba(255,255,255,.7); }
.ah-kr-body {
  font-size:10px;color:#9898a8;line-height:1.65;
  background:#111115;border:1px solid rgba(255,255,255,.06);
  border-radius:8px;padding:10px 12px;margin-bottom:14px;
}
.ah-kr-optout { display:flex;align-items:center;gap:7px;color:#9898a8;font-size:9px;line-height:1.2;margin-bottom:11px;user-select:none; }
.ah-kr-optout input { accent-color:#ff5e5b;width:12px;height:12px;cursor:pointer; }
.ah-kr-actions { display:flex;gap:8px;align-items:center; }
.ah-kr-dismiss {
  padding:9px 14px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);
  border-radius:6px;color:rgba(255,255,255,.35);
  font-family:'Space Mono',monospace;font-size:8.5px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;cursor:pointer;transition:all .15s;white-space:nowrap;
}
.ah-kr-dismiss:hover { background:rgba(255,255,255,.08);color:rgba(255,255,255,.6); }
.ah-kr-kofi-btn {
  flex:1;display:flex;align-items:center;justify-content:center;
  padding:10px 12px;
  background:rgba(255,94,91,.12);border:1px solid rgba(255,94,91,.35);
  border-radius:6px;color:#ff8f8d;
  font-family:'Space Mono',monospace;font-size:9px;font-weight:700;
  letter-spacing:1.5px;text-transform:uppercase;
  text-decoration:none;cursor:pointer;transition:all .15s;
}
.ah-kr-kofi-btn:hover { background:rgba(255,94,91,.22);border-color:rgba(255,94,91,.6);color:#ffb7b6; }
`;
    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;
    if (!isSiteEnabled()) return;
    injectUI();
    const isNewVersion = GM_getValue("ah-changelog-seen", "") !== CURRENT_VERSION;
    maybeShowChangelog();
    if (!isNewVersion) maybeShowKofiRandomPopup();
  }

  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 });

})();