Advanced Word Replacer

Advanced word replacer: cross-node matching, sorted list, auto-merge novel groups, floating UI.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Advanced Word Replacer
// @namespace    http://tampermonkey.net
// @version      99.12.45
// @description  Advanced word replacer: cross-node matching, sorted list, auto-merge novel groups, floating UI.
// @author       You
// @match        *://*/*
// @exclude      *://greatest.deepsurf.us/*
// @exclude      *://sleazyfork.org/*
// @connect      api.github.com
// @connect      github.com
// @connect      update.greatest.deepsurf.us
// @connect      greatest.deepsurf.us
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  const SCRIPT_VERSION =
    typeof GM_info !== "undefined" && GM_info.script && GM_info.script.version
      ? String(GM_info.script.version).trim()
      : "50.0";
  const currentHost = window.location.hostname.toLowerCase();
  const HIGHLIGHT_CLASS = "word-replacer-highlight";
  let observer, replacerTimeout, cloudTitleSyncTimeout;
  let lastProcessedValueMap = new WeakMap(),
    originalTextMap = new WeakMap(),
    hashOriginalTextMap = new WeakMap(); // FIX v54: terpisah dari originalTextMap agar walker utama tidak me-restore node hash

  // Anti-Flicker Rate Limiter: per-node mutation throttle maps
  let nodeLastReplacedTimeMap = new WeakMap();
  let nodeReplaceCountMap = new WeakMap();
  let nodeFlickerLockSet = new WeakSet();
  const NODE_FLICKER_WINDOW_MS = 600;
  const NODE_FLICKER_MAX_COUNT = 2;

  // Cache in-memory untuk active novel ID
  let _activeNovelIdCache = null;
  // Guard untuk mencegah self-mutation loop (flickering)
  let _replacingNow = false;

  let toast,
    toastTimeout,
    cachedToken = "",
    cachedGistId = "";

  let cachedGistDetails = null;
  let cachedUserProfile = null;

  let startX = 0,
    startY = 0;
  let tabAktif = "daftar";
  let settingSubTab = "filter";
  let subjekEdit = null;
  let showOtherTerms = !!GM_getValue("awr_show_other_terms", false);
  // ── FIX Bug 2: Scanner tab persistent state ────────────────────────────────
  // Disimpan di scope modul agar searchQuery/filterMode/sortMode/bulkSelected
  // tidak direset tiap rerenderScanner() -> renderTampilan() dipanggil ulang.
  let _scannerState = { searchQuery: "", filterMode: "all", sortMode: "alpha", bulkSelected: new Set() };
  let panel;

  // ── UI Health Monitor & Watchdog ──────────────────────────────────────────
  // Variabel untuk sistem auto-recovery UI
  let _uiHealthCheckInterval = null;
  let _lastRenderTimestamp = 0;
  let _renderInProgress = false;
  let _renderFailCount = 0;
  let _autoRetryTimer = null;
  let _lastHealthRecoveryTime = 0;
  const UI_HEALTH_CHECK_MS = 2000;     // cek setiap 2 detik
  const UI_FROZEN_THRESHOLD_MS = 8000; // render dianggap macet setelah 8 detik
  const RENDER_FAIL_MAX = 6;           // maks auto-retry sebelum berhenti
  const HEALTH_RECOVERY_COOLDOWN = 3000; // jeda min antar health-monitor recovery

  // ── Undo Stack ─────────────────────────────────────────────────────────────
  // Menyimpan snapshot kamus sebelum setiap operasi destruktif (simpan/hapus/revert)
  let _undoStack = [];
  const UNDO_STACK_MAX = 15; // maksimum entry undo yang disimpan per sesi

  // Watchdog untuk replacer: jika _replacingNow terlalu lama, reset
  let _replacerWatchdogTimer = null;
  const REPLACER_TIMEOUT_MS = 5000; // replacer dianggap macet setelah 5 detik

  function safeRenderTampilan() {
    // Batalkan auto-retry yang sedang terjadwal — kita akan render sekarang
    if (_autoRetryTimer) { clearTimeout(_autoRetryTimer); _autoRetryTimer = null; }

    if (_renderInProgress) {
      const elapsed = Date.now() - _lastRenderTimestamp;
      if (elapsed < UI_FROZEN_THRESHOLD_MS) return; // masih dalam batas wajar
      // Render macet — paksa reset state
      _renderInProgress = false;
      _replacingNow = false;
      console.warn("[AWR] Render macet (", elapsed, "ms), memaksa reset...");
    }
    _renderInProgress = true;
    _lastRenderTimestamp = Date.now();
    try {
      renderTampilan();
      _renderFailCount = 0; // sukses — reset counter
    } catch (err) {
      _renderFailCount++;
      console.error("[AWR] renderTampilan error (#" + _renderFailCount + "):", err);
      // Auto-retry bertahap tanpa tombol: 1s → 2s → 4s → ... (backoff)
      if (_renderFailCount <= RENDER_FAIL_MAX) {
        const delay = Math.min(1000 * Math.pow(1.5, _renderFailCount - 1), 8000);
        console.warn("[AWR] Akan auto-retry dalam", Math.round(delay / 100) / 10, "detik...");
        _autoRetryTimer = setTimeout(() => {
          _autoRetryTimer = null;
          _renderInProgress = false; // pastikan flag bersih sebelum retry
          safeRenderTampilan();
        }, delay);
      } else {
        console.error("[AWR] Terlalu banyak gagal (" + _renderFailCount + "x), berhenti auto-retry.");
        _renderFailCount = 0; // reset agar health monitor bisa coba lagi nanti
      }
    } finally {
      // Hanya reset jika tidak ada auto-retry yang dijadwalkan
      if (!_autoRetryTimer) _renderInProgress = false;
    }
  }

  function startReplacerWatchdog() {
    if (_replacerWatchdogTimer) clearTimeout(_replacerWatchdogTimer);
    _replacerWatchdogTimer = setTimeout(() => {
      if (_replacingNow) {
        console.warn("[AWR] Replacer watchdog: _replacingNow macet, memaksa reset.");
        _replacingNow = false;
      }
    }, REPLACER_TIMEOUT_MS);
  }

  // Simpan snapshot kamus ke undo stack sebelum operasi destruktif
  function pushUndoSnapshot(label) {
    try {
      const snapshot = JSON.stringify(core ? core.getKamus() : {});
      _undoStack.push({ snapshot, label: label || "Perubahan", ts: Date.now() });
      if (_undoStack.length > UNDO_STACK_MAX) _undoStack.shift();
    } catch(e) { console.warn("[AWR] pushUndoSnapshot gagal:", e); }
  }

  function popUndoAndRestore() {
    if (_undoStack.length === 0) {
      panggilToast("⟲ Tidak ada yang bisa di-undo.", "warn");
      return;
    }
    const last = _undoStack.pop();
    try {
      const restored = JSON.parse(last.snapshot);
      core.saveKamus(restored);
      core.simpanKeAwan(restored);
      core.jalankanPengganti(true);
      renderTampilan();
      panggilToast(`⟲ Undo "${last.label}" berhasil dikembalikan.`, "success");
    } catch(e) {
      panggilToast("⟲ Undo gagal: " + (e.message || "error"), "warn");
    }
  }

  function _triggerHealthRecovery(reason) {
    const now = Date.now();
    // Cooldown: jangan recovery lebih sering dari HEALTH_RECOVERY_COOLDOWN
    if (now - _lastHealthRecoveryTime < HEALTH_RECOVERY_COOLDOWN) return;
    if (_renderInProgress || _autoRetryTimer) return;
    _lastHealthRecoveryTime = now;
    console.warn("[AWR] Health recovery:", reason);
    safeRenderTampilan();
  }

  function startUIHealthMonitor(shadowRoot, wrapperEl, hostEl) {
    if (_uiHealthCheckInterval) clearInterval(_uiHealthCheckInterval);
    _uiHealthCheckInterval = setInterval(() => {
      try {
        // 1. Pastikan hostElement masih di document.body
        if (!document.body.contains(hostEl)) {
          console.warn("[AWR] hostElement terlepas dari DOM, menambahkan ulang...");
          document.body.appendChild(hostEl);
        }

        // 2. Pastikan wrapper masih di shadow root
        if (!shadowRoot.contains(wrapperEl)) {
          console.warn("[AWR] wrapper terlepas dari shadow DOM, menambahkan ulang...");
          shadowRoot.appendChild(wrapperEl);
        }

        // 3. Pastikan panel masih di wrapper
        if (panel && !wrapperEl.contains(panel)) {
          console.warn("[AWR] panel terlepas dari wrapper, menambahkan ulang...");
          wrapperEl.appendChild(panel);
        }

        // 4. Cek kesehatan panel jika terlihat (tidak hidden)
        if (panel && !panel.classList.contains("hidden")) {
          const body = panel.querySelector(".replacer-body");
          const hasHeader = panel.querySelector(".replacer-header");

          // Panel sama sekali blank (tidak ada header atau body)
          if (!body || !hasHeader) {
            _triggerHealthRecovery("Panel blank — tidak ada header/body");
            return;
          }

          const activePane = body.querySelector(".tab-pane.active");

          // Tab pane aktif kosong sama sekali
          if (activePane && activePane.childElementCount === 0) {
            _triggerHealthRecovery("Tab pane aktif kosong");
            return;
          }

          // Cek khusus Scanner: pane aktif harus punya .awr-scanner-list
          if (tabAktif === "scanner" && activePane) {
            const hasScannerList = activePane.querySelector(".awr-scanner-list");
            const hasEmptyMsg   = activePane.querySelector("[style*='scanner_empty']");
            // v99.12.36: awr-scanner-error-state = error yang sudah ditangani, BUKAN stuck state
            // Jangan trigger recovery karena akan membuat retry loop
            const hasErrorState = activePane.querySelector(".awr-scanner-error-state");
            // Pane ada tapi tidak punya elemen scanner sama sekali (dan bukan error/empty state)
            if (!hasScannerList && !hasEmptyMsg && !hasErrorState && activePane.childElementCount < 2) {
              _triggerHealthRecovery("Scanner pane tidak punya konten");
              return;
            }
          }
        }

        // 5. Watchdog untuk _replacingNow yang macet
        if (_replacingNow) {
          startReplacerWatchdog();
        }

      } catch (healthErr) {
        console.error("[AWR] Health monitor error:", healthErr);
      }
    }, UI_HEALTH_CHECK_MS);
  }

  const SVGS = {
    pen: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/></svg>`,
    trash: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
    book: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1-2.5-2.5z"/></svg>`,
    recycle: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>`,
    gear: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1-2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1-2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>`,
    cloud: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
    radar: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a10 10 0 0 1 10 10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>`,
    folder: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`,
    eye: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>`,
    link: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>`,
    disk: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>`,
    close: `<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
    undo: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>`,
    copy: `<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
    refresh: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>`,
    lock: `<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`,
  };

  function smartSanitizeTerm(text) {
    if (!text) return "";
    let trimmed = text.trim();
    // Hanya strip titik/koma/titikdua/titik-koma di akhir jika ada huruf/angka sebelumnya
    // TIDAK strip: !, ?, = agar kalimat panjang dengan karakter tsb tetap bisa dicocokkan
    if (/[\p{L}\p{N}]/u.test(trimmed)) {
      trimmed = trimmed.replace(/[.\:;]+$/, "");
    }
    return trimmed.trim();
  }

  function buildRegexPattern(term) {
    if (!term) return "";
    const normalized = term.normalize("NFC").toLowerCase();
    const vowelMap = {
      a: "(?:a|ā|aa)",
      ā: "(?:a|ā|aa)",
      e: "(?:e|ē|ee)",
      ē: "(?:e|ē|ee)",
      i: "(?:i|ī|ii)",
      ī: "(?:i|ī|ii)",
      o: "(?:o|ō|oo|ou)",
      ō: "(?:o|ō|oo|ou)",
      u: "(?:u|ū|uu)",
      ū: "(?:u|ū|uu)",
    };

    // Deteksi term multi-kata (ada spasi)
    const isMultiWord = /\s/.test(normalized);

    // Multi-kata: split hanya di spasi; satu-kata: split di spasi+tanda-hubung
    const parts = isMultiWord
      ? normalized.split(/\s+/)
      : normalized.split(/[\s\-_—–]+/);

    const patternParts = parts.map((part) => {
      const chars = part.split("");
      const escapedChars = chars.map((ch) => {
        if (ch === "*") return ".*?";
        // Vowel alternation HANYA untuk term satu-kata (lebih ringan, kurangi backtracking)
        if (!isMultiWord && vowelMap[ch]) return vowelMap[ch];
        return ch.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
      });
      return escapedChars.join("[\\u200b\\u200c\\u200d\\ufeff]?");
    });

    // Multi-kata: separator mengizinkan spasi, tanda baca, dan zero-width chars
    // FIX: Juga mengizinkan tanda baca (koma, titik, dll.) di antara kata
    // Satu-kata: separator kompleks untuk menangani tanda-hubung & zero-width chars
    const separator = isMultiWord
      ? "([\\s\\u00a0\\u200b\\u200c\\u200d\\ufeff,;:.!?'\"\\-\\u2018\\u2019\\u201c\\u201d]*[\\s\\u00a0\\u200b\\u200c\\u200d\\ufeff][\\s\\u00a0\\u200b\\u200c\\u200d\\ufeff,;:.!?'\"\\-\\u2018\\u2019\\u201c\\u201d]*)"
      : "([\\s\\-_—–\\u200b\\u200c\\u200d\\ufeff\\u00a0]*)";

    return patternParts.join(separator);
  }

  function buildBoundaryRegex(term, pattern, flags = "giu") {
    if (!term) return new RegExp("", flags);
    const normalized = term.normalize("NFC");
    const startsWithWordChar = /^[\p{L}\p{N}]/u.test(normalized);
    const endsWithWordChar = /[\p{L}\p{N}]$/u.test(normalized);
    const prefix = startsWithWordChar ? "(?<![\\p{L}\\b\\p{N}])" : "";
    const suffix = endsWithWordChar ? "(?![\\p{L}\\b\\p{N}])" : "";
    return new RegExp(prefix + pattern + suffix, flags);
  }

  function reconstructReplacement(replacement, capturedSeparators) {
    const repWords = replacement.split(/\s+/);
    const m = repWords.length;
    const numSeps = capturedSeparators.length;

    if (m <= 1) return replacement;

    const sepsToUse = [];
    if (numSeps === 0) {
      for (let i = 0; i < m - 1; i++) sepsToUse.push(" ");
    } else if (m - 1 === numSeps) {
      for (let i = 0; i < numSeps; i++)
        sepsToUse.push(capturedSeparators[i] || " ");
    } else if (m - 1 < numSeps) {
      for (let i = 0; i < m - 1; i++) {
        sepsToUse.push(
          i === m - 2
            ? capturedSeparators.slice(i).join("")
            : capturedSeparators[i] || " ",
        );
      }
    } else {
      for (let i = 0; i < m - 1; i++) {
        sepsToUse.push(i < numSeps ? capturedSeparators[i] || " " : " ");
      }
    }

    let result = "";
    for (let i = 0; i < m; i++) {
      result += repWords[i];
      if (i < m - 1) result += sepsToUse[i];
    }
    return result;
  }

  function unwrapFontTags() {
    if (!document.body) return;
    const fontTags = document.body.querySelectorAll("font");
    if (fontTags.length === 0) return;

    fontTags.forEach((font) => {
      const parent = font.parentNode;
      if (!parent) return;
      while (font.firstChild) {
        parent.insertBefore(font.firstChild, font);
      }
      parent.removeChild(font);
    });
    document.body.normalize();
  }

  function gmFetch(url, options = {}) {
    return new Promise((resolve, reject) => {
      const headers = options.headers || {};
      if (
        options.body &&
        !headers["Content-Type"] &&
        !headers["content-type"]
      ) {
        headers["Content-Type"] = "application/json";
      }
      GM_xmlhttpRequest({
        method: options.method || "GET",
        url: url,
        headers: headers,
        data: options.body || null,
        timeout: 10000,
        onload: function (response) {
          const isSuccess =
            (response.status >= 200 && response.status < 300) ||
            response.status === 302 ||
            response.status === 307;
          resolve({
            ok: isSuccess,
            status: response.status,
            statusText: response.statusText,
            json: () => {
              try {
                return Promise.resolve(JSON.parse(response.responseText));
              } catch (e) {
                return Promise.reject(e);
              }
            },
            text: () => Promise.resolve(response.responseText),
          });
        },
        ontimeout: () => reject(new Error("Request timeout")),
        onerror: (error) => reject(error),
      });
    });
  }

  async function verifyGitHubToken(token) {
    if (!token) return null;
    try {
      const response = await gmFetch(
        `https://api.github.com/user?t=${Date.now()}`,
        {
          method: "GET",
          headers: {
            Authorization: `token ${token}`,
            Accept: "application/vnd.github.v3+json",
          },
        },
      );
      if (response.ok) {
        return await response.json();
      }
    } catch (e) {
      console.error("Token verification failed:", e);
    }
    return null;
  }

  function extractGistId(str) {
    if (!str) return "";
    const clean = str.trim();
    const match =
      clean.match(/\/([a-f0-9]{32,})$/i) || clean.match(/^([a-f0-9]{32,})$/i);
    if (match) return match[1];
    const parts = clean.split("/");
    return parts[parts.length - 1].trim();
  }

  // GitHub credentials & Cloud storage
  function getGitHubToken() {
    if (cachedToken) return cachedToken;
    const val = GM_getValue("awr_github_token");
    return val ? String(val).trim() : "";
  }

  function getGistId() {
    if (cachedGistId) return cachedGistId;
    const val = GM_getValue("awr_gist_id");
    return val ? String(val).trim() : "";
  }

  function saveGitHubCredentials(token, gistId) {
    cachedToken = token.trim();
    cachedGistId = gistId.trim();
    GM_setValue("awr_github_token", cachedToken);
    GM_setValue("awr_gist_id", cachedGistId);
  }

  async function findExistingGist(token) {
    try {
      const response = await gmFetch(
        `https://api.github.com/gists?per_page=100&t=${Date.now()}`,
        {
          method: "GET",
          headers: {
            Authorization: `token ${token}`,
            Accept: "application/vnd.github.v3+json",
          },
        },
      );
      if (response.ok) {
        const gists = await response.json();
        if (Array.isArray(gists)) {
          const targetGist = gists.find(
            (gist) => gist.files && gist.files["awr_replacer_config.json"],
          );
          if (targetGist) return targetGist.id;
        }
      }
    } catch (e) {
      console.error("Gagal mendeteksi Gist otomatis:", e);
    }
    return null;
  }

  async function fetchGistDetails(token, gistId) {
    if (!token || !gistId) return null;
    try {
      const response = await gmFetch(
        `https://api.github.com/gists/${gistId}?t=${Date.now()}`,
        {
          method: "GET",
          headers: {
            Authorization: `token ${token}`,
            Accept: "application/vnd.github.v3+json",
          },
        },
      );
      if (response.ok) return await response.json();
    } catch (e) {
      console.error("Gagal mengambil rincian Gist:", e);
    }
    return null;
  }

  async function createGist(token, payload) {
    if (!token) return null;
    try {
      const body = {
        description: "Advanced Word Replacer Cloud Configuration",
        public: false,
        files: {
          "awr_replacer_config.json": {
            content: JSON.stringify(payload, null, 2),
          },
        },
      };
      const response = await gmFetch("https://api.github.com/gists", {
        method: "POST",
        headers: {
          Authorization: `token ${token}`,
          Accept: "application/vnd.github.v3+json",
        },
        body: JSON.stringify(body),
      });
      if (response.ok) {
        const data = await response.json();
        return data.id;
      }
    } catch (e) {
      console.error("Gagal membuat Gist baru:", e);
    }
    return null;
  }

  async function simpanKeAwan(
    kamus = null,
    domains = null,
    deletedWords = null,
    forceSnapshot = false,
  ) {
    const token = getGitHubToken();
    const gistId = getGistId();
    if (!token || !gistId) return;

    const finalKamus = kamus || getKamus();
    const finalDomains = domains || getTargetDomains();
    const finalBlacklist = getBlacklistDomains();
    const finalFilterMode = getFilterMode();
    const finalNovelTitles = JSON.parse(
      GM_getValue("awr_novel_titles_v2", "{}"),
    );
    const finalDeletedWords = deletedWords || getDeletedWords();

    // Sertakan semua pengaturan pengguna dalam backup cloud
    const payload = {
      kamus: finalKamus,
      domains: finalDomains,
      blacklist: finalBlacklist,
      filterMode: finalFilterMode,
      novelTitles: finalNovelTitles,
      deletedWords: finalDeletedWords,
      highlightAktif: GM_getValue("highlight_aktif_v4", true),
      lang: GM_getValue("awr_lang_v1", "id"),
      updateMode: GM_getValue("awr_update_mode_v1", "auto"),
      recycleAutoDeleteDays: GM_getValue("awr_recycle_auto_delete_days", "30"),
      showOtherTerms: GM_getValue("awr_show_other_terms", false),
    };

    const payloadStr = JSON.stringify(payload, null, 2);
    const filesToUpload = {
      "awr_replacer_config.json": { content: payloadStr },
    };

    if (forceSnapshot) {
      filesToUpload[getBackupFilename()] = { content: payloadStr };
    }

    try {
      const response = await gmFetch(`https://api.github.com/gists/${gistId}`, {
        method: "PATCH",
        headers: {
          Authorization: `token ${token}`,
          Accept: "application/vnd.github.v3+json",
        },
        body: JSON.stringify({ files: filesToUpload }),
      });
      if (response.ok) {
        const updatedDetails = await response.json();
        cachedGistDetails = updatedDetails;
      }
    } catch (e) {
      console.error("Kesalahan koneksi saat menyimpan ke awan:", e);
    }
  }

  async function sinkronisasiDariAwan(forceUpdate = false) {
    const token = getGitHubToken();
    const gistId = getGistId();
    if (!token || !gistId) return;

    try {
      const details = await fetchGistDetails(token, gistId);
      if (
        details &&
        details.files &&
        details.files["awr_replacer_config.json"]
      ) {
        let contentText = details.files["awr_replacer_config.json"].content;
        if (!contentText && details.files["awr_replacer_config.json"].raw_url) {
          const rawRes = await gmFetch(
            details.files["awr_replacer_config.json"].raw_url,
          );
          if (rawRes.ok) contentText = await rawRes.text();
        }
        if (contentText) {
          const parsed = JSON.parse(contentText);
          if (parsed) {
            if (parsed.kamus)
              GM_setValue("kamus_kata_v5", JSON.stringify(parsed.kamus));
            if (parsed.domains)
              GM_setValue("target_domains_v4", JSON.stringify(parsed.domains));
            if (parsed.blacklist)
              GM_setValue(
                "blacklist_domains_v1",
                JSON.stringify(parsed.blacklist),
              );
            if (parsed.filterMode)
              GM_setValue("filter_mode_v1", parsed.filterMode);
            if (parsed.deletedWords)
              GM_setValue(
                "awr_deleted_words_v1",
                JSON.stringify(parsed.deletedWords),
              );
            if (parsed.novelTitles)
              GM_setValue(
                "awr_novel_titles_v2",
                JSON.stringify(parsed.novelTitles),
              );
            // Pulihkan semua pengaturan tambahan dari cloud
            if (typeof parsed.highlightAktif !== "undefined")
              GM_setValue("highlight_aktif_v4", !!parsed.highlightAktif);
            if (parsed.lang)
              GM_setValue("awr_lang_v1", parsed.lang);
            if (parsed.updateMode)
              GM_setValue("awr_update_mode_v1", parsed.updateMode);
            if (parsed.recycleAutoDeleteDays !== undefined)
              GM_setValue("awr_recycle_auto_delete_days", String(parsed.recycleAutoDeleteDays));
            if (typeof parsed.showOtherTerms !== "undefined") {
              GM_setValue("awr_show_other_terms", !!parsed.showOtherTerms);
              showOtherTerms = !!parsed.showOtherTerms;
            }
            cachedGistDetails = details;
            // Sinkronisasi cloud TIDAK mengubah grup aktif yang dipilih pengguna

            if (forceUpdate) jalankanPengganti(true);
          }
        }
      }
    } catch (e) {
      console.error("Gagal sinkronisasi data dari awan:", e);
    }
  }

  function getBackupFilename() {
    const now = new Date();
    const pad = (num) => String(num).padStart(2, "0");
    return `backup_${now.getFullYear()}_${pad(now.getMonth() + 1)}_${pad(now.getDate())}_${pad(now.getHours())}_${pad(now.getMinutes())}_${pad(now.getSeconds())}.json`;
  }

  function parseBackupFilename(filename) {
    const match = filename.match(
      /^backup_(\d{4})_(\d{2})_(\d{2})_(\d{2})_(\d{2})_(\d{2})\.json$/,
    );
    if (match) {
      const [_, yyyy, mm, dd, hh, min, ss] = match;
      return `${dd}/${mm}/${yyyy}, ${hh}:${min}:${ss}`;
    }
    return filename;
  }

  function getActiveNovelId() {
    if (_activeNovelIdCache !== null) return _activeNovelIdCache;
    _activeNovelIdCache = GM_getValue("awr_active_novel_id_v1", "");
    return _activeNovelIdCache;
  }
  function setActiveNovelId(id) {
    _activeNovelIdCache = null; // Invalidate cache first to prevent race condition [1]
    GM_setValue("awr_active_novel_id_v1", id);
    _activeNovelIdCache = id;
  }

  function getDeletedWords() {
    try {
      const val = GM_getValue("awr_deleted_words_v1");
      return val ? JSON.parse(val) : {};
    } catch (e) {
      return {};
    }
  }

  function saveDeletedWords(obj) {
    try {
      GM_setValue("awr_deleted_words_v1", JSON.stringify(obj));
    } catch (e) {
      console.error("Gagal menyimpan kata terhapus", e);
    }
  }

  function bersihkanRecycleBinOtomatis() {
    const limitDays = parseInt(
      GM_getValue("awr_recycle_auto_delete_days", "30"),
    );
    if (limitDays <= 0) return;

    const deletedWords = getDeletedWords(),
      now = Date.now(),
      msLimit = limitDays * 24 * 60 * 60 * 1000;
    let hasChanges = false;

    for (const key in deletedWords) {
      const item = deletedWords[key];
      if (item && item.deletedAt) {
        if (now - item.deletedAt >= msLimit) {
          delete deletedWords[key];
          hasChanges = true;
        }
      } else if (item) {
        item.deletedAt = now;
        hasChanges = true;
      }
    }

    if (hasChanges) {
      saveDeletedWords(deletedWords);
      simpanKeAwan(null, null, deletedWords);
    }
  }

  async function exportCredentials() {
    const token = getGitHubToken();
    const gistId = getGistId();
    const tanggal = new Date().toLocaleString("id-ID");
    const isi = [
      "// AWR Credentials - Advanced Word Replacer",
      "// Dibuat: " + tanggal,
      "// JANGAN bagikan file ini kepada siapapun!",
      "",
      'var AWR_TOKEN = ' + JSON.stringify(token) + ';',
      'var AWR_GIST_ID = ' + JSON.stringify(gistId) + ';',
      ""
    ].join("\n");

    // Coba showSaveFilePicker agar user bisa memilih lokasi simpan
    if (typeof window.showSaveFilePicker === "function") {
      try {
        const handle = await window.showSaveFilePicker({
          suggestedName: "awr_credentials.txt",
          types: [
            { description: "File Teks (.txt)", accept: { "text/plain": [".txt"] } },
            { description: "File JavaScript (.js)", accept: { "text/javascript": [".js"] } },
          ],
        });
        const writable = await handle.createWritable();
        await writable.write(isi);
        await writable.close();
        return;
      } catch (err) {
        if (err.name === "AbortError") return; // dibatalkan user
        // Gagal selain dibatalkan — lanjut ke fallback
      }
    }

    // Fallback: browser tidak mendukung dialog — unduh ke folder Downloads
    const blob = new Blob([isi], { type: "text/javascript" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "awr_credentials.js";
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    panggilToast("⚠️ Browser tidak mendukung dialog lokasi. File diunduh ke folder Downloads.", "info");
  }

  function compareVersions(v1, v2) {
    const parts1 = v1.split(".").map(Number);
    const parts2 = v2.split(".").map(Number);
    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
      const p1 = parts1[i] || 0;
      const p2 = parts2[i] || 0;
      if (p1 > p2) return 1;
      if (p1 < p2) return -1;
    }
    return 0;
  }

  function checkForUpdates(manual = false) {
    if (manual) panggilToast(t("toast_checking_update"), "info");
    GM_xmlhttpRequest({
      method: "GET",
      url:
        "https://update.greatest.deepsurf.us/scripts/580034/Advanced%20Word%20Replacer.meta.js?t=" +
        Date.now(),
      onload: function (response) {
        if (response.status === 200) {
          const match = response.responseText.match(/@version\s+([\d.]+)/);
          if (match) {
            const latestVersion = match[1].trim();
            const currentVersion = SCRIPT_VERSION.trim();
            if (compareVersions(latestVersion, currentVersion) > 0) {
              if (
                typeof window.AWR_UI_LIBRARY !== "undefined" &&
                typeof window.AWR_UI_LIBRARY.tampilkanKonfirmasi === "function"
              ) {
                window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
                  t("modal_update_title"),
                  t("modal_update_desc", latestVersion),
                  () => {
                    window.open(
                      "https://update.greatest.deepsurf.us/scripts/580034/Advanced%20Word%20Replacer.user.js",
                      "_blank",
                    );
                  },
                );
              } else {
                if (confirm(t("modal_update_desc", latestVersion))) {
                  window.open(
                    "https://update.greatest.deepsurf.us/scripts/580034/Advanced%20Word%20Replacer.user.js",
                    "_blank",
                  );
                }
              }
            } else {
              if (manual)
                panggilToast(
                  t("toast_already_latest", currentVersion),
                  "success",
                );
            }
          }
        } else if (manual) {
          panggilToast("Gagal memeriksa pembaruan.", "warn");
        }
      },
      onerror: () => {
        if (manual) panggilToast("Gagal memeriksa pembaruan.", "warn");
      },
    });
  }

  function isInsideNativeGlossary(el) {
    if (!el) return false;
    const element = el.nodeType === Node.TEXT_NODE ? el.parentElement : el;
    if (!element) return false;
    return !!element.closest(
      // Selektor umum (dibatasi ke inline elements agar tidak salah deteksi wrapper/parent)
      ".wtr-glossary, [data-term-id], [data-term], .glossary-term, .translation-term, [data-translation], .term-tooltip, .wtr-term, [data-wtr-term]," +
      // wtr-lab.com kata unik: HANYA span dengan data-slot="popover-trigger" (Radix UI popover trigger).
      // Dibatasi ke span agar wrapper <div>/<p>/<article> yang juga mungkin punya data-hash tidak
      // menyebabkan seluruh konten paragraf dianggap "di dalam glosari" dan skip semua penggantian.
      "span[data-slot='popover-trigger'], span[data-hash].text-patch",
    );
  }

  function getNovelBaseDomain(host) {
    if (!host) return "";
    let clean = host.toLowerCase().trim();
    let parts = clean.split(".");
    if (parts.length <= 2) return clean;
    const chapterPrefixRegex =
      /^(www|m|ch(apter)?-\d+|c\d+|vol(ume)?-\d+|\d+|vol(ume)?-\d+-ch(apter)?-\d+|ch(apter)?-\d+-vol(ume)?-\d+)$/i;
    while (parts.length > 2 && chapterPrefixRegex.test(parts[0])) {
      parts.shift();
    }
    return parts.join(".");
  }

  function titlesMatchFuzzy(title1, title2) {
    if (!title1 || !title2) return false;
    const t1 = title1.toLowerCase().trim();
    const t2 = title2.toLowerCase().trim();
    if (t1 === t2) return true;
    const stopWords = new Set([
      "a","an","the","of","in","on","at","to","for","by","and","or","is","its",
      "it","that","this","my","your","i","we","are","was","be","has","with","from"
    ]);
    function extractKeywords(t) {
      return t.split(/[\s\-_,.:;!?()[\]{}'"\/\\]+/)
        .map(w => w.replace(/[^a-z0-9\u00C0-\u024F\u4e00-\u9fff\u3040-\u30ff]/gi, "").toLowerCase())
        .filter(w => w.length > 2 && !stopWords.has(w));
    }
    const kw1 = extractKeywords(t1);
    const kw2 = extractKeywords(t2);
    if (kw1.length === 0 || kw2.length === 0) return false;
    const set1 = new Set(kw1);
    const set2 = new Set(kw2);
    let matchCount = 0;
    for (const w of set1) { if (set2.has(w)) matchCount++; }
    const minLen = Math.min(set1.size, set2.size);
    return matchCount >= 1 && matchCount >= Math.ceil(minLen * 0.6);
  }

  function sameNovelByUrl(url1, url2) {
    if (!url1 || !url2) return false;
    try {
      const toAbs = (u) => u.startsWith("http") ? u : "https://" + u;
      const u1 = new URL(toAbs(url1));
      const u2 = new URL(toAbs(url2));
      if (u1.hostname.replace(/^www\./, "") !== u2.hostname.replace(/^www\./, "")) return false;

      const NOVEL_KEYWORDS = new Set([
        "novel", "series", "serie", "book", "b",
        "story", "fiction", "f", "read", "manga", "manhua", "manhwa"
      ]);
      const isLocale = (s) =>
        /^[a-z]{1,3}(-[a-z]{2,4})?$/i.test(s) && !NOVEL_KEYWORDS.has(s.toLowerCase());

      function extractNovelKey(pathname) {
        const parts = pathname.split("/").filter(Boolean);
        let i = 0;
        while (i < parts.length && isLocale(parts[i])) i++;
        for (; i < parts.length - 1; i++) {
          if (NOVEL_KEYWORDS.has(parts[i].toLowerCase())) {
            return parts[i + 1].toLowerCase();
          }
        }
        i = 0;
        while (i < parts.length && isLocale(parts[i])) i++;
        return (parts[i] || "").toLowerCase();
      }

      const key1 = extractNovelKey(u1.pathname);
      const key2 = extractNovelKey(u2.pathname);
      return !!(key1 && key2 && key1 === key2);
    } catch (_) {
      return false;
    }
  }

  function getCachedNovelTitle(novelId) {
    if (!novelId) return "";
    try {
      const cacheVal = GM_getValue("awr_novel_titles_v2", "{}");
      const cache =
        typeof cacheVal === "string" ? JSON.parse(cacheVal) : cacheVal;
      const rawTitle = cache[novelId] || "";
      const cleaned = cleanTitleText(rawTitle);

      if (cleaned && cleaned !== rawTitle) {
        cache[novelId] = cleaned;
        GM_setValue("awr_novel_titles_v2", JSON.stringify(cache));
      }
      return cleaned;
    } catch (e) {
      return "";
    }
  }

  function triggerLazyTitleSync() {
    if (cloudTitleSyncTimeout) clearTimeout(cloudTitleSyncTimeout);
    cloudTitleSyncTimeout = setTimeout(() => {
      simpanKeAwan();
    }, 5000);
  }

  function saveCachedNovelTitle(novelId, title) {
    if (!novelId || !title) return;
    try {
      const cacheVal = GM_getValue("awr_novel_titles_v2", "{}");
      const cache =
        typeof cacheVal === "string" ? JSON.parse(cacheVal) : cacheVal;
      if (cache[novelId] !== title) {
        cache[novelId] = title;
        GM_setValue("awr_novel_titles_v2", JSON.stringify(cache));
        triggerLazyTitleSync();
      }
    } catch (e) {
      console.error("Gagal menyimpan cache judul", e);
    }
  }

  function cleanTitleText(str) {
    if (!str) return "";
    let parts = str
      .split(/\s+[-|–]\s+|\s*\|\s*/)
      .map((p) => p.trim())
      .filter(Boolean);
    let novelParts = parts.filter((p) => {
      let pLower = p.toLowerCase().trim();
      if (/^(chapter|ch|chap|volume|vol|bab|b\.)\.?\s*\d+/i.test(p))
        return false;
      if (/^\d+$/.test(p)) return false;
      if (
        pLower === "wtr-lab" ||
        pLower === "wtr" ||
        pLower === "lab" ||
        pLower === "wtr lab" ||
        pLower === "mtl" ||
        pLower === "lightnovel"
      )
        return false;
      if (
        /^(webnovel|wuxiaworld|novelupdates|qidian|readlightnovel|novelhall|boxnovel|royalroad|scribblehub|wattpad|story|novel|book|b|lightnovel|fiction|series)$/i.test(
          p,
        )
      )
        return false;
      return true;
    });

    if (novelParts.length > 0) {
      let t = novelParts[0];
      t = t.replace(/^\s*Read\s+/i, "");
      t = t.replace(
        /\s+RAW\s+(Indonesia|English|Spanish|German|Japanese)?\s+Translation\s*$/i,
        "",
      );
      t = t.replace(
        /\s+(Indonesia|English|Spanish|German|Japanese)?\s+Translation\s*$/i,
        "",
      );
      t = t.replace(/\s+RAW\s*$/i, "");
      t = t.replace(/\s+(online|free|novel|chapter|b\.)\s*$/i, "");

      t = t.replace(
        /\s*[:-|–|—]*\s*\b(?:chapter|ch|chap|volume|vol|bab|b\.|c|episode|ep)\.?\s*\d+\b.*/i,
        "",
      );
      t = t.replace(/\s+[:-|–|—]\s*\d+\s*$/i, "");
      t = t.replace(
        /^\s*(?:chapter|ch|chap|volume|vol|bab|b\.|c|episode|ep)\.?\s*\d+\s*[:-|–|—]*\s*/i,
        "",
      );

      return t.trim();
    }
    return "";
  }

  function extractNovelTitleFromDOM() {
    const metaSelectors = [
      'meta[property="og:novel:book_name"]',
      'meta[name="novel:book_name"]',
      'meta[name="book_name"]',
      'meta[property="og:title"]',
    ];
    for (const sel of metaSelectors) {
      const el = document.querySelector(sel);
      if (el && el.getAttribute("content")) {
        const clean = cleanTitleText(el.getAttribute("content"));
        if (clean && clean.length > 2) return clean;
      }
    }

    const breadcrumbs = document.querySelectorAll(
      '.breadcrumb-item a, .breadcrumb a, [class*="breadcrumb"] a, .breadcrumbs a',
    );
    for (const a of breadcrumbs) {
      const text = a.textContent.trim(),
        href = a.getAttribute("href") || "";
      if (
        text &&
        href &&
        (href.includes("/novel/") ||
          href.includes("/book/") ||
          href.includes("/story/") ||
          href.includes("/fiction/") ||
          href.includes("/series/"))
      ) {
        const clean = cleanTitleText(text);
        if (clean && clean.length > 2) return clean;
      }
    }

    const classPatterns = [
      ".book-title",
      ".novel-title",
      ".story-title",
      ".series-title",
      ".series-name",
      ".seriestitle",
      ".fic-title",
      ".fic_title",
      ".book-name",
      ".novel-name",
      ".title-book",
      "h1",
    ];
    for (const sel of classPatterns) {
      const el = document.querySelector(sel);
      if (el) {
        if (
          el.closest(
            "header, .header, #header, .navbar, .nav, .logo, #logo, footer, .footer, #footer",
          )
        )
          continue;
        if (el.textContent.trim()) {
          const clean = cleanTitleText(el.textContent);
          if (clean && clean.length > 2) return clean;
        }
      }
    }
    return cleanTitleText(document.title);
  }

  function getNovelContext() {
    const url = window.location.href,
      host = window.location.hostname.toLowerCase();
    let novelId = "",
      novelTitle = "",
      novelUrl = url;

    const storyMatch =
      url.match(/(https?:\/\/[^\/]+.*?\/story\/\d+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/book\/.*?_\d+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/book\/\d+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/novel\/\d+\/[^/]+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/novel\/\d+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/novel\/[^/]+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/fiction\/\d+)/i) ||
      url.match(/(https?:\/\/[^\/]+.*?\/series\/\d+)/i);

    if (storyMatch) {
      novelId = storyMatch[1].toLowerCase().trim();
    } else {
      const pathParts = window.location.pathname.split("/").filter(Boolean);
      const idx = pathParts.findIndex((p) =>
        ["novel", "series", "book", "b", "story", "fiction", "f"].includes(
          p.toLowerCase(),
        ),
      );
      if (idx !== -1 && pathParts[idx + 1]) {
        novelId = `${host}_novel_${pathParts[idx + 1]}`;
      } else if (pathParts.length > 0) {
        novelId = `${host}_novel_${pathParts[0]}`;
      } else {
        novelId = host;
      }
    }

    novelTitle = getCachedNovelTitle(novelId);
    if (!novelTitle) {
      const extracted = extractNovelTitleFromDOM();
      if (extracted && extracted.length > 2) {
        novelTitle = extracted;
        saveCachedNovelTitle(novelId, novelTitle);
      }
    }
    if (!novelTitle) {
      const lastPart = novelId.split("/").pop() || novelId.split("_").pop();
      novelTitle = lastPart
        .replace(/[-_]/g, " ")
        .replace(/\b\w/g, (c) => c.toUpperCase());
    }

    return { id: novelId, title: novelTitle, url: novelUrl };
  }

  function getKamus() {
    try {
      let val = GM_getValue("kamus_kata_v5");
      let kamus = val ? JSON.parse(val) : null;
      if (kamus) {
        let cleaned = {},
          hasChanges = false;
        for (const key in kamus) {
          // hash: entries must preserve their original casing (data-hash values are case-sensitive)
          const normalizedKey = key.startsWith("hash:")
            ? key.normalize("NFC").trim()
            : key.normalize("NFC").trim().toLowerCase();
          if (key !== normalizedKey) hasChanges = true;
          if (!cleaned[normalizedKey]) {
            let entryVal = kamus[key];
            if (entryVal && typeof entryVal === "object" && entryVal.novelId) {
              const normNId = entryVal.novelId.normalize("NFC").trim().toLowerCase();
              if (normNId !== entryVal.novelId) {
                hasChanges = true;
                entryVal = Object.assign({}, entryVal, { novelId: normNId });
              }
            }
            cleaned[normalizedKey] = entryVal;
          }
        }
        if (hasChanges) {
          GM_setValue("kamus_kata_v5", JSON.stringify(cleaned));
          kamus = cleaned;
        }
        return kamus;
      }

      const v4Val = GM_getValue("kamus_kata_v4");
      if (v4Val) {
        const v4 = JSON.parse(v4Val),
          v5 = {};
        for (const key in v4) {
          v5[key.normalize("NFC").trim().toLowerCase()] = {
            to: v4[key],
            global: true,
            domain: getNovelBaseDomain(currentHost),
          };
        }
        GM_setValue("kamus_kata_v5", JSON.stringify(v5));
        return v5;
      }

      const defaultKamus = {
        silahkan: { to: "silakan", global: true, domain: "wikipedia.org" },
        wikipedia: {
          to: "Ensiklopedia Bebas",
          global: true,
          domain: "wikipedia.org",
        },
        salah: { to: "keliru", global: true, domain: "detik.com" },
      };
      GM_setValue("kamus_kata_v5", JSON.stringify(defaultKamus));
      return defaultKamus;
    } catch (e) {
      return {};
    }
  }

  function saveKamus(obj) {
    try {
      GM_setValue("kamus_kata_v5", JSON.stringify(obj));
    } catch (e) {
      console.error("Gagal menyimpan kamus v5", e);
    }
  }

  // remapMode: "auto" (smart match), "force" (paksa remap ke grup aktif), "keep" (pertahankan ID asli)
  function mergeKamus(backupKamus, backupNovelTitles, remapMode = "auto") {
    const currentKamus = getKamus();
    const activeNovelId = getActiveNovelId();
    const currentNovel = getNovelContext();
    // Target ID untuk remap: grup yang sedang aktif diprioritaskan
    const targetId = (activeNovelId && activeNovelId !== "GLOBAL_ONLY")
      ? activeNovelId.normalize("NFC").trim().toLowerCase()
      : (currentNovel.id || "").normalize("NFC").trim().toLowerCase();

    const localTitlesCache = (() => {
      try { return JSON.parse(GM_getValue("awr_novel_titles_v2", "{}")); } catch (e) { return {}; }
    })();
    const combinedTitles = Object.assign({}, localTitlesCache, backupNovelTitles || {});

    let mergedCount = 0;
    for (const key in backupKamus) {
      const normKey = key.normalize("NFC").trim().toLowerCase();
      // Skip jika kata sudah ada di kamus lokal
      if (currentKamus[normKey]) continue;

      const srcEntry = backupKamus[key];
      let entry = (srcEntry && typeof srcEntry === "object")
        ? Object.assign({}, srcEntry)
        : srcEntry;

      if (entry && typeof entry === "object" && entry.novelId) {
        entry.novelId = entry.novelId.normalize("NFC").trim().toLowerCase();
      }

      if (entry && typeof entry === "object" && entry.novelId && !entry.global) {
        const entryId = entry.novelId;

        if (remapMode === "force" && targetId) {
          // Paksa remap semua ID ke grup aktif
          entry.novelId = targetId;
          entry.novelTitle = entry.novelTitle || currentNovel.title || "";
        } else if (remapMode === "auto") {
          // Smart remap: coba cocokan via judul/URL
          if (entryId !== targetId) {
            const backupTitle = (
              entry.novelTitle || combinedTitles[entryId] || ""
            ).toLowerCase().trim();
            const currentTitle = (currentNovel.title || "").toLowerCase().trim();

            if (titlesMatchFuzzy(backupTitle, currentTitle)) {
              entry.novelId = targetId || entryId;
              if (!entry.novelTitle) entry.novelTitle = currentNovel.title;
            } else if (sameNovelByUrl(entryId, currentNovel.id || "")) {
              entry.novelId = targetId || entryId;
              if (!entry.novelTitle) entry.novelTitle = currentNovel.title;
            } else if (entry.novelUrl && sameNovelByUrl(entry.novelUrl, currentNovel.url || currentNovel.id || "")) {
              entry.novelId = targetId || entryId;
              if (!entry.novelTitle) entry.novelTitle = currentNovel.title;
            }
          }
        }
        // remapMode === "keep": tidak ada perubahan ID, pertahankan ID asli
      }

      currentKamus[normKey] = entry;
      mergedCount++;
    }
    saveKamus(currentKamus);
    simpanKeAwan(currentKamus);
    return mergedCount;
  }

  function perbaikiIdNovel() {
    const kamus = getKamus();
    const currentNovel = getNovelContext();
    const pageTitle = (currentNovel.title || "").toLowerCase().trim();
    if (!pageTitle || !currentNovel.id) return 0;

    const titlesCache = (() => {
      try { return JSON.parse(GM_getValue("awr_novel_titles_v2", "{}")); } catch(e) { return {}; }
    })();

    let count = 0;
    let berubah = false;
    for (const key in kamus) {
      const entry = kamus[key];
      if (!entry || typeof entry !== "object" || entry.global || !entry.novelId) continue;
      if (entry.novelId === currentNovel.id) continue;

      const entryTitle = (
        entry.novelTitle ||
        titlesCache[entry.novelId] ||
        getCachedNovelTitle(entry.novelId) ||
        ""
      ).toLowerCase().trim();

      if (titlesMatchFuzzy(entryTitle, pageTitle)) {
        entry.novelId = currentNovel.id;
        if (!entry.novelTitle) entry.novelTitle = currentNovel.title;
        kamus[key] = entry;
        count++;
        berubah = true;
      }
    }

    if (berubah) {
      saveKamus(kamus);
      simpanKeAwan(kamus);
    }
    return count;
  }

  function getTargetDomains() {
    try {
      return JSON.parse(
        GM_getValue(
          "target_domains_v4",
          JSON.stringify([
            "wtr-lab.com","webnovel.com","novelbin.com","novelbin.net",
            "novelupdates.com","wuxiaworld.com","royalroad.com","scribblehub.com",
            "lightnovelworld.com","mtlnovel.com","novelhall.com","boxnovel.com",
            "readlightnovel.me","noveltranslate.com","readnovelfull.com",
            "zinnovel.com","novelsonline.net","fanfiction.net",
            "archiveofourown.org","wattpad.com"
          ]),
        ),
      );
    } catch (e) {
      return [
        "wtr-lab.com","webnovel.com","novelbin.com","novelbin.net",
        "novelupdates.com","wuxiaworld.com","royalroad.com","scribblehub.com",
        "lightnovelworld.com","mtlnovel.com","novelhall.com","boxnovel.com",
        "readlightnovel.me","noveltranslate.com","readnovelfull.com",
        "zinnovel.com","novelsonline.net","fanfiction.net",
        "archiveofourown.org","wattpad.com"
      ];
    }
  }

  function saveTargetDomains(domains) {
    try {
      GM_setValue("target_domains_v4", JSON.stringify(domains));
    } catch (e) {
      console.error("Gagal domain whitelist", e);
    }
  }

  function getBlacklistDomains() {
    try {
      return JSON.parse(
        GM_getValue(
          "blacklist_domains_v1",
          '["google.com", "facebook.com", "youtube.com"]',
        ),
      );
    } catch (e) {
      return ["google.com", "facebook.com", "youtube.com"];
    }
  }

  function saveBlacklistDomains(domains) {
    try {
      GM_setValue("blacklist_domains_v1", JSON.stringify(domains));
    } catch (e) {
      console.error("Gagal domain blacklist", e);
    }
  }

  function getFilterMode() {
    return GM_getValue("filter_mode_v1", "whitelist");
  }
  function saveFilterMode(mode) {
    GM_setValue("filter_mode_v1", mode);
  }

  function getKamusAktif(domainAllowed) {
    const kamus = getKamus();
    const aktif = {};
    const currentNovel = getNovelContext();
    const pageBaseDomain = getNovelBaseDomain(currentHost);
    const activeNovelId = getActiveNovelId();

    // Mode GLOBAL_ONLY: hanya aktifkan kata global
    if (activeNovelId === "GLOBAL_ONLY") {
      for (const salah in kamus) {
        if (salah.startsWith("_off_:")) continue; // v99.12.37: rule dinonaktifkan sementara
        const item = kamus[salah];
        if (item && typeof item === "object" && item.global) {
          const toVal = typeof item.to === "string" ? item.to : "";
          aktif[salah] = { to: toVal, cs: !!(item.caseSensitive) };
        }
      }
      return aktif;
    }

    // ID efektif: jika grup diaktifkan secara eksplisit, gunakan itu; jika tidak, gunakan ID halaman saat ini
    const effectiveId = activeNovelId
      ? activeNovelId.normalize("NFC").trim().toLowerCase()
      : (currentNovel.id || "").normalize("NFC").trim().toLowerCase();

    for (const salah in kamus) {
      if (salah.startsWith("_off_:")) continue; // v99.12.37: rule dinonaktifkan sementara
      const item = kamus[salah];
      if (!item) continue;

      // Entri lama (plain string)
      if (typeof item === "string") {
        aktif[salah] = { to: item, cs: false };
        continue;
      }
      if (typeof item !== "object") continue;

      const toVal = typeof item.to === "string" ? item.to : "";
      const cs = !!(item.caseSensitive);

      // Kata global: selalu aktif (ketika domain diizinkan)
      if (item.global) {
        aktif[salah] = { to: toVal, cs };
        continue;
      }

      // Kata spesifik novel/grup
      if (item.novelId) {
        const itemId = item.novelId.normalize("NFC").trim().toLowerCase();

        // Cocokkan ID langsung (termasuk grup yang diaktifkan secara eksplisit)
        if (effectiveId && itemId === effectiveId) {
          aktif[salah] = { to: toVal, cs };
          continue;
        }

        // FIX v53 (Hipotesis D): Kembalikan guard !activeNovelId untuk fuzzy match,
        // tapi tetap aktifkan jika item berasal dari domain yang sama dengan halaman saat ini.
        // Ini mencegah aturan dari novel lain yang judulnya mirip ikut aktif, tapi tetap
        // mendukung kasus "nama novel sama, ID berbeda, situs sama" (seperti WebNovel, wtr-lab).
        const sameHostDomain = item.domain &&
          getNovelBaseDomain(item.domain) === getNovelBaseDomain(currentHost);
        if (!activeNovelId || sameHostDomain) {
          const itemTitle = (item.novelTitle || getCachedNovelTitle(itemId) || "").toLowerCase().trim();
          const pageTitle = (currentNovel.title || "").toLowerCase().trim();
          if (itemTitle && pageTitle && titlesMatchFuzzy(itemTitle, pageTitle)) {
            aktif[salah] = { to: toVal, cs };
            continue;
          }
          if (sameNovelByUrl(itemId, currentNovel.id || window.location.href)) {
            aktif[salah] = { to: toVal, cs };
            continue;
          }
          if (item.novelUrl && sameNovelByUrl(item.novelUrl, currentNovel.url || window.location.href)) {
            aktif[salah] = { to: toVal, cs };
            continue;
          }
        }
        continue;
      }

      // Kata berbasis domain (tidak ada novelId, tidak global)
      if (domainAllowed && item.domain) {
        const termBaseDomain = getNovelBaseDomain(item.domain);
        if (
          termBaseDomain === pageBaseDomain ||
          (termBaseDomain && pageBaseDomain.endsWith("." + termBaseDomain))
        ) {
          aktif[salah] = { to: toVal, cs };
        }
      }
    }
    return aktif;
  }

  function getHighlightAktif() {
    return GM_getValue("highlight_aktif_v4", true);
  }
  function saveHighlightAktif(val) {
    GM_setValue("highlight_aktif_v4", val);
  }

  function injectHighlightStyle() {
    if (document.getElementById("awr-highlight-style")) return;
    const s = document.createElement("style");
    s.id = "awr-highlight-style";
    s.textContent = [
      "." + HIGHLIGHT_CLASS + " {",
      "  color: #3b82f6 !important;",
      "  font-weight: bold !important;",
      "  cursor: pointer !important;",
      "  background: rgba(59,130,246,0.08) !important;",
      "  border-radius: 2px !important;",
      "  padding: 0 1px !important;",
      "  outline: none !important;",
      "  text-decoration: none !important;",
      "}",
    ].join("\n");
    (document.head || document.documentElement).appendChild(s);
  }

  const NOVEL_SITE_PRESETS = [
    "wtr-lab.com",
    "webnovel.com",
    "novelbin.com",
    "novelbin.net",
    "novelupdates.com",
    "wuxiaworld.com",
    "royalroad.com",
    "scribblehub.com",
    "lightnovelworld.com",
    "mtlnovel.com",
    "novelhall.com",
    "boxnovel.com",
    "readlightnovel.me",
    "noveltranslate.com",
    "readnovelfull.com",
    "zinnovel.com",
    "novelsonline.net",
    "fanfiction.net",
    "archiveofourown.org",
    "wattpad.com",
  ];

  // v52.5: Site-specific kata-unik selector presets
  // Mendukung situs yang membungkus nama dalam span khusus dengan data-hash
  const SITE_KATA_UNIK_PRESETS = {
    "wtr-lab.com": {
      // FIX v53: Hanya targetkan chapter spans menggunakan data-slot='popover-trigger'
      // yang UNIK milik teks chapter. Sidebar glossary native TIDAK memiliki atribut ini,
      // sehingga sidebar tidak ikut diganti oleh applyHashBasedReplacements.
      // isInsideNativeGlossary tetap melindungi KEDUA jenis span dari dihapus oleh replacer reguler.
      glossarySelector: "span[data-slot='popover-trigger'][data-hash]",
      hashAttr: "data-hash",
      label: "wtr-lab (Radix popover — chapter only)"
    },
    "webnovel.com": {
      glossarySelector: "span[data-hash]",
      hashAttr: "data-hash",
      label: "WebNovel"
    },
    "novelbin.com": {
      glossarySelector: "span[data-hash]",
      hashAttr: "data-hash",
      label: "NovelBin"
    },
    "novelfull.com": {
      glossarySelector: "span[data-hash]",
      hashAttr: "data-hash",
      label: "NovelFull"
    }
  };

  function getSiteKataUnikSelector() {
    const baseDomain = getNovelBaseDomain(currentHost);
    for (const domain in SITE_KATA_UNIK_PRESETS) {
      if (baseDomain === domain || baseDomain.endsWith("." + domain)) {
        return SITE_KATA_UNIK_PRESETS[domain];
      }
    }
    // Default fallback: any span with data-hash
    return { glossarySelector: "span[data-hash]", hashAttr: "data-hash", label: "Generic" };
  }

  function isDomainAllowed() {
    const mode = getFilterMode(),
      pageBaseDomain = getNovelBaseDomain(currentHost);
    if (mode === "whitelist") {
      const whitelist = getTargetDomains();
      return whitelist.some((d) => {
        const clean = getNovelBaseDomain(d.trim().toLowerCase());
        return (
          clean &&
          (pageBaseDomain === clean || pageBaseDomain.endsWith("." + clean))
        );
      });
    } else {
      const blacklist = getBlacklistDomains();
      return !blacklist.some((d) => {
        const clean = getNovelBaseDomain(d.trim().toLowerCase());
        return (
          clean &&
          (pageBaseDomain === clean || pageBaseDomain.endsWith("." + clean))
        );
      });
    }
  }

  function hapusSemuaHighlight() {
    const spans = document.querySelectorAll("." + HIGHLIGHT_CLASS),
      parentsToNormalize = new Set();
    spans.forEach((span) => {
      const parent = span.parentNode;
      if (!parent) return;
      const originalText =
        span.getAttribute("data-original") || span.textContent;
      parent.replaceChild(document.createTextNode(originalText), span);
      parentsToNormalize.add(parent);
    });
    parentsToNormalize.forEach((parent) => parent.normalize());
  }

  function restoreAllDirectReplacements() {
    const walker = document.createTreeWalker(
      document.body,
      NodeFilter.SHOW_TEXT,
      null,
      false,
    );
    let node;
    while ((node = walker.nextNode())) {
      if (originalTextMap.has(node)) {
        node.nodeValue = originalTextMap.get(node);
        originalTextMap.delete(node);
      }
      // FIX v54: Restore hash-replaced nodes juga (pakai map terpisah)
      if (hashOriginalTextMap.has(node)) {
        node.nodeValue = hashOriginalTextMap.get(node);
        hashOriginalTextMap.delete(node);
      }
    }
  }

  function jalankanPengganti(forceRebuild = false) {
    if (observer) observer.disconnect();
    const domainAllowed = isDomainAllowed();

    if (!domainAllowed) {
      hapusSemuaHighlight();
      restoreAllDirectReplacements();
      lastProcessedValueMap = new WeakMap();
      if (observer && document.body)
        observer.observe(document.body, {
          childList: true,
          subtree: true,
          characterData: true,
        });
      return;
    }

    const kamus = getKamusAktif(domainAllowed);
    if (Object.keys(kamus).length === 0) {
      hapusSemuaHighlight();
      restoreAllDirectReplacements();
      lastProcessedValueMap = new WeakMap();
      if (observer && document.body)
        observer.observe(document.body, {
          childList: true,
          subtree: true,
          characterData: true,
        });
      return;
    }

    try {
      unwrapFontTags();
    } catch (e) {
      console.warn("Gagal unwrap tag terjemahan:", e);
    }
    if (forceRebuild) {
      hapusSemuaHighlight();
      restoreAllDirectReplacements();
      lastProcessedValueMap = new WeakMap();
      nodeLastReplacedTimeMap = new WeakMap();
      nodeReplaceCountMap = new WeakMap();
      nodeFlickerLockSet = new WeakSet();
    }

    const highlightOn = getHighlightAktif();
    // Selalu re-inject style agar tidak hilang saat SPA navigasi / page reload CSS
    if (highlightOn) { try { injectHighlightStyle(); } catch(_) {} }
    const textNodes = [];
    const walker = document.createTreeWalker(
      document.body,
      NodeFilter.SHOW_TEXT,
      {
        acceptNode: (node) => {
          const textValue = node.nodeValue.normalize("NFC");
          if (
            lastProcessedValueMap.has(node) &&
            lastProcessedValueMap.get(node) === textValue
          )
            return NodeFilter.FILTER_REJECT;
          const tag = node.parentElement && node.parentElement.tagName;
          if (
            !tag ||
            [
              "SCRIPT",
              "STYLE",
              "NOSCRIPT",
              "TEXTAREA",
              "INPUT",
              "BUTTON",
            ].includes(tag)
          )
            return NodeFilter.FILTER_REJECT;
          if (
            node.parentElement.closest("#word-replacer-host") ||
            node.parentElement.classList.contains(HIGHLIGHT_CLASS)
          )
            return NodeFilter.FILTER_REJECT;
          // Catatan: node di dalam span glosari (kata unik) TETAP diproses untuk
          // penggantian teks biasa (nama karakter, dsb). Highlight dimatikan di dalam
          // glosari (lihat diDalamGlosari di bawah) agar tidak merusak elemen popover.
          return NodeFilter.FILTER_ACCEPT;
        },
      },
      false,
    );

    let node;
    while ((node = walker.nextNode())) textNodes.push(node);

    textNodes.forEach((node) => {
      if (!node.parentNode) return;
      let teksAsli = node.nodeValue.normalize("NFC");
      if (originalTextMap.has(node)) {
        teksAsli = originalTextMap.get(node).normalize("NFC");
        node.nodeValue = teksAsli;
      }
      lastProcessedValueMap.set(node, teksAsli);

      let adaPerubahan = false;
      // Urutkan term dari terpanjang ke terpendek agar term panjang dicek duluan
      // Skip hash: entries — mereka ditangani oleh applyHashBasedReplacements()
      const sortedKeys = Object.keys(kamus).filter(k => !k.startsWith("hash:") && !k.startsWith("exact:")).sort((a, b) => b.length - a.length);
      for (const salah of sortedKeys) {
        const entryFlags = kamus[salah].cs ? "gu" : "giu";
        if (
          buildBoundaryRegex(salah, buildRegexPattern(salah), entryFlags).test(
            teksAsli,
          )
        ) {
          adaPerubahan = true;
          break;
        }
      }
      if (!adaPerubahan) {
        originalTextMap.delete(node);
        return;
      }
      if (!originalTextMap.has(node)) originalTextMap.set(node, teksAsli);

      const diDalamGlosari = isInsideNativeGlossary(node),
        actualHighlightOn = highlightOn && !diDalamGlosari;

      if (!actualHighlightOn) {
        let teksBaru = teksAsli;
        // Proses term terpanjang dulu → cegah term pendek "memakan" bagian term panjang
        for (const salah of sortedKeys) {
          const entryFlags = kamus[salah].cs ? "gu" : "giu";
          const _isMulti = /\s/.test(salah);
          teksBaru = teksBaru.replace(
            buildBoundaryRegex(salah, buildRegexPattern(salah), entryFlags),
            (matchStr, ...groups) => {
              // Multi-kata: hitung separator dari split spasi (sesuai pola sederhana)
              // Satu-kata: hitung separator dari split spasi+tanda-hubung (sesuai pola kompleks)
              const numSeps = _isMulti
                ? salah.trim().split(/\s+/).length - 1
                : salah.split(/[\s\-_—–]+/).length - 1;
              return reconstructReplacement(
                kamus[salah].to,
                groups.slice(0, numSeps),
              );
            },
          );
        }
        
        // Anti-flicker rate limiter: skip if node is mutation-locked [1]
        if (nodeFlickerLockSet.has(node)) return;

        // Secure mutation guard & connected DOM check [1]
        if (node.isConnected) {
          const _now = Date.now();
          const _lastT = nodeLastReplacedTimeMap.get(node) || 0;
          if (_now - _lastT < NODE_FLICKER_WINDOW_MS) {
            const _cnt = (nodeReplaceCountMap.get(node) || 0) + 1;
            nodeReplaceCountMap.set(node, _cnt);
            if (_cnt >= NODE_FLICKER_MAX_COUNT) {
              nodeFlickerLockSet.add(node);
              return;
            }
          } else {
            nodeLastReplacedTimeMap.set(node, _now);
            nodeReplaceCountMap.set(node, 1);
          }
          _replacingNow = true;
          node.nodeValue = teksBaru;
          _replacingNow = false;
          lastProcessedValueMap.set(node, teksBaru);
        }
      } else {
        const allKeysPattern = Object.keys(kamus).map((k) => {
          const starts = /^[\p{L}\p{N}]/u.test(k.normalize("NFC")),
            ends = /[\p{L}\p{N}]$/u.test(k.normalize("NFC"));
          return (
            (starts ? "(?<![\\p{L}\\b\\p{N}])" : "") +
            buildRegexPattern(k) +
            (ends ? "(?![\\p{L}\\b\\p{N}])" : "")
          );
        });
        if (allKeysPattern.length === 0) return;

        const regexGabungan = new RegExp(
          "(" + allKeysPattern.join("|") + ")",
          "giu",
        );
        const fragment = document.createDocumentFragment();
        let lastIndex = 0,
          match;

        regexGabungan.lastIndex = 0;
        while ((match = regexGabungan.exec(teksAsli)) !== null) {
          if (match.index > lastIndex) {
            const segmentText = teksAsli.slice(lastIndex, match.index),
              segmentNode = document.createTextNode(segmentText);
            lastProcessedValueMap.set(segmentNode, segmentText);
            fragment.appendChild(segmentNode);
          }

          let penggantinya = match[0];
          for (const salah in kamus) {
            const csFlag = kamus[salah].cs ? "" : "i";
            const subMatch = match[0].match(
              new RegExp("^" + buildRegexPattern(salah) + "$", "u" + csFlag),
            );
            if (subMatch) {
              penggantinya = reconstructReplacement(
                kamus[salah].to,
                subMatch.slice(1, 1 + (salah.split(/[\s\-_—–]+/).length - 1)),
              );
              break;
            }
          }

          const span = document.createElement("span");
          span.className = HIGHLIGHT_CLASS;
          span.textContent = penggantinya;
          span.setAttribute("data-original", match[0]);
          span.title = 'Diganti dari: "' + match[0] + '"';
          span.childNodes.forEach((child) =>
            lastProcessedValueMap.set(child, child.nodeValue),
          );
          fragment.appendChild(span);
          lastIndex = match.index + match[0].length;
        }

        if (lastIndex < teksAsli.length) {
          const segmentText = teksAsli.slice(lastIndex),
            lastSegmentNode = document.createTextNode(segmentText);
          lastProcessedValueMap.set(lastSegmentNode, segmentText);
          fragment.appendChild(lastSegmentNode);
        }

        // Anti-flicker rate limiter: skip if node is mutation-locked [1]
        if (nodeFlickerLockSet.has(node)) return;

        // Secure mutation guard & parent node connection checks [1]
        if (node.isConnected && node.parentNode) {
          const _now = Date.now();
          const _lastT = nodeLastReplacedTimeMap.get(node) || 0;
          if (_now - _lastT < NODE_FLICKER_WINDOW_MS) {
            const _cnt = (nodeReplaceCountMap.get(node) || 0) + 1;
            nodeReplaceCountMap.set(node, _cnt);
            if (_cnt >= NODE_FLICKER_MAX_COUNT) {
              nodeFlickerLockSet.add(node);
              return;
            }
          } else {
            nodeLastReplacedTimeMap.set(node, _now);
            nodeReplaceCountMap.set(node, 1);
          }
          _replacingNow = true;
          try {
            node.parentNode.replaceChild(fragment, node);
          } catch (e) {
            console.warn("Gagal memproses elemen:", e);
          }
          _replacingNow = false;
        }
      }
    });

    // Terapkan penggantian lintas-node setelah walker utama selesai
    if (Object.keys(kamus).length > 0) {
      try { applyMultiNodeReplacements(kamus); } catch(e) {}
      // FIX v51: Juga terapkan penggantian lintas-paragraf
      try { applyMultiBlockReplacements(kamus); } catch(e) {}
      // v52.4: Terapkan penggantian berbasis hash untuk kata unik (data-hash targeting)
      try { applyHashBasedReplacements(kamus); } catch(e) {}
      // FIX v99.12.24b: Terapkan penggantian teks panjang/literal (exact: prefix)
      try { applyExactTextReplacements(kamus); } catch(e) {}
    }

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

    // Zero-delay timeout flush to safely clear queued mutations [1]
    setTimeout(() => {
      _replacingNow = false;
    }, 0);
  }

  // ── Penggantian lintas-node (Cross-node multi-element replacement) ─────────
  // Menangani kata/kalimat panjang yang terpecah oleh elemen inline
  // (span, b, i, em, strong, dll) atau tersebar di beberapa text node satu blok.
  //
  // FIX v51: Perbaikan bug utama:
  // 1. Menggunakan Set modifiedNodes agar node yang sudah diproses tidak diproses ulang
  //    oleh blok parent (div > p: keduanya dipilih, menyebabkan repetisi)
  // 2. Prefix/suffix dihitung dari teks ASLI (origVal) bukan nodeValue terkini
  // 3. Guard _replacingNow mencakup seluruh iterasi, bukan per-match

  // FIX v52: Distribusi teks pengganti di sekitar node glosari (kata unik).
  // Teks glosari dipakai sebagai "anchor" di replacement, sehingga:
  //   - Span glosari beserta tooltip/klik tetap utuh
  //   - Teks di sekitar glosari (non-glossary nodes) mendapat teks pengganti yang tepat
  // Mengembalikan objek { absIdx -> newText } untuk non-glosari, null jika gagal.
  function splitAroundGlossary(replacement, nodeInfos, firstIdx, lastIdx, cmStart, cmEnd, combined) {
    const segments = [];
    for (let i = firstIdx; i <= lastIdx; i++) {
      const ni = nodeInfos[i];
      const segStart = Math.max(ni.start, cmStart);
      const segEnd   = Math.min(ni.start + ni.length, cmEnd);
      const segText  = combined.slice(segStart, segEnd);
      segments.push({ absIdx: i, text: segText, isGlossary: ni.isGlossary });
    }

    const results = {};
    let repPos = 0;

    for (let i = 0; i < segments.length; i++) {
      const seg = segments[i];
      if (!seg.isGlossary) continue;

      // Cari teks glosari di replacement mulai dari posisi saat ini
      const lower = replacement.toLowerCase();
      const foundAt = lower.indexOf(seg.text.toLowerCase(), repPos);
      if (foundAt < 0) return null; // tidak ditemukan → jangan modifikasi

      // Teks antara repPos dan foundAt → non-glosari terakhir sebelum node ini
      const beforeGloss = replacement.slice(repPos, foundAt);
      let prevNGIdx = -1;
      for (let j = i - 1; j >= 0; j--) {
        if (!segments[j].isGlossary) { prevNGIdx = j; break; }
      }
      if (prevNGIdx >= 0) {
        results[segments[prevNGIdx].absIdx] = (results[segments[prevNGIdx].absIdx] || "") + beforeGloss;
      }

      results[seg.absIdx] = null; // penanda: jangan sentuh node glosari ini
      repPos = foundAt + seg.text.length;
    }

    // Sisa replacement → non-glosari terakhir
    let lastNGIdx = -1;
    for (let j = segments.length - 1; j >= 0; j--) {
      if (!segments[j].isGlossary) { lastNGIdx = j; break; }
    }
    if (lastNGIdx >= 0) {
      results[segments[lastNGIdx].absIdx] = (results[segments[lastNGIdx].absIdx] || "") + replacement.slice(repPos);
    } else if (repPos < replacement.length) {
      return null; // tidak ada tempat untuk sisa teks
    }

    return results;
  }

  function applyMultiNodeReplacements(kamus) {
    if (!document.body) return;
    const highlightOn = getHighlightAktif();

    // FIX: Set ini melacak node yang sudah dimodifikasi dalam pemanggilan ini.
    // Mencegah pemrosesan ganda ketika blok parent dan child keduanya dipilih
    // (contoh: <div> parent memproses ulang node yang sudah ditangani <p> child).
    const modifiedNodes = new Set();

    // Hanya elemen blok langsung (tanpa div agar tidak double-process child p)
    // div ditangani di applyMultiBlockReplacements secara terpisah
    const BLOCK_SELECTOR = "p, li, td, th, blockquote, h1, h2, h3, h4, h5, h6, pre, figcaption, dt, dd";
    const blockEls = document.body.querySelectorAll(BLOCK_SELECTOR);

    _replacingNow = true;
    try {
      blockEls.forEach(function(blockEl) {
        if (blockEl.closest("#word-replacer-host")) return;

        // Kumpulkan semua text node yang layak dalam blok ini
        const nodeInfos = [];
        let combined = "";
        const twBlock = document.createTreeWalker(
          blockEl,
          NodeFilter.SHOW_TEXT,
          {
            acceptNode: function(n) {
              const tag = n.parentElement && n.parentElement.tagName;
              if (!tag || ["SCRIPT","STYLE","NOSCRIPT","TEXTAREA","INPUT","BUTTON"].includes(tag))
                return NodeFilter.FILTER_REJECT;
              if (n.parentElement.closest("#word-replacer-host") ||
                  n.parentElement.classList.contains(HIGHLIGHT_CLASS))
                return NodeFilter.FILTER_REJECT;
              // FIX v52: Jangan reject node glosari — sertakan di combined untuk pencocokan frasa.
              // Node glosari tetap tidak diubah teks-nya saat penerapan (splitAroundGlossary).
              // FIX: Skip node yang sudah dimodifikasi di iterasi sebelumnya
              if (modifiedNodes.has(n)) return NodeFilter.FILTER_REJECT;
              return NodeFilter.FILTER_ACCEPT;
            }
          },
          false
        );
        let n2;
        while ((n2 = twBlock.nextNode())) {
          // Gunakan teks ASLI (sebelum walker utama memodifikasi) jika tersedia
          const val = (originalTextMap.has(n2) ? originalTextMap.get(n2) : n2.nodeValue).normalize("NFC");
          // FIX v52: Tandai apakah node ini adalah bagian dari glosari/kata-unik situs
          nodeInfos.push({ node: n2, start: combined.length, length: val.length, origVal: val, isGlossary: isInsideNativeGlossary(n2) });
          combined += val;
        }

        // Hanya proses blok dengan 2+ text node (single node sudah ditangani walker utama)
        if (nodeInfos.length <= 1) return;
        if (!combined.trim()) return;

        // Cari semua kecocokan yang melintas antar node
        const crossMatches = [];
        const sortedCrossTerms = Object.keys(kamus).filter(k => !k.startsWith("hash:") && !k.startsWith("exact:")).sort((a, b) => b.length - a.length);
        for (const term of sortedCrossTerms) {
          const flags = kamus[term].cs ? "gu" : "giu";
          let rx;
          try {
            rx = buildBoundaryRegex(term, buildRegexPattern(term), flags);
          } catch(e) { continue; }
          rx.lastIndex = 0;
          let m;
          while ((m = rx.exec(combined)) !== null) {
            if (m[0].length === 0) { rx.lastIndex++; continue; }
            const mStart = m.index, mEnd = m.index + m[0].length;
            let firstIdx = -1, lastIdx = -1;
            for (let i = 0; i < nodeInfos.length; i++) {
              const ni = nodeInfos[i];
              if (ni.start < mEnd && ni.start + ni.length > mStart) {
                if (firstIdx === -1) firstIdx = i;
                lastIdx = i;
              }
            }
            // Hanya simpan jika melintas ≥2 node
            if (firstIdx !== -1 && lastIdx > firstIdx) {
              const _isMultiTerm = /\s/.test(term);
              const numSeps = _isMultiTerm
                ? term.trim().split(/\s+/).length - 1
                : term.split(/[\s\-_—–]+/).length - 1;
              const replacement = reconstructReplacement(
                kamus[term].to,
                Array.from(m).slice(1, 1 + numSeps)
              );
              crossMatches.push({
                start: mStart, end: mEnd,
                replacement, firstIdx, lastIdx
              });
            }
          }
        }

        if (crossMatches.length === 0) return;

        // Urutkan dari belakang (descending) agar indeks tidak geser saat edit
        crossMatches.sort((a, b) => b.start - a.start);

        // Hapus tumpang tindih
        const nonOverlapping = [];
        let lastUsedStart = Infinity;
        for (const cm of crossMatches) {
          if (cm.end <= lastUsedStart) {
            nonOverlapping.push(cm);
            lastUsedStart = cm.start;
          }
        }

        // Terapkan penggantian lintas-node (dengan dukungan glosari/kata-unik)
        for (const cm of nonOverlapping) {
          const firstNI = nodeInfos[cm.firstIdx];
          const lastNI  = nodeInfos[cm.lastIdx];
          if (nodeFlickerLockSet.has(firstNI.node)) continue;
          if (!firstNI.node.isConnected) continue;

          const hasGlossary = nodeInfos.slice(cm.firstIdx, cm.lastIdx + 1).some(ni => ni.isGlossary);

          if (hasGlossary) {
            // FIX v52: Penerapan sadar-glosari — jangan ubah text node glosari,
            // hanya ubah text node di sekitar glosari (non-glosari).
            const splitRes = splitAroundGlossary(cm.replacement, nodeInfos, cm.firstIdx, cm.lastIdx, cm.start, cm.end, combined);

            // FIX v53 (Hipotesis A): Jika splitAroundGlossary gagal (teks glosari tidak ada di
            // replacement), jangan lewati seluruh match. Terapkan fallback: isi node pertama
            // dengan replacement, kosongkan node antara yang non-glosari, biarkan node glosari utuh.
            if (!splitRes) {
              const prefixLen = Math.max(0, cm.start - firstNI.start);
              const prefix = firstNI.origVal.slice(0, prefixLen);
              const suffixStart = Math.max(0, cm.end - lastNI.start);
              const suffix = lastNI.origVal.slice(suffixStart);
              try {
                if (!originalTextMap.has(firstNI.node))
                  originalTextMap.set(firstNI.node, firstNI.node.nodeValue);
                firstNI.node.nodeValue = prefix + cm.replacement;
                lastProcessedValueMap.set(firstNI.node, prefix + cm.replacement);
                modifiedNodes.add(firstNI.node);
                for (let i = cm.firstIdx + 1; i <= cm.lastIdx; i++) {
                  const ni = nodeInfos[i];
                  if (ni.isGlossary) continue; // lindungi node kata unik
                  if (!ni.node.isConnected) continue;
                  if (!originalTextMap.has(ni.node))
                    originalTextMap.set(ni.node, ni.node.nodeValue);
                  const newVal = i === cm.lastIdx ? suffix : "";
                  ni.node.nodeValue = newVal;
                  lastProcessedValueMap.set(ni.node, newVal);
                  modifiedNodes.add(ni.node);
                }
              } catch(e) {}
              continue;
            }

            for (let i = cm.firstIdx; i <= cm.lastIdx; i++) {
              const ni = nodeInfos[i];
              if (splitRes[i] === null) continue; // node glosari: jangan sentuh
              if (!ni.node.isConnected) continue;
              if (nodeFlickerLockSet.has(ni.node)) continue;

              let newText = splitRes[i] || "";
              // Tambahkan prefix jika node ini adalah node pertama dan bukan glosari
              if (i === cm.firstIdx && !ni.isGlossary) {
                const prefixLen = Math.max(0, cm.start - ni.start);
                newText = ni.origVal.slice(0, prefixLen) + newText;
              }
              // Tambahkan suffix jika node ini adalah node terakhir dan bukan glosari
              if (i === cm.lastIdx && !ni.isGlossary) {
                const suffixStart = Math.max(0, cm.end - ni.start);
                newText = newText + ni.origVal.slice(suffixStart);
              }
              try {
                if (!originalTextMap.has(ni.node))
                  originalTextMap.set(ni.node, ni.node.nodeValue);
                ni.node.nodeValue = newText;
                lastProcessedValueMap.set(ni.node, newText);
                modifiedNodes.add(ni.node);
              } catch(e) {}
            }
          } else {
            // Penerapan biasa (tidak ada node glosari dalam range)
            // FIX: Gunakan origVal (teks asli) untuk menghitung prefix dan suffix
            const prefixLen = Math.max(0, cm.start - firstNI.start);
            const prefix = firstNI.origVal.slice(0, prefixLen);
            const suffixStart = Math.max(0, cm.end - lastNI.start);
            const suffix = lastNI.origVal.slice(suffixStart);

            try {
              if (!originalTextMap.has(firstNI.node))
                originalTextMap.set(firstNI.node, firstNI.node.nodeValue);

              if (highlightOn) {
                firstNI.node.nodeValue = prefix;
                const span = document.createElement("span");
                span.className = HIGHLIGHT_CLASS;
                span.textContent = cm.replacement;
                span.setAttribute("data-original", combined.slice(cm.start, cm.end));
                span.title = "Diganti dari: \"" + combined.slice(cm.start, cm.end) + "\"";
                const parentNode = firstNI.node.parentNode;
                if (parentNode) {
                  parentNode.insertBefore(span, firstNI.node.nextSibling);
                }
                lastProcessedValueMap.set(firstNI.node, prefix);
              } else {
                firstNI.node.nodeValue = prefix + cm.replacement;
                lastProcessedValueMap.set(firstNI.node, prefix + cm.replacement);
              }

              modifiedNodes.add(firstNI.node);

              for (let i = cm.firstIdx + 1; i <= cm.lastIdx; i++) {
                const ni = nodeInfos[i];
                // FIX v53 (Hipotesis B): Jangan kosongkan node kata unik — biarkan
                // applyHashBasedReplacements yang menanganinya.
                if (ni.isGlossary) continue;
                if (!ni.node.isConnected) continue;
                if (!originalTextMap.has(ni.node))
                  originalTextMap.set(ni.node, ni.node.nodeValue);
                const newVal = i === cm.lastIdx ? suffix : "";
                ni.node.nodeValue = newVal;
                lastProcessedValueMap.set(ni.node, newVal);
                modifiedNodes.add(ni.node);
              }
            } catch(e) {}
          }
        }
      });
    } finally {
      _replacingNow = false;
    }
  }

  // ── Penggantian lintas-paragraf (Cross-block multi-paragraph replacement) ──
  // FIX v51: Menangani kata/kalimat yang terpecah di dua baris/paragraf berbeda.
  // Contoh: FROM="Shura tidak" tapi "Shura" di akhir paragraf 1 dan "tidak" di awal paragraf 2.
  function applyMultiBlockReplacements(kamus) {
    if (!document.body) return;
    const highlightOn = getHighlightAktif();

    // Cari kontainer konten utama novel
    const contentSelectors = [
      ".chapter-content", "#chapter-content", ".reading-content",
      ".novel-content", ".content-area", ".chapter-text",
      "[class*='chapter']", "article", ".post-content",
      "#content", "main"
    ];
    let contentEl = null;
    for (const sel of contentSelectors) {
      try {
        const el = document.querySelector(sel);
        if (el && !el.closest("#word-replacer-host")) { contentEl = el; break; }
      } catch(e) {}
    }
    if (!contentEl) return;

    // Ambil semua paragraf langsung dalam kontainer
    const paragraphs = Array.from(
      contentEl.querySelectorAll("p, div > br, li")
    ).filter(el => el.tagName === "P" && !el.closest("#word-replacer-host"));

    if (paragraphs.length < 2) return;

    // Helper: kumpulkan semua text node dari sebuah elemen
    function getTextNodes(el) {
      const nodes = [];
      const tw = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
        acceptNode: function(n) {
          const tag = n.parentElement && n.parentElement.tagName;
          if (!tag || ["SCRIPT","STYLE","NOSCRIPT","TEXTAREA","INPUT","BUTTON"].includes(tag))
            return NodeFilter.FILTER_REJECT;
          if (n.parentElement.closest("#word-replacer-host") ||
              n.parentElement.classList.contains(HIGHLIGHT_CLASS))
            return NodeFilter.FILTER_REJECT;
          // FIX v52: Sertakan node glosari dalam combined untuk pencocokan frasa lintas-paragraf.
          // Node glosari tetap tidak diubah saat penerapan (splitAroundGlossary).
          return NodeFilter.FILTER_ACCEPT;
        }
      }, false);
      let n;
      while ((n = tw.nextNode())) nodes.push(n);
      return nodes;
    }

    // Proses pasangan paragraf: coba hingga 3 paragraf ke depan, lewati yang kosong
    // FIX: tambah spasi batas antar-paragraf di combined agar pencocokan lintas baris berfungsi
    // FIX: gunakan lookAhead untuk melewati paragraf kosong (jarak 2 baris atau lebih)
    for (let bi = 0; bi < paragraphs.length - 1; bi++) {
      const p1Nodes = getTextNodes(paragraphs[bi]);
      if (p1Nodes.length === 0) continue;

      // Cari paragraf berikutnya yang tidak kosong dalam jangkauan lookAhead
      let p2Nodes = null;
      for (let la = 1; la <= Math.min(3, paragraphs.length - 1 - bi); la++) {
        const candidate = getTextNodes(paragraphs[bi + la]);
        if (candidate.length > 0) { p2Nodes = candidate; break; }
      }
      if (!p2Nodes) continue;

      // Gabungkan semua node dari kedua paragraf dengan spasi batas di tengah
      const allNodes = [...p1Nodes, ...p2Nodes];
      const nodeInfos = [];
      let combined = "";

      for (let ni = 0; ni < allNodes.length; ni++) {
        // Sisipkan spasi di batas antar-paragraf agar regex bisa menjembatani
        if (ni === p1Nodes.length) combined += " ";
        const n = allNodes[ni];
        const val = (originalTextMap.has(n) ? originalTextMap.get(n) : n.nodeValue).normalize("NFC");
        // FIX v52: Tandai node glosari agar tidak diubah saat penerapan
        nodeInfos.push({ node: n, start: combined.length, length: val.length, origVal: val, isGlossary: isInsideNativeGlossary(n) });
        combined += val;
      }

      if (!combined.trim()) continue;

      // Cari kecocokan yang hanya melintas antar paragraf
      // (firstIdx dalam p1, lastIdx dalam p2)
      const p1Count = p1Nodes.length;
      const crossMatches = [];
      const sortedTerms = Object.keys(kamus).filter(k => !k.startsWith("hash:") && !k.startsWith("exact:")).sort((a, b) => b.length - a.length);

      for (const term of sortedTerms) {
        const flags = kamus[term].cs ? "gu" : "giu";
        let rx;
        try {
          rx = buildBoundaryRegex(term, buildRegexPattern(term), flags);
        } catch(e) { continue; }
        rx.lastIndex = 0;
        let m;
        while ((m = rx.exec(combined)) !== null) {
          if (m[0].length === 0) { rx.lastIndex++; continue; }
          const mStart = m.index, mEnd = m.index + m[0].length;
          let firstIdx = -1, lastIdx = -1;
          for (let i = 0; i < nodeInfos.length; i++) {
            const ni = nodeInfos[i];
            if (ni.start < mEnd && ni.start + ni.length > mStart) {
              if (firstIdx === -1) firstIdx = i;
              lastIdx = i;
            }
          }
          // Hanya ambil kecocokan yang benar-benar MELINTAS batas paragraf
          // (firstIdx di p1, lastIdx di p2)
          if (firstIdx !== -1 && firstIdx < p1Count && lastIdx >= p1Count) {
            const _isMultiTerm = /\s/.test(term);
            const numSeps = _isMultiTerm
              ? term.trim().split(/\s+/).length - 1
              : term.split(/[\s\-_—–]+/).length - 1;
            const replacement = reconstructReplacement(
              kamus[term].to,
              Array.from(m).slice(1, 1 + numSeps)
            );
            crossMatches.push({ start: mStart, end: mEnd, replacement, firstIdx, lastIdx });
          }
        }
      }

      if (crossMatches.length === 0) continue;

      crossMatches.sort((a, b) => b.start - a.start);
      const nonOverlapping = [];
      let lastUsedStart = Infinity;
      for (const cm of crossMatches) {
        if (cm.end <= lastUsedStart) {
          nonOverlapping.push(cm);
          lastUsedStart = cm.start;
        }
      }

      _replacingNow = true;
      try {
        for (const cm of nonOverlapping) {
          const firstNI = nodeInfos[cm.firstIdx];
          const lastNI  = nodeInfos[cm.lastIdx];
          if (nodeFlickerLockSet.has(firstNI.node)) continue;
          if (!firstNI.node.isConnected) continue;

          const hasGlossary = nodeInfos.slice(cm.firstIdx, cm.lastIdx + 1).some(ni => ni.isGlossary);

          if (hasGlossary) {
            // FIX v52: Penerapan sadar-glosari lintas-paragraf
            const splitRes = splitAroundGlossary(cm.replacement, nodeInfos, cm.firstIdx, cm.lastIdx, cm.start, cm.end, combined);

            // FIX v53 (Hipotesis A): Fallback jika splitAroundGlossary gagal — terapkan pada
            // non-glosari, biarkan node glosari utuh.
            if (!splitRes) {
              const prefixLen = Math.max(0, cm.start - firstNI.start);
              const prefix = firstNI.origVal.slice(0, prefixLen);
              const suffixStart = Math.max(0, cm.end - lastNI.start);
              const suffix = lastNI.origVal.slice(suffixStart);
              try {
                if (!originalTextMap.has(firstNI.node))
                  originalTextMap.set(firstNI.node, firstNI.node.nodeValue);
                firstNI.node.nodeValue = prefix + cm.replacement;
                lastProcessedValueMap.set(firstNI.node, prefix + cm.replacement);
                for (let i = cm.firstIdx + 1; i <= cm.lastIdx; i++) {
                  const ni = nodeInfos[i];
                  if (ni.isGlossary) continue; // lindungi node kata unik
                  if (!ni.node.isConnected) continue;
                  if (!originalTextMap.has(ni.node))
                    originalTextMap.set(ni.node, ni.node.nodeValue);
                  const newVal = i === cm.lastIdx ? suffix : "";
                  ni.node.nodeValue = newVal;
                  lastProcessedValueMap.set(ni.node, newVal);
                }
              } catch(e) {}
              continue;
            }

            for (let i = cm.firstIdx; i <= cm.lastIdx; i++) {
              const ni = nodeInfos[i];
              if (splitRes[i] === null) continue; // node glosari: jangan sentuh
              if (!ni.node.isConnected) continue;
              if (nodeFlickerLockSet.has(ni.node)) continue;

              let newText = splitRes[i] || "";
              if (i === cm.firstIdx && !ni.isGlossary) {
                const prefixLen = Math.max(0, cm.start - ni.start);
                newText = ni.origVal.slice(0, prefixLen) + newText;
              }
              if (i === cm.lastIdx && !ni.isGlossary) {
                const suffixStart = Math.max(0, cm.end - ni.start);
                newText = newText + ni.origVal.slice(suffixStart);
              }
              try {
                if (!originalTextMap.has(ni.node))
                  originalTextMap.set(ni.node, ni.node.nodeValue);
                ni.node.nodeValue = newText;
                lastProcessedValueMap.set(ni.node, newText);
              } catch(e) {}
            }
          } else {
            // Penerapan biasa (tidak ada node glosari dalam range)
            const prefixLen = Math.max(0, cm.start - firstNI.start);
            const prefix = firstNI.origVal.slice(0, prefixLen);
            const suffixStart = Math.max(0, cm.end - lastNI.start);
            const suffix = lastNI.origVal.slice(suffixStart);

            try {
              if (!originalTextMap.has(firstNI.node))
                originalTextMap.set(firstNI.node, firstNI.node.nodeValue);

              if (highlightOn) {
                firstNI.node.nodeValue = prefix;
                const span = document.createElement("span");
                span.className = HIGHLIGHT_CLASS;
                span.textContent = cm.replacement;
                span.setAttribute("data-original", combined.slice(cm.start, cm.end));
                span.title = "Diganti dari: \"" + combined.slice(cm.start, cm.end) + "\"";
                const parentNode = firstNI.node.parentNode;
                if (parentNode) parentNode.insertBefore(span, firstNI.node.nextSibling);
                lastProcessedValueMap.set(firstNI.node, prefix);
              } else {
                firstNI.node.nodeValue = prefix + cm.replacement;
                lastProcessedValueMap.set(firstNI.node, prefix + cm.replacement);
              }

              for (let i = cm.firstIdx + 1; i <= cm.lastIdx; i++) {
                const ni = nodeInfos[i];
                // FIX v53 (Hipotesis B): Jangan kosongkan node kata unik.
                if (ni.isGlossary) continue;
                if (!ni.node.isConnected) continue;
                if (!originalTextMap.has(ni.node))
                  originalTextMap.set(ni.node, ni.node.nodeValue);
                const newVal = i === cm.lastIdx ? suffix : "";
                ni.node.nodeValue = newVal;
                lastProcessedValueMap.set(ni.node, newVal);
              }
            } catch(e) {}
          }
        }
      } finally {
        _replacingNow = false;
      }
    }
  }

  // ── Penggantian teks panjang/literal (Exact-text replacement) v99.12.24c ───
  // Menangani kasus di mana teks sumber berisi tanda baca, pemisah khusus,
  // atau frasa sangat panjang yang sulit dicocokkan dengan regex biasa.
  //
  // Cara pakai: tambahkan entri dengan KEY dimulai "exact:"
  //   "exact:Bab 1, Bagian 2. Keberangkatan" → { to: "Bab 1, Bagian 2. Keberangkatan" }
  //
  // Fitur:
  //   - Pencocokan case-insensitive
  //   - Normalisasi whitespace: spasi berganda, NBSP, zero-width space → disamakan
  //   - Mendukung teks lintas text-node dalam satu blok paragraf
  //   - Frasa terpanjang diproses lebih dulu (paling spesifik menang)
  function applyExactTextReplacements(kamus) {
    if (!document.body) return;

    // Kumpulkan aturan dengan prefix "exact:"
    const exactRules = [];
    for (const key in kamus) {
      if (!key.startsWith("exact:")) continue;
      const pattern = key.slice(6).trim();
      if (!pattern) continue;
      const entry = kamus[key];
      const to = (entry && typeof entry === "object") ? (entry.to || "") : (typeof entry === "string" ? entry : "");
      exactRules.push({ pattern, to });
    }
    if (exactRules.length === 0) return;

    // Paling panjang -> paling pendek (spesifik duluan)
    exactRules.sort(function(a, b) { return b.pattern.length - a.pattern.length; });

    // Bangun regex dari pattern: escape special chars, normalisasi whitespace -> \s+
    // Contoh: "kata, lain?" -> /kata,\s+lain\?/i
    function buildExactRegex(pattern) {
      var escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      var rxSrc = escaped.replace(/[\s\u00a0\u200b\u200c\u200d\ufeff]+/g, "[\\s\\u00a0\\u200b\\u200c\\u200d\\ufeff]+");
      return new RegExp(rxSrc, "i");
    }

    // FIX Bug 1+3: Helper terapkan satu kecocokan exact ke nodeInfos yang mungkin berisi
    // node glosari (isGlossary:true). Node glosari tidak ditulis -- hanya non-glosari.
    // Mengembalikan combined string yang sudah diperbarui, atau null jika gagal.
    function applyExactMatchToNodes(nodeInfos, combined, origStart, origEnd, ruleTo) {
      var firstIdx = -1, lastIdx = -1;
      for (var i = 0; i < nodeInfos.length; i++) {
        var nfo = nodeInfos[i];
        if (nfo.start < origEnd && nfo.start + nfo.length > origStart) {
          if (firstIdx === -1) firstIdx = i;
          lastIdx = i;
        }
      }
      if (firstIdx === -1) return null;

      var firstNI = nodeInfos[firstIdx];
      var lastNI  = nodeInfos[lastIdx];
      var prefix  = firstNI.origVal.slice(0, Math.max(0, origStart - firstNI.start));
      var suffix  = lastNI.origVal.slice(Math.max(0, origEnd - lastNI.start));

      if (nodeFlickerLockSet.has(firstNI.node) || !firstNI.node.isConnected) return null;

      try {
        // FIX Bug 1: Tulis ke node pertama hanya jika BUKAN node glosari
        if (!firstNI.isGlossary) {
          if (!originalTextMap.has(firstNI.node))
            originalTextMap.set(firstNI.node, firstNI.node.nodeValue);
          firstNI.node.nodeValue = prefix + ruleTo;
          lastProcessedValueMap.set(firstNI.node, prefix + ruleTo);
        }

        for (var j = firstIdx + 1; j <= lastIdx; j++) {
          var jInfo = nodeInfos[j];
          // FIX Bug 1: Skip node glosari -- jangan ubah teks konten span kata-unik
          if (!jInfo.node.isConnected || jInfo.isGlossary) continue;
          if (!originalTextMap.has(jInfo.node))
            originalTextMap.set(jInfo.node, jInfo.node.nodeValue);
          var newVal = j === lastIdx ? suffix : "";
          jInfo.node.nodeValue = newVal;
          lastProcessedValueMap.set(jInfo.node, newVal);
        }

        // Perbarui combined dan posisi nodeInfos agar rule berikutnya akurat
        var delta = ruleTo.length - (origEnd - origStart);
        combined = combined.slice(0, origStart) + ruleTo + combined.slice(origEnd);
        nodeInfos[firstIdx] = {
          node: nodeInfos[firstIdx].node,
          start: nodeInfos[firstIdx].start,
          length: prefix.length + ruleTo.length,
          origVal: prefix + ruleTo,
          isGlossary: nodeInfos[firstIdx].isGlossary
        };
        for (var k = firstIdx + 1; k < nodeInfos.length; k++) {
          var kl = 0, kov = "";
          if (k <= lastIdx) {
            kl = k === lastIdx ? suffix.length : 0;
            kov = k === lastIdx ? suffix : "";
          } else {
            kl = nodeInfos[k].length;
            kov = nodeInfos[k].origVal;
          }
          nodeInfos[k] = {
            node: nodeInfos[k].node,
            start: nodeInfos[k].start + delta,
            length: kl,
            origVal: kov,
            isGlossary: nodeInfos[k].isGlossary
          };
        }
        return combined;
      } catch(e2) { return null; }
    }

    // FIX Bug 1+3: Kumpulkan text node dari elemen (termasuk node glosari untuk pencocokan frasa)
    function collectNodeInfos(el) {
      var infos = [];
      var combined = "";
      var tw2 = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, {
        acceptNode: function(n) {
          var tag = n.parentElement && n.parentElement.tagName;
          if (!tag || ["SCRIPT","STYLE","NOSCRIPT","TEXTAREA","INPUT","BUTTON"].includes(tag))
            return NodeFilter.FILTER_REJECT;
          if (n.parentElement.closest("#word-replacer-host") ||
              n.parentElement.classList.contains(HIGHLIGHT_CLASS))
            return NodeFilter.FILTER_REJECT;
          // FIX Bug 1: JANGAN reject node glosari -- sertakan dalam combined agar frasa
          // yang melewati/mengandung kata-unik (data-hash span) bisa dicocokkan.
          // isGlossary=true menandai node ini tidak boleh ditulis saat penerapan.
          return NodeFilter.FILTER_ACCEPT;
        }
      }, false);
      var tn;
      while ((tn = tw2.nextNode())) {
        var val = (originalTextMap.has(tn) ? originalTextMap.get(tn) : tn.nodeValue) || "";
        infos.push({ node: tn, start: combined.length, length: val.length, origVal: val, isGlossary: isInsideNativeGlossary(tn) });
        combined += val;
      }
      return { infos: infos, text: combined };
    }

    var BLOCK_SELECTOR = "p, li, td, th, blockquote, h1, h2, h3, h4, h5, h6, pre";

    _replacingNow = true;
    try {
      // -- PASS 1: Intra-block exact replacement (dalam satu blok) ---------------
      document.body.querySelectorAll(BLOCK_SELECTOR).forEach(function(blockEl) {
        if (blockEl.closest("#word-replacer-host")) return;

        var res = collectNodeInfos(blockEl);
        var nodeInfos = res.infos;
        var combined  = res.text;
        if (!combined.trim() || nodeInfos.length === 0) return;

        for (var ri = 0; ri < exactRules.length; ri++) {
          var rule = exactRules[ri];
          var rx;
          try { rx = buildExactRegex(rule.pattern); } catch(e) { continue; }
          var m = rx.exec(combined);
          if (!m) continue;
          var upd = applyExactMatchToNodes(nodeInfos, combined, m.index, m.index + m[0].length, rule.to);
          if (upd !== null) combined = upd;
        }
      });

      // -- FIX Bug 3: PASS 2: Cross-block (lintas paragraf) exact replacement ----
      // Pasangkan paragraf bersebelahan dan cari exact: rule yang melintasi batas antar paragraf.
      var contentSelectors2 = [
        ".chapter-content", "#chapter-content", ".reading-content",
        ".novel-content", ".content-area", ".chapter-text",
        "[class*='chapter']", "article", ".post-content", "#content", "main"
      ];
      var contentEl2 = null;
      for (var ci = 0; ci < contentSelectors2.length; ci++) {
        try {
          var elc = document.querySelector(contentSelectors2[ci]);
          if (elc && !elc.closest("#word-replacer-host")) { contentEl2 = elc; break; }
        } catch(e) {}
      }
      if (contentEl2) {
        var paragraphs2 = Array.from(contentEl2.querySelectorAll("p")).filter(function(el) {
          return !el.closest("#word-replacer-host");
        });
        if (paragraphs2.length >= 2) {
          for (var bi = 0; bi < paragraphs2.length - 1; bi++) {
            var r1 = collectNodeInfos(paragraphs2[bi]);
            if (r1.infos.length === 0) continue;
            // Cari paragraf berikutnya yang tidak kosong (lookAhead maks 3)
            var r2 = null;
            for (var la = 1; la <= Math.min(3, paragraphs2.length - 1 - bi); la++) {
              var cand = collectNodeInfos(paragraphs2[bi + la]);
              if (cand.infos.length > 0) { r2 = cand; break; }
            }
            if (!r2) continue;

            // Gabungkan node dua paragraf dengan spasi pemisah di batas antar paragraf
            var p1Len     = r1.infos.length;
            var combined2 = r1.text + " ";
            var nodeInfos2 = r1.infos.slice();
            var offset2    = combined2.length;
            var r2BaseStart = r2.infos.length > 0 ? r2.infos[0].start : 0;
            for (var ni2idx = 0; ni2idx < r2.infos.length; ni2idx++) {
              var ni2entry = r2.infos[ni2idx];
              nodeInfos2.push({
                node: ni2entry.node,
                start: offset2 + (ni2entry.start - r2BaseStart),
                length: ni2entry.length,
                origVal: ni2entry.origVal,
                isGlossary: ni2entry.isGlossary
              });
            }
            combined2 += r2.text;

            for (var ri2 = 0; ri2 < exactRules.length; ri2++) {
              var rule2 = exactRules[ri2];
              var rx2;
              try { rx2 = buildExactRegex(rule2.pattern); } catch(e) { continue; }
              var m2 = rx2.exec(combined2);
              if (!m2) continue;

              // Hitung firstIdx dan lastIdx di nodeInfos2
              var fIdx = -1, lIdx = -1;
              for (var ii2 = 0; ii2 < nodeInfos2.length; ii2++) {
                var n2i = nodeInfos2[ii2];
                if (n2i.start < m2.index + m2[0].length && n2i.start + n2i.length > m2.index) {
                  if (fIdx === -1) fIdx = ii2;
                  lIdx = ii2;
                }
              }
              if (fIdx === -1) continue;
              // Hanya terapkan jika benar-benar lintas paragraf (fIdx di p1, lIdx di p2)
              if (fIdx >= p1Len || lIdx < p1Len) continue;

              var upd2 = applyExactMatchToNodes(nodeInfos2, combined2, m2.index, m2.index + m2[0].length, rule2.to);
              if (upd2 !== null) combined2 = upd2;
            }
          }
        }
      }
    } finally {
      _replacingNow = false;
    }
  }

  // ── Penggantian berbasis hash (Hash-based glossary span targeting) v52.4 ────
  // Menangani kasus di mana 2+ karakter berbeda ditranslate ke nama yang SAMA
  // (misal: dua tokoh berbeda keduanya disebut "Fugaku").
  //
  // Cara kerja:
  //   - Tambahkan entri di kamus dengan KEY = "hash:NILAI_DATA_HASH"
  //     Contoh: "hash:某城" → { to: "Fugaku Uchiha", novelId: "..." }
  //   - Script akan menemukan semua <span data-hash="某城"> di halaman dan
  //     mengganti teks dalamnya menjadi "Fugaku Uchiha"
  //   - Span itu sendiri (termasuk popover/klik) TIDAK disentuh — hanya teks node
  //   - Untuk menambah rule hash: klik kanan pada kata unik di halaman novel
  //
  // Format key: "hash:" + nilai atribut data-hash secara literal (case-sensitive)
  //
  // v52.6 — POSITIONAL HASH OVERRIDE:
  //   Jika dua karakter berbeda punya data-hash yang SAMA (nama asli identik),
  //   gunakan format "hash:NILAI:INDEX" untuk membedakan per kemunculan:
  //     "hash:某城:0" → { to: "Fugaku Uchiha" }    ← kemunculan pertama (index 0)
  //     "hash:某城:1" → { to: "Fugaku Namikaze" }  ← kemunculan kedua  (index 1)
  //     "hash:某城"   → { to: "Fugaku (default)" } ← fallback jika index tidak ada rule
  function applyHashBasedReplacements(kamus) {
    if (!document.body) return;

    // Kumpulkan entri: default (hash:VAL) dan positional (hash:VAL:INDEX)
    const hashEntries = {};    // { hashVal: entry }  — default rule
    const posEntries  = {};    // { hashVal: { idx: entry, ... } }  — positional rules

    for (const key in kamus) {
      if (!key.startsWith("hash:")) continue;
      const rest = key.slice(5); // hapus prefix "hash:"
      // Cek apakah ada bagian ":INDEX" di akhir (hanya angka)
      const lastColon = rest.lastIndexOf(":");
      if (lastColon !== -1) {
        const maybeIdx = rest.slice(lastColon + 1);
        if (/^\d+$/.test(maybeIdx)) {
          const hashVal = rest.slice(0, lastColon);
          const idx = parseInt(maybeIdx, 10);
          if (hashVal) {
            if (!posEntries[hashVal]) posEntries[hashVal] = {};
            posEntries[hashVal][idx] = kamus[key];
          }
          continue;
        }
      }
      // Default rule tanpa index
      if (rest) hashEntries[rest] = kamus[key];
    }

    const hasDefault    = Object.keys(hashEntries).length > 0;
    const hasPositional = Object.keys(posEntries).length > 0;
    if (!hasDefault && !hasPositional) return;

    // Kumpulkan semua hash values yang perlu diproses
    const allHashVals = new Set([...Object.keys(hashEntries), ...Object.keys(posEntries)]);

    // Cari semua span kata unik yang punya data-hash (gunakan preset situs)
    const sitePreset = getSiteKataUnikSelector();
    const spans = document.querySelectorAll(sitePreset.glossarySelector);

    // Kelompokkan span per hash value (pertahankan urutan DOM = urutan kemunculan)
    const hashSpanMap = {};
    spans.forEach(span => {
      const hash = span.getAttribute("data-hash");
      if (!hash || !allHashVals.has(hash)) return;
      if (!hashSpanMap[hash]) hashSpanMap[hash] = [];
      hashSpanMap[hash].push(span);
    });

    _replacingNow = true;
    try {
      for (const hashVal of allHashVals) {
        const spanList = hashSpanMap[hashVal];
        if (!spanList) continue;

        spanList.forEach((span, idx) => {
          // Prioritas: positional rule (hash:VAL:IDX) > default rule (hash:VAL)
          let entry = null;
          if (posEntries[hashVal] && posEntries[hashVal][idx] !== undefined) {
            entry = posEntries[hashVal][idx];
          } else if (hashEntries[hashVal]) {
            entry = hashEntries[hashVal];
          }
          if (!entry) return;

          const newText = (typeof entry === "object") ? (entry.to || "") : String(entry);
          if (!newText) return;

          // Ganti hanya text node langsung di dalam span (tidak ubah elemen span itu sendiri)
          // FIX v54: Gunakan hashOriginalTextMap (bukan originalTextMap) agar walker utama
          // tidak ikut me-restore node ini ke teks aslinya dan membatalkan penggantian hash.
          span.childNodes.forEach(child => {
            if (child.nodeType === Node.TEXT_NODE && child.nodeValue !== newText) {
              if (!hashOriginalTextMap.has(child)) hashOriginalTextMap.set(child, child.nodeValue);
              child.nodeValue = newText;
              lastProcessedValueMap.set(child, newText);
            }
          });
        });
      }
    } finally {
      _replacingNow = false;
    }
  }

  const TRANSLATIONS = {
    en: {
      flag: "🇺🇸",
      title: "Advanced Word Replacer",
      current_site: "Current site:",
      active: "ACTIVE",
      off: "OFF",
      disable: "Disable",
      enable: "Enable",
      tab_editor: "Editor",
      tab_terms: "Your Terms",
      tab_filter: "Filter",
      tab_recycle: "Recycle Word",
      tab_setting: "Settings",
      tab_cloud: "Cloud Manager",
      tab_config: "Config",
      search_placeholder: "Search old/new words...",
      select_all: "SELECT ALL",
      bulk_delete: "Delete ({0})",
      empty_state: "Dictionary empty / no results found.",
      show_other_terms: "Show Other Novel Terms ({0})",
      hide_other_terms: "Hide Other Novel Terms ({0})",
      other_terms_title: "{0} TERMS ({1})",
      original_text: "Original Text ({0})",
      replacement_text: "Replacement Text ({0})",
      global_replacer: "All Novels (Global Replacer)",
      local_replacer: "This Novel Only",
      this_novel_desc: "This term will only apply to this novel.",
      delete_btn: "Delete",
      close_btn: "Close",
      save_btn: "Save",
      update_btn: "Update",
      suggested_title: "Suggested Misspelled Words:",
      site_manager: "SITE MANAGER (FILTER)",
      mode_label: "Mode:",
      only_whitelist: "Only Whitelist",
      block_blacklist: "Block Blacklist",
      desc_whitelist: "Script will ONLY run on Whitelist sites listed below.",
      desc_blacklist:
        "Script will run on ALL sites, EXCEPT those listed in Blacklist below.",
      new_whitelist_placeholder: "New whitelist domain...",
      new_blacklist_placeholder: "New blacklist domain...",
      script_settings: "REPLACER SCRIPT CONFIG",
      blue_highlight: "Blue Highlight",
      blue_highlight_desc: "Bold and color replaced words in blue",
      restore_defaults: "Restore Defaults",
      restore_desc: "Delete custom data and reset to default",
      reset_data: "Reset Data",
      toast_removed_whitelist: "Site removed from Whitelist: {0}",
      toast_added_whitelist: "Site added to Whitelist: {0}",
      toast_added_blacklist: "Site added to Blacklist: {0}",
      toast_removed_blacklist: "Site removed from Blacklist: {0}",
      toast_deleted: "Rule '{0}' moved to Recycle Word",
      toast_updated: "Word '{0}' updated",
      toast_added: "Word '{0}' added",
      toast_filter_mode: "Filter mode changed to {0}",
      toast_whitelist_deleted: "Whitelist {0} deleted",
      toast_blacklist_deleted: "Blacklist {0} deleted",
      toast_copied: "Sync Key copied to clipboard!",
      toast_sync_connecting: "Connecting & syncing...",
      toast_sync_success: "Sync connection successful!",
      toast_reset_success: "All settings and words successfully reset!",
      alert_both_fields: "Both word fields are required!",
      alert_already_registered: "Site is already registered!",
      alert_enter_key: "Please enter a Sync Key first!",
      alert_same_key: "Sync Key is identical to this device!",
      alert_overwrite_confirm:
        "Connecting will overwrite local data with cloud data. Continue?",
      alert_bulk_delete_confirm:
        "Are you sure you want to move {0} selected rules to Recycle Word?",
      deleted_words_banner: "Deleted {0} words",
      undo_btn: "Undo",
      toast_restored: "Successfully restored {0} words!",
      sure_btn: "Sure?",
      yakin_btn: "Sure?",
      yakin_reset: "⚠️ SURE RESET? CLICK AGAIN",
      replaced_from: "From {0}",
      replaced_to: "To {0}",
      word_salah_placeholder: "Example: man",
      word_benar_placeholder: "Example: man(woman)",
      undo_tooltip: "Undo / Restore",
      confirm_undo_bulk: "Are you sure you want to restore {0} selected rules?",
      confirm_delete_perm_bulk:
        "Are you sure you want to permanently delete {0} selected rules?",
      toast_undone: "Word '{0}' successfully restored!",
      toast_deleted_perm: "Word '{0}' permanently deleted!",
      toast_bulk_undone: "Successfully restored {0} words!",
      toast_bulk_deleted_perm: "Successfully permanently deleted {0} words!",
      bulk_undo: "Restore ({0})",
      bulk_delete_perm: "Delete Perm ({0})",
      cloud_storage_status: "Cloud Storage Status",
      baskets_used: "Baskets (Configs) Used",
      used_of_max: "{0} of {1} Baskets",
      load_config: "Stored Configurations",
      loading_cloud_data: "Connecting to GitHub Gist...",
      no_backups_found: "No backups found on cloud.",
      current_active: "Active Gist State",
      btn_load: "Load",
      toast_config_loaded: "Configuration restored successfully!",
      toast_config_deleted: "Gist disconnected!",
      toast_github_connected: "Connected to GitHub Gist successfully!",
      toast_account_switched: "Logged out from current GitHub account.",
      toast_revision_restored: "Version '{0}' restored successfully!",
      btn_confirm: "Confirm",
      btn_cancel: "Cancel",
      merge_group: "Merge Group",
      merge_group_desc:
        'Select a target group to merge all terms of "{0}" into:',
      merge_group_success:
        'Successfully merged "{0}" into "{1}" ({2} terms moved)!',
      merge_group_err_same: "Cannot merge a group into itself!",
      rename_group_prompt: "Enter new group / novel title:",
      rename_group_success:
        'Successfully renamed group to "{0}" ({1} terms updated)!',
      logout_confirm:
        "Are you sure you want to disconnect your GitHub account? Local data will not be deleted.",
      delete_group_confirm:
        'Are you sure you want to move all terms in "{0}" to the Recycle Bin?',
      delete_group_success:
        'Moved group "{0}" ({1} terms deleted) to Recycle Word!',
      select_custom_group_placeholder: "Enter new custom group name...",
      alert_enter_custom_name: "Please enter a custom group name!",
      make_group_active: "Make This Group Active (Disable Other Groups)",
      suggested_wrong: "Suggested Misspelled Words:",
      cloud_connected_as: "Connected as:",
      cloud_manual_backup_starting: "Creating backup...",
      cloud_manual_backup_success: "Backup completed successfully!",
      cloud_manual_backup_fail_history: "Failed to load recent history.",
      gist_id_copied: "Gist ID copied to clipboard!",
      token_copied: "GitHub Token copied to clipboard!",
      backup_now: "Backup Now",
      logout: "Logout",
      revision_history: "Revision History",
      gist_err_invalid:
        "Gist ID is invalid or inaccessible!\n\nPossible Causes:\n1. Pasted token in Gist ID field.\n2. Incorrect Gist ID/URL.\n3. Token lacks 'gist' scope.\n4. Gist was deleted.",
      conn_err_verify:
        "Connection error verifying account. Please verify that your GitHub Token has 'gist' permissions and is copied correctly without spaces.",
      gist_load_fail:
        "Failed to load GitHub cloud data. Please check connection or token.",
      case_sensitive: "Case sensitive",
      toast_group_disabled:
        "Group novel deactivated (Returning to native site mode)",
      toast_group_activated:
        'Group novel "{0}" successfully activated exclusively',
      delete_revision_confirm:
        'Are you sure you want to delete the backup from "{0}" permanently?',
      reset_confirm_desc:
        "This action will restore default settings, delete custom dictionaries, recycle history, and disconnect GitHub Gist integration.",
      target_category: "Target Category",
      custom_novel_input_placeholder: "Enter new novel name...",
      highlight_enabled: "Highlight enabled",
      highlight_disabled: "Highlight disabled",
      gist_scan_toast: "Scanning your old Gist dictionaries...",
      gist_scan_found: "Your old Gist was found and auto-filled!",
      gist_scan_not_found: "No old Gist detected. Leave empty to create new.",
      warn_no_custom_name: "Please enter a custom novel/group name!",
      auth_token_classic_link:
        "👉 Get GitHub Access Token Here (Classic Token)",
      auth_token_manage_link: "⚙️ Manage Existing Tokens",
      auth_github_token_label: "GitHub Token:",
      auth_gist_id_label: "Gist ID (Optional):",
      auth_connect_btn: "Connect GitHub",
      auth_connecting_btn: "⌛ Connecting...",
      auth_conn_fail_reason:
        "Connection failed during account verification. Please verify that your GitHub Token has 'gist' permissions and is copied correctly without spaces.",
      auth_cloud_err_msg:
        "⚠️ Failed to load GitHub cloud data.<br>Please check your internet connection or token.",
      awr_version_label: "AWR Tools Version",
      editor_example_tip: "Tip: Use | for variations, * for wildcards",
      update_mode_label: "Update Mode",
      update_mode_desc: "Choose how AWR checks for script updates",
      update_mode_auto: "Auto (On Launch)",
      update_mode_manual: "Manual Only",
      btn_check_update: "Check for Updates",
      toast_checking_update: "Checking for updates...",
      toast_already_latest: "You are already using the latest version (v{0})!",
      toast_update_available:
        "Update available! v{0} is available on Greasy Fork.",
      modal_update_title: "Update Script?",
      modal_update_desc:
        "A new version (v{0}) is available on Greasy Fork. Do you want to update now?",
      btn_import_creds_trigger: "Import local credentials file",
      btn_forgot_gist_link: "Search Gists directly on GitHub",
      btn_backup_creds_file: "Save local credentials backup file",
      btn_auto_detect_gist: "Auto-detect Gist configuration",
      btn_cloud_credentials_vault: "Backup credentials inside Gist vault",
      btn_open_gist_github: "Buka Gist di GitHub",
      toast_creds_imported: "Credentials loaded successfully!",
      toast_creds_exported: "Local credentials backup downloaded!",
      toast_creds_backed_up_cloud: "Credentials Vault updated successfully!",
      recycle_auto_delete_title: "Auto-Clean Recycle Bin",
      recycle_auto_delete_desc:
        "Automatically delete terms from Recycle Bin after a set period",
      auto_delete_never: "Never (Manual Only)",
      auto_delete_days: "Days",
      auto_delete_year: "Year",
      toast_auto_delete_changed:
        "Recycle Bin auto-delete period changed to {0}",
      add_novel_presets_btn: "Add Popular Novel Sites to Whitelist",
      toast_novel_presets_added: "Added {0} novel sites to whitelist!",
      toast_novel_presets_already: "All popular novel sites already in whitelist.",
      tab_scanner: "Hash Scanner",
      scanner_title: "Kata Unik (Glossary Spans) on This Page",
      scanner_empty: "No kata-unik spans (span[data-hash]) found on this page.",
      scanner_add_rule: "+ Add Rule",
      scanner_has_rule: "✓ Has Rule",
      scanner_hint: "Click '+ Add Rule' to create a hash: replacement for that character.",
      scanner_del_rule: "🗑 Delete Rule",
      scanner_edit_rule: "✏ Edit",
      scanner_bulk_placeholder: "Replacement text for all selected...",
      scanner_bulk_save: "💾 Save {0} Rules",
      scanner_bulk_select_hint: "Select rows below, then type replacement text to save all at once.",
      scanner_select_all_rows: "Select All",
      scanner_deselect_all: "Deselect All",
      scanner_pin_occurrence: "⚙ Per Occurrence",
      scanner_occurrence_label: "Occurrence #{0}",
      scanner_occurrence_hint: "This hash appears {0}× on page. Assign different names per occurrence to distinguish same-hash characters.",
      scanner_occurrence_context: "Context",
      scanner_pos_save: "💾 Save Positions",
      scanner_pos_saved: "Positional rules saved!",
      scanner_rule_deleted: "Rule '{0}' deleted.",
      scanner_search_placeholder: "Search name or hash...",
      scanner_filter_all: "All",
      scanner_filter_has_rule: "Has Rule",
      scanner_filter_no_rule: "No Rule",
      scanner_filter_conflict: "⚠ Conflict",
      scanner_sort_alpha: "A–Z",
      scanner_sort_count: "Most ×",
      scanner_sort_rule_first: "Rules First",
      scanner_stat_total: "{0} hash",
      scanner_stat_has_rule: "✓ {0}",
      scanner_stat_no_rule: "○ {0}",
      scanner_stat_conflict: "⚠ {0}",
      scanner_conflict_detail: "Appears in different contexts — may be different characters",
      scanner_note_placeholder: "Note (optional)...",
      scanner_note_label: "Note",
      scanner_bulk_del: "🗑 Delete {0}",
      scanner_visible_count: "shown",
      tab_data: "Data",
      export_novel_btn: "Export This Novel's Rules (JSON)",
      export_novel_desc: "Download all replacement rules for the current active novel as a JSON file.",
      import_novel_btn: "Import Rules from JSON",
      import_novel_desc: "Merge rules from a previously exported JSON file into the current novel.",
      export_success: "Exported {0} rules for novel \"{1}\"",
      export_none: "No rules found for this novel to export.",
      import_success: "Imported {0} rules successfully!",
      import_error: "Import failed — invalid JSON file.",
      import_duplicate: "Skipped {0} existing rules, added {1} new rules.",
    },
    id: {
      flag: "🇮🇩",
      title: "Advanced Word Replacer",
      current_site: "Situs saat ini:",
      active: "AKTIF",
      off: "OFF",
      disable: "Matikan",
      enable: "Aktifkan",
      tab_editor: "Editor",
      tab_terms: "Your Terms",
      tab_filter: "Filter",
      tab_recycle: "Recycle Word",
      tab_setting: "Kelola",
      tab_cloud: "Cloud Manager",
      tab_config: "Config",
      search_placeholder: "Cari kata lama/baru...",
      select_all: "PILIH SEMUA",
      bulk_delete: "Hapus ({0})",
      empty_state: "Kamus kosong / tidak ada hasil.",
      show_other_terms: "Tampilkan Istilah Novel Lain ({0})",
      hide_other_terms: "Sembunyikan Istilah Novel Lain ({0})",
      other_terms_title: "TERMS {0} ({1})",
      original_text: "Teks Asli ({0})",
      replacement_text: "Teks Pengganti ({0})",
      global_replacer: "Semua Novel (Global Replacer)",
      local_replacer: "Hanya Novel Ini",
      this_novel_desc: "Istilah ini hanya akan berlaku pada novel ini.",
      delete_btn: "Hapus",
      close_btn: "Tutup",
      save_btn: "Simpan",
      update_btn: "Perbarui",
      suggested_title: "Rekomendasi Kata Salah:",
      site_manager: "SITUS MANAJER (FILTER)",
      mode_label: "Mode:",
      only_whitelist: "Hanya Whitelist",
      block_blacklist: "Blokir Blacklist",
      desc_whitelist:
        "Skrip HANYA akan berjalan pada situs Whitelist di bawah ini.",
      desc_blacklist:
        "Skrip akan berjalan pada SEMUA situs, KECUALI yang terdaftar di Blacklist di bawah ini.",
      new_whitelist_placeholder: "Domain whitelist baru...",
      new_blacklist_placeholder: "Domain blokir baru...",
      script_settings: "PENGATURAN SKRIP REPLACER (CONFIG)",
      blue_highlight: "Highlight Biru",
      blue_highlight_desc: "Berikan warna biru tebal pada kata yang diganti",
      restore_defaults: "Kembalikan Default",
      restore_desc: "Hapus data kustom dan reset ke bawaan",
      reset_data: "Reset Data",
      toast_removed_whitelist: "Situs dihapus dari Whitelist: {0}",
      toast_added_whitelist: "Situs ditambahkan ke Whitelist: {0}",
      toast_added_blacklist: "Situs ditambahkan ke daftar Blokir: {0}",
      toast_removed_blacklist: "Situs dihapus dari daftar Blokir: {0}",
      toast_deleted: "Aturan '{0}' dipindahkan ke Recycle Word",
      toast_updated: "Kata '{0}' diperbarui",
      toast_added: "Kata '{0}' ditambahkan",
      toast_filter_mode: "Mode filter diubah ke {0}",
      toast_whitelist_deleted: "Whitelist {0} dihapus",
      toast_blacklist_deleted: "Blokir {0} dihapus",
      toast_copied: "Kunci Sinkronisasi disalin ke clipboard!",
      toast_sync_connecting: "Menghubungkan & menyinkronkan...",
      toast_sync_success: "Koneksi sinkronisasi berhasil!",
      toast_reset_success: "Semua setelan dan kata berhasil di-reset!",
      alert_both_fields: "Kedua kolom kata wajib diisi!",
      alert_already_registered: "Situs sudah terdaftar!",
      alert_enter_key: "Silakan masukkan Kunci Sinkronisasi terlebih dahulu!",
      alert_same_key: "Kunci Sinkronisasi sama dengan perangkat ini!",
      alert_overwrite_confirm:
        "Menghubungkan perangkat akan menimpa data lokal dengan data awan baru (jika ada). Lanjutkan?",
      alert_bulk_delete_confirm:
        "Yakin ingin memindahkan {0} aturan kata terpilih ke Recycle Word?",
      deleted_words_banner: "Terhapus {0} kata",
      undo_btn: "Urungkan",
      toast_restored: "Berhasil mengembalikan {0} kata!",
      sure_btn: "Yakin?",
      yakin_btn: "Yakin?",
      yakin_reset: "⚠️ YAKIN RESET? KLIK LAGI",
      replaced_from: "From {0}",
      replaced_to: "To {0}",
      word_salah_placeholder: "Contoh: pria",
      word_benar_placeholder: "Contoh: pria(wanita)",
      undo_tooltip: "Urungkan / Kembalikan",
      confirm_undo_bulk:
        "Apakah Anda yakin ingin mengembalikan {0} kata terpilih?",
      confirm_delete_perm_bulk:
        "Apakah Anda yakin ingin menghapus permanen {0} kata terpilih?",
      toast_undone: "Kata '{0}' berhasil dikembalikan!",
      toast_deleted_perm: "Kata '{0}' berhasil dihapus permanen!",
      toast_bulk_undone: "Berhasil mengembalikan {0} kata!",
      toast_bulk_deleted_perm: "Berhasil menghapus permanen {0} kata!",
      bulk_undo: "Urungkan ({0})",
      bulk_delete_perm: "Hapus Permanen ({0})",
      cloud_storage_status: "Status Penyimpanan Cloud",
      baskets_used: "Basket (Config) Terpakai",
      used_of_max: "{0} dari {1} Basket",
      load_config: "Daftar Konfigurasi Tersimpan",
      loading_cloud_data: "Menghubungkan ke GitHub Gist...",
      no_backups_found: "Tidak ada konfigurasi tersimpan di Gist ini.",
      current_active: "Status Gist Aktif",
      btn_load: "Muat",
      toast_config_loaded: "Konfigurasi berhasil dipulihkan!",
      toast_config_deleted: "Gist diputuskan!",
      toast_github_connected: "Berhasil terhubung ke GitHub Gist!",
      toast_account_switched: "Berhasil keluar dari akun GitHub.",
      toast_revision_restored: "Versi '{0}' berhasil dipulihkan!",
      btn_confirm: "Lanjutkan",
      btn_cancel: "Batal",
      merge_group: "Gabungkan Grup",
      merge_group_desc:
        'Pilih grup tujuan untuk menggabungkan semua istilah dari "{0}" ke:',
      merge_group_success:
        'Berhasil menggabungkan "{0}" ke "{1}" ({2} istilah dipindahkan)!',
      merge_group_err_same:
        "Tidak dapat menggabungkan grup ke dirinya sendiri!",
      rename_group_prompt: "Masukkan nama novel / grup kustom baru:",
      rename_group_success:
        'Berhasil mengubah nama grup menjadi "{0}" ({1} kata diperbarui)!',
      logout_confirm:
        "Apakah Anda yakin ingin memutuskan koneksi akun GitHub saat ini? Data lokal Anda tidak akan terhapus.",
      delete_group_confirm:
        'Apakah Anda yakin ingin memindahkan seluruh kata/aturan dalam grup novel "{0}" ke Recycle Bin?',
      delete_group_success:
        'Berhasil memindahkan seluruh grup "{0}" ({1} kata dihapus) ke Recycle Word!',
      select_custom_group_placeholder: "Masukkan nama novel/grup baru...",
      alert_enter_custom_name: "Silakan masukkan nama novel/grup baru!",
      make_group_active: "Jadikan Grup Ini Aktif (Nonaktifkan Grup Lain)",
      suggested_wrong: "Rekomendasi Kata Salah:",
      cloud_connected_as: "Terhubung sebagai:",
      cloud_manual_backup_starting: "Mencadangkan kata ke GitHub...",
      cloud_manual_backup_success: "Kamus kata berhasil dicadangkan!",
      cloud_manual_backup_fail_history: "Gagal mengambil riwayat terbaru.",
      gist_id_copied: "Gist ID disalin ke clipboard!",
      token_copied: "GitHub Token disalin ke clipboard!",
      backup_now: "Cadangkan Sekarang",
      logout: "Logout",
      revision_history: "Riwayat Revisi",
      gist_err_invalid:
        "Gist ID tidak valid atau tidak dapat diakses!\n\nKemungkinan Penyebab:\n1. Menempelkan token di kolom Gist ID secara tidak sengaja.\n2. Gist ID/URL salah.\n3. Token tidak memiliki izin 'gist'.\n4. Gist telah dihapus.",
      conn_err_verify:
        "Terjadi kesalahan koneksi saat memverifikasi akun. Pastikan Token GitHub Anda memiliki izin 'gist' dan disalin dengan benar tanpa spasi tambahan.",
      gist_load_fail:
        "Gagal memuat data cloud GitHub. Silakan periksa koneksi internet atau token Anda.",
      case_sensitive: "Sensitif huruf",
      toast_group_disabled:
        "Grup novel dinonaktifkan (Kembali ke mode alami situs)",
      toast_group_activated:
        'Grup novel "{0}" berhasil diaktifkan secara eksklusif',
      delete_revision_confirm:
        'Apakah Anda yakin ingin menghapus berkas cadangan tanggal "{0}" dari cloud secara permanen?',
      reset_confirm_desc:
        "Tindakan ini akan mengembalikan setelan skrip ke kondisi awal serta menghapus seluruh data kamus kustom, log sampah, dan memutus integrasi cloud GitHub Anda.",
      target_category: "Target Kategori",
      custom_novel_input_placeholder: "Masukkan nama novel baru...",
      highlight_enabled: "Highlight aktif",
      highlight_disabled: "Highlight nonaktif",
      gist_scan_toast: "Memindai kamus Gist lama Anda...",
      gist_scan_found: "Gist lama Anda ditemukan dan diisi secara otomatis!",
      gist_scan_not_found:
        "Tidak ada Gist lama terdeteksi. Silakan biarkan kosong jika ingin membuat baru.",
      warn_no_custom_name: "Silakan masukkan nama novel/grup baru!",
      auth_token_classic_link:
        "👉 Get GitHub Access Token Here (Classic Token)",
      auth_token_manage_link:
        "⚙️ Manage Existing Tokens (Kelola Token Lama Anda)",
      auth_github_token_label: "GitHub Token:",
      auth_gist_id_label: "Gist ID (Optional):",
      auth_connect_btn: "Connect GitHub",
      auth_connecting_btn: "⌛ Menghubungkan...",
      auth_conn_fail_reason:
        "Terjadi kesalahan koneksi saat memverifikasi akun. Pastikan Token GitHub Anda memiliki izin 'gist' dan disalin dengan benar tanpa spasi tambahan.",
      auth_cloud_err_msg:
        "⚠️ Gagal memuat data cloud GitHub.<br>Silakan periksa koneksi internet atau token Anda.",
      awr_version_label: "Versi Skrip AWR",
      editor_example_tip: "Tip: Gunakan | untuk variasi, * untuk wildcard",
      update_mode_label: "Mode Pembaruan",
      update_mode_desc: "Pilih cara AWR memeriksa pembaruan skrip",
      update_mode_auto: "Otomatis (Saat Start)",
      update_mode_manual: "Hanya Manual",
      btn_check_update: "Periksa Pembaruan",
      toast_checking_update: "Memeriksa pembaruan...",
      toast_already_latest:
        "Skrip Anda sudah menggunakan versi terbaru (v{0})!",
      toast_update_available:
        "Pembaruan tersedia! v{0} sudah rilis di Greasy Fork.",
      modal_update_title: "Perbarui Skrip?",
      modal_update_desc:
        "Versi terbaru (v{0}) telah tersedia di Greasy Fork. Apakah Anda ingin memperbarui sekarang?",
      btn_import_creds_trigger: "Impor file kredensial lokal (.json)",
      btn_forgot_gist_link: "Cari data Gist Anda langsung di GitHub",
      btn_backup_creds_file: "Unduh file cadangan kredensial lokal",
      btn_auto_detect_gist: "Deteksi otomatis konfigurasi Gist",
      btn_cloud_credentials_vault: "Cadangkan kredensial login ke Gist",
      btn_open_gist_github: "Buka Gist di GitHub",
      toast_creds_imported: "Kredensial berhasil dimuat!",
      toast_creds_exported: "File cadangan lokal berhasil diunduh!",
      toast_creds_backed_up_cloud: "Kredensial berhasil dicadangkan!",
      recycle_auto_delete_title: "Pembersihan Otomatis Sampah",
      recycle_auto_delete_desc:
        "Hapus otomatis Recycle Bin secara permanen setelah jangka waktu tertentu",
      auto_delete_never: "Jangan Pernah (Manual)",
      auto_delete_days: "Hari",
      auto_delete_year: "Tahun",
      toast_auto_delete_changed:
        "Jangka waktu pembersihan otomatis sampah diubah menjadi {0}",
      add_novel_presets_btn: "Tambah Situs Novel Populer ke Whitelist",
      toast_novel_presets_added: "{0} situs novel berhasil ditambahkan ke whitelist!",
      toast_novel_presets_already: "Semua situs novel populer sudah ada di whitelist.",
      tab_scanner: "Hash Scanner",
      scanner_title: "Kata Unik (Glossary Spans) di Halaman Ini",
      scanner_empty: "Tidak ada span kata unik (span[data-hash]) di halaman ini.",
      scanner_add_rule: "+ Tambah Rule",
      scanner_has_rule: "✓ Ada Rule",
      scanner_hint: "Klik '+ Tambah Rule' untuk buat penggantian hash: bagi karakter itu.",
      scanner_del_rule: "🗑 Hapus Rule",
      scanner_edit_rule: "✏ Edit",
      scanner_bulk_placeholder: "Teks pengganti untuk semua yang dipilih...",
      scanner_bulk_save: "💾 Simpan {0} Rule",
      scanner_bulk_select_hint: "Pilih baris di bawah, lalu ketik teks pengganti untuk menyimpan semua sekaligus.",
      scanner_select_all_rows: "Pilih Semua",
      scanner_deselect_all: "Batal Pilih",
      scanner_pin_occurrence: "⚙ Per Kemunculan",
      scanner_occurrence_label: "Kemunculan ke-{0}",
      scanner_occurrence_hint: "Hash ini muncul {0}× di halaman. Beri nama berbeda per kemunculan untuk membedakan karakter dengan hash yang sama.",
      scanner_occurrence_context: "Konteks",
      scanner_pos_save: "💾 Simpan Posisi",
      scanner_pos_saved: "Rule posisional berhasil disimpan!",
      scanner_rule_deleted: "Rule '{0}' dihapus.",
      scanner_search_placeholder: "Cari nama atau hash...",
      scanner_filter_all: "Semua",
      scanner_filter_has_rule: "Ada Rule",
      scanner_filter_no_rule: "Belum Ada",
      scanner_filter_conflict: "⚠ Konflik",
      scanner_sort_alpha: "A–Z",
      scanner_sort_count: "Terbanyak ×",
      scanner_sort_rule_first: "Ada Rule Dulu",
      scanner_stat_total: "{0} hash",
      scanner_stat_has_rule: "✓ {0}",
      scanner_stat_no_rule: "○ {0}",
      scanner_stat_conflict: "⚠ {0}",
      scanner_conflict_detail: "Muncul di konteks berbeda — kemungkinan karakter berbeda",
      scanner_note_placeholder: "Catatan (opsional)...",
      scanner_note_label: "Catatan",
      scanner_bulk_del: "🗑 Hapus {0}",
      scanner_visible_count: "ditampilkan",
      tab_data: "Data",
      export_novel_btn: "Export Rule Novel Ini (JSON)",
      export_novel_desc: "Unduh semua aturan penggantian untuk novel aktif saat ini sebagai file JSON.",
      import_novel_btn: "Import Rule dari JSON",
      import_novel_desc: "Gabungkan aturan dari file JSON yang sebelumnya diekspor ke novel saat ini.",
      export_success: "Berhasil mengekspor {0} aturan untuk novel \"{1}\"",
      export_none: "Tidak ada aturan yang ditemukan untuk novel ini.",
      import_success: "Berhasil mengimpor {0} aturan!",
      import_error: "Impor gagal — file JSON tidak valid.",
      import_duplicate: "Dilewati {0} aturan duplikat, ditambahkan {1} aturan baru.",
    },
  };

  let core = null;

  function getLang() {
    return GM_getValue("awr_lang_v1", "en");
  }
  function saveLang(langCode) {
    GM_setValue("awr_lang_v1", langCode);
  }

  function t(key, ...args) {
    const lang = getLang();
    const dict = TRANSLATIONS[lang] || TRANSLATIONS["en"];
    const template = dict[key] || TRANSLATIONS["en"][key] || "";
    if (!template) return key;
    return template.replace(/{(\d+)}/g, (match, index) => {
      return typeof args[index] !== "undefined" ? args[index] : match;
    });
  }

  function panggilToast(pesan, tipe = "success") {
    if (!toast) {
      console.log(`[Toast Fallback] ${pesan}`);
      return;
    }
    toast.innerHTML = "";
    const textSpan = document.createElement("span");
    textSpan.textContent = pesan;
    toast.appendChild(textSpan);
    toast.className = "replacer-toast show " + tipe;
    if (toastTimeout) clearTimeout(toastTimeout);
    toastTimeout = setTimeout(() => {
      toast.className = "replacer-toast";
    }, 3000);
  }

  // ── Diagnosa Kata ─────────────────────────────────────────────────────────
  function diagnosisKata(key, itemData) {
    const currentNovel = getNovelContext();
    const activeNovelId = getActiveNovelId();
    const domainAllowed = isDomainAllowed();
    const pageBaseDomain = getNovelBaseDomain(currentHost);

    const toVal = itemData && typeof itemData === "object"
      ? itemData.to || ""
      : itemData || "";
    const isGlobal = !!(itemData && typeof itemData === "object" && itemData.global);
    const storedNovelId   = (itemData && typeof itemData === "object" && itemData.novelId)   ? itemData.novelId   : "";
    const storedNovelTitle= (itemData && typeof itemData === "object" && itemData.novelTitle) ? itemData.novelTitle: "";
    const storedNovelUrl  = (itemData && typeof itemData === "object" && itemData.novelUrl)   ? itemData.novelUrl  : "";
    const storedDomain    = (itemData && typeof itemData === "object" && itemData.domain)     ? itemData.domain    : "";
    const caseSensitive   = !!(itemData && typeof itemData === "object" && itemData.caseSensitive);

    const targetActiveId = activeNovelId ? activeNovelId : currentNovel.id;

    let layer0aMatch = false, layer0bMatch = false;
    let layer1Match  = false, layer2Match  = false, layer3Match = false;

    if (storedNovelId) {
      layer0aMatch = storedNovelId === targetActiveId;
      layer0bMatch = storedNovelId === currentNovel.id;

      const itemTitle  = (storedNovelTitle || getCachedNovelTitle(storedNovelId) || "").toLowerCase().trim();
      const pageTitle  = (currentNovel.title || "").toLowerCase().trim();
      layer1Match = !!(itemTitle && pageTitle && titlesMatchFuzzy(itemTitle, pageTitle));

      try { layer2Match = sameNovelByUrl(storedNovelId, currentNovel.id || window.location.href); } catch(_) {}
      try { layer3Match = storedNovelUrl ? sameNovelByUrl(storedNovelUrl, currentNovel.url || window.location.href) : false; } catch(_) {}
    }

    const novelMatchAny = layer0aMatch || layer0bMatch || layer1Match || layer2Match || layer3Match;

    let isActive = false;
    let activationReason = "";
    if (isGlobal) {
      isActive = true;
      activationReason = "✅ Global — aktif di semua halaman";
    } else if (storedNovelId) {
      if (novelMatchAny && activeNovelId !== "GLOBAL_ONLY") {
        isActive = true;
        if      (layer0aMatch) activationReason = "✅ Aktif — novelId === targetActiveId (Layer 0A)";
        else if (layer0bMatch) activationReason = "✅ Aktif — novelId === currentNovel.id (Layer 0B)";
        else if (layer1Match)  activationReason = "✅ Aktif via Layer 1 — judul novel cocok";
        else if (layer2Match)  activationReason = "✅ Aktif via Layer 2 — novelId URL cocok";
        else if (layer3Match)  activationReason = "✅ Aktif via Layer 3 — novelUrl cocok";
      } else if (activeNovelId === "GLOBAL_ONLY") {
        activationReason = "❌ Tidak aktif — mode GLOBAL_ONLY aktif";
      } else {
        activationReason = "❌ Tidak aktif — tidak ada layer yang cocok dengan novel ini";
      }
    } else if (storedDomain) {
      const termBase = getNovelBaseDomain(storedDomain);
      const domOk = domainAllowed && (termBase === pageBaseDomain || pageBaseDomain.endsWith("." + termBase));
      isActive = domOk;
      activationReason = domOk
        ? "✅ Aktif — domain cocok dan situs diizinkan"
        : "❌ Tidak aktif — domain tidak cocok atau situs tidak diizinkan";
    }

    const scopeLabel = isGlobal ? "🌐 Global" : storedNovelId ? "📖 Novel/Grup Spesifik" : "🏠 Domain Spesifik";
    const statusColor = isActive ? "#34d399" : "#f87171";
    const statusBg    = isActive ? "rgba(16,185,129,0.1)" : "rgba(239,68,68,0.1)";
    const statusBorder= isActive ? "rgba(16,185,129,0.3)" : "rgba(239,68,68,0.3)";

    const srow = (label, value) =>
      `<div style="display:flex!important;gap:8px!important;align-items:baseline!important;padding:4px 0!important;border-bottom:1px solid #1e293b!important;">
         <span style="color:#64748b!important;font-size:10px!important;min-width:130px!important;flex-shrink:0!important;">${label}</span>
         <span style="color:#cbd5e1!important;font-size:11px!important;font-family:monospace!important;word-break:break-all!important;">${value || `<em style="color:#475569!important;">kosong</em>`}</span>
       </div>`;

    const lrow = (label, match) =>
      `<div style="display:flex!important;gap:8px!important;align-items:center!important;padding:3px 0!important;">
         <span style="font-size:12px!important;">${match ? "✅" : "⬜"}</span>
         <span style="color:${match ? "#34d399" : "#64748b"}!important;font-size:10px!important;">${label}</span>
       </div>`;

    const section = (title) =>
      `<div style="font-size:9px!important;font-weight:bold!important;color:#64748b!important;text-transform:uppercase!important;letter-spacing:0.05em!important;margin-top:10px!important;margin-bottom:6px!important;border-top:1px solid #1e293b!important;padding-top:8px!important;">${title}</div>`;

    const bodyHTML = `
      <div style="font-size:11px!important;line-height:1.5!important;max-height:55vh!important;overflow-y:auto!important;padding-right:4px!important;">
        <div style="background:${statusBg}!important;border:1px solid ${statusBorder}!important;border-radius:8px!important;padding:8px 12px!important;margin-bottom:10px!important;font-size:12px!important;font-weight:bold!important;color:${statusColor}!important;">
          ${activationReason || (isActive ? "✅ AKTIF di halaman ini" : "❌ TIDAK AKTIF di halaman ini")}
        </div>

        ${section("📋 Detail Entri")}
        ${srow("Kata Asli (key)", `<strong style="color:#e5e7eb!important;">${key}</strong>`)}
        ${srow("Diganti Dengan", toVal)}
        ${srow("Ruang Lingkup", scopeLabel)}
        ${srow("Case Sensitive", caseSensitive ? "Ya" : "Tidak")}

        ${section("📖 Info Novel Tersimpan")}
        ${srow("novelId tersimpan", storedNovelId)}
        ${srow("novelTitle tersimpan", storedNovelTitle)}
        ${srow("novelUrl tersimpan", storedNovelUrl)}
        ${srow("domain tersimpan", storedDomain)}

        ${section("📍 Konteks Halaman Saat Ini")}
        ${srow("currentNovel.id", currentNovel.id)}
        ${srow("currentNovel.title", currentNovel.title)}
        ${srow("currentNovel.url", currentNovel.url)}
        ${srow("activeNovelId (grup aktif)", activeNovelId || `<em style="color:#475569!important;">tidak ada</em>`)}
        ${srow("targetActiveId", targetActiveId)}
        ${srow("Domain halaman", pageBaseDomain)}
        ${srow("Domain diizinkan", domainAllowed ? "Ya ✅" : "Tidak ❌")}

        ${storedNovelId ? `
        ${section("🔗 Hasil Pencocokan Layer")}
        ${lrow("Layer 0A: novelId === targetActiveId (grup aktif / currentId)", layer0aMatch)}
        ${lrow("Layer 0B: novelId === currentNovel.id (langsung, independen)", layer0bMatch)}
        ${lrow("Layer 1 : Judul novel cocok secara teks", layer1Match)}
        ${lrow("Layer 2 : sameNovelByUrl(novelId, currentId)", layer2Match)}
        ${lrow("Layer 3 : sameNovelByUrl(novelUrl, currentUrl)", layer3Match)}
        ` : ""}
      </div>`;

    window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
      `🔍 Diagnosa Kata: "${key}"`,
      bodyHTML,
      () => {},
    );

    // Ganti tombol konfirmasi di footer untuk menyuntikkan tombol "Perbaiki Semua"
    setTimeout(() => {
      const ov = panel.querySelector(".replacer-confirm-overlay");
      if (!ov) return;
      const footer = ov.querySelector(".replacer-confirm-footer");
      if (footer) {
        footer.innerHTML = `
          <button class="form-btn btn-perbaiki-semua-diag btn-pill-primary" style="background:#1d4ed8!important;border-color:#1d4ed8!important;color:#fff!important;font-size:11px!important;padding:6px 12px!important;margin-right:auto!important;">🔧 Perbaiki Semua</button>
          <button class="form-btn confirm-ok-btn" style="background:#1e293b!important;border-color:#475569!important;color:#e2e8f0!important;font-size:11px!important;padding:6px 12px!important;">Tutup</button>
        `;
        const fixAllBtn = footer.querySelector(".btn-perbaiki-semua-diag");
        const closeBtn = footer.querySelector(".confirm-ok-btn");

        if (fixAllBtn) {
          fixAllBtn.onclick = (e) => {
            e.stopPropagation();
            ov.remove();
            
            const targetId = currentNovel.id;
            const staleId = storedNovelId;
            let count = 0;

            // 1. Remap elements sharing the exact obsolete novelId of this diagnosed item
            if (staleId && targetId && staleId !== targetId) {
              const kamus = core.getKamus();
              for (const k in kamus) {
                const entry = kamus[k];
                if (entry && typeof entry === "object" && entry.novelId === staleId) {
                  entry.novelId = targetId;
                  if (currentNovel.title) entry.novelTitle = currentNovel.title;
                  count++;
                }
              }
              if (count > 0) {
                core.saveKamus(kamus);
                core.simpanKeAwan(kamus);
              }
            }

            // 2. Also run the general perbaikiIdNovel to catch other title-matched groups
            const generalCount = core.perbaikiIdNovel();
            const totalUpdated = Math.max(count, generalCount);

            if (totalUpdated > 0) {
              panggilToast(`✅ Berhasil memetakan ulang ${totalUpdated} novelId usang ke currentNovel.id!`, "success");
              core.jalankanPengganti(true);
              renderTampilan();
            } else {
              panggilToast("Tidak ada novelId usang yang cocok untuk diperbaiki.", "info");
            }
          };
        }
        if (closeBtn) {
          closeBtn.onclick = (e) => {
            e.stopPropagation();
            ov.remove();
          };
        }
      }
    }, 0);
  }

  function renderUnifiedItem(
    k,
    container,
    isFromOther,
    isRecycle,
    terpilih,
    seluruhData,
    parentPane,
  ) {
    const itemData = seluruhData[k];
    const item = document.createElement("div");
    item.className = "word-item flex-row p-box";
    if (isFromOther) item.style.opacity = "0.7";

    const checked = terpilih.includes(k) ? "checked" : "";
    const toVal =
      itemData && typeof itemData === "object"
        ? itemData.to || ""
        : itemData || "";

    let badgeHTML = "";
    if (k.startsWith("hash:")) {
      const hashVal = k.slice(5);
      badgeHTML = `<span class="term-badge" style="background:rgba(234,179,8,0.15)!important;color:#fbbf24!important;border:1px solid rgba(234,179,8,0.3)!important;">🔑 Hash Target: <code style="font-size:10px">${hashVal}</code></span>`;
    } else if (itemData && typeof itemData === "object") {
      if (itemData.global) {
        badgeHTML = `<span class="term-badge global">🌐 ${t("global_replacer")}</span>`;
      } else {
        const cleanNovel =
          core.getCachedNovelTitle(itemData.novelId) ||
          itemData.novelTitle ||
          core.getNovelBaseDomain(itemData.domain) ||
          "Novel";
        badgeHTML = `<span class="term-badge local">${isRecycle ? "📖 " : ""}${t("local_replacer")} (${cleanNovel})</span>`;
      }
    } else {
      badgeHTML = `<span class="term-badge global">🌐 ${t("global_replacer")}</span>`;
    }

    // v99.12.37: Cek apakah rule sedang dinonaktifkan (_off_: prefix di kamus)
    const isDisabledRule = k.startsWith("_off_:");
    const displayKey = isDisabledRule ? k.slice(6) : k;

    const leftActionBtn = isRecycle
      ? `<button class="awr-tooltip-btn modern-icon-btn undo-btn-spec" data-tooltip="${t("undo_tooltip")}">${core.SVGS.undo}</button>`
      : `<button class="awr-tooltip-btn modern-icon-btn edit-btn-spec" data-tooltip="Edit Term">${core.SVGS.pen}</button>`;

    const toggleActiveBtn = !isRecycle
      ? `<button class="awr-tooltip-btn modern-icon-btn awr-toggle-rule-btn" data-key="${k.replace(/"/g, "&quot;")}"
           data-tooltip="${isDisabledRule ? "Aktifkan Rule" : "Nonaktifkan Rule Sementara"}"
           style="font-size:11px!important;opacity:${isDisabledRule ? "0.5" : "1"}!important;">
           ${isDisabledRule ? "🔘" : "✅"}
         </button>`
      : "";

    item.style.opacity = isDisabledRule ? "0.5" : "1";

    item.innerHTML = `
            <input type="checkbox" class="word-checkbox" data-key="${k.replace(/"/g, "&quot;")}" ${checked} style="cursor: pointer !important; margin: 0 4px 0 0 !important; flex-shrink: 0 !important;" />
            ${leftActionBtn}
            <div class="word-pair flex-col" style="gap: 2px !important; min-width: 0 !important; flex: 1 !important; margin-left: 6px;">
                <span class="ellipsis" style="font-size: 12px !important; font-weight: 500 !important; color: ${isDisabledRule ? "#6b7280" : "#e5e7eb"} !important;">
                  <span style="color: #6b7280 !important; font-weight: bold !important;">FROM:</span>
                  ${isDisabledRule ? `<s style="color:#6b7280">${displayKey}</s>` : displayKey}
                </span>
                <span class="ellipsis" style="font-size: 12px !important; font-weight: 500 !important; color: #e5e7eb !important;"><span style="color: #6b7280 !important; font-weight: bold !important;">TO:</span> ${toVal}</span>
                ${badgeHTML}
                ${isDisabledRule ? '<span style="font-size:9px;color:#f87171;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:4px;padding:1px 5px;display:inline-block;margin-top:2px;">⏸ Dinonaktifkan</span>' : ""}
            </div>
            <div class="action-buttons" style="flex-shrink: 0 !important; margin-left: 6px !important; display: flex !important; align-items: center !important; gap: 2px !important;">
                ${toggleActiveBtn}
                ${!isRecycle ? `<button class="awr-tooltip-btn modern-icon-btn diagnose-btn-spec" data-tooltip="Diagnosa Kata" style="font-size: 12px !important; padding: 6px !important;">🔍</button>` : ""}
                <button class="awr-tooltip-btn modern-icon-btn danger delete" data-tooltip="${isRecycle ? "Permanent Delete" : "Delete Term"}">
                    ${core.SVGS.trash}
                </button>
            </div>
        `;

    // v99.12.37: Handler tombol toggle aktif/nonaktif rule
    const toggleRuleBtn = item.querySelector(".awr-toggle-rule-btn");
    if (toggleRuleBtn) {
      toggleRuleBtn.onclick = (e) => {
        e.stopPropagation();
        pushUndoSnapshot(isDisabledRule ? "Aktifkan rule" : "Nonaktifkan rule");
        const tk = core.getKamus();
        if (isDisabledRule) {
          // Aktifkan: hapus prefix _off_:
          const realKey = k.slice(6);
          tk[realKey] = tk[k];
          delete tk[k];
          panggilToast('✅ Rule "' + realKey + '" diaktifkan kembali.', "success");
        } else {
          // Nonaktifkan: tambah prefix _off_:
          tk["_off_:" + k] = tk[k];
          delete tk[k];
          panggilToast('⏸ Rule "' + k + '" dinonaktifkan sementara.', "warn");
        }
        core.saveKamus(tk);
        core.simpanKeAwan(tk);
        core.jalankanPengganti(true);
        renderTampilan();
      };
    }

    item.querySelector(".word-checkbox").onchange = (e) => {
      if (e.target.checked) {
        if (!terpilih.includes(k)) terpilih.push(k);
      } else {
        const idx = terpilih.indexOf(k);
        if (idx !== -1) terpilih.splice(idx, 1);
      }
      parentPane.perbaruiStatusMassal();
    };

    if (isRecycle) {
      item.querySelector(".undo-btn-spec").onclick = (e) => {
        e.stopPropagation();
        window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          "Pulihkan Aturan Kata?",
          `Aturan kata "${k}" akan dipulihkan ke kamus utama Anda.`,
          () => {
            const tempKamus = core.getKamus(),
              deletedWords = core.getDeletedWords();
            if (deletedWords[k]) {
              tempKamus[k] = deletedWords[k];
              delete deletedWords[k];
              core.saveKamus(tempKamus);
              core.saveDeletedWords(deletedWords);
              core.simpanKeAwan(tempKamus, null, deletedWords);
              panggilToast(t("toast_undone", k), "success");
              core.jalankanPengganti(true);
              renderTampilan();
            }
          },
        );
      };
    } else {
      item.querySelector(".edit-btn-spec").onclick = (e) => {
        e.stopPropagation();
        subjekEdit = k;
        tabAktif = "tambah";
        renderTampilan();
      };

      const diagBtn = item.querySelector(".diagnose-btn-spec");
      if (diagBtn) {
        diagBtn.onclick = (e) => {
          e.stopPropagation();
          diagnosisKata(k, itemData);
        };
      }
    }

    item.querySelector(".delete").onclick = (e) => {
      e.stopPropagation();
      if (isRecycle) {
        window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          "Hapus Permanen?",
          `Aturan kata "${k}" akan dihapus permanen secara total. Tindakan ini tidak dapat dibatalkan.`,
          () => {
            const deletedWords = core.getDeletedWords();
            if (deletedWords[k]) {
              delete deletedWords[k];
              core.saveDeletedWords(deletedWords);
              core.simpanKeAwan(null, null, deletedWords);
              panggilToast(t("toast_deleted_perm", k), "warn");
              renderTampilan();
            }
          },
        );
      } else {
        window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          "Hapus Kata?",
          `Aturan kata dari "${k}" ke "${toVal}" akan dipindahkan ke Recycle Bin.`,
          () => {
            const tempKamus = core.getKamus(),
              deletedWords = core.getDeletedWords(),
              backupWord = k,
              backupValue = tempKamus[k];
            delete tempKamus[k];
            core.saveKamus(tempKamus);

            const backupValueCopy = JSON.parse(JSON.stringify(backupValue));
            backupValueCopy.deletedAt = Date.now();
            deletedWords[backupWord] = backupValueCopy;
            core.saveDeletedWords(deletedWords);

            core.simpanKeAwan(tempKamus, null, deletedWords);
            panggilToast(t("toast_deleted", k), "warn");
            core.jalankanPengganti(true);
            renderTampilan();
          },
        );
      }
    };

    container.appendChild(item);
  }

  function ubahJudulNovel(nId, currentTitle) {
    if (!nId) return;

    window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
      "✏️ Ubah Nama Grup",
      t("rename_group_prompt"),
      () => {},
    );

    const overlay = panel.querySelector(".replacer-confirm-overlay");
    if (!overlay) return;

    const bodyEl = overlay.querySelector(".replacer-confirm-body");
    if (bodyEl) {
      bodyEl.innerHTML = `
        <div style="display:flex!important;flex-direction:column!important;gap:10px!important;padding:2px 0!important;">
          <p style="margin:0!important;color:#cbd5e1!important;font-size:13px!important;">${t("rename_group_prompt")}</p>
          <input
            type="text"
            class="form-input rename-group-input"
            value="${(currentTitle || "").replace(/"/g, "&quot;")}"
            placeholder="Nama novel / grup baru..."
            style="width:100%!important;box-sizing:border-box!important;font-size:13px!important;"
          />
        </div>`;
    }

    setTimeout(() => {
      const inp = overlay.querySelector(".rename-group-input");
      if (inp) { inp.focus(); inp.select(); }
    }, 60);

    const confirmBtn = overlay.querySelector(".replacer-confirm-ok");
    if (confirmBtn) {
      confirmBtn.onclick = () => {
        const newTitle = (
          overlay.querySelector(".rename-group-input")?.value || ""
        ).trim();
        if (!newTitle) {
          panggilToast(t("alert_enter_custom_name") || "Masukkan nama baru!", "warn");
          return;
        }
        overlay.remove();

        const tempKamus = core.getKamus();
        let count = 0;
        for (const key in tempKamus) {
          const item = tempKamus[key];
          if (
            item &&
            typeof item === "object" &&
            !item.global &&
            item.novelId === nId
          ) {
            item.novelTitle = newTitle;
            count++;
          }
        }

        core.saveCachedNovelTitle(nId, newTitle);
        core.saveKamus(tempKamus);
        core.simpanKeAwan(tempKamus);

        panggilToast(t("rename_group_success", newTitle, count), "success");
        core.jalankanPengganti(true);
        renderTampilan();
      };
    }

    overlay.querySelector(".rename-group-input")?.addEventListener("keydown", (e) => {
      if (e.key === "Enter") confirmBtn?.click();
      if (e.key === "Escape") overlay.remove();
    });
  }

  function tampilkanMergeGroup(nId, nTitle) {
    if (!nId) return;

    const semuaKamus = core.getKamus();
    const groupIdSet = new Set();
    for (const k in semuaKamus) {
      const item = semuaKamus[k];
      if (item && typeof item === "object" && !item.global && item.novelId) {
        groupIdSet.add(item.novelId);
      }
    }

    if (nTitle && !groupIdSet.has(nId)) groupIdSet.add(nId);

    const groupIds = Array.from(groupIdSet);
    groupIds.sort((a, b) => {
      const ta = core.getCachedNovelTitle(a) || a;
      const tb = core.getCachedNovelTitle(b) || b;
      return ta.localeCompare(tb);
    });

    window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
      t("merge_group"),
      t("merge_group_desc", nTitle || nId),
      () => {},
    );

    const overlay = panel.querySelector(".replacer-confirm-overlay");
    if (!overlay) return;
    const box = overlay.querySelector(".replacer-confirm-box");
    if (!box) return;

    const existingFooter = overlay.querySelector(".replacer-confirm-footer");
    // FIX: sembunyikan GLOBAL_OPTION saat sumber sudah global (tidak perlu memindahkan ke dirinya sendiri)
    const targetSelectOptions = [
      ...(nId !== "GLOBAL" ? [`<option value="GLOBAL_OPTION">🌐 ${t("global_replacer")}</option>`] : []),
      ...groupIds
        .filter((gid) => gid !== nId)
        .map((gid) => {
          const label = core.getCachedNovelTitle(gid) || gid;
          return `<option value="${gid}">📖 ${label}</option>`;
        }),
    ].join("");

    overlay.querySelector(".replacer-confirm-body").innerHTML = `
      <div style="display:flex!important;flex-direction:column!important;gap:8px!important;overflow:visible!important;">
        <div style="font-size:11px!important;color:#94a3b8!important;line-height:1.4!important;">
          Source: <span style="color:#60a5fa!important;font-weight:700!important;">${nTitle || nId}</span>
        </div>
        <label style="font-size:11px!important;color:#cbd5e1!important;font-weight:700!important;">Target Group</label>
        <select class="form-input merge-target-select" style="width:100%!important;">
          ${targetSelectOptions}
        </select>
      </div>
    `;

    if (existingFooter) {
      existingFooter.innerHTML = `
        <button class="form-btn confirm-cancel-btn">${t("btn_cancel")}</button>
        <button class="form-btn btn-pill-primary confirm-ok-btn" style="background:#ef4444!important;border-color:#ef4444!important;">${t("btn_confirm")}</button>
      `;
    }

    const cancelBtn = overlay.querySelector(".confirm-cancel-btn");
    const okBtn = overlay.querySelector(".confirm-ok-btn");

    if (cancelBtn)
      cancelBtn.onclick = (e) => {
        e.stopPropagation();
        overlay.remove();
      };

    if (okBtn)
      okBtn.onclick = (e) => {
        e.stopPropagation();
        const sel = overlay.querySelector(".merge-target-select");
        const targetId = sel ? sel.value : "GLOBAL_OPTION";

        if (targetId === nId) {
          overlay.remove();
          panggilToast(t("merge_group_err_same"), "warn");
          return;
        }

        overlay.remove();
        let movedCount = 0;

        const tempKamus = core.getKamus();

        for (const key in tempKamus) {
          const item = tempKamus[key];
          if (!item || typeof item !== "object") continue;

          // FIX: izinkan penggabungan dari sumber global (nId === "GLOBAL")
          const isSourceMatch = nId === "GLOBAL"
            ? item.global === true
            : (!item.global && item.novelId === nId);

          if (isSourceMatch) {
            if (targetId === "GLOBAL_OPTION") {
              if (item.global) continue; // sudah global
              item.global = true;
              item.novelId = "";
              item.novelTitle = core.getCachedNovelTitle(nTitle) || "";
              item.domain = core.getNovelBaseDomain(core.currentHost);
              movedCount++;
            } else {
              if (!item.global && item.novelId === targetId) continue; // sudah di target
              item.global = false;
              item.novelId = targetId;
              item.novelTitle = core.getCachedNovelTitle(targetId) || targetId;
              item.domain = core.getNovelBaseDomain(core.currentHost);
              movedCount++;
            }
          }
        }

        core.saveKamus(tempKamus);

        if (targetId !== "GLOBAL_OPTION") {
          const label = core.getCachedNovelTitle(targetId) || targetId;
          if (label) core.saveCachedNovelTitle(targetId, label);
        }

        core.simpanKeAwan(tempKamus);
        panggilToast(
          t(
            "merge_group_success",
            nTitle || nId,
            targetId === "GLOBAL_OPTION"
              ? t("global_replacer")
              : core.getCachedNovelTitle(targetId) || targetId,
            movedCount,
          ),
          "success",
        );
        core.jalankanPengganti(true);
        renderTampilan();
      };
  }

  // FIX v52: hilangkanFokusShadow dideklarasikan di outer scope agar bisa diakses
  // oleh renderTampilan dan setupUnifiedWordList. Implementasi sebenarnya di-assign
  // di dalam init() setelah shadow DOM tersedia.
  let hilangkanFokusShadow = function() {};

  function setupUnifiedWordList(pane, isRecycle) {
    pane.innerHTML = `
            <div class="search-action-bar" style="display: flex !important; gap: 6px !important; align-items: center !important; margin-bottom: 8px !important; overflow: visible !important;">
                <input type="text" class="form-input search-input" placeholder="${t("search_placeholder")}" style="flex: 1 !important;" />
                ${
                  isRecycle
                    ? `<button class="form-btn bulk-undo-btn btn-pill-primary" style="display: none !important; width: auto !important; margin-right: 4px !important;">${t("bulk_undo", "0")}</button>
                       <button class="form-btn bulk-delete-perm-btn btn-pill-danger" style="display: none !important; width: auto !important; margin-right: 4px !important;">${t("bulk_delete_perm", "0")}</button>`
                    : `<button class="form-btn bulk-delete-btn" style="display: none !important; width: auto !important; background: #ef4444 !important; color: white !important; padding: 7px 14px !important; font-size: 11px !important; border-radius: 20px !important; border: none !important; cursor: pointer !important; font-weight: bold !important;">${t("bulk_delete", "0")}</button>`
                }
            </div>
            <div class="select-all-row" style="display: flex !important; align-items: center !important; gap: 8px !important; padding: 6px 10px !important; overflow: visible !important;">
                <input type="checkbox" class="select-all-checkbox" style="cursor: pointer !important; margin: 0 !important;" />
                <span>${t("select_all")} (<span class="total-count">0</span>)</span>
            </div>
            <div class="toggle-other-placeholder" style="overflow: visible !important;"></div>
            <div class="word-list" style="overflow: visible !important;"></div>
            <div style="display: flex !important; justify-content: space-between !important; align-items: center !important; margin-top: 10px !important; overflow: visible !important; gap: 6px !important;">
                ${!isRecycle ? `<button class="form-btn btn-perbaiki-id btn-pill" title="Pindai semua kata di kamus dan perbaiki ID novel yang basi agar cocok dengan novel halaman ini (berdasarkan judul)" style="background: #1d4ed8 !important; color: #fff !important; padding: 6px 14px !important; font-size: 11px !important; display: inline-flex; align-items: center; justify-content: center; gap: 5px; white-space: nowrap;">🔧 Perbaiki ID Novel</button>` : `<span></span>`}
                <button class="form-btn close-btn-pane btn-pill" style="background: #374151 !important; color: #d1d5db !important; padding: 6px 16px !important; font-size: 12px !important; display: inline-flex; align-items: center; justify-content: center; gap: 6px;">
                    ${core.SVGS.close} ${t("close_btn")}
                </button>
            </div>
        `;

    const listContainer = pane.querySelector(".word-list");
    const searchInput = pane.querySelector(".search-input");
    const selectAllCheckbox = pane.querySelector(".select-all-checkbox");
    const totalCountSpan = pane.querySelector(".total-count");
    const bulkDeleteBtn = pane.querySelector(".bulk-delete-btn");
    const bulkUndoBtn = pane.querySelector(".bulk-undo-btn");
    const bulkDeletePermBtn = pane.querySelector(".bulk-delete-perm-btn");

    let terpilih = [];
    let visibleKeys = [];

    pane.querySelector(".close-btn-pane").onclick = () => {
      panel.classList.add("hidden");
      hilangkanFokusShadow();
    };

    if (!isRecycle) {
      pane.querySelector(".btn-perbaiki-id").onclick = () => {
        const jumlah = core.perbaikiIdNovel();
        if (jumlah > 0) {
          panggilToast(`✅ ${jumlah} kata berhasil diperbaiki ID novel-nya! Penggantian kata diperbarui.`, "success");
          core.jalankanPengganti(true);
          saringDanTampilkan(searchInput.value.trim().toLowerCase());
        } else {
          panggilToast("Tidak ada kata yang perlu diperbaiki pada novel ini (judul tidak cocok atau ID sudah benar).", "info");
        }
      };
    }

    pane.perbaruiStatusMassal = function () {
      const isAllSelected =
        visibleKeys.length > 0 &&
        visibleKeys.every((k) => terpilih.includes(k));
      selectAllCheckbox.checked = isAllSelected;
      totalCountSpan.textContent = terpilih.length;

      if (isRecycle) {
        if (terpilih.length > 0) {
          bulkUndoBtn.style.display = "block";
          bulkUndoBtn.textContent = t("bulk_undo", terpilih.length);
          bulkDeletePermBtn.style.display = "block";
          bulkDeletePermBtn.textContent = t(
            "bulk_delete_perm",
            terpilih.length,
          );
        } else {
          bulkUndoBtn.style.display = "none";
          bulkDeletePermBtn.style.display = "none";
        }
      } else {
        if (terpilih.length > 0) {
          bulkDeleteBtn.style.display = "block";
          bulkDeleteBtn.textContent = t("bulk_delete", terpilih.length);
        } else {
          bulkDeleteBtn.style.display = "none";
        }
      }
    };

    // v99.12.37: Search diperluas — cocokkan kueri pada key (FROM), toVal (TO),
    // dan nama novel agar user bisa cari berdasarkan teks pengganti atau novel
    function saringDanTampilkan(kueri = "") {
      listContainer.innerHTML = "";
      const seluruhData = isRecycle ? core.getDeletedWords() : core.getKamus();

      const keysFiltered = Object.keys(seluruhData).filter((k) => {
        const item = seluruhData[k];
        const toVal  = item && typeof item === "object" ? item.to || "" : item || "";
        // v99.12.37: cari juga di nama novel / domain
        const novStr = item && typeof item === "object"
          ? (core.getCachedNovelTitle(item.novelId) || item.novelTitle || item.domain || "")
          : "";
        return (
          k.toLowerCase().includes(kueri) ||
          toVal.toLowerCase().includes(kueri) ||
          novStr.toLowerCase().includes(kueri)
        );
      });

      const globalKeys = [];
      const currentLocalKeys = [];
      const otherLocalKeys = [];
      const currentNovel = core.getNovelContext();

      keysFiltered.forEach((k) => {
        const item = seluruhData[k];
        if (item && typeof item === "object") {
          if (item.global) {
            globalKeys.push(k);
          } else if (item.novelId) {
            if (item.novelId === currentNovel.id) {
              currentLocalKeys.push(k);
            } else {
              otherLocalKeys.push(k);
            }
          } else {
            const pageBaseDomain = core.getNovelBaseDomain(core.currentHost);
            const termBaseDomain = core.getNovelBaseDomain(item.domain);
            if (
              termBaseDomain === pageBaseDomain ||
              (termBaseDomain && pageBaseDomain.endsWith("." + termBaseDomain))
            ) {
              currentLocalKeys.push(k);
            } else {
              otherLocalKeys.push(k);
            }
          }
        } else if (typeof item === "string") {
          globalKeys.push(k);
        }
      });

      globalKeys.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
      currentLocalKeys.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));

      visibleKeys = [];
      globalKeys.forEach((k) => visibleKeys.push(k));
      currentLocalKeys.forEach((k) => visibleKeys.push(k));
      if (showOtherTerms) {
        otherLocalKeys.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
        otherLocalKeys.forEach((k) => visibleKeys.push(k));
      }

      if (visibleKeys.length === 0) {
        listContainer.innerHTML = `<div class="empty-state">${t("empty_state")}</div>`;
        pane.perbaruiStatusMassal();
        renderToggleOtherBtn(otherLocalKeys);
        return;
      }

      if (globalKeys.length > 0) {
        const gHeader = document.createElement("div");
        gHeader.setAttribute(
          "style",
          "display:flex!important;align-items:center!important;gap:6px!important;margin:10px 10px 6px 10px!important;overflow:visible!important;",
        );
        if (isRecycle) {
          gHeader.innerHTML = `<span class="btn-pill-primary" style="width:fit-content!important;">Global</span>`;
        } else {
          gHeader.innerHTML = `
            <span class="btn-pill-primary" style="width:fit-content!important;">Global</span>
            <div class="group-menu-container" style="position:relative!important;">
              <button class="group-menu-btn" title="Grup Menu Global">⚙️</button>
              <div class="group-dropdown-menu">
                <button class="group-dropdown-item merge-novel-group-btn" data-id="GLOBAL" data-title="Global">🔗 ${t("merge_group")}</button>
              </div>
            </div>`;
        }
        listContainer.appendChild(gHeader);
        globalKeys.forEach((k) => {
          renderUnifiedItem(
            k,
            listContainer,
            false,
            isRecycle,
            terpilih,
            seluruhData,
            pane,
          );
        });
      }

      if (currentLocalKeys.length > 0) {
        const localHeader = document.createElement("div");
        localHeader.setAttribute(
          "style",
          "display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 10px 10px 6px 10px !important; margin-top: 10px !important; overflow: visible !important;",
        );
        const localTitle = currentNovel.title || "Current Novel";
        const isActiveLocal = isRecycle
          ? true
          : core.getActiveNovelId() === currentNovel.id ||
            !core.getActiveNovelId();

        localHeader.innerHTML = isRecycle
          ? `<span class="btn-pill" style="font-size: 11px !important; font-weight: bold !important;">${localTitle}</span>`
          : `
                        <div style="display: flex !important; align-items: center !important; gap: 6px !important; width: 100% !important; position: relative !important; overflow: visible !important;">
                            <div class="group-menu-container">
                                <button class="group-menu-btn" title="Grup Menu">⚙️</button>
                                <div class="group-dropdown-menu">
                                    <button class="group-dropdown-item toggle-active-novel-btn" data-id="${currentNovel.id}">${isActiveLocal ? "❌ Nonaktifkan" : "🟢 Aktifkan"}</button>
                                    <button class="group-dropdown-item merge-novel-group-btn" data-id="${currentNovel.id}" data-title="${localTitle}">🔗 ${t("merge_group")}</button>
                                    <button class="group-dropdown-item danger delete-novel-group-btn" data-id="${currentNovel.id}" data-title="${localTitle}">🗑 ${t("delete_btn")}</button>
                                </div>
                            </div>
                            <span class="local-novel-title-btn btn-pill" data-id="${currentNovel.id}" title="Klik untuk mengubah nama novel" style="flex: 1 !important; text-align: left !important;">
                                ${localTitle} ✏️ ${isActiveLocal ? "🟢" : ""}
                            </span>
                        </div>
                        ${
                          currentNovel.url
                            ? `
                        <a href="${currentNovel.url}" target="_blank" class="awr-tooltip-btn btn-pill" data-tooltip="Open Book Link" style="border-radius: 20px !important; text-decoration: none !important; margin-left: 6px !important; display: inline-flex; align-items: center; justify-content: center; padding: 4px;">
                            ${core.SVGS.link}
                        </a>`
                            : ""
                        }
                    `;
        listContainer.appendChild(localHeader);
        currentLocalKeys.forEach((k) => {
          renderUnifiedItem(
            k,
            listContainer,
            false,
            isRecycle,
            terpilih,
            seluruhData,
            pane,
          );
        });
      }

      renderToggleOtherBtn(otherLocalKeys);

      if (showOtherTerms && otherLocalKeys.length > 0) {
        // Bug Fix #5: Satukan grup dengan judul novel sama meskipun text-ID berbeda
        const otherGroups = {};
        otherLocalKeys.forEach((k) => {
          const item = seluruhData[k];
          let gId =
            item?.novelId ||
            `domain_${core.getNovelBaseDomain(item.domain) || "unknown"}`;
          let gTitle = (
            core.getCachedNovelTitle(gId) ||
            item?.novelTitle ||
            core.getNovelBaseDomain(item.domain) ||
            "Unknown Novel"
          ).trim();

          // Cari grup yang sudah ada dengan judul yang sama/mirip
          const existingGId = Object.keys(otherGroups).find((eid) => {
            const existTitle = otherGroups[eid].title.toLowerCase();
            const thisTitle  = gTitle.toLowerCase();
            return existTitle === thisTitle || titlesMatchFuzzy(existTitle, thisTitle);
          });

          const groupKey = existingGId || gId;
          if (!otherGroups[groupKey]) otherGroups[groupKey] = { title: gTitle, keys: [] };
          otherGroups[groupKey].keys.push(k);
        });

        Object.keys(otherGroups)
          .sort((a, b) => {
            return otherGroups[a].title.localeCompare(otherGroups[b].title);
          })
          .forEach((gId) => {
            const group = otherGroups[gId];
            group.keys.sort((a, b) => a.localeCompare(b, undefined, { sensitivity: "base" }));
            const isActiveGroup = isRecycle
              ? true
              : core.getActiveNovelId() === gId ||
                (!core.getActiveNovelId() && gId === currentNovel.id);

            const groupHeader = document.createElement("div");
            groupHeader.setAttribute(
              "style",
              "display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 10px 10px 6px 10px !important; margin-top: 10px !important; overflow: visible !important;",
            );

            groupHeader.innerHTML = isRecycle
              ? `<span class="btn-pill" style="font-size: 11px !important; font-weight: bold !important;">${group.title} (${group.keys.length})</span>`
              : `
                            <div style="display: flex !important; align-items: center !important; gap: 6px !important; width: 100% !important; position: relative !important; overflow: visible !important;">
                                <div class="group-menu-container">
                                    <button class="group-menu-btn" title="Grup Menu">⚙️</button>
                                    <div class="group-dropdown-menu">
                                        <button class="group-dropdown-item toggle-active-novel-btn" data-id="${gId}">${isActiveGroup ? "❌ Nonaktifkan" : "🟢 Aktifkan"}</button>
                                        <button class="group-dropdown-item merge-novel-group-btn" data-id="${gId}" data-title="${group.title}">🔗 ${t("merge_group")}</button>
                                        <button class="group-dropdown-item danger delete-novel-group-btn" data-id="${gId}" data-title="${group.title}">🗑 ${t("delete_btn")}</button>
                                    </div>
                                </div>
                                <span class="local-novel-title-btn btn-pill" data-id="${gId}" title="Klik untuk mengubah nama novel" style="flex: 1 !important; text-align: left !important;">
                                    ${group.title} (${group.keys.length}) ${isActiveGroup ? "🟢" : ""}
                                </span>
                            </div>
                        `;
            listContainer.appendChild(groupHeader);
            group.keys.forEach((k) => {
              renderUnifiedItem(
                k,
                listContainer,
                true,
                isRecycle,
                terpilih,
                seluruhData,
                pane,
              );
            });
          });
      }

      if (!isRecycle) {
        pane.querySelectorAll(".group-menu-btn").forEach((btn) => {
          btn.onclick = (e) => {
            e.stopPropagation();
            const dropdown = btn.nextElementSibling;
            pane.querySelectorAll(".group-dropdown-menu").forEach((menu) => {
              if (menu !== dropdown) menu.classList.remove("show");
            });
            dropdown.classList.toggle("show");
          };
        });

        pane.querySelectorAll(".local-novel-title-btn").forEach((btn) => {
          btn.onclick = (e) => {
            e.stopPropagation();
            const nId = btn.getAttribute("data-id");
            const currentTitle =
              core.getCachedNovelTitle(nId) ||
              seluruhData[
                Object.keys(seluruhData).find(
                  (k) => seluruhData[k].novelId === nId,
                )
              ]?.novelTitle ||
              "Novel";
            ubahJudulNovel(nId, currentTitle);
          };
        });

        pane.querySelectorAll(".toggle-active-novel-btn").forEach((btn) => {
          btn.onclick = (e) => {
            e.stopPropagation();
            const nId = btn.getAttribute("data-id");
            const freshActiveId = core.getActiveNovelId();
            const isActive =
              freshActiveId === nId ||
              (!freshActiveId && nId === currentNovel.id);

            if (isActive) {
              core.setActiveNovelId("GLOBAL_ONLY");
              panggilToast(t("toast_group_disabled"), "info");
            } else {
              core.setActiveNovelId(nId);
              panggilToast(
                t(
                  "toast_group_activated",
                  core.getCachedNovelTitle(nId) || nId,
                ),
                "success",
              );
            }
            renderTampilan();
            try { core.jalankanPengganti(true); } catch (_) {}
            setTimeout(() => { try { core.jalankanPengganti(true); } catch (_) {} }, 200);
            setTimeout(() => { try { core.jalankanPengganti(true); } catch (_) {} }, 800);
          };
        });

        pane.querySelectorAll(".merge-novel-group-btn").forEach((btn) => {
          btn.onclick = (e) => {
            e.stopPropagation();
            pane
              .querySelectorAll(".group-dropdown-menu")
              .forEach((menu) => menu.classList.remove("show"));
            const nId = btn.getAttribute("data-id"),
              nTitle = btn.getAttribute("data-title");
            tampilkanMergeGroup(nId, nTitle);
          };
        });

        pane.querySelectorAll(".delete-novel-group-btn").forEach((btn) => {
          btn.onclick = (e) => {
            e.stopPropagation();
            const nId = btn.getAttribute("data-id"),
              nTitle = btn.getAttribute("data-title");
            window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
              t("delete_btn"),
              t("delete_group_confirm", nTitle),
              () => {
                const tempKamus = core.getKamus(),
                  deletedWords = core.getDeletedWords();
                let count = 0;
                for (const salah in tempKamus) {
                  if (tempKamus[salah].novelId === nId) {
                    const itemCopy = JSON.parse(
                      JSON.stringify(tempKamus[salah]),
                    );
                    itemCopy.deletedAt = Date.now();
                    deletedWords[salah] = itemCopy;
                    delete tempKamus[salah];
                    count++;
                  }
                }
                core.saveKamus(tempKamus);
                core.saveDeletedWords(deletedWords);
                try {
                  const cache = JSON.parse(
                    GM_getValue("awr_novel_titles_v2", "{}"),
                  );
                  delete cache[nId];
                  GM_setValue("awr_novel_titles_v2", JSON.stringify(cache));
                } catch (err) {}
                if (core.getActiveNovelId() === nId) core.setActiveNovelId("");
                core.simpanKeAwan(tempKamus, null, deletedWords);
                panggilToast(t("delete_group_success", nTitle, count), "warn");
                core.jalankanPengganti(true);
                renderTampilan();
              },
            );
          };
        });
      }

      pane.perbaruiStatusMassal();
    }

    function renderToggleOtherBtn(otherKeys) {
      const placeholder = pane.querySelector(".toggle-other-placeholder");
      if (!placeholder) return;
      placeholder.innerHTML = "";
      if (otherKeys.length === 0) return;

      const btn = document.createElement("button");
      btn.className = "toggle-other-btn";
      btn.setAttribute(
        "style",
        "background:#1a1d24!important;color:#9ca3af!important;border:1px solid #272a34!important;padding:10px 16px!important;border-radius:20px!important;font-size:11px!important;font-weight:bold!important;cursor:pointer!important;width:calc(100% - 20px)!important;margin:12px 10px!important;transition:all 0.2s ease!important;",
      );
      btn.textContent = showOtherTerms
        ? t("hide_other_terms", otherKeys.length)
        : t("show_other_terms", otherKeys.length);
      btn.onclick = () => {
        showOtherTerms = !showOtherTerms;
        GM_setValue("awr_show_other_terms", showOtherTerms);
        saringDanTampilkan(searchInput.value.toLowerCase().trim());
      };
      placeholder.appendChild(btn);
    }

    selectAllCheckbox.onchange = (e) => {
      if (e.target.checked) {
        visibleKeys.forEach((k) => {
          if (!terpilih.includes(k)) terpilih.push(k);
        });
      } else {
        terpilih = terpilih.filter((k) => !visibleKeys.includes(k));
      }
      pane.querySelectorAll(".word-checkbox").forEach((cb) => {
        cb.checked = e.target.checked;
      });
      pane.perbaruiStatusMassal();
    };

    if (isRecycle) {
      bulkUndoBtn.onclick = () => {
        if (terpilih.length === 0) return;
        window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          "Pulihkan Banyak Kata?",
          `Apakah Anda yakin ingin memulihkan ${terpilih.length} kata terpilih?`,
          () => {
            const tempKamus = core.getKamus(),
              deletedWords = core.getDeletedWords();
            terpilih.forEach((k) => {
              if (deletedWords[k]) {
                tempKamus[k] = deletedWords[k];
                delete deletedWords[k];
              }
            });
            core.saveKamus(tempKamus);
            core.saveDeletedWords(deletedWords);
            core.simpanKeAwan(tempKamus, null, deletedWords);
            panggilToast(t("toast_bulk_undone", terpilih.length), "success");
            terpilih = [];
            core.jalankanPengganti(true);
            renderTampilan();
          },
        );
      };

      bulkDeletePermBtn.onclick = () => {
        if (terpilih.length === 0) return;
        window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          "Hapus Banyak Kata Secara Permanen?",
          `Apakah Anda yakin ingin hapus permanen ${terpilih.length} kata terpilih?`,
          () => {
            const deletedWords = core.getDeletedWords();
            terpilih.forEach((k) => {
              delete deletedWords[k];
            });
            core.saveDeletedWords(deletedWords);
            core.simpanKeAwan(null, null, deletedWords);
            panggilToast(t("toast_bulk_deleted_perm", terpilih.length), "warn");
            terpilih = [];
            renderTampilan();
          },
        );
      };
    } else {
      bulkDeleteBtn.onclick = () => {
        if (terpilih.length === 0) return;
        window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          "Pindahkan Banyak Kata?",
          `Apakah Anda yakin ingin memindahkan ${terpilih.length} kata terpilih ke Recycle Bin?`,
          () => {
            const tempKamus = core.getKamus(),
              deletedWords = core.getDeletedWords();
            terpilih.forEach((k) => {
              const itemCopy = JSON.parse(JSON.stringify(tempKamus[k]));
              itemCopy.deletedAt = Date.now();
              deletedWords[k] = itemCopy;
              delete tempKamus[k];
            });
            core.saveKamus(tempKamus);
            core.saveDeletedWords(deletedWords);
            core.simpanKeAwan(tempKamus, null, deletedWords);
            panggilToast(t("toast_deleted", terpilih.length), "warn");
            terpilih = [];
            core.jalankanPengganti(true);
            renderTampilan();
          },
        );
      };
    }

    searchInput.oninput = (e) =>
      saringDanTampilkan(e.target.value.toLowerCase().trim());
    saringDanTampilkan();
  }

  function renderTampilan() {
    const domainAktif = core.isDomainAllowed();
    const seluruhKamus = core.getKamus();
    const activeLang = getLang();
    const activeFlag = TRANSLATIONS[activeLang]?.flag || "🇺🇸";
    const activeNovelId = core.getActiveNovelId();
    const lastSelectedGroup = GM_getValue(
      "awr_last_selected_group_id_v2",
      "GLOBAL_OPTION",
    );
    const lastCheckboxState = GM_getValue(
      "awr_last_active_group_checkbox_state_v2",
      true,
    );
    const updateMode = GM_getValue("awr_update_mode_v1", "auto");
    const autoDeleteDays = parseInt(
      GM_getValue("awr_recycle_auto_delete_days", "30"),
    );
    const currentNovel = core.getNovelContext();

    const langNames = { en: "English", id: "Indonesia" };
    panel.innerHTML = "";

    const header = document.createElement("div");
    header.className = "replacer-header";
    header.innerHTML = `
            <div class="replacer-header-row" style="display: flex !important; justify-content: space-between !important; align-items: center !important; gap: 8px !important;">
                <span class="replacer-title">🐸 ${t("title")}</span>
                <div class="lang-dropdown-container">
                    <button class="active-lang-btn">${activeFlag} <span style="font-size: 8px !important; line-height: 1 !important;">▼</span></button>
                    <div class="lang-dropdown-menu"></div>
                </div>
                <button class="replacer-close">${core.SVGS.close}</button>
            </div>
            <div class="replacer-host-status">
                <div class="host-info">
                    <span class="host-label">${t("current_site")}</span>
                    <span class="host-name">${core.getNovelBaseDomain(core.currentHost)}</span>
                </div>
                <div class="status-actions">
                    <span class="status-badge ${domainAktif ? "active" : "inactive"}">
                        <span class="status-indicator ${domainAktif ? "active" : "inactive"}"></span>
                        ${domainAktif ? t("active") : t("off")}
                    </span>
                    <button class="toggle-btn ${domainAktif ? "btn-active" : "btn-inactive"}">
                        ${domainAktif ? t("disable") : t("enable")}
                    </button>
                </div>
            </div>
        `;

    const dropdownMenu = header.querySelector(".lang-dropdown-menu");
    const activeBtn = header.querySelector(".active-lang-btn");
    activeBtn.onclick = (e) => {
      e.stopPropagation();
      dropdownMenu.classList.toggle("show");
    };

    Object.keys(TRANSLATIONS).forEach((langKey) => {
      const itemBtn = document.createElement("button");
      itemBtn.className = "lang-dropdown-item";
      if (langKey === activeLang) itemBtn.classList.add("active");
      itemBtn.innerHTML = `<span>${TRANSLATIONS[langKey].flag}</span> <span>${langNames[langKey]}</span>`;
      itemBtn.onclick = (e) => {
        e.stopPropagation();
        saveLang(langKey);
        dropdownMenu.classList.remove("show");
        renderTampilan();
      };
      dropdownMenu.appendChild(itemBtn);
    });

    header.querySelector(".replacer-close").onclick = () => {
      panel.classList.add("hidden");
      hilangkanFokusShadow();
    };

    header.querySelector(".toggle-btn").onclick = () => {
      const normalizedCurrent = core.getNovelBaseDomain(core.currentHost);
      const filterMode = core.getFilterMode();
      if (filterMode === "whitelist") {
        let targets = core.getTargetDomains();
        if (domainAktif) {
          targets = targets.filter(
            (d) => core.getNovelBaseDomain(d) !== normalizedCurrent,
          );
          panggilToast(t("toast_removed_whitelist", normalizedCurrent), "warn");
        } else {
          targets.push(normalizedCurrent);
          panggilToast(
            t("toast_added_whitelist", normalizedCurrent),
            "success",
          );
        }
        core.saveTargetDomains(targets);
        core.simpanKeAwan(null, targets);
      } else {
        let blacklist = core.getBlacklistDomains();
        if (domainAktif) {
          blacklist.push(normalizedCurrent);
          panggilToast(t("toast_added_blacklist", normalizedCurrent), "warn");
        } else {
          blacklist = blacklist.filter(
            (d) => core.getNovelBaseDomain(d) !== normalizedCurrent,
          );
          panggilToast(
            t("toast_removed_blacklist", normalizedCurrent),
            "success",
          );
        }
        core.saveBlacklistDomains(blacklist);
        core.simpanKeAwan();
      }
      core.jalankanPengganti(true);
      renderTampilan();
    };

    panel.appendChild(header);

    // v99.12.37: Stats bar — ringkasan singkat kamus aktif
    (function renderStatsBar() {
      const allKeys   = Object.keys(seluruhKamus);
      const activeKeys = allKeys.filter(k => !k.startsWith("_off_:") && !k.startsWith("hash:") && !/:\d+$/.test(k));
      const disabledCount = allKeys.filter(k => k.startsWith("_off_:")).length;
      const hashCount  = allKeys.filter(k => /^hash:[^:]+$/.test(k)).length;
      const novelName  = currentNovel.title || core.getNovelBaseDomain(core.currentHost) || "—";
      const statsBar   = document.createElement("div");
      statsBar.className = "awr-stats-bar";
      statsBar.style.cssText =
        "background:#0f1117!important;border-bottom:1px solid #1e293b!important;" +
        "padding:4px 14px!important;display:flex!important;align-items:center!important;" +
        "gap:10px!important;flex-wrap:wrap!important;font-size:9px!important;color:#64748b!important;" +
        "line-height:1.4!important;";
      statsBar.innerHTML =
        '<span style="color:#94a3b8;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:120px;" title="' + novelName + '">📖 ' + novelName + '</span>' +
        '<span style="color:#334155">|</span>' +
        '<span><b style="color:#10b981">' + activeKeys.length + '</b> rule aktif</span>' +
        (hashCount > 0 ? '<span style="color:#334155">·</span><span><b style="color:#fbbf24">' + hashCount + '</b> hash</span>' : '') +
        (disabledCount > 0 ? '<span style="color:#334155">·</span><span><b style="color:#f87171">' + disabledCount + '</b> nonaktif</span>' : '') +
        (_undoStack.length > 0
          ? '<button class="awr-undo-bar-btn" style="margin-left:auto!important;background:rgba(99,102,241,0.15)!important;border:1px solid rgba(99,102,241,0.4)!important;' +
            'color:#818cf8!important;border-radius:6px!important;padding:1px 7px!important;font-size:9px!important;cursor:pointer!important;white-space:nowrap!important;">⟲ Undo (' + _undoStack.length + ')</button>'
          : '<span style="margin-left:auto;color:#1e293b;">⟲</span>');
      panel.appendChild(statsBar);
      const undoBtn = statsBar.querySelector(".awr-undo-bar-btn");
      if (undoBtn) undoBtn.onclick = () => popUndoAndRestore();
    })();

    const tabsRow = document.createElement("div");
    tabsRow.className = "replacer-tabs";
    tabsRow.innerHTML = `
            <button class="awr-tooltip-btn tooltip-bottom replacer-tab-btn ${tabAktif === "tambah" ? "active" : ""}" data-tab="tambah" data-tooltip="${t("tab_editor")}">${core.SVGS.pen}</button>
            <button class="awr-tooltip-btn tooltip-bottom replacer-tab-btn ${tabAktif === "daftar" ? "active" : ""}" data-tab="daftar" data-tooltip="${t("tab_terms")}">${core.SVGS.book}</button>
            <button class="awr-tooltip-btn tooltip-bottom replacer-tab-btn ${tabAktif === "recycle" ? "active" : ""}" data-tab="recycle" data-tooltip="${t("tab_recycle")}">${core.SVGS.recycle}</button>
            <button class="awr-tooltip-btn tooltip-bottom replacer-tab-btn ${tabAktif === "scanner" ? "active" : ""}" data-tab="scanner" data-tooltip="${t("tab_scanner")}" style="font-size:14px!important;">🔍</button>
            <button class="awr-tooltip-btn tooltip-bottom replacer-tab-btn ${tabAktif === "setting" ? "active" : ""}" data-tab="setting" data-tooltip="${t("tab_setting")}">${core.SVGS.gear}</button>
        `;

    tabsRow.querySelectorAll(".replacer-tab-btn").forEach((btn) => {
      btn.onclick = () => {
        const targetTab = btn.getAttribute("data-tab");
        if (targetTab === "tambah") subjekEdit = null;
        tabAktif = targetTab;
        renderTampilan();
      };
    });
    panel.appendChild(tabsRow);

    const body = document.createElement("div");
    body.className = "replacer-body";
    panel.appendChild(body);

    if (tabAktif === "daftar") {
      const pane = document.createElement("div");
      pane.className = "tab-pane active";
      body.appendChild(pane);
      setupUnifiedWordList(pane, false);
    } else if (tabAktif === "recycle") {
      const pane = document.createElement("div");
      pane.className = "tab-pane active";
      body.appendChild(pane);
      setupUnifiedWordList(pane, true);
    } else if (tabAktif === "scanner") {
      // v52.7: Hash Scanner — search/filter/sort + stats + conflict + bulk delete + preview + notes
      const pane = document.createElement("div");
      pane.className = "tab-pane active";
      pane.style.cssText = "box-sizing:border-box;min-width:0;overflow:hidden;";
      body.appendChild(pane);

      const sitePreset = core.getSiteKataUnikSelector();
      const allSpans = document.querySelectorAll(sitePreset.glossarySelector + ", span[data-hash]");

      // Kelompokkan per hash value (simpan spans asli untuk context + preview)
      const hashMap = {};
      allSpans.forEach(span => {
        const hash = span.getAttribute(sitePreset.hashAttr || "data-hash");
        if (!hash) return;
        const txt = span.textContent.trim();
        if (!hashMap[hash]) hashMap[hash] = { texts: new Set(), count: 0, spans: [] };
        hashMap[hash].texts.add(txt);
        hashMap[hash].count++;
        hashMap[hash].spans.push(span);
      });
      const allHashKeys = Object.keys(hashMap).sort();

      if (allHashKeys.length === 0) {
        pane.innerHTML = `
          <div style="font-size:11px;font-weight:bold;color:#9ca3af;margin-bottom:6px;border-bottom:1px solid #272a34;padding-bottom:6px;">🔍 ${t("scanner_title")}</div>
          <div style="color:#6b7280;font-size:11px;text-align:center;padding:20px 0;">${t("scanner_empty")}</div>`;
        return;
      }

      // ── State ──────────────────────────────────────────────────────
      // FIX Bug 2: State persisten via _scannerState (scope modul)
      // Tidak direset saat rerenderScanner() -> renderTampilan() dipanggil ulang.
      const bulkSelected = _scannerState.bulkSelected;
      let searchQuery = _scannerState.searchQuery || "";
      let filterMode  = _scannerState.filterMode || "all";
      let sortMode    = _scannerState.sortMode || "alpha";
      // FIX: _liveKamus — referensi kamus yang bisa di-refresh oleh rerenderScanner()
      // agar hasRule, filter "Ada Rule/Belum Ada", dan stats selalu menampilkan data terkini.
      let _liveKamus  = seluruhKamus;

      // ── Helpers ────────────────────────────────────────────────────
      function getSpanContext(span) {
        try {
          const parent = span.parentElement;
          if (!parent) return "";
          const full = parent.textContent || "";
          const spanTxt = span.textContent || "";
          const idx = full.indexOf(spanTxt);
          if (idx === -1) return "";
          const before = full.slice(Math.max(0, idx - 20), idx).replace(/\s+/g, " ").trim();
          const after  = full.slice(idx + spanTxt.length, idx + spanTxt.length + 20).replace(/\s+/g, " ").trim();
          return (before ? "…" + before + " " : "") + "[" + spanTxt + "]" + (after ? " " + after + "…" : "");
        } catch(_) { return ""; }
      }

      function isHashConflict(hash) {
        const sp = hashMap[hash] && hashMap[hash].spans;
        if (!sp || sp.length < 2) return false;
        const ctxSet = new Set(sp.map(s => {
          const p = s.closest("p, li, td, h1, h2, h3, h4, h5, h6");
          return p ? p.textContent.replace(s.textContent, "").trim().slice(0, 40) : "";
        }));
        return ctxSet.size > 1;
      }

      function deleteHashRule(hashVal) {
        const tempKamus = core.getKamus(), deletedWords = core.getDeletedWords();
        const key = "hash:" + hashVal;
        if (tempKamus[key]) {
          deletedWords[key] = tempKamus[key];
          delete tempKamus[key];
          core.saveKamus(tempKamus);
          core.saveDeletedWords(deletedWords);
          core.simpanKeAwan(tempKamus, null, deletedWords);
        }
      }

      function rerenderScanner() {
        // FIX: Jangan hapus pane.innerHTML — shell pencarian (search/filter/sort) dipertahankan
        // agar kolom pencari tetap berfungsi dan tidak kehilangan event binding-nya.
        // renderScannerUI() sudah menangani partial update (hanya list yang di-refresh).
        tabAktif = "scanner";
        if (pane && pane.isConnected) {
          // Refresh kamus agar data terkini (rule baru, hapus rule, dll.) langsung terlihat
          _liveKamus = core.getKamus();
          try {
            renderScannerUI();
          } catch (reErr) {
            console.error("[AWR] rerenderScanner error:", reErr);
            pane.innerHTML = `<div class="awr-scanner-error-state" style="padding:16px;color:#f87171;font-size:11px;text-align:center;line-height:1.6;">
              ⚠️ Gagal memperbarui scanner.<br>
              <span style="color:#64748b;font-size:10px;">${reErr && reErr.message ? reErr.message : "Unknown error"}</span><br>
              <button class="form-btn" style="margin-top:8px;padding:4px 12px!important;font-size:10px!important;border-radius:8px!important;" id="awr-scanner-retry-btn2">🔄 Coba Lagi</button>
            </div>`;
            const rb = pane.querySelector("#awr-scanner-retry-btn2");
            if (rb) rb.onclick = () => { _liveKamus = core.getKamus(); try { renderScannerUI(); } catch(_) {} };
          }
          return;
        }
        // Fallback: pane sudah tidak di DOM — rebuild seluruh panel
        safeRenderTampilan();
      }

      // Preview real-time: highlight span di halaman sementara
      let _previewSpans = [];
      function applyPagePreview(hashes) {
        clearPagePreview();
        (Array.isArray(hashes) ? hashes : [hashes]).forEach(h => {
          (hashMap[h] && hashMap[h].spans || []).forEach(span => {
            _previewSpans.push({ span, oline: span.style.outline, obg: span.style.background, obr: span.style.borderRadius });
            span.style.outline = "2px solid #f59e0b";
            span.style.background = "rgba(245,158,11,0.18)";
            span.style.borderRadius = "3px";
          });
        });
      }
      function clearPagePreview() {
        _previewSpans.forEach(({ span, oline, obg, obr }) => {
          span.style.outline = oline; span.style.background = obg; span.style.borderRadius = obr;
        });
        _previewSpans = [];
      }

      // Hitung stats — dihitung ulang di dalam renderScannerUI() agar selalu fresh
      let statsHasRule = 0, statsNoRule = 0, statsConflict = 0;
      function refreshStats() {
        statsHasRule = 0; statsNoRule = 0; statsConflict = 0;
        allHashKeys.forEach(h => {
          if (_liveKamus["hash:" + h]) statsHasRule++; else statsNoRule++;
          if (isHashConflict(h)) statsConflict++;
        });
      }
      refreshStats();

      function getVisibleKeys() {
        let keys = allHashKeys.filter(h => {
          const info = hashMap[h];
          const allText = Array.from(info.texts).join(" ") + " hash:" + h;
          if (searchQuery && !allText.toLowerCase().includes(searchQuery.toLowerCase())) return false;
          if (filterMode === "has_rule"  && !_liveKamus["hash:" + h]) return false;
          if (filterMode === "no_rule"   &&  _liveKamus["hash:" + h]) return false;
          if (filterMode === "conflict"  && !isHashConflict(h)) return false;
          return true;
        });
        if (sortMode === "count")      keys.sort((a, b) => hashMap[b].count - hashMap[a].count);
        else if (sortMode === "rule_first") keys.sort((a, b) => {
          const ra = !!(_liveKamus["hash:" + a]), rb = !!(_liveKamus["hash:" + b]);
          return ra === rb ? a.localeCompare(b) : (rb ? 1 : -1);
        });
        return keys;
      }

      // ── Render UI (re-usable inner function) ───────────────────────
      function renderScannerUI() {
        try { // v99.12.30: try/catch agar error internal tidak bikin scanner blank
        refreshStats(); // FIX: hitung ulang stats dengan _liveKamus terkini
        const visibleKeys = getVisibleKeys();
        const statsTotal  = allHashKeys.length;

        // FIX v99.12.38: Shell statis (search input, filter, sort) hanya dirender SEKALI.
        // Ini mencegah search field terhapus saat user mengetik — input tidak dihancurkan
        // oleh pane.innerHTML pada setiap re-filter/re-sort.
        if (!pane.querySelector(".awr-sc-search")) {
        pane.innerHTML = `
          <!-- Stats Bar -->
          <div style="display:flex;gap:4px;flex-wrap:wrap;align-items:center;margin-bottom:5px;padding-bottom:5px;border-bottom:1px solid #272a34;min-width:0;">
            <span style="font-size:10px;font-weight:bold;color:#9ca3af;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex-shrink:1;">🔍 ${t("scanner_title")}</span>
            <div style="display:flex;gap:3px;flex-wrap:wrap;margin-left:auto;flex-shrink:0;">
              <span style="font-size:8px;background:#1a1d24;border:1px solid #272a34;border-radius:4px;padding:1px 4px;color:#94a3b8;">${t("scanner_stat_total", statsTotal)}</span>
              <span style="font-size:8px;background:rgba(16,185,129,0.1);border:1px solid rgba(16,185,129,0.3);border-radius:4px;padding:1px 4px;color:#10b981;">${t("scanner_stat_has_rule", statsHasRule)}</span>
              <span style="font-size:8px;background:rgba(234,179,8,0.1);border:1px solid rgba(234,179,8,0.3);border-radius:4px;padding:1px 4px;color:#fbbf24;">${t("scanner_stat_no_rule", statsNoRule)}</span>
              ${statsConflict > 0 ? `<span style="font-size:8px;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);border-radius:4px;padding:1px 4px;color:#f87171;">${t("scanner_stat_conflict", statsConflict)}</span>` : ""}
            </div>
          </div>

          <!-- Search + Sort -->
          <div style="display:flex;gap:4px;margin-bottom:5px;min-width:0;">
            <input class="form-input awr-sc-search" placeholder="${t("scanner_search_placeholder")}" value="${(searchQuery || "").replace(/"/g, "&quot;")}" style="flex:1;min-width:0;font-size:10px!important;padding:3px 7px!important;height:24px!important;min-height:0!important;" />
            <select class="form-input awr-sc-sort" style="width:auto!important;flex-shrink:0;font-size:9px!important;padding:2px 3px!important;height:24px!important;min-height:0!important;background:#1a1d24!important;color:#cbd5e1!important;cursor:pointer!important;">
              <option value="alpha"      ${sortMode==="alpha"?"selected":""}>${t("scanner_sort_alpha")}</option>
              <option value="count"      ${sortMode==="count"?"selected":""}>${t("scanner_sort_count")}</option>
              <option value="rule_first" ${sortMode==="rule_first"?"selected":""}>${t("scanner_sort_rule_first")}</option>
            </select>
          </div>

          <!-- Filter tabs -->
          <div style="display:flex;gap:3px;margin-bottom:5px;flex-wrap:wrap;min-width:0;">
            ${["all","has_rule","no_rule","conflict"].map(fm => {
              const labels = { all: t("scanner_filter_all"), has_rule: t("scanner_filter_has_rule"), no_rule: t("scanner_filter_no_rule"), conflict: t("scanner_filter_conflict") };
              const isA = filterMode === fm;
              return `<button class="form-btn awr-sc-filter" data-filter="${fm}" style="padding:2px 7px!important;font-size:9px!important;border-radius:7px!important;flex-shrink:0!important;${isA?"background:rgba(99,102,241,0.2)!important;border-color:rgba(99,102,241,0.5)!important;color:#818cf8!important;":""}">${labels[fm]}${fm==="conflict"&&statsConflict>0?" ("+statsConflict+")":""}</button>`;
            }).join("")}
          </div>

          <!-- Bulk bar -->
          <div class="awr-bulk-bar" style="display:none;background:rgba(59,130,246,0.08);border:1px solid rgba(59,130,246,0.3);border-radius:7px;padding:6px 8px;margin-bottom:5px;gap:5px;flex-direction:column;min-width:0;box-sizing:border-box;">
            <div style="font-size:9px;color:#60a5fa;">${t("scanner_bulk_select_hint")}</div>
            <div style="display:flex;gap:4px;align-items:center;min-width:0;">
              <input class="form-input awr-bulk-input" placeholder="${t("scanner_bulk_placeholder")}" style="flex:1;min-width:0;font-size:10px!important;padding:3px 6px!important;height:24px!important;min-height:0!important;" />
              <button class="form-btn awr-bulk-save-btn btn-pill-primary" style="padding:2px 7px!important;font-size:9px!important;border-radius:8px!important;white-space:nowrap!important;flex-shrink:0!important;">${t("scanner_bulk_save", 0)}</button>
              <button class="form-btn awr-bulk-del-btn" style="padding:2px 7px!important;font-size:9px!important;border-radius:8px!important;white-space:nowrap!important;flex-shrink:0!important;background:rgba(239,68,68,0.12)!important;color:#f87171!important;border-color:rgba(239,68,68,0.35)!important;">${t("scanner_bulk_del", 0)}</button>
            </div>
          </div>

          <!-- Select-all + count -->
          <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;min-width:0;gap:4px;">
            <span class="awr-scan-visible-count" style="font-size:9px;color:#6b7280;flex-shrink:0;">${visibleKeys.length} ${t("scanner_visible_count")}</span>
            <div style="display:flex;gap:3px;flex-shrink:0;">
              <button class="form-btn awr-scan-selall" style="padding:1px 6px!important;font-size:9px!important;border-radius:7px!important;">${t("scanner_select_all_rows")}</button>
              <button class="form-btn awr-scan-desel" style="padding:1px 6px!important;font-size:9px!important;border-radius:7px!important;display:none;">${t("scanner_deselect_all")}</button>
            </div>
          </div>

          <!-- List -->
          <div class="awr-scanner-list" style="display:flex;flex-direction:column;gap:3px;max-height:290px;overflow-y:auto;overflow-x:hidden;min-width:0;"></div>
        `;

        // Bind search (debounced)
        // v99.12.37 FIX: Guard pane.isConnected — jika panel di-rebuild oleh health monitor
        // saat debounce 250ms sedang berjalan, pane closure sudah mati. Tanpa guard ini,
        // renderScannerUI() berjalan pada pane mati → search tidak berefek → UI "menghilang".
        // FIX: Event search lebih robust — pakai addEventListener agar tidak bisa di-overwrite,
        // dan tambahkan keyup sebagai fallback untuk browser/environment yang tidak fire oninput.
        let _sd = null;
        const _searchEl = pane.querySelector(".awr-sc-search");
        function _onSearchInput(e) {
          const _val = e.target.value;
          const _pos = e.target.selectionStart;
          clearTimeout(_sd);
          _sd = setTimeout(() => {
            if (!pane || !pane.isConnected) return; // Guard: pane mati, batalkan
            searchQuery = _val;
            _scannerState.searchQuery = searchQuery;
            renderScannerUI();
            // Pulihkan fokus & posisi kursor setelah re-render (tanpa mengganggu list scroll)
            const inp = pane.querySelector(".awr-sc-search");
            if (inp && document.activeElement !== inp) {
              inp.focus();
              try { inp.setSelectionRange(_pos, _pos); } catch(_) {}
            }
          }, 150); // Respons lebih cepat: 150ms bukan 250ms
        }
        _searchEl.addEventListener("input", _onSearchInput);
        _searchEl.addEventListener("keyup", function(e) {
          // Fallback: hanya fire jika nilai benar-benar berubah dan input event tidak ter-trigger
          if (e.target.value !== searchQuery) _onSearchInput(e);
        });

        // Bind sort — guard isConnected agar tidak update pane mati
        pane.querySelector(".awr-sc-sort").onchange = (e) => {
          if (!pane || !pane.isConnected) return;
          sortMode = e.target.value; _scannerState.sortMode = sortMode; renderScannerUI();
        };

        // Bind filter tabs — guard isConnected
        pane.querySelectorAll(".awr-sc-filter").forEach(btn => {
          btn.onclick = () => {
            if (!pane || !pane.isConnected) return;
            filterMode = btn.getAttribute("data-filter"); _scannerState.filterMode = filterMode; renderScannerUI();
          };
        });
        } // end if (!shell exists)

        // ── Selalu: ambil referensi elemen & perbarui konten list ──────────
        const listEl       = pane.querySelector(".awr-scanner-list");
        const bulkBar      = pane.querySelector(".awr-bulk-bar");
        const bulkInput    = pane.querySelector(".awr-bulk-input");
        const bulkSaveBtn  = pane.querySelector(".awr-bulk-save-btn");
        const bulkDelBtn   = pane.querySelector(".awr-bulk-del-btn");
        const selAllBtn    = pane.querySelector(".awr-scan-selall");
        const deselBtn     = pane.querySelector(".awr-scan-desel");

        function updateBulkUI() {
          const n = bulkSelected.size;
          if (n > 0) {
            bulkBar.style.display = "flex";
            bulkSaveBtn.textContent = t("scanner_bulk_save", n);
            bulkDelBtn.textContent  = t("scanner_bulk_del", n);
            deselBtn.style.display  = "";
          } else {
            bulkBar.style.display  = "none";
            deselBtn.style.display = "none";
            clearPagePreview();
          }
        }
        updateBulkUI();

        // Preview on bulk input typing
        let _pd = null;
        bulkInput.oninput = () => {
          clearTimeout(_pd); _pd = setTimeout(() => {
            if (bulkInput.value.trim()) applyPagePreview(Array.from(bulkSelected));
            else clearPagePreview();
          }, 200);
        };

        // ── Rows: hanya list yang di-refresh, shell (search/filter/sort) dipertahankan ──
        // Perbarui jumlah visible & active state filter tab tanpa mengganti shell
        const _vc = pane.querySelector(".awr-scan-visible-count");
        if (_vc) _vc.textContent = visibleKeys.length + " " + t("scanner_visible_count");
        pane.querySelectorAll(".awr-sc-filter").forEach(btn => {
          const fm = btn.getAttribute("data-filter");
          const isActive = fm === filterMode;
          btn.style.background    = isActive ? "rgba(99,102,241,0.2)" : "";
          btn.style.borderColor   = isActive ? "rgba(99,102,241,0.5)" : "";
          btn.style.color         = isActive ? "#818cf8" : "";
        });
        listEl.innerHTML = "";
        if (visibleKeys.length === 0) {
          listEl.innerHTML = `<div class="empty-state" style="padding:20px;text-align:center;color:#64748b;font-size:11px;">${t("empty_state")}</div>`;
        }
        visibleKeys.forEach(hash => {
          const info         = hashMap[hash];
          const textsArr     = Array.from(info.texts);
          const hasRule      = !!(_liveKamus["hash:" + hash]);
          const existingRule = hasRule ? _liveKamus["hash:" + hash] : null;
          const existingTo   = existingRule ? (typeof existingRule === "object" ? existingRule.to : existingRule) : "";
          const existingNote = (existingRule && typeof existingRule === "object" && existingRule.note) ? existingRule.note : "";
          const isMultiOcc   = info.count > 1;
          const isConflict   = isHashConflict(hash);
          const posRuleKeys  = Object.keys(_liveKamus).filter(k =>
            k.startsWith("hash:" + hash + ":") && /^\d+$/.test(k.slice(("hash:" + hash + ":").length))
          );
          const isBulkChk    = bulkSelected.has(hash);

          const borderColor = isConflict ? "rgba(239,68,68,0.45)"
            : (hasRule || posRuleKeys.length > 0) ? "rgba(16,185,129,0.3)" : "#272a34";

          const row = document.createElement("div");
          row.style.cssText = `background:#1a1d24;border-radius:7px;padding:5px 7px;border:1px solid ${borderColor};min-width:0;box-sizing:border-box;`;

          row.innerHTML = `
            <div style="display:flex;align-items:center;gap:4px;min-width:0;">
              <input type="checkbox" class="awr-scan-chk" ${isBulkChk?"checked":""} style="flex-shrink:0;cursor:pointer;width:12px;height:12px;" />
              <div style="min-width:0;flex:1;overflow:hidden;">
                <div style="font-size:10px;font-weight:bold;color:${isConflict?"#f87171":"#e2e8f0"};overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${textsArr.join(" / ")}">
                  ${isConflict?`<span title="${t("scanner_conflict_detail")}" style="margin-right:2px;">⚠</span>`:""}${textsArr.join(" / ")}
                </div>
                <div style="font-size:8px;color:#6b7280;margin-top:1px;display:flex;flex-wrap:wrap;gap:3px;align-items:center;min-width:0;overflow:hidden;">
                  <code style="background:#0f1117;padding:1px 3px;border-radius:3px;color:#94a3b8;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-block;vertical-align:middle;" title="hash:${hash}">hash:${hash}</code>
                  <span style="flex-shrink:0;">×${info.count}</span>
                  ${hasRule?`<span style="color:#10b981;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:70px;display:inline-block;vertical-align:middle;" title="→ ${existingTo}">→ <b>${existingTo}</b></span>`:""}
                  ${posRuleKeys.length>0?`<span style="color:#22d3ee;flex-shrink:0;">⚙${posRuleKeys.length}</span>`:""}
                  ${existingNote?`<span style="color:#64748b;font-style:italic;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:60px;display:inline-block;vertical-align:middle;" title="${existingNote}">📝${existingNote}</span>`:""}
                </div>
              </div>
              <div style="display:flex;gap:2px;flex-shrink:0;align-items:center;">
                <button class="form-btn awr-scan-note-btn" style="padding:1px 4px!important;font-size:11px!important;border-radius:5px!important;line-height:1!important;${existingNote?"background:rgba(100,116,139,0.25)!important;border-color:rgba(100,116,139,0.5)!important;":"background:none!important;border-color:rgba(71,85,105,0.35)!important;"}color:#64748b!important;" title="${t("scanner_note_label")}">📝</button>
                ${isMultiOcc?`<button class="form-btn awr-scan-pin-btn" style="padding:1px 4px!important;font-size:11px!important;border-radius:5px!important;line-height:1!important;background:rgba(34,211,238,0.15)!important;color:#22d3ee!important;border-color:rgba(34,211,238,0.4)!important;" title="${t("scanner_pin_occurrence")}">⚙</button>`:""}
                ${hasRule
                  ?`<button class="form-btn awr-scan-edit-btn" style="padding:1px 4px!important;font-size:10px!important;border-radius:5px!important;background:rgba(16,185,129,0.15)!important;color:#10b981!important;border-color:rgba(16,185,129,0.3)!important;" title="${t("scanner_edit_rule")}">✏</button>
                    <button class="form-btn awr-scan-del-btn" style="padding:1px 4px!important;font-size:10px!important;border-radius:5px!important;background:rgba(239,68,68,0.12)!important;color:#f87171!important;border-color:rgba(239,68,68,0.35)!important;" title="${t("scanner_del_rule")}">🗑</button>`
                  :`<button class="form-btn awr-scan-add-btn" style="padding:1px 5px!important;font-size:9px!important;border-radius:5px!important;white-space:nowrap!important;background:rgba(234,179,8,0.15)!important;color:#fbbf24!important;border-color:rgba(234,179,8,0.3)!important;">${t("scanner_add_rule")}</button>`
                }
              </div>
            </div>
            <!-- Note panel -->
            <div class="awr-note-panel" style="display:none;margin-top:4px;min-width:0;">
              <div style="display:flex;gap:4px;align-items:center;min-width:0;">
                <input type="text" class="form-input awr-note-input" value="${existingNote.replace(/"/g,"&quot;")}" placeholder="${t("scanner_note_placeholder")}" style="flex:1;min-width:0;font-size:10px!important;padding:3px 6px!important;height:24px!important;min-height:0!important;" />
                <button class="form-btn awr-note-save-btn btn-pill-primary" style="padding:2px 7px!important;font-size:9px!important;border-radius:7px!important;flex-shrink:0;">💾</button>
              </div>
            </div>
            <!-- Positional override panel -->
            <div class="awr-pos-panel" style="display:none;margin-top:5px;padding:5px 6px;background:rgba(34,211,238,0.06);border:1px solid rgba(34,211,238,0.25);border-radius:6px;min-width:0;box-sizing:border-box;"></div>
          `;

          // Checkbox
          const chk = row.querySelector(".awr-scan-chk");
          chk.onchange = () => {
            if (chk.checked) { bulkSelected.add(hash); applyPagePreview(Array.from(bulkSelected)); }
            else { bulkSelected.delete(hash); applyPagePreview(Array.from(bulkSelected)); }
            updateBulkUI();
          };

          // Tambah Rule
          const addBtn = row.querySelector(".awr-scan-add-btn");
          if (addBtn) addBtn.onclick = () => {
            clearPagePreview();
            subjekEdit = null; tabAktif = "tambah"; renderTampilan();
            setTimeout(() => {
              const _sr = document.querySelector('#word-replacer-host')?.shadowRoot;
              const iS = _sr?.querySelector(".input-salah"), iB = _sr?.querySelector(".input-benar"), hE = _sr?.querySelector(".awr-hash-hint");
              if (iS) { iS.value = "hash:" + hash; iS.dispatchEvent(new Event("input")); }
              if (iB && !iB.value) iB.value = textsArr[0] || "";
              if (hE) hE.style.display = "block";
            }, 80);
          };

          // Edit Rule
          const editBtn = row.querySelector(".awr-scan-edit-btn");
          if (editBtn) editBtn.onclick = () => { clearPagePreview(); subjekEdit = "hash:" + hash; tabAktif = "tambah"; renderTampilan(); };

          // Hapus Rule
          const delBtn = row.querySelector(".awr-scan-del-btn");
          if (delBtn) delBtn.onclick = () => {
            window.AWR_UI_LIBRARY.tampilkanKonfirmasi(t("scanner_del_rule"), `Yakin hapus rule "hash:${hash}" → "${existingTo}"?`, () => {
              deleteHashRule(hash);
              panggilToast(t("scanner_rule_deleted", "hash:" + hash), "warn");
              core.jalankanPengganti(true); rerenderScanner();
            });
          };

          // Note panel
          const noteBtn   = row.querySelector(".awr-scan-note-btn");
          const notePanel = row.querySelector(".awr-note-panel");
          const noteInput = row.querySelector(".awr-note-input");
          const noteSave  = row.querySelector(".awr-note-save-btn");
          noteBtn.onclick = () => {
            const vis = notePanel.style.display !== "none";
            notePanel.style.display = vis ? "none" : "block";
            if (!vis) setTimeout(() => noteInput.focus(), 50);
          };
          noteSave.onclick = () => {
            const noteVal = noteInput.value.trim();
            const tempKamus = core.getKamus();
            const key = "hash:" + hash;
            if (!tempKamus[key]) { panggilToast("Buat rule dulu sebelum menambah catatan.", "warn"); return; }
            if (typeof tempKamus[key] === "object") {
              if (noteVal) tempKamus[key].note = noteVal; else delete tempKamus[key].note;
            } else {
              tempKamus[key] = { to: tempKamus[key], ...(noteVal ? { note: noteVal } : {}) };
            }
            core.saveKamus(tempKamus);
            panggilToast("Catatan disimpan!", "success");
            rerenderScanner();
          };

          // Per-Kemunculan (Positional Override)
          const pinBtn  = row.querySelector(".awr-scan-pin-btn");
          const posPanel = row.querySelector(".awr-pos-panel");
          if (pinBtn && posPanel) {
            pinBtn.onclick = () => {
              if (posPanel.style.display !== "none") { posPanel.style.display = "none"; return; }
              posPanel.innerHTML = `<div style="font-size:9px;color:#22d3ee;font-weight:bold;margin-bottom:4px;">${t("scanner_occurrence_hint", info.spans.length)}</div>`;
              const curKamus = core.getKamus();
              info.spans.forEach((span, idx) => {
                const posKey  = "hash:" + hash + ":" + idx;
                const posEntry = curKamus[posKey];
                const posVal  = posEntry ? (typeof posEntry === "object" ? posEntry.to : posEntry) : "";
                const ctx     = getSpanContext(span);
                const occRow  = document.createElement("div");
                occRow.style.cssText = "margin-bottom:4px;padding:4px 6px;background:#0f1117;border-radius:5px;border:1px solid #1e293b;min-width:0;box-sizing:border-box;";
                occRow.innerHTML = `
                  <div style="font-size:8px;color:#64748b;margin-bottom:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${ctx}">
                    <b style="color:#22d3ee;">${t("scanner_occurrence_label", idx + 1)}</b>${ctx ? `<span style="margin-left:4px;font-style:italic;">${ctx}</span>` : ""}
                  </div>
                  <div style="display:flex;gap:4px;align-items:center;min-width:0;">
                    <input type="text" class="form-input awr-pos-input" data-poskey="${posKey}" placeholder="${existingTo || (textsArr[0] || "")}" value="${posVal}" style="flex:1;min-width:0;font-size:10px!important;padding:3px 5px!important;height:22px!important;min-height:0!important;" />
                    ${posVal?`<button class="form-btn awr-pos-clear-btn" data-poskey="${posKey}" style="padding:1px 5px!important;font-size:9px!important;border-radius:5px!important;background:rgba(239,68,68,0.12)!important;color:#f87171!important;border-color:rgba(239,68,68,0.3)!important;flex-shrink:0;">✕</button>`:""}
                  </div>`;
                // v99.12.30: Scroll ke span yang sesuai saat user fokus ke input occurrence mana pun
                const posInput = occRow.querySelector(".awr-pos-input");
                posInput.onfocus = () => {
                  // Highlight span occurrence ini
                  clearPagePreview();
                  _previewSpans = [{ span, oline: span.style.outline, obg: span.style.background, obr: span.style.borderRadius }];
                  span.style.outline = "2px solid #22d3ee"; span.style.background = "rgba(34,211,238,0.18)"; span.style.borderRadius = "3px";
                  // Scroll ke span occurrence ini (bukan hanya kemunculan ke-1)
                  if (span.isConnected) {
                    try { span.scrollIntoView({ behavior: "smooth", block: "center" }); } catch(_) {}
                  }
                };
                posInput.oninput = (e) => {
                  clearPagePreview();
                  if (e.target.value) {
                    _previewSpans = [{ span, oline: span.style.outline, obg: span.style.background, obr: span.style.borderRadius }];
                    span.style.outline = "2px solid #22d3ee"; span.style.background = "rgba(34,211,238,0.18)"; span.style.borderRadius = "3px";
                  }
                };
                const clrBtn = occRow.querySelector(".awr-pos-clear-btn");
                if (clrBtn) clrBtn.onclick = () => {
                  const pk = clrBtn.getAttribute("data-poskey");
                  const tk = core.getKamus(); delete tk[pk];
                  core.saveKamus(tk); core.simpanKeAwan(tk);
                  core.jalankanPengganti(true); rerenderScanner();
                };
                posPanel.appendChild(occRow);
              });
              const savePosBtn = document.createElement("button");
              savePosBtn.className = "form-btn btn-pill-primary";
              savePosBtn.style.cssText = "width:100%;padding:3px!important;font-size:9px!important;margin-top:3px!important;box-sizing:border-box!important;";
              savePosBtn.textContent = t("scanner_pos_save");
              // v99.12.30 FIX: savePosBtn.onclick -- scroll ke SEMUA occurrence yang terisi,
              // bukan hanya kemunculan ke-1. Guard if(!_scrollTarget) dihapus agar setiap
              // occurrence dengan nilai dikumpulkan sebagai scroll target. Scroll dilakukan
              // SEBELUM jalankanPengganti agar span belum di-detach dari DOM.
              savePosBtn.onclick = () => {
                // FIX: Kunci posisi scroll halaman sebelum aksi apapun,
                // agar simpan posisi tidak menggeser tampilan halaman novel.
                const _savedScrollX = window.scrollX || window.pageXOffset || 0;
                const _savedScrollY = window.scrollY || window.pageYOffset || 0;

                const tk = core.getKamus();
                posPanel.querySelectorAll(".awr-pos-input").forEach(inp => {
                  // v99.12.30: simpan nilai asli (tanpa trim) agar spasi sebagai pemisah antar kata unik terjaga
                  const pk = inp.getAttribute("data-poskey"), val = inp.value, valTrimmed = val.trim();
                  if (valTrimmed) {
                    tk[pk] = { to: val, global: false, novelId: currentNovel.id, novelTitle: currentNovel.title, novelUrl: currentNovel.url, domain: core.getNovelBaseDomain(core.currentHost), caseSensitive: true };
                  } else delete tk[pk];
                });
                // Simpan & jalankan replacer tanpa scroll
                core.saveKamus(tk); core.simpanKeAwan(tk); core.jalankanPengganti(true);
                clearPagePreview(); panggilToast(t("scanner_pos_saved"), "success"); rerenderScanner();

                // Pulihkan posisi scroll setelah semua operasi selesai
                requestAnimationFrame(() => {
                  window.scrollTo({ top: _savedScrollY, left: _savedScrollX, behavior: "instant" });
                });
              };
              posPanel.appendChild(savePosBtn);
              posPanel.style.display = "block";
            };
          }

          listEl.appendChild(row);
        });

        // Select All / Deselect All
        selAllBtn.onclick = () => {
          visibleKeys.forEach(h => bulkSelected.add(h));
          listEl.querySelectorAll(".awr-scan-chk").forEach(c => { c.checked = true; });
          applyPagePreview(Array.from(bulkSelected)); updateBulkUI();
        };
        deselBtn.onclick = () => {
          bulkSelected.clear();
          listEl.querySelectorAll(".awr-scan-chk").forEach(c => { c.checked = false; });
          clearPagePreview(); updateBulkUI();
        };

        // Bulk Save
        bulkSaveBtn.onclick = () => {
          const toText = (bulkInput.value || "").trim();
          if (!toText) { panggilToast("Teks pengganti tidak boleh kosong!", "warn"); return; }
          if (!bulkSelected.size) { panggilToast("Pilih minimal 1 baris!", "warn"); return; }
          const tk = core.getKamus();
          bulkSelected.forEach(h => {
            const key = "hash:" + h, ex = tk[key];
            tk[key] = { to: toText, global: false, novelId: currentNovel.id, novelTitle: currentNovel.title, novelUrl: currentNovel.url, domain: core.getNovelBaseDomain(core.currentHost), caseSensitive: true, ...(ex && ex.note ? { note: ex.note } : {}) };
          });
          core.saveKamus(tk); core.simpanKeAwan(tk); core.jalankanPengganti(true);
          clearPagePreview(); panggilToast(t("scanner_bulk_save", bulkSelected.size) + " ✓", "success");
          bulkSelected.clear(); rerenderScanner();
        };

        // Bulk Delete
        bulkDelBtn.onclick = () => {
          const toDel = Array.from(bulkSelected).filter(h => seluruhKamus["hash:" + h]);
          if (!toDel.length) { panggilToast("Tidak ada rule untuk dihapus!", "warn"); return; }
          window.AWR_UI_LIBRARY.tampilkanKonfirmasi(t("scanner_bulk_del", toDel.length), `Yakin hapus ${toDel.length} rule hash ke Recycle Bin?`, () => {
            const tk = core.getKamus(), dw = core.getDeletedWords();
            toDel.forEach(h => { const key = "hash:" + h; if (tk[key]) { dw[key] = tk[key]; delete tk[key]; } });
            core.saveKamus(tk); core.saveDeletedWords(dw); core.simpanKeAwan(tk, null, dw); core.jalankanPengganti(true);
            clearPagePreview(); panggilToast(t("scanner_bulk_del", toDel.length) + " ✓", "warn");
            bulkSelected.clear(); rerenderScanner();
          });
        };
        } catch (scanErr) {
          // v99.12.36: Tampilkan pesan ERROR (bukan loading spinner) dengan class khusus.
          // Kelas "awr-scanner-error-state" dikenali health monitor sebagai kondisi yang SUDAH
          // ditangani — tidak perlu memicu recovery, sehingga retry loop TIDAK terjadi.
          console.error("[AWR] renderScannerUI error:", scanErr);
          try {
            pane.innerHTML = `<div class="awr-scanner-error-state" style="padding:16px;color:#f87171;font-size:11px;text-align:center;line-height:1.6;display:flex;flex-direction:column;align-items:center;gap:6px;">
              <span style="font-size:20px;">⚠️</span>
              <b>Gagal memuat scanner</b>
              <span style="color:#64748b;font-size:10px;">${scanErr && scanErr.message ? scanErr.message : "Unknown error"}</span>
              <button class="form-btn" style="margin-top:4px;padding:4px 14px!important;font-size:10px!important;border-radius:8px!important;" id="awr-scanner-retry-btn">🔄 Coba Lagi</button>
            </div>`;
            const retryBtn = pane.querySelector("#awr-scanner-retry-btn");
            if (retryBtn) retryBtn.onclick = () => {
              pane.innerHTML = "";
              try { renderScannerUI(); } catch(e2) {
                pane.innerHTML = `<div class="awr-scanner-error-state" style="padding:16px;color:#f87171;font-size:11px;text-align:center;">⚠️ Scanner tetap gagal: ${e2 && e2.message ? e2.message : "unknown"}<br><span style="color:#64748b;font-size:9px;">Coba refresh halaman jika masalah berlanjut.</span></div>`;
              }
            };
          } catch(_) {}
        }
      } // end renderScannerUI()

      renderScannerUI();

    } else if (tabAktif === "tambah") {
      const pane = document.createElement("div");
      pane.className = "tab-pane active";

      const salahVal = subjekEdit || "";
      const itemData = seluruhKamus[salahVal];
      const benarVal =
        itemData && typeof itemData === "object"
          ? itemData.to || ""
          : itemData || "";

      let initialSelectedGroup =
        subjekEdit && itemData && typeof itemData === "object"
          ? itemData.global
            ? "GLOBAL_OPTION"
            : itemData.novelId || currentNovel.id
          : lastSelectedGroup;

      const isGlobal = initialSelectedGroup === "GLOBAL_OPTION";
      const isNewCustom = initialSelectedGroup === "NEW_CUSTOM";
      const localNovelsMap = {};
      localNovelsMap[currentNovel.id] = currentNovel.title;

      for (const salah in seluruhKamus) {
        const item = seluruhKamus[salah];
        if (item && typeof item === "object" && !item.global && item.novelId) {
          localNovelsMap[item.novelId] =
            item.novelTitle ||
            core.getCachedNovelTitle(item.novelId) ||
            item.novelId;
        }
      }

      if (
        initialSelectedGroup &&
        initialSelectedGroup !== "GLOBAL_OPTION" &&
        !localNovelsMap[initialSelectedGroup]
      ) {
        localNovelsMap[initialSelectedGroup] =
          core.getCachedNovelTitle(initialSelectedGroup) || "Custom Group";
      }

      let selectOptionsHTML = `<option value="GLOBAL_OPTION" ${initialSelectedGroup === "GLOBAL_OPTION" ? "selected" : ""}>🌐 ${t("global_replacer")}</option>`;
      for (const id in localNovelsMap) {
        selectOptionsHTML += `<option value="${id}" ${initialSelectedGroup === id ? "selected" : ""}>📖 ${localNovelsMap[id]}${id === currentNovel.id ? " (Active)" : ""}</option>`;
      }
      selectOptionsHTML += `<option value="NEW_CUSTOM" ${initialSelectedGroup === "NEW_CUSTOM" ? "selected" : ""}>${t("select_custom_group_placeholder")}</option>`;

      const chkActiveState = subjekEdit
        ? activeNovelId === initialSelectedGroup
        : lastCheckboxState;

      pane.innerHTML = `
                <div class="editor-section">
                    <div class="editor-label-row">
                        <span class="editor-label">${t("original_text", salahVal.length)}</span>
                        <div class="editor-pills">
                            <span class="editor-pill btn-add-variation">+ Variation</span>
                            <span class="editor-pill btn-add-wildcard">+ Wild Char</span>
                            <span class="editor-pill btn-add-hash" style="background:rgba(234,179,8,0.2)!important;color:#fbbf24!important;border-color:rgba(234,179,8,0.4)!important;" title="Target kata unik by data-hash (untuk nama yang ditranslate sama)">🔑 Hash Rule</span>
                            <span class="editor-pill btn-add-exact" style="background:rgba(34,197,94,0.15)!important;color:#4ade80!important;border-color:rgba(34,197,94,0.4)!important;" title="Cocokkan teks panjang/literal secara persis (tanpa regex) — cocok untuk kalimat panjang dengan tanda baca">📋 Exact</span>
                        </div>
                    </div>
                    <textarea class="form-input input-salah" placeholder="${t("word_salah_placeholder")}" ${subjekEdit ? "disabled" : ""}>${salahVal}</textarea>
                    <div class="awr-match-preview" style="font-size:9px;color:#64748b;margin-top:3px;min-height:14px;"></div>
                    ${salahVal.startsWith("hash:") ? `<div style="margin-top:6px;padding:8px 10px;background:rgba(234,179,8,0.08);border:1px solid rgba(234,179,8,0.3);border-radius:8px;font-size:11px;color:#fbbf24;">🔑 <b>Hash Rule</b> — Akan mengganti semua <code style="background:rgba(0,0,0,0.3);padding:1px 4px;border-radius:3px;">data-hash="${salahVal.slice(5)}"</code> di halaman ini. Teks pengganti di bawah akan mengisi span kata unik tersebut.</div>` : `<div class="awr-hash-hint" style="display:none;margin-top:6px;padding:8px 10px;background:rgba(234,179,8,0.08);border:1px solid rgba(234,179,8,0.3);border-radius:8px;font-size:11px;color:#fbbf24;">🔑 <b>Hash Rule</b> — Ketik <code style="background:rgba(0,0,0,0.3);padding:1px 4px;border-radius:3px;">hash:NILAI_DATA_HASH</code> untuk menarget span kata unik tertentu. Atau klik kanan pada kata unik di halaman novel.</div>`}
                    <div class="editor-sub-row" style="display: flex !important; justify-content: space-between !important; align-items: center !important; font-size: 10px !important; color: #64748b !important; margin-top: 4px !important; overflow: visible !important;">
                        <span>${t("editor_example_tip")}</span>
                        <label class="case-sensitive-toggle" style="display: inline-flex !important; align-items: center !important; gap: 6px !important; cursor: pointer !important; user-select: none !important;">
                            <input type="checkbox" class="chk-case-sensitive" ${itemData && itemData.caseSensitive ? "checked" : ""} />
                            <span style="color: #94a3b8 !important; font-size: 11px !important;">${t("case_sensitive")}</span>
                        </label>
                    </div>
                </div>
                <div class="editor-section" style="margin-top: 16px;">
                    <span class="editor-label">${t("replacement_text", benarVal.length)}</span>
                    <textarea class="form-input input-benar" placeholder="${t("word_benar_placeholder")}">${benarVal}</textarea>
                </div>
                <div class="editor-section" style="margin-top: 16px; display: flex; flex-direction: column; gap: 6px; overflow: visible !important;">
                    <span class="editor-label">${t("target_category")}:</span>
                    <select class="form-input sel-novel-group">${selectOptionsHTML}</select>
                    <input type="text" class="form-input txt-custom-novel-title" placeholder="${t("custom_novel_input_placeholder")}" style="display: ${isNewCustom ? "block" : "none"}; margin-top: 4px;" />
                    <div class="chk-active-group-container" style="display: ${isGlobal ? "none" : "flex"} !important; align-items: center !important; gap: 6px !important; margin-top: 6px !important;">
                        <input type="checkbox" class="chk-make-group-active" ${chkActiveState ? "checked" : ""} />
                        <span style="font-size: 11px !important; color: #9ca3af !important;">${t("make_group_active")}</span>
                    </div>
                </div>
                <div class="card-box" style="margin-top: 16px; border: 1px dashed #3b82f6 !important; background: rgba(59, 130, 246, 0.05) !important; overflow: visible !important;">
                    <span class="editor-label" style="color: #60a5fa !important; font-weight: bold !important;">🔍 Live Rule Sandbox</span>
                    <div style="font-size: 10px; color: #94a3b8; margin-top: 2px; margin-bottom: 6px;">Test your rules & wildcards live on any custom text below:</div>
                    <textarea class="form-input txt-sandbox-input" placeholder="Type test paragraph here..." style="font-size: 11px !important; min-height: 45px !important; height: 45px !important; resize: vertical !important;"></textarea>
                    <div class="txt-sandbox-output" style="margin-top: 6px; padding: 6px 10px; background: #0f172a; border: 1px solid #334155; border-radius: 6px; font-size: 11px; min-height: 20px; color: #cbd5e1; word-break: break-all;">
                        <span style="color: #64748b; font-style: italic;">Replacement output will show here live...</span>
                    </div>
                </div>
                <div class="editor-footer" style="margin-top: auto; padding-top: 20px; display: flex !important; justify-content: space-between !important; align-items: center !important; gap: 8px !important; overflow: visible !important;">
                    ${subjekEdit ? `<button class="form-btn delete-btn-editor" style="background: #991b1b !important; color: white !important; display: inline-flex; align-items: center; justify-content: center; gap: 6px;">${core.SVGS.trash} ${t("delete_btn")}</button>` : "<div></div>"}
                    <div style="display: flex !important; align-items: center !important; gap: 8px !important; margin-left: auto !important; overflow: visible !important;">
                        <button class="form-btn close-btn-editor">${t("close_btn")}</button>
                        <button class="form-btn simpan-btn btn-pill-primary" style="padding: 8px 20px !important; display: inline-flex; align-items: center; justify-content: center; gap: 6px;">${core.SVGS.disk} ${subjekEdit ? t("update_btn") : t("save_btn")}</button>
                    </div>
                </div>
            `;

      const inputSalah = pane.querySelector(".input-salah");
      const inputBenar = pane.querySelector(".input-benar");
      const selNovelGroup = pane.querySelector(".sel-novel-group");
      const txtCustomNovelTitle = pane.querySelector(".txt-custom-novel-title");

      const txtSandboxInput = pane.querySelector(".txt-sandbox-input");
      const txtSandboxOutput = pane.querySelector(".txt-sandbox-output");
      if (txtSandboxInput && txtSandboxOutput) {
        const updateSandbox = () => {
          const rawSalah = inputSalah.value.trim();
          const rawBenar = inputBenar.value; // v99.12.30: jangan trim — spasi awal/akhir bisa disengaja sebagai pemisah
          const text = txtSandboxInput.value;
          if (!rawSalah || !text) {
            txtSandboxOutput.innerHTML =
              '<span style="color: #64748b; font-style: italic;">Replacement output will show here live...</span>';
            return;
          }
          try {
            const csChecked =
              pane.querySelector(".chk-case-sensitive")?.checked || false;
            const pattern = buildRegexPattern(rawSalah);
            const rx = buildBoundaryRegex(
              rawSalah,
              pattern,
              csChecked ? "gu" : "giu",
            );
            const result = text.replace(rx, (matchStr, ...groups) => {
              const numSeps = rawSalah.split(/[\s\-_—–]+/).length - 1;
              const capturedSeparators = groups.slice(0, numSeps);
              const replaced = reconstructReplacement(
                rawBenar,
                capturedSeparators,
              );
              return `<strong style="color: #3b82f6 !important; text-decoration: underline;">${replaced}</strong>`;
            });
            txtSandboxOutput.innerHTML =
              result ||
              '<span style="color: #64748b; font-style: italic;">Replacement output will show here live...</span>';
          } catch (err) {
            txtSandboxOutput.textContent = "Regex Error: " + err.message;
          }
        };
        txtSandboxInput.oninput = updateSandbox;
        let _matchDebounce = null;
        inputSalah.oninput = () => {
          updateSandbox();
          pane.querySelector(".editor-label").textContent = t(
            "original_text",
            inputSalah.value.length,
          );
          // v99.12.37: Preview jumlah match di halaman saat ini (debounce 400ms)
          clearTimeout(_matchDebounce);
          _matchDebounce = setTimeout(() => {
            const matchEl = pane.querySelector(".awr-match-preview");
            if (!matchEl || subjekEdit) return; // jangan scan saat edit (key disabled)
            const rawTerm = inputSalah.value.trim();
            if (!rawTerm || rawTerm.startsWith("hash:") || rawTerm.startsWith("exact:")) {
              matchEl.textContent = "";
              return;
            }
            try {
              const pattern = buildRegexPattern(rawTerm);
              const rx = buildBoundaryRegex(rawTerm, pattern, "giu");
              const bodyText = document.body ? document.body.innerText : "";
              const hits = (bodyText.match(rx) || []).length;
              matchEl.innerHTML = hits > 0
                ? '<span style="color:#10b981;">● ' + hits + ' match ditemukan di halaman ini</span>'
                : '<span style="color:#f87171;">● Tidak ada match di halaman ini</span>';
            } catch(_) { matchEl.textContent = ""; }
          }, 400);
        };
        inputBenar.oninput = () => {
          updateSandbox();
          pane.querySelectorAll(".editor-label")[1].textContent = t(
            "replacement_text",
            inputBenar.value.length,
          );
        };
      }

      pane.querySelector(".btn-add-variation").onclick = () => {
        inputSalah.value += "|";
        inputSalah.focus();
      };
      pane.querySelector(".btn-add-wildcard").onclick = () => {
        inputSalah.value += "*";
        inputSalah.focus();
      };
      pane.querySelector(".btn-add-hash").onclick = () => {
        if (!inputSalah.value.startsWith("hash:")) {
          inputSalah.value = "hash:" + inputSalah.value;
        }
        const hint = pane.querySelector(".awr-hash-hint");
        if (hint) hint.style.display = "block";
        inputSalah.focus();
      };
      pane.querySelector(".btn-add-exact").onclick = () => {
        if (!inputSalah.value.startsWith("exact:")) {
          inputSalah.value = "exact:" + inputSalah.value;
        }
        inputSalah.focus();
      };
      // Show hash hint live as user types "hash:"
      if (!subjekEdit) {
        inputSalah.addEventListener("input", () => {
          const hint = pane.querySelector(".awr-hash-hint");
          if (hint) hint.style.display = inputSalah.value.startsWith("hash:") ? "block" : "none";
        });
      }

      selNovelGroup.onchange = (e) => {
        const selectedVal = e.target.value;
        if (selectedVal !== "NEW_CUSTOM")
          GM_setValue("awr_last_selected_group_id_v2", selectedVal);
        txtCustomNovelTitle.style.display =
          selectedVal === "NEW_CUSTOM" ? "block" : "none";
        pane.querySelector(".chk-active-group-container").style.display =
          selectedVal === "GLOBAL_OPTION" ? "none" : "flex";
      };

      pane.querySelector(".close-btn-editor").onclick = () => {
        subjekEdit = null;
        tabAktif = "daftar";
        renderTampilan();
      };

      const deleteBtnEditor = pane.querySelector(".delete-btn-editor");
      if (deleteBtnEditor) {
        deleteBtnEditor.onclick = () => {
          window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
            t("delete_btn"),
            t("delete_group_confirm", subjekEdit),
            () => {
              const tempKamus = core.getKamus(),
                deletedWords = core.getDeletedWords();
              // v99.12.37: Simpan snapshot untuk undo sebelum menghapus
              pushUndoSnapshot('Hapus "' + subjekEdit + '"');
              if (subjekEdit) {
                const actKey = Object.keys(tempKamus).find(
                  (k) =>
                    k.trim().toLowerCase() === subjekEdit.trim().toLowerCase(),
                );
                if (actKey) {
                  deletedWords[actKey] = tempKamus[actKey];
                  delete tempKamus[actKey];
                }
              }
              core.saveKamus(tempKamus);
              core.saveDeletedWords(deletedWords);
              core.simpanKeAwan(tempKamus, null, deletedWords);
              panggilToast(t("toast_deleted", subjekEdit), "warn");
              subjekEdit = null;
              tabAktif = "daftar";
              core.jalankanPengganti(true);
              renderTampilan();
            },
          );
        };
      }

      pane.querySelector(".simpan-btn").onclick = () => {
        const csChecked = pane.querySelector(".chk-case-sensitive").checked;
        let rawSalah = inputSalah.value.trim().normalize("NFC");
        // v99.12.30: jangan trim nilai benar — spasi awal/akhir bisa disengaja sebagai pemisah antar kata unik
        // smartSanitizeTerm TIDAK diterapkan ke benar (fungsi itu untuk kata sumber, bukan kata pengganti)
        let benar = inputBenar.value.normalize("NFC");

        rawSalah = (rawSalah.startsWith("hash:") || rawSalah.startsWith("exact:")) ? rawSalah : core.smartSanitizeTerm(rawSalah);
        const selectedVal = selNovelGroup.value,
          isGlobalChecked = selectedVal === "GLOBAL_OPTION";
        // hash: dan exact: entries selalu preserve case (hash: sensitif, exact: dicocokkan case-insensitive tapi key disimpan as-is)
        const salah = (csChecked || rawSalah.startsWith("hash:") || rawSalah.startsWith("exact:")) ? rawSalah : rawSalah.toLowerCase();

        if (!salah || !benar.trim()) { // .trim() hanya untuk cek kosong, tidak untuk menyimpan
          alert(t("alert_both_fields"));
          return;
        }

        // v99.12.37: Deteksi konflik — cek apakah rule baru overlap dengan rule yang ada
        if (!salah.startsWith("hash:") && !salah.startsWith("exact:") && !salah.startsWith("_off_:")) {
          const salahLower = salah.toLowerCase();
          const conflictKeys = Object.keys(seluruhKamus).filter(existKey => {
            if (existKey === subjekEdit) return false; // skip rule yang sedang diedit
            if (existKey.startsWith("hash:") || existKey.startsWith("exact:") || existKey.startsWith("_off_:")) return false;
            const eLower = existKey.toLowerCase();
            return (salahLower.includes(eLower) && eLower !== salahLower) ||
                   (eLower.includes(salahLower) && eLower !== salahLower);
          });
          if (conflictKeys.length > 0) {
            const conflictList = conflictKeys.slice(0, 3).join('", "');
            panggilToast(
              '⚠ Peringatan: Rule ini mungkin overlap dengan "' + conflictList + '"' +
              (conflictKeys.length > 3 ? ' (+' + (conflictKeys.length - 3) + ' lainnya)' : '') +
              '. Rule yang overlap bisa saling mengganggu. Rule tetap disimpan.',
              "warn"
            );
          }
        }
        const tempKamus = core.getKamus();
        let savedNovelId = "",
          savedNovelTitle = "",
          savedNovelUrl = "";

        if (!isGlobalChecked) {
          if (selectedVal === "NEW_CUSTOM") {
            const customTitle = txtCustomNovelTitle.value.trim();
            if (!customTitle) {
              alert(t("warn_no_custom_name"));
              return;
            }
            savedNovelId =
              "custom_novel_" + Math.random().toString(36).substring(2, 11);
            savedNovelTitle = customTitle;
            savedNovelUrl = currentNovel.url;
            core.saveCachedNovelTitle(savedNovelId, savedNovelTitle);
          } else {
            savedNovelId = selectedVal;
            savedNovelTitle =
              core.getCachedNovelTitle(savedNovelId) ||
              localNovelsMap[savedNovelId] ||
              "Novel";
            savedNovelUrl =
              Object.values(tempKamus).find((x) => x.novelId === savedNovelId)
                ?.novelUrl || currentNovel.url;
          }
        }

        // v99.12.37: Simpan snapshot untuk undo sebelum mengubah kamus
        pushUndoSnapshot(subjekEdit ? 'Update "' + subjekEdit + '"' : 'Tambah "' + salah + '"');
        if (subjekEdit) delete tempKamus[subjekEdit];
        tempKamus[salah] = {
          to: benar,
          global: isGlobalChecked,
          novelId: savedNovelId,
          novelTitle: savedNovelTitle,
          novelUrl: savedNovelUrl,
          domain: core.getNovelBaseDomain(core.currentHost),
          caseSensitive: csChecked,
        };

        const chkMakeGroup = pane.querySelector(".chk-make-group-active");
        if (!isGlobalChecked) {
          const isCheckActive = chkMakeGroup && chkMakeGroup.checked;
          GM_setValue("awr_last_selected_group_id_v2", savedNovelId);
          GM_setValue("awr_last_active_group_checkbox_state_v2", isCheckActive);
          if (isCheckActive) core.setActiveNovelId(savedNovelId);
          else core.setActiveNovelId("");
        } else {
          GM_setValue("awr_last_selected_group_id_v2", "GLOBAL_OPTION");
        }

        core.saveKamus(tempKamus);
        core.simpanKeAwan(tempKamus);
        panggilToast(
          subjekEdit ? t("toast_updated", salah) : t("toast_added", salah),
          "success",
        );
        subjekEdit = null;
        tabAktif = "daftar";
        renderTampilan();
        setTimeout(() => {
          core.jalankanPengganti(true);
        }, 50);
      };

      body.appendChild(pane);
    } else if (tabAktif === "setting") {
      const pane = document.createElement("div");
      pane.className = "tab-pane active";
      body.appendChild(pane);

      pane.innerHTML = `
                <div class="setting-sub-tabs-bar" style="display: flex !important; gap: 4px !important; margin-bottom: 12px !important; border-bottom: 1px solid #272a34 !important; padding-bottom: 8px !important; overflow: visible !important; flex-wrap: wrap !important;">
                    <button class="form-btn sub-tab-btn ${settingSubTab === "filter" ? "active" : ""}" data-subtab="filter" style="flex: 1 !important; padding: 5px 8px !important; font-size: 10px !important; border-radius: 12px !important; white-space: nowrap !important; transition: all 0.2s ease !important; cursor: pointer !important; text-align: center !important;">🌐 ${t("tab_filter")}</button>
                    <button class="form-btn sub-tab-btn ${settingSubTab === "config" ? "active" : ""}" data-subtab="config" style="flex: 1 !important; padding: 5px 8px !important; font-size: 10px !important; border-radius: 12px !important; white-space: nowrap !important; transition: all 0.2s ease !important; cursor: pointer !important; text-align: center !important;">⚙️ ${t("tab_config")}</button>
                    <button class="form-btn sub-tab-btn ${settingSubTab === "data" ? "active" : ""}" data-subtab="data" style="flex: 1 !important; padding: 5px 8px !important; font-size: 10px !important; border-radius: 12px !important; white-space: nowrap !important; transition: all 0.2s ease !important; cursor: pointer !important; text-align: center !important;">📦 ${t("tab_data")}</button>
                    <button class="form-btn sub-tab-btn ${settingSubTab === "cloud" ? "active" : ""}" data-subtab="cloud" style="flex: 1 !important; padding: 5px 8px !important; font-size: 10px !important; border-radius: 12px !important; white-space: nowrap !important; transition: all 0.2s ease !important; cursor: pointer !important; text-align: center !important;">☁️ Cloud</button>
                </div>
                <div class="setting-sub-content" style="overflow: visible !important;"></div>
            `;

      const subContent = pane.querySelector(".setting-sub-content");
      pane.querySelectorAll(".sub-tab-btn").forEach((btn) => {
        btn.onclick = (e) => {
          e.stopPropagation();
          settingSubTab = btn.getAttribute("data-subtab");
          renderTampilan();
        };
      });

      if (settingSubTab === "filter") {
        const currentMode = core.getFilterMode(),
          whitelist = core.getTargetDomains(),
          blacklist = core.getBlacklistDomains();

        subContent.innerHTML = `
                    <div style="font-size:11px;font-weight:bold;margin-bottom:8px;color:#9ca3af;border-bottom:1px solid #272a34;padding-bottom:4px;display:flex!important;justify-content:space-between!important;align-items:center!important;overflow:visible!important;">
                        <span>🌐 ${t("site_manager")}</span>
                        <div style="display:flex!important;align-items:center!important;gap:4px!important;">
                            <span style="font-size:9px!important;color:#9ca3af!important;">${t("mode_label")}</span>
                            <select class="form-input select-filter-mode" style="width:auto!important;padding:2px 6px!important;font-size:9px!important;background:#1a1d24!important;border-color:#272a34!important;border-radius:10px!important;color:white!important;height:auto!important;line-height:1!important;cursor:pointer!important;">
                                <option value="whitelist" ${currentMode === "whitelist" ? "selected" : ""}>${t("only_whitelist")}</option>
                                <option value="blacklist" ${currentMode === "blacklist" ? "selected" : ""}>${t("block_blacklist")}</option>
                            </select>
                        </div>
                    </div>
                    <div class="filter-desc" style="font-size:9px!important;color:#9ca3af!important;margin-bottom:10px!important;">${currentMode === "whitelist" ? t("desc_whitelist") : t("desc_blacklist")}</div>
                    ${currentMode === "whitelist" ? `<div style="margin-bottom:8px!important;"><button class="form-btn add-novel-presets-btn" style="width:100%!important;background:rgba(59,130,246,0.1)!important;border:1px dashed #3b82f6!important;color:#60a5fa!important;border-radius:10px!important;padding:6px 10px!important;font-size:10px!important;cursor:pointer!important;">📚 ${t("add_novel_presets_btn")}</button></div>` : ""}
                    <div class="domain-list" style="overflow:visible!important;"></div>
                    <div class="form-group" style="display:flex!important;gap:6px!important;align-items:center!important;"><input type="text" class="form-input input-domain" placeholder="${currentMode === "whitelist" ? t("new_whitelist_placeholder") : t("new_blacklist_placeholder")}" style="flex:1!important;border-radius:10px!important;" /><button type="button" class="form-btn add-domain-btn btn-pill-primary" style="padding:10px 14px!important;border-radius:20px!important;border:none!important;cursor:pointer!important;">➕</button></div>
                `;

        const selectMode = subContent.querySelector(".select-filter-mode");
        selectMode.onchange = (e) => {
          core.saveFilterMode(e.target.value);
          panggilToast(t("toast_filter_mode", e.target.value), "info");
          core.jalankanPengganti(true);
          renderTampilan();
          core.simpanKeAwan();
        };

        const addPresetsBtn = subContent.querySelector(".add-novel-presets-btn");
        if (addPresetsBtn) {
          addPresetsBtn.onclick = () => {
            let list = core.getTargetDomains();
            let added = 0;
            NOVEL_SITE_PRESETS.forEach((site) => {
              if (!list.includes(site)) { list.push(site); added++; }
            });
            if (added > 0) {
              core.saveTargetDomains(list);
              core.simpanKeAwan(null, list);
              core.jalankanPengganti(true);
              renderTampilan();
              panggilToast(t("toast_novel_presets_added", added), "success");
            } else {
              panggilToast(t("toast_novel_presets_already"), "info");
            }
          };
        }

        const listContainer = subContent.querySelector(".domain-list"),
          listData = currentMode === "whitelist" ? whitelist : blacklist;
        if (listData.length === 0) {
          listContainer.innerHTML = `<div class="empty-state">${t("empty_state")}</div>`;
        } else {
          listData.forEach((dom) => {
            const row = document.createElement("div");
            row.className = "domain-item";
            row.innerHTML = `<span>${dom}</span><button class="awr-tooltip-btn modern-icon-btn danger del-dom-btn" data-tooltip="Delete Site">${core.SVGS.trash}</button>`;
            row.querySelector(".del-dom-btn").onclick = () => {
              window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
                "Hapus Domain Filter?",
                `Apakah Anda yakin ingin menghapus domain "${dom}"?`,
                () => {
                  if (currentMode === "whitelist") {
                    let list = core.getTargetDomains().filter((d) => d !== dom);
                    core.saveTargetDomains(list);
                    panggilToast(t("toast_whitelist_deleted", dom), "warn");
                    core.simpanKeAwan(null, list);
                  } else {
                    let list = core
                      .getBlacklistDomains()
                      .filter((d) => d !== dom);
                    core.saveBlacklistDomains(list);
                    panggilToast(t("toast_blacklist_deleted", dom), "warn");
                    core.simpanKeAwan();
                  }
                  core.jalankanPengganti(true);
                  renderTampilan();
                },
              );
            };
            listContainer.appendChild(row);
          });
        }

        subContent.querySelector(".add-domain-btn").onclick = (e) => {
          // Hentikan bubbling ke elemen lain di shadow DOM
          e.stopPropagation();
          e.preventDefault();

          // Re-query setiap klik agar tidak gunakan referensi DOM yang usang
          const inputEl = subContent.querySelector(".input-domain");
          if (!inputEl) return;

          // Bersihkan karakter tak terlihat (zero-width, non-breaking space, dll)
          const rawVal = (inputEl.value || "")
            .replace(/[\u200b\u200c\u200d\ufeff\u00a0\u2060]+/g, "")
            .trim()
            .toLowerCase();

          // UPDATE: Jika textbox kosong → otomatis gunakan domain halaman yang sedang dibuka
          const targetDomain = rawVal || core.getNovelBaseDomain(core.currentHost);

          if (!targetDomain) {
            panggilToast("Domain tidak terdeteksi. Buka halaman website terlebih dahulu.", "warn");
            return;
          }

          const normalizedText = core.getNovelBaseDomain(targetDomain);

          if (!normalizedText) {
            panggilToast("Domain tidak valid. Contoh: example.com", "warn");
            if (rawVal) inputEl.focus();
            return;
          }

          if (currentMode === "whitelist") {
            let list = core.getTargetDomains();
            if (list.some((d) => core.getNovelBaseDomain(d) === normalizedText)) {
              panggilToast(t("alert_already_registered"), "warn");
              return;
            }
            list.push(normalizedText);
            core.saveTargetDomains(list);
            core.simpanKeAwan(null, list);
            panggilToast(
              rawVal
                ? "\u2705 \"" + normalizedText + "\" ditambahkan ke whitelist."
                : "\u2705 Halaman ini (\"" + normalizedText + "\") diizinkan.",
              "success"
            );
          } else {
            let list = core.getBlacklistDomains();
            if (list.some((d) => core.getNovelBaseDomain(d) === normalizedText)) {
              panggilToast(t("alert_already_registered"), "warn");
              return;
            }
            list.push(normalizedText);
            core.saveBlacklistDomains(list);
            core.simpanKeAwan();
            panggilToast(
              rawVal
                ? "\ud83d\udeab \"" + normalizedText + "\" ditambahkan ke blacklist."
                : "\ud83d\udeab Halaman ini (\"" + normalizedText + "\") diblokir.",
              "warn"
            );
          }

          inputEl.value = "";
          core.jalankanPengganti(true);
          // Tunda sedikit render agar panel tidak berkedip saat status domain berubah
          setTimeout(() => { try { renderTampilan(); } catch(err) {} }, 50);
        };
      } else if (settingSubTab === "config") {
        subContent.innerHTML = `
                    <div class="setting-row">
                        <div class="setting-info"><span class="setting-title">${t("blue_highlight")}</span><span class="setting-desc">${t("blue_highlight_desc")}</span></div>
                        <label class="switch"><input type="checkbox" class="chk-highlight" ${core.getHighlightAktif() ? "checked" : ""} /><span class="slider"></span></label>
                    </div>
                    <div class="setting-row">
                        <div class="setting-info"><span class="setting-title">${t("recycle_auto_delete_title")}</span><span class="setting-desc">${t("recycle_auto_delete_desc")}</span></div>
                        <select class="form-input select-recycle-auto-delete" style="width:auto!important;padding:2px 6px!important;font-size:11px!important;background:#1a1d24!important;border-color:#272a34!important;border-radius:10px!important;color:white!important;height:auto!important;line-height:1!important;cursor:pointer!important;">
                            <option value="0" ${autoDeleteDays === 0 ? "selected" : ""}>${t("auto_delete_never")}</option>
                            <option value="7" ${autoDeleteDays === 7 ? "selected" : ""}>7 ${t("auto_delete_days")}</option>
                            <option value="30" ${autoDeleteDays === 30 ? "selected" : ""}>30 ${t("auto_delete_days")}</option>
                            <option value="90" ${autoDeleteDays === 90 ? "selected" : ""}>90 ${t("auto_delete_days")}</option>
                            <option value="365" ${autoDeleteDays === 365 ? "selected" : ""}>1 ${t("auto_delete_year")}</option>
                        </select>
                    </div>
                    <div class="setting-row" style="margin-top:20px;border-top:1px dashed #334155;padding-top:15px;overflow:visible!important;">
                        <div class="setting-info"><span class="setting-title" style="color:#ef4444;">${t("restore_defaults")}</span><span class="setting-desc">${t("restore_desc")}</span></div>
                        <button class="form-btn reset-config-btn btn-pill-danger" style="padding:5px 12px!important;border-radius:20px!important;">${t("reset_data")}</button>
                    </div>
                    <div class="setting-row" style="margin-top:20px;border-top:1px dashed #334155;padding-top:15px;user-select:none;">
                        <div class="setting-info">
                            <span class="setting-title">${t("awr_version_label")}</span>
                            <span class="setting-desc" style="font-family:monospace;font-weight:bold;color:#10b981;">v${core.version} (Stable)</span>
                        </div>
                    </div>
                    <div class="setting-row">
                        <div class="setting-info"><span class="setting-title">${t("update_mode_label")}</span><span class="setting-desc">${t("update_mode_desc")}</span></div>
                        <select class="form-input select-update-mode" style="width:auto!important;padding:2px 6px!important;font-size:11px!important;background:#1a1d24!important;border-color:#272a34!important;border-radius:10px!important;color:white!important;height:auto!important;line-height:1!important;cursor:pointer!important;">
                            <option value="auto" ${updateMode === "auto" ? "selected" : ""}>${t("update_mode_auto")}</option><option value="manual" ${updateMode === "manual" ? "selected" : ""}>${t("update_mode_manual")}</option>
                        </select>
                    </div>
                    <div style="margin-top:8px;display:flex;justify-content:flex-end;"><button class="form-btn check-update-now-btn btn-pill-primary" style="padding:5px 12px!important;font-size:10px!important;border-radius:12px!important;display:inline-flex;align-items:center;justify-content:center;gap:6px;">${core.SVGS.refresh} ${t("btn_check_update")}</button></div>
                `;

        subContent.querySelector(".chk-highlight").onchange = (e) => {
          core.saveHighlightAktif(e.target.checked);
          panggilToast(
            e.target.checked ? t("highlight_enabled") : t("highlight_disabled"),
            "info",
          );
          core.jalankanPengganti(true);
        };

        const selectAutoDelete = subContent.querySelector(
          ".select-recycle-auto-delete",
        );
        if (selectAutoDelete) {
          selectAutoDelete.onchange = (e) => {
            const val = e.target.value;
            GM_setValue("awr_recycle_auto_delete_days", val);
            panggilToast(
              t(
                "toast_auto_delete_changed",
                val === "0"
                  ? t("auto_delete_never")
                  : val + " " + t("auto_delete_days"),
              ),
              "success",
            );
          };
        }

        const selectUpdateMode = subContent.querySelector(
          ".select-update-mode",
        );
        if (selectUpdateMode) {
          selectUpdateMode.onchange = (e) => {
            GM_setValue("awr_update_mode_v1", e.target.value);
            panggilToast(
              t(
                "toast_filter_mode",
                e.target.value === "auto"
                  ? t("update_mode_auto")
                  : t("update_mode_manual"),
              ),
              "info",
            );
          };
        }

        subContent.querySelector(".check-update-now-btn").onclick = (e) => {
          e.stopPropagation();
          core.checkForUpdates(true);
        };

        subContent.querySelector(".reset-config-btn").onclick = () => {
          window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
            t("restore_defaults"),
            "⚠️ RESET LENGKAP akan menghapus SEMUA data lokal:\n\n• Semua kata kustom dan grup novel\n• Semua pengaturan domain (filter/whitelist/blacklist)\n• Semua preferensi tampilan & konfigurasi\n\nBackup cloud GitHub Gist Anda TIDAK akan terpengaruh dan kredensial login tetap tersimpan.\n\nLanjutkan?",
            () => {
              // Hapus semua kunci GM lokal kecuali kredensial cloud
              const ALL_LOCAL_KEYS = [
                "kamus_kata_v5", "kamus_kata_v4",
                "target_domains_v4", "blacklist_domains_v1", "filter_mode_v1",
                "highlight_aktif_v4", "awr_deleted_words_v1",
                "awr_novel_titles_v2", "awr_active_novel_id_v1",
                "awr_lang_v1", "awr_show_other_terms",
                "awr_last_selected_group_id_v2", "awr_last_active_group_checkbox_state_v2",
                "awr_recycle_auto_delete_days", "awr_update_mode_v1",
              ];
              ALL_LOCAL_KEYS.forEach(k => GM_setValue(k, null));

              // Terapkan nilai default setelah reset
              const defaultKamus = {
                silahkan: { to: "silakan", global: true, domain: "wikipedia.org" },
                wikipedia: { to: "Ensiklopedia Bebas", global: true, domain: "wikipedia.org" },
                salah: { to: "keliru", global: true, domain: "detik.com" },
              };
              GM_setValue("kamus_kata_v5", JSON.stringify(defaultKamus));
              GM_setValue("target_domains_v4", JSON.stringify([
                "wtr-lab.com","webnovel.com","novelbin.com","novelbin.net",
                "novelupdates.com","wuxiaworld.com","royalroad.com","scribblehub.com",
                "lightnovelworld.com","mtlnovel.com","novelhall.com","boxnovel.com",
                "readlightnovel.me","noveltranslate.com","readnovelfull.com",
                "zinnovel.com","novelsonline.net","fanfiction.net",
                "archiveofourown.org","wattpad.com"
              ]));
              GM_setValue("blacklist_domains_v1", JSON.stringify(["google.com", "facebook.com", "youtube.com"]));
              GM_setValue("filter_mode_v1", "whitelist");
              GM_setValue("highlight_aktif_v4", true);
              GM_setValue("awr_deleted_words_v1", "{}");
              GM_setValue("awr_novel_titles_v2", "{}");
              GM_setValue("awr_update_mode_v1", "auto");
              GM_setValue("awr_recycle_auto_delete_days", "30");

              // Reset variabel in-memory
              _activeNovelIdCache = null;
              showOtherTerms = false;

              panggilToast("✅ Reset lengkap selesai! Semua pengaturan lokal telah dikembalikan ke default. Backup cloud tidak terpengaruh.", "info");
              core.jalankanPengganti(true);
              renderTampilan();
            },
          );
        };
      } else if (settingSubTab === "data") {
        // v52.5: Export/Import per-novel rules
        const activeNovelId = core.getActiveNovelId();
        const currentNovel = core.getNovelContext();
        const novelLabel = currentNovel.title || currentNovel.id || "novel ini";

        subContent.innerHTML = `
          <div style="font-size:11px;font-weight:bold;color:#9ca3af;margin-bottom:10px;border-bottom:1px solid #272a34;padding-bottom:6px;">📦 ${t("tab_data")}</div>

          <div style="background:#1a1d24;border-radius:10px;padding:10px 12px;margin-bottom:8px;border:1px solid #272a34;">
            <div style="font-size:11px;font-weight:bold;color:#e2e8f0;margin-bottom:4px;">⬇️ ${t("export_novel_btn")}</div>
            <div style="font-size:9px;color:#6b7280;margin-bottom:8px;">${t("export_novel_desc")}</div>
            <div style="font-size:9px;color:#94a3b8;margin-bottom:6px;">Novel aktif: <b style="color:#60a5fa;">${novelLabel}</b>${activeNovelId ? ` <code style="background:#0f1117;padding:1px 4px;border-radius:3px;font-size:8px;">${activeNovelId.slice(0,16)}...</code>` : " (global)"}</div>
            <button class="form-btn awr-export-novel-btn btn-pill-primary" style="width:100%!important;padding:7px!important;font-size:10px!important;border-radius:10px!important;">⬇️ ${t("export_novel_btn")}</button>
          </div>

          <div style="background:#1a1d24;border-radius:10px;padding:10px 12px;border:1px solid #272a34;">
            <div style="font-size:11px;font-weight:bold;color:#e2e8f0;margin-bottom:4px;">⬆️ ${t("import_novel_btn")}</div>
            <div style="font-size:9px;color:#6b7280;margin-bottom:8px;">${t("import_novel_desc")}</div>
            <input type="file" class="awr-import-file-input" accept=".json,application/json" style="display:none;" />
            <button class="form-btn awr-import-novel-btn" style="width:100%!important;padding:7px!important;font-size:10px!important;border-radius:10px!important;background:rgba(234,179,8,0.1)!important;color:#fbbf24!important;border-color:rgba(234,179,8,0.3)!important;">⬆️ ${t("import_novel_btn")}</button>
          </div>
        `;

        // Export handler
        subContent.querySelector(".awr-export-novel-btn").onclick = () => {
          const kamus = core.getKamus();
          const toExport = {};
          for (const key in kamus) {
            const entry = kamus[key];
            const entryNovelId = entry && typeof entry === "object" ? entry.novelId : null;
            const isGlobal = entry && typeof entry === "object" ? !!entry.global : false;
            // Include: entries matching active novel OR global entries (if no active novel, export all)
            if (!activeNovelId) {
              toExport[key] = entry;
            } else if (entryNovelId === activeNovelId || key.startsWith("hash:")) {
              toExport[key] = entry;
            }
          }
          const count = Object.keys(toExport).length;
          if (count === 0) { panggilToast(t("export_none"), "warn"); return; }

          const payload = JSON.stringify({ novelId: activeNovelId, novelTitle: novelLabel, exportedAt: new Date().toISOString(), entries: toExport }, null, 2);
          const blob = new Blob([payload], { type: "application/json" });
          const url = URL.createObjectURL(blob);
          const a = document.createElement("a");
          a.href = url;
          a.download = "awr_" + (novelLabel.replace(/[^a-zA-Z0-9]+/g, "_").slice(0, 30) || "novel") + "_rules.json";
          a.click();
          URL.revokeObjectURL(url);
          panggilToast(t("export_success", count, novelLabel), "success");
        };

        // Import handler
        const fileInput = subContent.querySelector(".awr-import-file-input");
        subContent.querySelector(".awr-import-novel-btn").onclick = () => fileInput.click();
        fileInput.onchange = (e) => {
          const file = e.target.files[0];
          if (!file) return;
          const reader = new FileReader();
          reader.onload = (ev) => {
            try {
              const data = JSON.parse(ev.target.result);
              const imported = data.entries || data; // support both wrapped and raw
              if (typeof imported !== "object" || imported === null) throw new Error("invalid");
              const kamus = core.getKamus();
              let added = 0, skipped = 0;
              for (const key in imported) {
                if (kamus[key]) { skipped++; continue; }
                // If entry has no novelId and activeNovelId exists, assign it
                let entry = imported[key];
                if (activeNovelId && entry && typeof entry === "object" && !entry.novelId && !entry.global) {
                  entry = Object.assign({}, entry, { novelId: activeNovelId });
                }
                kamus[key] = entry;
                added++;
              }
              core.saveKamus(kamus);
              core.jalankanPengganti(true);
              core.simpanKeAwan();
              const msg = skipped > 0 ? t("import_duplicate", skipped, added) : t("import_success", added);
              panggilToast(msg, "success");
              fileInput.value = "";
            } catch (err) {
              panggilToast(t("import_error"), "error");
              fileInput.value = "";
            }
          };
          reader.readAsText(file);
        };
      } else if (settingSubTab === "cloud") {
        const token = core.getGitHubToken(),
          gistId = core.getGistId();
        if (!token || !gistId) {
          subContent.innerHTML = `
                        <div class="flex-col" style="gap:10px!important;overflow:visible!important;">
                            <div style="font-size:11px;color:#9ca3af;line-height:1.4;">Hubungkan ke akun GitHub pribadi Anda untuk menyimpan cadangan kata secara aman dan gratis di **Private Gist** milik Anda sendiri.</div>
                            <div class="flex-col" style="margin:4px 0 8px 0;gap:6px!important;"><a href="https://github.com/settings/tokens/new?scopes=gist&description=AWR-Replacer-Sync-Token" target="_blank" style="color:#60a5fa!important;font-size:11px!important;text-decoration:underline!important;font-weight:bold!important;">${t("auth_token_classic_link")}</a><a href="https://github.com/settings/tokens" target="_blank" style="color:#34d399!important;font-size:11px!important;text-decoration:underline!important;font-weight:bold!important;">${t("auth_token_manage_link")}</a></div>
                            <div class="editor-section" style="overflow:visible!important;"><span class="editor-label">${t("auth_github_token_label")}</span><div class="input-wrapper-box" style="overflow:visible!important;"><input type="password" class="form-input txt-github-token" placeholder="ghp_xxxxxxxxxxxxxxxxxxxx" style="border:none!important;background:transparent!important;outline:none!important;width:100%!important;border-radius:10px!important;padding-right:35px!important;box-sizing:border-box!important;" /><button class="toggle-token-visibility-btn" style="position:absolute!important;right:8px!important;background:none!important;border:none!important;color:#9ca3af!important;cursor:pointer!important;font-size:14px!important;padding:0!important;display:flex!important;align-items:center!important;outline:none!important;">👁️</button></div></div>
                            <div class="editor-section" style="margin-top:8px;overflow:visible!important;"><div style="display:flex!important;justify-content:space-between!important;align-items:center!important;margin-bottom:4px!important;overflow:visible!important;"><span class="editor-label" style="margin:0!important;">${t("auth_gist_id_label")}</span><button class="awr-tooltip-btn modern-icon-btn btn-auto-detect-gist" data-tooltip="${t("btn_auto_detect_gist")}" style="padding:3px!important;border-radius:6px!important;display:none;background:transparent!important;">${core.SVGS.radar}</button></div><input type="text" class="form-input txt-gist-id" placeholder="Biarkan kosong jika ingin membuat baru" style="border-radius:10px!important;" value="${gistId}" /></div>
                            <div style="margin-top:15px;display:flex!important;justify-content:space-between!important;align-items:center!important;overflow:visible!important;gap:6px;">
                                <input type="file" class="file-import-creds" style="display:none;" accept=".json,.txt,.js" />
                                <div style="display:flex;gap:6px;">
                                    <button class="awr-tooltip-btn modern-icon-btn btn-import-creds-trigger" data-tooltip="${t("btn_import_creds_trigger")}">${core.SVGS.folder}</button>
                                    <button class="awr-tooltip-btn modern-icon-btn btn-backup-creds-trigger" data-tooltip="${t("btn_backup_creds_file")}">${core.SVGS.disk}</button>
                                </div>
                                <button class="form-btn btn-connect-github-action" style="background:#10b981!important;border-radius:20px!important;color:white!important;">${t("auth_connect_btn")}</button>
                            </div>
                            <div style="margin-top:12px;border-top:1px dashed #334155!important;padding-top:10px!important;display:flex!important;flex-direction:column!important;gap:6px!important;overflow:visible!important;"><span class="editor-label">🔑 Helper & Recovery:</span><div style="display:flex!important;gap:6px!important;overflow:visible!important;"><button class="awr-tooltip-btn modern-icon-btn btn-forgot-gist-link" data-tooltip="${t("btn_forgot_gist_link")}" style="flex:1!important;">${core.SVGS.link}</button></div></div>
                        </div>
                    `;

          const tokenInput = subContent.querySelector(".txt-github-token"),
            gistInput = subContent.querySelector(".txt-gist-id"),
            visibilityBtn = subContent.querySelector(
              ".toggle-token-visibility-btn",
            ),
            autoDetectBtn = subContent.querySelector(".btn-auto-detect-gist"),
            fileInput = subContent.querySelector(".file-import-creds"),
            importTrigger = subContent.querySelector(
              ".btn-import-creds-trigger",
            ),
            forgotGistBtn = subContent.querySelector(".btn-forgot-gist-link"),
            backupTrigger = subContent.querySelector(
              ".btn-backup-creds-trigger",
            );

          visibilityBtn.onclick = (e) => {
            e.stopPropagation();
            const isPass = tokenInput.type === "password";
            tokenInput.type = isPass ? "text" : "password";
            visibilityBtn.textContent = isPass ? "🙈" : "👁️";
          };

          const updateAutoDetectVisibility = () => {
            autoDetectBtn.style.display =
              tokenInput.value.trim().length >= 35 ? "block" : "none";
          };
          tokenInput.addEventListener("input", updateAutoDetectVisibility);
          updateAutoDetectVisibility();

          autoDetectBtn.onclick = async (e) => {
            e.preventDefault();
            const tokenVal = tokenInput.value.trim();
            if (!tokenVal) return;
            panggilToast(t("gist_scan_toast"), "info");
            const profile = await verifyGitHubToken(tokenVal);
            if (!profile) {
              panggilToast(
                "Token verification failed! Please verify token has gist permissions.",
                "warn",
              );
              return;
            }
            cachedUserProfile = profile;
            const foundId = await core.findExistingGist(tokenVal);
            if (foundId) {
              gistInput.value = foundId;
              panggilToast(t("gist_scan_found"), "success");
            } else {
              panggilToast(t("gist_scan_not_found"), "warn");
            }
          };

          importTrigger.onclick = (e) => {
            e.preventDefault();
            fileInput.click();
          };
          fileInput.onchange = (e) => {
            const file = e.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = (evt) => {
              try {
                const teks = evt.target.result;
                let importToken = "", importGistId = "";

                // Coba parse sebagai JSON dulu
                try {
                  const creds = JSON.parse(teks);
                  if (creds.token) importToken = creds.token;
                  if (creds.gistId) importGistId = creds.gistId;
                } catch (_) {
                  // Parse sebagai file .js / .txt format AWR
                  const matchToken = teks.match(/var\s+AWR_TOKEN\s*=\s*["\']([^\"\']*)["\'];?/);
                  const matchGist  = teks.match(/var\s+AWR_GIST_ID\s*=\s*["\']([^\"\']*)["\'];?/);
                  if (matchToken) importToken   = matchToken[1];
                  if (matchGist)  importGistId  = matchGist[1];
                }

                if (importToken) {
                  tokenInput.value = importToken;
                  updateAutoDetectVisibility();
                }
                if (importGistId) gistInput.value = importGistId;

                if (importToken || importGistId) {
                  panggilToast(t("toast_creds_imported"), "success");
                } else {
                  alert("File kredensial tidak valid atau format tidak dikenali!");
                }
              } catch (err) {
                alert("Gagal membaca file kredensial!");
              }
            };
            reader.readAsText(file);
          };

          if (backupTrigger) {
            backupTrigger.onclick = (e) => {
              e.preventDefault();
              core.exportCredentials();
              panggilToast(t("toast_creds_exported"), "success");
            };
          }

          forgotGistBtn.onclick = (e) => {
            e.preventDefault();
            window.open("https://gist.github.com/", "_blank");
          };

          subContent.querySelector(".btn-connect-github-action").onclick =
            async (e) => {
              const btn = e.currentTarget,
                originalText = btn.textContent,
                inputToken = tokenInput.value.trim();
              let inputGistId = gistInput.value.trim();

              if (!inputToken) {
                alert("Silakan masukkan Token Akses GitHub terlebih dahulu!");
                return;
              }
              btn.disabled = true;
              btn.textContent = t("auth_connecting_btn");
              btn.style.opacity = "0.7";
              panggilToast(t("toast_sync_connecting"), "info");

              const profile = await verifyGitHubToken(inputToken);
              if (!profile) {
                alert(t("auth_conn_fail_reason"));
                btn.disabled = false;
                btn.textContent = originalText;
                btn.style.opacity = "1";
                return;
              }
              cachedUserProfile = profile;

              if (!inputGistId) {
                const discoveredGistId = await findExistingGist(inputToken);
                if (discoveredGistId) {
                  inputGistId = discoveredGistId;
                  panggilToast(t("gist_scan_found"), "success");
                }
              } else {
                inputGistId = extractGistId(inputGistId);
              }

              try {
                let details = null;
                if (inputGistId) {
                  details = await core.fetchGistDetails(
                    inputToken,
                    inputGistId,
                  );
                  if (!details) {
                    alert(t("gist_err_invalid"));
                    btn.disabled = false;
                    btn.textContent = originalText;
                    btn.style.opacity = "1";
                    return;
                  }
                } else {
                  const payload = {
                    kamus: core.getKamus(),
                    domains: core.getTargetDomains(),
                    blacklist: core.getBlacklistDomains(),
                    filterMode: core.getFilterMode(),
                    novelTitles: JSON.parse(
                      GM_getValue("awr_novel_titles_v2", "{}"),
                    ),
                    deletedWords: core.getDeletedWords(),
                  };
                  const newGistId = await core.createGist(inputToken, payload);
                  if (newGistId) {
                    inputGistId = newGistId;
                    details = await core.fetchGistDetails(
                      inputToken,
                      inputGistId,
                    );
                  } else {
                    alert("Gagal membuat Gist baru.");
                    btn.disabled = false;
                    btn.textContent = originalText;
                    btn.style.opacity = "1";
                    return;
                  }
                }

                cachedGistDetails = details;
                core.saveGitHubCredentials(inputToken, inputGistId);
                panggilToast(t("toast_github_connected"), "success");
                if (pane && pane.parentNode)
                  renderCloudGistManager(pane, details);
              } catch (err) {
                alert(t("auth_conn_fail_reason"));
                btn.disabled = false;
                btn.textContent = originalText;
                btn.style.opacity = "1";
              }
            };
        } else {
          if (cachedGistDetails && cachedUserProfile) {
            renderCloudGistManager(subContent, cachedGistDetails);
          } else {
            subContent.innerHTML = `<div class="cloud-loading-spinner" style="display:flex!important;flex-direction:column!important;align-items:center!important;justify-content:center!important;padding:30px!important;gap:10px!important;"><span style="font-size:24px!important;animation:spin 1s linear infinite!important;display:inline-block!important;">⏳</span><span style="font-size:11px!important;color:#9ca3af!important;">${t("loading_cloud_data")}</span></div>`;
            Promise.all([
              verifyGitHubToken(token),
              core.fetchGistDetails(token, gistId),
            ])
              .then(([profile, details]) => {
                if (profile && details) {
                  cachedUserProfile = profile;
                  cachedGistDetails = details;
                  if (pane && pane.parentNode) {
                    renderCloudGistManager(subContent, details);
                  }
                } else {
                  throw new Error("Verification failed");
                }
              })
              .catch(() => {
                pane.innerHTML = `<div style="color:#ef4444!important;font-size:11px!important;text-align:center!important;padding:20px!important;line-height:1.4!important;display:flex!important;flex-direction:column!important;gap:10px!important;">${t("auth_cloud_err_msg")}<div><button class="form-btn btn-force-switch btn-pill-danger" style="padding: 5px 14px !important;">Logout</button></div></div>`;
                pane.querySelector(".btn-force-switch").onclick = () => {
                  core.saveGitHubCredentials("", "");
                  cachedGistDetails = null;
                  cachedUserProfile = null;
                  panggilToast(t("toast_account_switched"), "info");
                  renderTampilan();
                };
              });
          }
        }
      }
    }
  }

  window.AWR_UI_LIBRARY = {
    tampilkanKonfirmasi: function (
      judul,
      deskripsi,
      onConfirm,
      onCancel = null,
      options = {},
    ) {

      if (!panel || !panel.querySelector) {
        console.warn(
          "AWR_UI_LIBRARY.tampilkanKonfirmasi skipped: panel is not ready",
        );
        return;
      }
      const existing = panel.querySelector(".replacer-confirm-overlay");
      if (existing) existing.remove();

      const confirmLabel = options.confirmLabel || t("btn_confirm");
      const cancelLabel = options.cancelLabel || t("btn_cancel");
      const confirmColor = options.confirmColor || "#ef4444";

      const overlay = document.createElement("div");
      overlay.className = "replacer-confirm-overlay";
      overlay.innerHTML = `
                <div class="replacer-confirm-box">
                    <div class="replacer-confirm-header">${judul}</div>
                    <div class="replacer-confirm-body" style="white-space: pre-line !important;">${deskripsi}</div>
                    <div class="replacer-confirm-footer">
                        <button class="form-btn confirm-cancel-btn">${cancelLabel}</button>
                        <button class="form-btn btn-pill-primary confirm-ok-btn" style="background: ${confirmColor} !important; border-color: ${confirmColor} !important;">${confirmLabel}</button>
                    </div>
                </div>
            `;
      overlay.querySelector(".confirm-cancel-btn").onclick = (e) => {
        e.stopPropagation();
        overlay.remove();
        if (onCancel) onCancel();
      };
      overlay.querySelector(".confirm-ok-btn").onclick = (e) => {
        e.stopPropagation();
        overlay.remove();
        onConfirm();
      };
      panel.appendChild(overlay);
    },

    init: function (coreAPI) {
      core = coreAPI;

      const hostElement = document.createElement("div");
      hostElement.id = "word-replacer-host";
      hostElement.setAttribute(
        "style",
        "position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; z-index: 999999999 !important; pointer-events: none !important;",
      );
      document.body.appendChild(hostElement);

      const shadow = hostElement.attachShadow({ mode: "open" });

      ["keydown", "keyup", "keypress"].forEach((eventType) => {
        shadow.addEventListener(eventType, (e) => { e.stopPropagation(); });
      });

      ["mousedown", "mouseup", "click", "dblclick", "pointerdown", "pointerup",
       "contextmenu"].forEach((eventType) => {
        shadow.addEventListener(eventType, (e) => { e.stopPropagation(); });
      });

      ["focusin", "focusout"].forEach((eventType) => {
        shadow.addEventListener(eventType, (e) => { e.stopPropagation(); });
      });

      shadow.addEventListener("click", (e) => {
        const langDropdown = shadow.querySelector(".lang-dropdown-menu");
        if (
          langDropdown &&
          langDropdown.classList.contains("show") &&
          !e.target.closest(".lang-dropdown-container")
        ) {
          langDropdown.classList.remove("show");
        }
        const groupMenus = shadow.querySelectorAll(".group-dropdown-menu");
        groupMenus.forEach((menu) => {
          if (menu.classList.contains("show")) {
            const container = menu.closest(".group-menu-container");
            if (!container || !container.contains(e.target))
              menu.classList.remove("show");
          }
        });
      });

      const style = document.createElement("style");
      style.textContent = `
                .replacer-wrapper { font-family: system-ui, sans-serif !important; color: #f1f5f9 !important; overflow: visible !important; pointer-events: none !important; }
                .replacer-panel { position: fixed !important; bottom: 0 !important; left: 0 !important; z-index: 999999999 !important; width: 380px !important; height: 60vh !important; background: #0f172a !important; border: 1px solid #334155 !important; border-radius: 16px 16px 0 0 !important; box-shadow: 0 -10px 25px rgba(0,0,0,.6) !important; display: flex !important; flex-direction: column !important; overflow: visible !important; transition: transform .3s ease !important; pointer-events: auto !important; }
                .replacer-panel.hidden { transform: translateX(-100%) !important; pointer-events: none !important; }
                .replacer-launcher { position: fixed !important; bottom: 24px !important; left: 24px !important; z-index: 100000000 !important; background: #1e293b !important; color: #f8fafc !important; border: 1px solid #475569 !important; border-radius: 9999px !important; padding: 10px 18px !important; font-size: 13px !important; font-weight: 700 !important; cursor: pointer !important; box-shadow: 0 10px 15px -3px rgba(0,0,0,.4) !important; display: flex !important; align-items: center !important; gap: 8px !important; pointer-events: auto !important; }
                .replacer-header { background: #1e293b !important; color: #f8fafc !important; padding: 14px 16px !important; display: flex !important; flex-direction: column !important; border-bottom: 1px solid #334155 !important; }
                .replacer-title { font-size: 15px !important; font-weight: 800 !important; color: #f8fafc !important; }
                .replacer-close { background: none !important; border: none !important; color: #94a3b8 !important; font-size: 18px !important; cursor: pointer !important; display: flex !important; align-items: center !important; }
                .replacer-host-status { background: #0f172a !important; border: 1px solid #334155 !important; border-radius: 12px !important; padding: 10px 14px !important; margin-top: 10px !important; display: flex !important; justify-content: space-between !important; align-items: center !important; gap: 12px !important; font-size: 11px !important; }
                .host-info { display: flex !important; flex-direction: column !important; gap: 2px !important; min-width: 0 !important; flex: 1 !important; }
                .host-label { color: #64748b !important; font-size: 9px !important; font-weight: 800 !important; text-transform: uppercase !important; }
                .host-name { font-family: monospace !important; font-weight: 700 !important; color: #cbd5e1 !important; font-size: 11px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; }
                .status-actions { display: flex !important; align-items: center !important; gap: 8px !important; }
                .status-badge { padding: 4px 10px !important; border-radius: 9999px !important; font-size: 10px !important; font-weight: 700 !important; display: inline-flex !important; align-items: center !important; gap: 6px !important; }
                .status-badge.active { background: rgba(16,185,129,0.1) !important; color: #34d399 !important; border: 1px solid rgba(16,185,129,0.3) !important; }
                .status-badge.inactive { background: rgba(239,68,68,0.1) !important; color: #f87171 !important; border: 1px solid rgba(239,68,68,0.3) !important; }
                .status-indicator { width: 6px !important; height: 6px !important; border-radius: 50% !important; display: inline-block !important; }
                .status-indicator.active { background-color: #10b981 !important; }
                .status-indicator.inactive { background-color: #ef4444 !important; }
                .toggle-btn { background: #1e293b !important; border: 1px solid #475569 !important; color: #cbd5e1 !important; padding: 5px 12px !important; border-radius: 8px !important; font-size: 11px !important; font-weight: 600 !important; cursor: pointer !important; }
                .replacer-tabs { background: #1e293b !important; border-bottom: 1px solid #334151 !important; display: flex !important; padding: 0 16px !important; gap: 16px !important; overflow: visible !important; }
                .replacer-tab-btn { background: none !important; border: none !important; padding: 12px 10px !important; color: #94a3b8 !important; cursor: pointer !important; position: relative !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; flex: 1 !important; transition: color 0.2s ease !important; border-bottom: 2.5px solid transparent !important; }
                .replacer-tab-btn.active { color: #3b82f6 !important; border-bottom-color: #3b82f6 !important; }
                .sub-tab-btn.active { background: #2563eb !important; border-color: #3b82f6 !important; color: #ffffff !important; }
                .awr-tooltip-btn { position: relative !important; }
                .awr-tooltip-btn::after { content: attr(data-tooltip) !important; position: absolute !important; bottom: 125% !important; left: 50% !important; transform: translateX(-50%) translateY(5px) !important; background: #1e293b !important; color: #f1f5f9 !important; padding: 5px 10px !important; border-radius: 6px !important; font-size: 10px !important; font-weight: 500 !important; white-space: nowrap !important; opacity: 0 !important; visibility: hidden !important; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important; box-shadow: 0 4px 12px rgba(0,0,0,0.5) !important; border: 1px solid #334155 !important; pointer-events: none !important; z-index: 100000040 !important; }
                .awr-tooltip-btn:hover::after { opacity: 1 !important; visibility: visible !important; transform: translateX(-50%) translateY(0) !important; }
                .awr-tooltip-btn.tooltip-bottom::after { top: 125% !important; bottom: auto !important; transform: translateX(-50%) translateY(-5px) !important; }
                .awr-tooltip-btn.tooltip-bottom:hover::after { transform: translateX(-50%) translateY(0) !important; }
                .replacer-body { flex: 1 !important; overflow-y: auto !important; background: #0f172a !important; display: flex !important; flex-direction: column !important; padding-bottom: 20px !important; }
                .tab-pane { display: none !important; padding: 16px !important; flex-direction: column !important; flex: 1 !important; overflow-y: auto !important; max-height: calc(60vh - 150px) !important; }
                .tab-pane.active { display: flex !important; }
                .card-box { background: #1e293b !important; border: 1px solid #334155 !important; border-radius: 10px !important; padding: 10px 12px !important; margin-bottom: 8px !important; overflow: visible !important; }
                .editor-label { font-size: 12px !important; font-weight: 600 !important; color: #94a3b8 !important; }
                .form-input { width: 100% !important; box-sizing: border-box !important; padding: 8px 12px !important; font-size: 13px !important; border: 1px solid #475569 !important; border-radius: 8px !important; background: #0f172a !important; color: #ffffff !important; }
                .select.form-input { appearance: none !important; background-image: url("data:image/svg+xml;utf8,<svg fill='%2394a3b8' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/><path d='M0 0h24v24H0z' fill='none'/></svg>") !important; background-repeat: no-repeat !important; background-position: right 10px center !important; background-size: 18px !important; padding-right: 32px !important; cursor: pointer !important; }
                .form-btn { background: #1e293b !important; border: 1px solid #475569 !important; color: #e2e8f0 !important; border-radius: 20px !important; padding: 8px 16px !important; font-size: 12px !important; font-weight: 600 !important; cursor: pointer !important; }
                .form-btn:hover { background: #334155 !important; border-color: #3b82f6 !important; color: #ffffff !important; }
                .btn-pill-primary { background: #2563eb !important; border: 1px solid #3b82f6 !important; color: #ffffff !important; border-radius: 20px !important; padding: 8px 16px !important; font-size: 12px !important; font-weight: 700 !important; cursor: pointer !important; }
                .word-item { display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 10px 12px !important; border-bottom: 1px solid #334155 !important; }
                .word-item:last-child { border-bottom: none !important; }
                .word-pair { display: flex !important; flex-direction: column !important; min-width: 0 !important; flex: 1 !important; gap: 2px !important; }
                .word-pair span { font-size: 12px !important; }
                .flex-col { display: flex !important; flex-direction: column !important; }
                .flex-row { display: flex !important; align-items: center !important; }
                .flex-between { display: flex !important; justify-content: space-between !important; align-items: center !important; gap: 8px !important; overflow: visible !important; }
                .input-wrapper-box { position: relative !important; display: flex !important; align-items: center !important; background: #0f172a !important; border: 1px solid #475569 !important; border-radius: 8px !important; box-sizing: border-box !important; overflow: visible !important; }
                .lang-dropdown-container { position: relative !important; display: inline-block; }
                .active-lang-btn { background: #1e293b !important; border: 1px solid #475569 !important; color: #f8fafc !important; padding: 4px 8px !important; border-radius: 8px !important; font-size: 11px !important; cursor: pointer !important; display: flex !important; align-items: center !important; gap: 4px !important; }
                .lang-dropdown-menu { display: none !important; position: absolute !important; right: 0 !important; top: 110% !important; margin-top: 4px !important; background: #1e293b !important; border: 1px solid #334155 !important; border-radius: 8px !important; box-shadow: 0 10px 25px rgba(0,0,0,0.5) !important; z-index: 100000002 !important; min-width: 120px !important; }
                .lang-dropdown-menu.show { display: block !important; }
                .lang-dropdown-item { width: 100% !important; background: none !important; border: none !important; color: #cbd5e1 !important; padding: 8px 12px !important; font-size: 11px !important; text-align: left !important; cursor: pointer !important; }
                .group-menu-container { position: relative !important; display: inline-flex !important; align-items: center !important; z-index: 100 !important; }
                .group-menu-btn { background: none !important; border: none !important; color: #94a3b8 !important; font-size: 14px !important; cursor: pointer !important; padding: 4px 6px !important; display: flex !important; align-items: center !important; justify-content: center !important; }
                .group-dropdown-menu { display: none !important; position: absolute !important; left: 0 !important; top: 100% !important; margin-top: 4px !important; background: #1e293b !important; border: 1px solid #334155 !important; border-radius: 8px !important; box-shadow: 0 10px 20px rgba(0,0,0,0.5) !important; z-index: 100000002 !important; min-width: 170px !important; }
                .group-dropdown-menu.show { display: block !important; }
                .group-dropdown-item { width: 100% !important; background: none !important; border: none !important; color: #cbd5e1 !important; padding: 8px 12px !important; font-size: 11px !important; text-align: left !important; cursor: pointer !important; }
                .group-dropdown-item.danger { color: #f87171 !important; }
                .switch { position: relative !important; display: inline-block !important; width: 36px !important; height: 20px !important; flex-shrink: 0 !important; }
                .switch input { opacity: 0 !important; width: 0 !important; height: 0 !important; }
                .slider { position: absolute !important; cursor: pointer !important; top: 0 !important; left: 0 !important; right: 0 !important; bottom: 0 !important; background-color: #334155 !important; transition: .3s !important; border-radius: 20px !important; }
                .slider::before { position: absolute !important; content: "" !important; height: 14px !important; width: 14px !important; left: 3px !important; bottom: 3px !important; background-color: white !important; transition: .3s !important; border-radius: 50% !important; }
                input:checked+.slider { background-color: #2563eb !important; }
                input:checked+.slider::before { transform: translateX(16px) !important; }
                .setting-row { display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 8px 0 !important; gap: 12px !important; overflow: visible !important; }
                .setting-info { display: flex !important; flex-direction: column !important; flex: 1 !important; }
                .setting-title { font-size: 12px !important; font-weight: 600 !important; color: #ffffff !important; }
                .setting-desc { font-size: 10px !important; color: #94a3b8 !important; }
                .modern-icon-btn { background: #1e293b !important; border: 1px solid #334155 !important; color: #94a3b8 !important; cursor: pointer !important; padding: 6px !important; border-radius: 8px !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; }
                .modern-icon-btn:hover { background: #334155 !important; color: #ffffff !important; border-color: #3b82f6 !important; }
                .modern-icon-btn.danger:hover { background: rgba(239, 68, 68, 0.1) !important; color: #f87171 !important; border-color: #ef4444 !important; }
                .editor-section { display: flex; flex-direction: column; gap: 4px; }
                .editor-label-row { display: flex; justify-content: space-between; align-items: center; }
                .editor-pills { display: flex; gap: 4px; }
                .editor-pill { background: #1e293b; border: 1px solid #334155; color: #94a3b8; font-size: 10px; padding: 2px 6px; border-radius: 4px; cursor: pointer; }
                .editor-pill:hover { color: #fff; border-color: #3b82f6; }
                .editor-sub-row { display: flex; justify-content: space-between; align-items: center; font-size: 11px; color: #64748b; margin-top: 4px; }
                .domain-item { display: flex !important; justify-content: space-between !important; align-items: center !important; padding: 6px 10px !important; background: #1e293b !important; border: 1px solid #334155 !important; border-radius: 8px !important; margin-bottom: 4px !important; gap: 8px !important; }
                .domain-item span { flex: 1 !important; font-size: 11px !important; color: #e2e8f0 !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; min-width: 0 !important; }
                .domain-item .modern-icon-btn { flex-shrink: 0 !important; }
                .replacer-confirm-overlay { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; background: rgba(15, 23, 42, 0.8) !important; backdrop-filter: blur(4px) !important; z-index: 100000050 !important; display: flex !important; align-items: center !important; justify-content: center !important; pointer-events: auto !important; box-sizing: border-box !important; padding: 12px !important; }
                .replacer-confirm-box { background: #1e293b !important; border: 1px solid #334155 !important; border-radius: 12px !important; padding: 16px !important; width: 100% !important; max-width: 100% !important; max-height: 85% !important; overflow-y: auto !important; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.6) !important; display: flex !important; flex-direction: column !important; gap: 12px !important; animation: modalSlideIn 0.2s cubic-bezier(0.16, 1, 0.3, 1) !important; box-sizing: border-box !important; }
                .replacer-confirm-header { font-size: 13px !important; font-weight: 800 !important; color: #f8fafc !important; border-bottom: 1px solid #334155 !important; padding-bottom: 8px !important; text-transform: uppercase !important; letter-spacing: 0.05em !important; }
                .replacer-confirm-body { font-size: 11px !important; color: #cbd5e1 !important; line-height: 1.5 !important; }
                .replacer-confirm-footer { display: flex !important; justify-content: flex-end !important; gap: 8px !important; margin-top: 4px !important; }
                .replacer-toast { position: fixed !important; bottom: 20px !important; right: 20px !important; background: #1e293b !important; border: 1px solid #334155 !important; border-left: 4px solid #3b82f6 !important; color: #ffffff !important; padding: 10px 16px !important; border-radius: 8px !important; font-size: 12px !important; font-weight: 600 !important; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5) !important; z-index: 100000010 !important; display: none !important; align-items: center !important; gap: 8px !important; max-width: 300px !important; pointer-events: auto !important; }
                .replacer-toast.show { display: flex !important; animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards !important; }
                .replacer-toast.success { border-left-color: #10b981 !important; }
                .replacer-toast.warn { border-left-color: #f59e0b !important; }
                .replacer-toast.info { border-left-color: #3b82f6 !important; }
                .replacer-tooltip-container { position: fixed !important; background: #0f172a !important; border: 1px solid #1e3a5f !important; border-radius: 10px !important; padding: 0 !important; box-shadow: 0 12px 28px -4px rgba(0,0,0,0.7), 0 0 0 1px rgba(59,130,246,0.15) !important; z-index: 100000005 !important; pointer-events: auto !important; display: none !important; flex-direction: column !important; gap: 0 !important; min-width: 170px !important; max-width: 250px !important; overflow: hidden !important; }
                .replacer-tooltip-container.visible { display: flex !important; }
                .tooltip-main-body { display: flex !important; align-items: center !important; gap: 6px !important; padding: 10px 12px !important; flex-wrap: wrap !important; }
                .tooltip-original-badge { font-size: 11px !important; color: #64748b !important; text-decoration: line-through !important; word-break: break-all !important; }
                .tooltip-arrow-icon { color: #3b82f6 !important; font-size: 13px !important; flex-shrink: 0 !important; font-weight: bold !important; }
                .tooltip-replaced-badge { font-size: 12px !important; font-weight: 700 !important; color: #60a5fa !important; background: rgba(59,130,246,0.12) !important; padding: 2px 7px !important; border-radius: 5px !important; word-break: break-all !important; }
                .tooltip-footer { border-top: 1px solid #1e293b !important; padding: 6px 10px !important; display: flex !important; justify-content: flex-end !important; background: #060f1e !important; }
                .tooltip-edit-btn { background: #1e293b !important; border: 1px solid #334155 !important; color: #94a3b8 !important; font-size: 10px !important; padding: 3px 10px !important; border-radius: 6px !important; cursor: pointer !important; font-weight: 600 !important; letter-spacing: 0.02em !important; }
                .tooltip-edit-btn:hover { color: #fff !important; border-color: #3b82f6 !important; background: #1e3a5f !important; }
                @keyframes modalSlideIn { from { transform: scale(0.95) translateY(10px) !important; opacity: 0 !important; } to { transform: scale(1) translateY(0) !important; opacity: 1 !important; } }
                @keyframes toastSlideIn { from { transform: translateY(100px) scale(0.9) !important; opacity: 0 !important; } to { transform: translateY(0) scale(1) !important; opacity: 1 !important; } }
            `;
      shadow.appendChild(style);

      const wrapper = document.createElement("div");
      wrapper.className = "replacer-wrapper";
      shadow.appendChild(wrapper);

      const tooltipEl = document.createElement("div");
      tooltipEl.className = "replacer-tooltip-container";
      wrapper.appendChild(tooltipEl);

      toast = document.createElement("div");
      toast.className = "replacer-toast";
      wrapper.appendChild(toast);

      const launcher = document.createElement("button");
      launcher.className = "replacer-launcher";
      launcher.innerHTML = `
          <span style="display:flex;align-items:center;justify-content:center;">
              <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="width:15px;height:15px;">
                  <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
                  <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
              </svg>
          </span>
          <span style="line-height:1;">AWR Tools</span>
      `;
      wrapper.appendChild(launcher);

      panel = document.createElement('div');
      panel.className = 'replacer-panel hidden';
      wrapper.appendChild(panel);

      launcher.onclick = () => {
          panel.classList.toggle('hidden');
          if (!panel.classList.contains('hidden')) {
              subjekEdit = null; safeRenderTampilan();
          } else {
              hilangkanFokusShadow();
          }
      };

      // FIX: Pastikan launcher selalu ada di shadow DOM, tidak hilang saat DOM berubah
      // Beberapa situs secara agresif menghapus elemen asing; ini mencegah itu.
      (function guardLauncher() {
        const launcherGuard = new MutationObserver(() => {
          if (!wrapper.contains(launcher)) {
            wrapper.appendChild(launcher);
          }
          if (!wrapper.contains(panel)) {
            wrapper.appendChild(panel);
          }
        });
        launcherGuard.observe(wrapper, { childList: true });
      })();

      let activeHoveredTarget = null, tooltipHideTimeout = null;
      document.body.addEventListener('mouseover', (e) => {
          const host = e.target.closest('#word-replacer-host');
          if (host) {
              if (tooltipHideTimeout) { clearTimeout(tooltipHideTimeout); tooltipHideTimeout = null; }
              return;
          }
          const target = e.target.closest('.' + HIGHLIGHT_CLASS);
          if (target) {
              if (isInsideNativeGlossary(target)) return;
              if (tooltipHideTimeout) { clearTimeout(tooltipHideTimeout); tooltipHideTimeout = null; }
              activeHoveredTarget = target;
              const original = target.getAttribute('data-original') || target.textContent, replacement = target.textContent;
              tampilkanTooltip(target, original, replacement);
          }
      }, false);

      document.body.addEventListener('mouseout', () => {
          if (tooltipHideTimeout) clearTimeout(tooltipHideTimeout);
          tooltipHideTimeout = setTimeout(() => { sembunyikanTooltip(); activeHoveredTarget = null; }, 350);
      }, false);

      document.body.addEventListener('click', (e) => {
          if (e.target.closest('#word-replacer-host')) return;
          const isMobileClick = e.clientX === 0 && e.clientY === 0;
          const diffX = isMobileClick ? 0 : Math.abs(e.clientX - startX);
          const diffY = isMobileClick ? 0 : Math.abs(e.clientY - startY);
          const selection = window.getSelection() ? window.getSelection().toString().trim() : '';

          const isInteractiveElement = e.target.closest('a, button, input, textarea, select, summary, [role="button"], [role="link"], [role="menuitem"]');
          const classNameStr = typeof e.target.className === 'string' ? e.target.className : '';
          const idStr = typeof e.target.id === 'string' ? e.target.id : '';
          const hasClickableKeywords = /more|less|expand|collapse|toggle|show|hide|btn|button|click/i.test(classNameStr + ' ' + idStr);

          const isGlossaryOrHighlight = isInsideNativeGlossary(e.target) || e.target.closest('.' + HIGHLIGHT_CLASS);
          const isInteractive = isInteractiveElement || hasClickableKeywords || isGlossaryOrHighlight;

          if (diffX > 6 || diffY > 6 || selection.length > 0 || isInteractive) return;

          if (panel.classList.contains('hidden')) {
              subjekEdit = null; panel.classList.remove('hidden'); safeRenderTampilan();
          } else {
              panel.classList.add('hidden'); hilangkanFokusShadow();
          }
      }, false);

      document.body.addEventListener('touchstart', (e) => { if (e.touches?.[0]) { startX = e.touches[0].clientX; startY = e.touches[0].clientY; } }, { passive: true, capture: true });
      document.body.addEventListener('mousedown', (e) => { startX = e.clientX; startY = e.clientY; }, true);

      // v52.4 + v99.12.36: Klik kanan pada kata unik (span[data-hash])
      // PERILAKU BARU:
      //   • Hash SUDAH punya rule (kata sudah diganti) → tampilkan daftar per kemunculan
      //     dengan tombol revert individual/semua (bukan lagi redirect ke editor)
      //   • Hash BELUM punya rule → tetap buka editor pre-filled "hash:XXXX" (perilaku lama)
      document.body.addEventListener('contextmenu', (e) => {
        if (e.target.closest('#word-replacer-host')) return;
        const glossarySpan = e.target.closest('span[data-hash].text-patch, span[data-slot="popover-trigger"][data-hash]');
        if (!glossarySpan) return;
        const hashVal = glossarySpan.getAttribute('data-hash');
        if (!hashVal) return;
        e.preventDefault();

        const kamus = core.getKamus();
        const hashKey = 'hash:' + hashVal;
        const existingRule = kamus[hashKey];
        const replacedTo = existingRule
          ? (typeof existingRule === 'object' ? existingRule.to : existingRule)
          : null;

        // ── Belum ada rule → perilaku lama (buka editor) ──────────────────────
        if (!replacedTo) {
          const currentText = glossarySpan.textContent.trim();
          subjekEdit = null;
          tabAktif = 'tambah';
          panel.classList.remove('hidden');
          safeRenderTampilan();
          setTimeout(() => {
            const iSalah = shadow.querySelector('.input-salah');
            const iBenar = shadow.querySelector('.input-benar');
            const hintEl = shadow.querySelector('.awr-hash-hint');
            if (iSalah) { iSalah.value = 'hash:' + hashVal; iSalah.dispatchEvent(new Event('input')); }
            if (iBenar && !iBenar.value) iBenar.value = currentText;
            if (hintEl) hintEl.style.display = 'block';
            panggilToast('🔑 Hash rule siap: data-hash="' + hashVal + '" (isi teks pengganti lalu simpan)', 'info');
          }, 80);
          return;
        }

        // ── Sudah ada rule → tampilkan daftar per kemunculan ─────────────────
        panel.classList.remove('hidden');

        // Kumpulkan semua span dengan hash yang sama di halaman
        const allMatchingSpans = Array.from(
          document.querySelectorAll(
            'span[data-hash="' + hashVal + '"].text-patch, ' +
            'span[data-slot="popover-trigger"][data-hash="' + hashVal + '"]'
          )
        );

        // Cari index kemunculan yang diklik
        const clickedIdx = allMatchingSpans.indexOf(glossarySpan);

        // Hapus overlay lama jika masih ada
        const existingOverlay = panel.querySelector('.awr-revert-overlay');
        if (existingOverlay) existingOverlay.remove();

        const overlay = document.createElement('div');
        overlay.className = 'awr-revert-overlay';
        overlay.style.cssText =
          'position:absolute!important;top:0!important;left:0!important;' +
          'width:100%!important;height:100%!important;' +
          'background:rgba(15,23,42,0.88)!important;backdrop-filter:blur(4px)!important;' +
          'z-index:100000050!important;display:flex!important;align-items:center!important;' +
          'justify-content:center!important;box-sizing:border-box!important;padding:12px!important;' +
          'pointer-events:auto!important;';

        const box = document.createElement('div');
        box.style.cssText =
          'background:#1e293b!important;border:1px solid #334155!important;' +
          'border-radius:12px!important;padding:14px!important;width:100%!important;' +
          'max-height:88%!important;overflow-y:auto!important;' +
          'box-shadow:0 20px 25px -5px rgba(0,0,0,0.6)!important;' +
          'display:flex!important;flex-direction:column!important;gap:10px!important;' +
          'box-sizing:border-box!important;animation:modalSlideIn 0.2s cubic-bezier(0.16,1,0.3,1)!important;';

        // Teks asli dari hash dapat diketahui via hashOriginalTextMap (WeakMap modul)
        // hashOriginalTextMap.get(span) → teks sebelum diganti AWR
        function getOrigTxt(span) {
          const fromMap = hashOriginalTextMap.get(span);
          return fromMap || span.getAttribute('data-awr-orig') || null;
        }

        // Cari teks asli mewakili hash ini (dari span pertama yang ada datanya)
        let knownOrigSample = null;
        for (const sp of allMatchingSpans) {
          const o = getOrigTxt(sp);
          if (o) { knownOrigSample = o; break; }
        }
        const origLabel = knownOrigSample
          ? '<b style="color:#60a5fa;">' + knownOrigSample + (allMatchingSpans.length > 1 ? ', …' : '') + '</b>'
          : '<span style="color:#64748b;">(teks asli tidak tersimpan di sesi ini)</span>';

        box.innerHTML =
          '<div style="font-size:13px!important;font-weight:800!important;color:#f8fafc!important;' +
          'border-bottom:1px solid #334155!important;padding-bottom:8px!important;letter-spacing:0.02em;">' +
          '↩ Kembalikan Kemunculan</div>' +

          '<div style="font-size:10px!important;color:#94a3b8!important;line-height:1.6;">' +
          'Hash: <code style="background:#0f1117;padding:1px 4px;border-radius:3px;color:#fbbf24;">' + hashKey + '</code><br>' +
          'Teks asli: ' + origLabel + '<br>' +
          'Diganti menjadi: <b style="color:#10b981;">' + replacedTo + '</b><br>' +
          (clickedIdx >= 0 ? 'Kemunculan ke-<b style="color:#e2e8f0;">' + (clickedIdx + 1) + '</b><br>' : '') +
          'Total kemunculan di halaman: <b style="color:#e2e8f0;">' + allMatchingSpans.length + '</b>' +
          '</div>' +

          '<div style="font-size:11px!important;color:#e2e8f0!important;font-weight:600!important;">' +
          'Pilih kemunculan yang ingin dikembalikan ke teks asli:</div>' +

          '<div class="awr-occ-list" style="display:flex;flex-direction:column;gap:5px;' +
          'max-height:220px;overflow-y:auto;padding-right:2px;"></div>' +

          '<div style="display:flex;gap:6px;justify-content:space-between;flex-wrap:wrap;margin-top:2px;">' +
          '<button class="form-btn awr-revert-all-btn" style="padding:5px 12px!important;font-size:10px!important;' +
          'border-radius:10px!important;background:rgba(239,68,68,0.12)!important;color:#f87171!important;' +
          'border-color:rgba(239,68,68,0.35)!important;">↩ Kembalikan Semua</button>' +
          '<div style="display:flex;gap:5px;">' +
          '<button class="form-btn awr-edit-rule-btn" style="padding:5px 12px!important;font-size:10px!important;' +
          'border-radius:10px!important;background:rgba(59,130,246,0.12)!important;color:#60a5fa!important;' +
          'border-color:rgba(59,130,246,0.35)!important;">✏️ Edit Rule</button>' +
          '<button class="form-btn awr-close-overlay-btn" style="padding:5px 12px!important;' +
          'font-size:10px!important;border-radius:10px!important;">✕ Tutup</button>' +
          '</div>' +
          '</div>';

        const occList = box.querySelector('.awr-occ-list');

        // Helper: konteks singkat di sekitar span
        function getSpanCtx(span) {
          try {
            const parent = span.parentElement;
            if (!parent) return '';
            const full = parent.textContent || '';
            const txt  = span.textContent || '';
            const idx  = full.indexOf(txt);
            if (idx === -1) return '';
            const before = full.slice(Math.max(0, idx - 25), idx).replace(/\s+/g, ' ').trim();
            const after  = full.slice(idx + txt.length, idx + txt.length + 25).replace(/\s+/g, ' ').trim();
            return (before ? '…' + before + ' ' : '') + '[' + txt + ']' + (after ? ' ' + after + '…' : '');
          } catch (_) { return ''; }
        }

        // Highlight span di halaman saat hover
        let _hlStore = [];
        function hlClear() {
          _hlStore.forEach(function(h) {
            h.s.style.outline = h.o; h.s.style.background = h.b; h.s.style.borderRadius = h.r;
          });
          _hlStore = [];
        }
        function hlSpan(span) {
          hlClear();
          _hlStore = [{ s: span, o: span.style.outline, b: span.style.background, r: span.style.borderRadius }];
          span.style.outline = '2px solid #ef4444';
          span.style.background = 'rgba(239,68,68,0.18)';
          span.style.borderRadius = '3px';
          try { span.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch (_) {}
        }

        allMatchingSpans.forEach(function(span, idx) {
          const occKey    = hashKey + ':' + idx;
          const posEntry  = kamus[occKey];
          const posVal    = posEntry ? (typeof posEntry === 'object' ? posEntry.to : posEntry) : null;
          const origTxt   = getOrigTxt(span); // teks asli sebelum diganti (null jika tidak tahu)
          const dispTxt   = span.textContent.trim(); // teks saat ini (sudah diganti)
          const ctx       = getSpanCtx(span);
          const isReverted = posVal !== null && posVal !== replacedTo;

          const row = document.createElement('div');
          row.style.cssText =
            'background:#0f1117!important;border-radius:7px!important;padding:7px 9px!important;' +
            'border:1px solid ' + (isReverted ? 'rgba(34,197,94,0.3)' : '#1e293b') + '!important;' +
            'display:flex!important;align-items:flex-start!important;gap:7px!important;cursor:pointer!important;';

          const statusBadge = isReverted
            ? '<span style="font-size:8px;background:rgba(34,197,94,0.15);border:1px solid rgba(34,197,94,0.3);' +
              'color:#4ade80;border-radius:4px;padding:1px 4px;flex-shrink:0;">✓ sudah revert</span>'
            : '';

          row.innerHTML =
            '<div style="min-width:0;flex:1;">' +
              '<div style="font-size:10px;font-weight:bold;color:#22d3ee;display:flex;align-items:center;gap:5px;">' +
                'Kemunculan ke-' + (idx + 1) + statusBadge +
              '</div>' +
              (ctx
                ? '<div style="font-size:9px;color:#64748b;margin-top:2px;overflow:hidden;' +
                  'text-overflow:ellipsis;white-space:nowrap;" title="' + ctx.replace(/"/g, '&quot;') + '">' + ctx + '</div>'
                : '') +
              (origTxt
                ? '<div style="font-size:9px;color:#94a3b8;margin-top:1px;">Asli: <b style="color:#60a5fa;">' + origTxt + '</b>' +
                  (posVal ? ' → Override: <b style="color:#10b981;">' + posVal + '</b>' : '') + '</div>'
                : '<div style="font-size:9px;color:#475569;margin-top:1px;font-style:italic;">Teks asli tidak diketahui (buka halaman ulang untuk memuat)</div>') +
            '</div>' +
            '<div style="display:flex;flex-direction:column;gap:3px;flex-shrink:0;">' +
              '<button class="form-btn awr-revert-one-btn" data-idx="' + idx + '" style="padding:2px 8px!important;' +
                'font-size:9px!important;border-radius:7px!important;white-space:nowrap;' +
                'background:rgba(239,68,68,0.12)!important;color:#f87171!important;' +
                'border-color:rgba(239,68,68,0.35)!important;">' +
                (origTxt ? '↩ Ke "' + origTxt.slice(0, 12) + (origTxt.length > 12 ? '…' : '') + '"' : '↩ Revert') +
              '</button>' +
              // Tombol Hapus Override hanya muncul jika kemunculan ini punya positional override
              (posEntry
                ? '<button class="form-btn awr-del-override-btn" data-idx="' + idx + '" style="padding:2px 8px!important;' +
                    'font-size:9px!important;border-radius:7px!important;white-space:nowrap;' +
                    'background:rgba(245,158,11,0.12)!important;color:#fbbf24!important;' +
                    'border-color:rgba(245,158,11,0.35)!important;" title="Hapus positional override untuk kemunculan ke-' + (idx+1) + ' saja. Kemunculan ini akan kembali mengikuti rule utama.">' +
                    '🗑 Hapus Override' +
                  '</button>'
                : '') +
            '</div>';

          row.addEventListener('mouseenter', function() { hlSpan(span); });
          row.addEventListener('mouseleave', hlClear);

          const revertBtn = row.querySelector('.awr-revert-one-btn');
          revertBtn.onclick = function(ev) {
            ev.stopPropagation();
            const tk = core.getKamus();
            const currentNovel = core.getNovelContext();

            if (origTxt) {
              // Teks asli diketahui → set positional override ke teks asli
              tk[occKey] = {
                to: origTxt,
                global: false,
                novelId: currentNovel.id,
                novelTitle: currentNovel.title,
                novelUrl: currentNovel.url,
                domain: core.getNovelBaseDomain(core.currentHost),
                caseSensitive: true
              };
              core.saveKamus(tk);
              core.simpanKeAwan(tk);
              core.jalankanPengganti(true);
              hlClear();
              overlay.remove();
              panggilToast('↩ Kemunculan ke-' + (idx + 1) + ' dikembalikan ke teks asli.', 'success');
            } else {
              // Teks asli tidak diketahui → hapus positional override jika ada,
              // atau beri tahu user bahwa teks asli tidak tersedia di sesi ini
              if (tk[occKey]) {
                delete tk[occKey];
                core.saveKamus(tk);
                core.simpanKeAwan(tk);
                core.jalankanPengganti(true);
                hlClear();
                overlay.remove();
                panggilToast('↩ Override kemunculan ke-' + (idx + 1) + ' dihapus.', 'success');
              } else {
                panggilToast(
                  '⚠ Teks asli tidak tersedia di sesi ini. Muat ulang halaman agar AWR mencatat teks aslinya, lalu coba lagi.',
                  'warn'
                );
              }
            }
          };

          // Handler tombol Hapus Override (hanya ada jika posEntry !== null)
          const delOverrideBtn = row.querySelector('.awr-del-override-btn');
          if (delOverrideBtn) {
            delOverrideBtn.onclick = function(ev) {
              ev.stopPropagation();
              const tk = core.getKamus();
              if (tk[occKey]) {
                delete tk[occKey];
                core.saveKamus(tk);
                core.simpanKeAwan(tk);
                core.jalankanPengganti(true);
                hlClear();
                overlay.remove();
                panggilToast(
                  '🗑 Override kemunculan ke-' + (idx + 1) + ' dihapus. ' +
                  'Kemunculan ini kembali mengikuti rule utama ("' + replacedTo + '").',
                  'success'
                );
              }
            };
          }

          occList.appendChild(row);
        });

        // Tombol Kembalikan Semua
        box.querySelector('.awr-revert-all-btn').onclick = function() {
          const tk = core.getKamus();
          const currentNovel = core.getNovelContext();
          let countReverted = 0;
          let countUnknown  = 0;

          allMatchingSpans.forEach(function(span, idx) {
            const origTxt = getOrigTxt(span);
            if (origTxt) {
              tk[hashKey + ':' + idx] = {
                to: origTxt,
                global: false,
                novelId: currentNovel.id,
                novelTitle: currentNovel.title,
                novelUrl: currentNovel.url,
                domain: core.getNovelBaseDomain(core.currentHost),
                caseSensitive: true
              };
              countReverted++;
            } else {
              countUnknown++;
            }
          });

          core.saveKamus(tk);
          core.simpanKeAwan(tk);
          core.jalankanPengganti(true);
          hlClear();
          overlay.remove();

          if (countUnknown > 0) {
            panggilToast(
              '↩ ' + countReverted + ' kemunculan dikembalikan. ' + countUnknown + ' kemunculan tidak bisa diproses (teks asli tidak tersedia — muat ulang halaman).',
              'warn'
            );
          } else {
            panggilToast('↩ Semua ' + countReverted + ' kemunculan dikembalikan ke teks asli.', 'success');
          }
        };

        box.querySelector('.awr-close-overlay-btn').onclick = function() { hlClear(); overlay.remove(); };

        // Tombol Edit Rule: tutup overlay lalu buka editor pre-filled dengan rule hash ini
        box.querySelector('.awr-edit-rule-btn').onclick = function() {
          hlClear();
          overlay.remove();
          // Buka tab editor, pre-fill dengan hashKey dan teks pengganti saat ini
          subjekEdit = null;
          tabAktif = 'tambah';
          panel.classList.remove('hidden');
          safeRenderTampilan();
          setTimeout(function() {
            const iSalah = shadow.querySelector('.input-salah');
            const iBenar = shadow.querySelector('.input-benar');
            const hintEl = shadow.querySelector('.awr-hash-hint');
            if (iSalah) {
              iSalah.value = hashKey;
              iSalah.dispatchEvent(new Event('input'));
            }
            if (iBenar) {
              // Pre-fill dengan teks pengganti saat ini agar mudah diedit
              iBenar.value = replacedTo;
            }
            if (hintEl) hintEl.style.display = 'block';
            panggilToast('✏️ Edit rule "' + hashKey + '" — ubah teks pengganti lalu klik Simpan.', 'info');
          }, 80);
        };

        overlay.appendChild(box);
        panel.appendChild(overlay);
      }, false);

      function tampilkanTooltip(target, original, replacement) {
          tooltipEl.innerHTML = `
              <div class="tooltip-main-body">
                  <span class="tooltip-original-badge">${original}</span>
                  <span class="tooltip-arrow-icon">→</span>
                  <span class="tooltip-replaced-badge">${replacement}</span>
              </div>
              <div class="tooltip-footer">
                  <button class="tooltip-edit-btn">✏️ ${t('tab_editor')}</button>
              </div>
          `;
          tooltipEl.classList.add('visible');
          const tooltipWidth = tooltipEl.getBoundingClientRect().width || 230;
          const tooltipHeight = tooltipEl.getBoundingClientRect().height || 80;

          const rect = target.getBoundingClientRect();
          let top = rect.top - tooltipHeight - 8, left = rect.left + (rect.width / 2) - (tooltipWidth / 2);

          if (top < 10) top = rect.bottom + 8;
          if (left < 10) left = 10;
          if (left + tooltipWidth > window.innerWidth - 10) left = window.innerWidth - tooltipWidth - 10;

          tooltipEl.style.top = top + 'px'; tooltipEl.style.left = left + 'px';

          tooltipEl.querySelector('.tooltip-edit-btn').onclick = (e) => {
              e.stopPropagation(); subjekEdit = original; tabAktif = 'tambah';
              panel.classList.remove('hidden'); safeRenderTampilan(); sembunyikanTooltip();
          };
      }

      function sembunyikanTooltip() { tooltipEl.classList.remove('visible'); }
      // FIX v52: Assign implementasi sebenarnya setelah shadow tersedia
      hilangkanFokusShadow = function() { if (shadow && shadow.activeElement) { shadow.activeElement.blur(); } };

      // ── v99.12.30: Jalankan UI Health Monitor setelah semua komponen siap ──
      // Memulai sistem watchdog yang cek panel setiap 2.5 detik dan auto-recover
      // jika blank/kosong tanpa menutup panel. Juga menjaga agar hostElement,
      // wrapper, dan panel tidak terlepas dari DOM akibat situs yang agresif.
      startUIHealthMonitor(shadow, wrapper, hostElement);
  }
};

function renderCloudGistManager(container, gistDetails) {
  const token = core.getGitHubToken(), gistId = core.getGistId(), ownerName = gistDetails?.owner?.login || cachedUserProfile?.login || "GitHub User";
  const avatarUrl = gistDetails?.owner?.avatar_url || cachedUserProfile?.avatar_url || "";

  container.innerHTML = `
      <div class="flex-col card-box" style="gap: 8px !important; margin-bottom: 12px !important;">
          <div class="flex-between" style="font-size: 11px !important;">
              <span style="color: #9ca3af !important; font-weight: 500 !important; display: flex; align-items: center; gap: 6px;">
                  ${avatarUrl ? `<img src="${avatarUrl}" style="width: 20px; height: 20px; border-radius: 50%; border: 1px solid #334155;" />` : ''}
                  ${t('cloud_connected_as')}
              </span>
              <span style="color: #38bdf8 !important; font-weight: bold !important;">🐱 @${ownerName}</span>
          </div>
          <div class="flex-between" style="font-size: 11px !important; border-top: 1px dashed #334155 !important; padding-top: 8px !important;">
              <span style="color: #9ca3af !important; flex-shrink: 0 !important;">Gist ID:</span>
              <span class="mono-font txt-ellipsis" style="color: #60a5fa !important; flex: 1 !important; text-align: right !important; margin-right: 6px !important;" title="${gistId}">
                  ${gistId}
              </span>
              <button class="action-btn copy-gist-id-btn" style="padding: 2px !important; background: none; border: none; cursor: pointer;" title="Copy Gist ID">📋</button>
              <a href="https://gist.github.com/${ownerName}/${gistId}" target="_blank" class="awr-tooltip-btn" data-tooltip="${t('btn_open_gist_github')}" style="text-decoration: none; font-size: 11px; margin-left: 2px;">🔗</a>
          </div>
          <div class="flex-between" style="font-size: 11px !important;">
              <span style="color: #9ca3af !important; flex-shrink: 0 !important;">${t('auth_github_token_label')}</span>
              <input type="password" readonly class="txt-copyable-token mono-font" value="${token}" style="background: transparent !important; border: none !important; color: #cbd5e1 !important; font-size: 11px !important; flex: 1 !important; text-align: right !important; outline: none !important; width: 100px !important; margin-right: 6px !important;" />
              <button class="action-btn toggle-token-view-btn" style="background: none !important; border: none !important; cursor: pointer !important; padding: 2px !important; color: #94a3b8 !important;" title="Show/Hide Token">👁️</button>
              <button class="action-btn copy-token-btn" style="background: none !important; border: none !important; cursor: pointer !important; padding: 2px !important;" title="Copy Token">📋</button>
          </div>
          <div class="flex-between" style="margin-top: 6px !important; border-top: 1px dashed #334155 !important; padding-top: 8px !important;">
              <button class="form-btn manual-upload-btn btn-pill-primary" style="padding: 5px 14px !important;">
                  ${t('backup_now')}
              </button>
              <button class="form-btn btn-backup-vault-cloud btn-pill" style="padding: 5px 14px !important;">
                  💾 Simpan Login Lokal
              </button>
              <button class="form-btn btn-switch-github btn-pill" style="padding: 5px 14px !important; color: #ef4444 !important; border-color: #ef4444 !important;">
                  ${t('logout')}
              </button>
          </div>
      </div>

      <div style="font-size: 11px !important; font-weight: bold !important; margin-bottom: 8px !important; color: #94a3b8 !important; border-bottom: 1px solid #334155 !important; padding-bottom: 4px !important; text-transform: uppercase !important; letter-spacing: 0.05em !important;">
          🕒 ${t('revision_history')}
      </div>

      <div class="cloud-baskets-list flex-col" style="gap: 6px !important; max-height: 180px !important; overflow-y: auto !important; padding-right: 4px !important;">
      </div>
  `;

  container.querySelector('.copy-gist-id-btn').onclick = () => {
      navigator.clipboard.writeText(gistId);
      panggilToast(t('gist_id_copied'), "success");
  };

  container.querySelector('.copy-token-btn').onclick = () => {
      navigator.clipboard.writeText(token);
      panggilToast(t('token_copied'), "success");
  };

  const tInput = container.querySelector('.txt-copyable-token');
  const viewBtn = container.querySelector('.toggle-token-view-btn');
  viewBtn.onclick = () => {
      if (tInput.type === 'password') {
          tInput.type = 'text'; viewBtn.textContent = '🙈';
      } else {
          tInput.type = 'password'; viewBtn.textContent = '👁️';
      }
  };

  container.querySelector('.btn-backup-vault-cloud').onclick = async () => {
      const tanggal = new Date().toLocaleString("id-ID");
      const isi = [
          "// AWR Credentials - Advanced Word Replacer",
          "// Dibuat: " + tanggal,
          "// JANGAN bagikan file ini kepada siapapun!",
          "",
          "var AWR_TOKEN = " + JSON.stringify(token) + ";",
          "var AWR_GIST_ID = " + JSON.stringify(gistId) + ";",
          ""
      ].join("\n");

      // Coba showSaveFilePicker agar user bisa memilih lokasi penyimpanan
      if (typeof window.showSaveFilePicker === "function") {
          try {
              const handle = await window.showSaveFilePicker({
                  suggestedName: "awr_credentials.txt",
                  types: [
                      { description: "File Teks (.txt)", accept: { "text/plain": [".txt"] } },
                      { description: "File JavaScript (.js)", accept: { "text/javascript": [".js"] } },
                  ],
              });
              const writable = await handle.createWritable();
              await writable.write(isi);
              await writable.close();
              panggilToast("✅ Kredensial login berhasil disimpan!", "success");
              return;
          } catch (err) {
              if (err.name === "AbortError") return; // user membatalkan dialog
              panggilToast("Gagal menyimpan: " + err.message + ". Mengunduh ke Downloads...", "warn");
          }
      }

      // Fallback: browser tidak mendukung dialog — unduh ke folder Downloads
      const blob = new Blob([isi], { type: "text/javascript" });
      const url = URL.createObjectURL(blob);
      const a = document.createElement("a");
      a.href = url;
      a.download = "awr_credentials.js";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
      panggilToast("⚠️ Browser tidak mendukung dialog lokasi. File diunduh ke folder Downloads.", "info");
  };

  container.querySelector('.manual-upload-btn').onclick = async () => {
      const listContainer = container.querySelector('.cloud-baskets-list');
      const originalHTML = listContainer.innerHTML;
      listContainer.innerHTML = `
          <div class="cloud-loading-spinner flex-col" style="align-items: center !important; justify-content: center !important; padding: 30px !important; gap: 10px !important;">
              <span style="font-size: 24px !important; animation: spin 1s linear infinite !important; display: inline-block !important;">⏳</span>
              <span style="font-size: 11px !important; color: #94a3b8 !important;">${t('cloud_manual_backup_starting')}</span>
          </div>
      `;
      panggilToast(t('cloud_manual_backup_starting'), "info");
      await core.simpanKeAwan(null, null, null, true);

      const freshDetails = await core.fetchGistDetails(token, gistId);
      if (freshDetails) {
          panggilToast(t('cloud_manual_backup_success'), "success");
          renderCloudGistManager(container, freshDetails);
      } else {
          listContainer.innerHTML = originalHTML;
          panggilToast(t('cloud_manual_backup_fail_history'), "warn");
      }
  };

  container.querySelector('.btn-switch-github').onclick = () => {
      window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
          t('logout'),
          t('logout_confirm'),
          () => {
              core.saveGitHubCredentials("", "");
              cachedGistDetails = null;
              cachedUserProfile = null;
              panggilToast(t('toast_account_switched'), 'info');
              renderTampilan();
          }
      );
  };

  const listContainer = container.querySelector('.cloud-baskets-list');
  if (!gistDetails || !gistDetails.files) {
      listContainer.innerHTML = `<div class="empty-state">${t('no_backups_found')}</div>`;
      return;
  }

  const backupFiles = Object.keys(gistDetails.files)
      .filter(name => name.startsWith('backup_') && name.endsWith('.json'))
      .sort()
      .reverse();

  if (backupFiles.length === 0) {
      listContainer.innerHTML = `<div class="empty-state">${t('no_backups_found')}</div>`;
      return;
  }

  backupFiles.forEach((filename) => {
      const friendlyName = parseBackupFilename(filename);
      const fileData = gistDetails.files[filename];

      const item = document.createElement('div');
      item.className = 'flex-between bg-box p-box';
      item.setAttribute('style', 'background: #1e293b !important; border: 1px solid #334155 !important; border-radius: 8px !important; padding: 8px 12px !important; margin-bottom: 4px !important;');

      item.innerHTML = `
          <div class="flex-col" style="min-width: 0 !important; flex: 1 !important; gap: 2px !important;">
              <span class="mono-font txt-ellipsis" style="font-size: 11px !important; font-weight: bold !important; color: #f1f5f9 !important;" title="${filename}">
                  ${friendlyName}
              </span>
          </div>
          <div class="flex-row" style="gap: 4px !important; flex-shrink: 0 !important;">
              <button class="form-btn restore-revision-btn btn-pill-primary" style="padding: 4px 10px !important; font-size: 10px !important; border-radius: 12px !important;" title="Muat & Timpa Kamus Aktif">${t('btn_load')}</button>
              <button class="form-btn merge-revision-btn btn-pill-primary" style="padding: 4px 10px !important; font-size: 10px !important; border-radius: 12px !important; background: #10b981 !important; border-color: #10b981 !important;" title="Gabungkan dengan Kamus Aktif">➕ Gabung</button>
              <button class="form-btn delete-revision-btn btn-pill" style="background: none !important; border: 1px solid #ef4444 !important; color: #f87171 !important; padding: 4px 8px !important; font-size: 10px !important; border-radius: 12px !important;" title="Hapus cadangan ini">🗑️</button>
          </div>
      `;

      item.querySelector('.restore-revision-btn').onclick = () => {
          window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
              t('btn_load'),
              t('alert_overwrite_confirm'),
              async () => {
                  panggilToast(t('toast_sync_connecting'), 'info');
                  try {
                      let contentText = fileData.content;
                      if (!contentText && fileData.raw_url) {
                          const rawRes = await gmFetch(fileData.raw_url);
                          if (rawRes.ok) contentText = await rawRes.text();
                      }
                      if (contentText) {
                          const parsedData = JSON.parse(contentText);
                          if (parsedData) {
                              if (parsedData.kamus) GM_setValue("kamus_kata_v5", JSON.stringify(parsedData.kamus));
                              if (parsedData.domains) GM_setValue("target_domains_v4", JSON.stringify(parsedData.domains));
                              if (parsedData.blacklist) GM_setValue("blacklist_domains_v1", JSON.stringify(parsedData.blacklist));
                              if (parsedData.filterMode) GM_setValue("filter_mode_v1", parsedData.filterMode);
                              if (parsedData.deletedWords) GM_setValue("awr_deleted_words_v1", JSON.stringify(parsedData.deletedWords));
                              if (parsedData.novelTitles) GM_setValue("awr_novel_titles_v2", JSON.stringify(parsedData.novelTitles));
                              // Pulihkan semua pengaturan tambahan dari backup
                              if (typeof parsedData.highlightAktif !== "undefined") GM_setValue("highlight_aktif_v4", !!parsedData.highlightAktif);
                              if (parsedData.lang) GM_setValue("awr_lang_v1", parsedData.lang);
                              if (parsedData.updateMode) GM_setValue("awr_update_mode_v1", parsedData.updateMode);
                              if (parsedData.recycleAutoDeleteDays !== undefined) GM_setValue("awr_recycle_auto_delete_days", String(parsedData.recycleAutoDeleteDays));
                              if (typeof parsedData.showOtherTerms !== "undefined") {
                                  GM_setValue("awr_show_other_terms", !!parsedData.showOtherTerms);
                                  showOtherTerms = !!parsedData.showOtherTerms;
                              }
                              // Kamus dipulihkan; tidak mengubah grup aktif yang dipilih pengguna
                              _activeNovelIdCache = null;
                          }
                          panggilToast(t('toast_revision_restored', friendlyName), 'success');
                          core.jalankanPengganti(true);
                          renderTampilan();
                      } else {
                          alert("Gagal memuat isi berkas cadangan!");
                      }
                  } catch (err) {
                      console.error(err);
                      alert("Koneksi gagal saat memulihkan cadangan.");
                  }
              }
          );
      };

      item.querySelector('.merge-revision-btn').onclick = () => {
          // Tanyakan mode remap sebelum menggabungkan
          window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
              "Gabungkan Kamus — Remap ID?",
              "Pilih cara penanganan ID grup kata dari backup ini:\n\n• Klik [Ya] → Paksa remap semua ID ke grup yang sedang aktif sekarang.\n• Klik [Tidak] → Pertahankan ID asli dari backup (cocok untuk backup dari akun/perangkat berbeda).",
              async () => {
                  // Tombol YA → remap paksa ke grup aktif
                  await _doMergeRevision(fileData, parsedData => core.mergeKamus(parsedData.kamus, parsedData.novelTitles || {}, "force"), friendlyName);
              },
              async () => {
                  // Tombol TIDAK → pertahankan ID asli (auto-smart match tetap jalan)
                  await _doMergeRevision(fileData, parsedData => core.mergeKamus(parsedData.kamus, parsedData.novelTitles || {}, "keep"), friendlyName);
              },
              { confirmLabel: "Ya, Remap Semua", cancelLabel: "Tidak, ID Asli" }
          );
      };

      async function _doMergeRevision(fileData, mergeFn, label) {
          panggilToast(t('toast_sync_connecting'), 'info');
          try {
              let contentText = fileData.content;
              if (!contentText && fileData.raw_url) {
                  const rawRes = await gmFetch(fileData.raw_url);
                  if (rawRes.ok) contentText = await rawRes.text();
              }
              if (contentText) {
                  const parsedData = JSON.parse(contentText);
                  if (parsedData && parsedData.kamus) {
                      if (parsedData.novelTitles && typeof parsedData.novelTitles === "object") {
                          try {
                              const localTitles = JSON.parse(GM_getValue("awr_novel_titles_v2", "{}"));
                              const merged = Object.assign({}, localTitles, parsedData.novelTitles);
                              GM_setValue("awr_novel_titles_v2", JSON.stringify(merged));
                          } catch (_) {}
                      }
                      const mergedCount = mergeFn(parsedData);
                      // Invalidate local active novel cache during configuration updates [1]
                      _activeNovelIdCache = null;
                      panggilToast(`Selesai! Berhasil menggabungkan ${mergedCount} kata kustom dari backup "${label}"!`, 'success');
                      core.jalankanPengganti(true);
                      renderTampilan();
                  } else {
                      alert("File cadangan tidak memiliki database kamus yang valid!");
                  }
              } else {
                  alert("Gagal mengambil data dari awan Gist!");
              }
          } catch (err) {
              console.error(err);
              alert("Koneksi bermasalah saat menggabungkan kamus.");
          }
      }

      const delRevBtn = item.querySelector('.delete-revision-btn');
      delRevBtn.onclick = (e) => {
          e.stopPropagation();
          window.AWR_UI_LIBRARY.tampilkanKonfirmasi(
              t('delete_btn'),
              t('delete_revision_confirm', friendlyName),
              async () => {
                  panggilToast("Menghapus berkas cadangan dari GitHub Gist...", "info");
                  try {
                      const response = await gmFetch(`https://api.github.com/gists/${gistId}`, {
                          method: "PATCH",
                          headers: {
                              "Authorization": `token ${token}`,
                              "Accept": "application/vnd.github.v3+json"
                          },
                          body: JSON.stringify({
                              files: {
                                  [filename]: null
                              }
                          })
                      });
                      if (response.ok || response.status === 200) {
                          panggilToast("Berkas cadangan berhasil dihapus!", "success");
                          const freshDetails = await core.fetchGistDetails(token, gistId);
                          cachedGistDetails = freshDetails;
                          renderCloudGistManager(container, freshDetails);
                      } else {
                          alert("Gagal menghapus berkas cadangan dari GitHub Gist. Status: " + response.status);
                      }
                  } catch (err) {
                      console.error(err);
                      alert("Koneksi gagal saat mencoba menghapus berkas cadangan.");
                  }
              }
          );
      };

      listContainer.appendChild(item);
  });
}

// Debounced MutationObserver with revert-detection & loop guard
observer = new MutationObserver((mutations) => {
  if (_replacingNow) return;

  const isInsidePanel = (target) => {
    const el = target.nodeType === Node.TEXT_NODE ? target.parentElement : target;
    return !!(el && el.closest && el.closest("#word-replacer-host"));
  };

  let hasRevert = false;
  let hasHashRevert = false; // FIX v54: revert pada hash span perlu re-apply LEBIH CEPAT
  let hasNewContent = false;

  for (const m of mutations) {
    if (isInsidePanel(m.target)) continue;

    if (m.type === "characterData") {
      const node = m.target;
      if (lastProcessedValueMap.has(node)) {
        // Node ini pernah kita proses — cek apakah nilainya berubah (revert oleh situs)
        const cachedVal = lastProcessedValueMap.get(node);
        const currentVal = node.nodeValue;
        if (cachedVal !== currentVal) {
          // Situs mengembalikan teks ke aslinya → hapus cache agar walker memproses ulang
          lastProcessedValueMap.delete(node);
          // FIX v54: Deteksi apakah node ini ada di dalam hash span (React-managed)
          // → gunakan debounce 0ms agar lebih cepat dari React reconciliation
          const pe = node.parentElement;
          if (pe && pe.dataset && pe.dataset.hash &&
              (pe.getAttribute('data-slot') === 'popover-trigger' ||
               (pe.classList && pe.classList.contains('text-patch')))) {
            hasHashRevert = true;
          } else {
            hasRevert = true;
          }
        }
        // Nilai sama = mutasi dari kita sendiri, skip
      } else {
        hasNewContent = true;
      }
    } else {
      hasNewContent = true;
    }
  }

  if (hasHashRevert) {
    // FIX v54: Hash span di-reset oleh React → re-apply SEGERA (0ms) untuk mengalahkan reconciliation
    if (replacerTimeout) clearTimeout(replacerTimeout);
    replacerTimeout = setTimeout(() => { jalankanPengganti(false); }, 0);
  } else if (hasRevert) {
    // Re-apply cepat (50ms) untuk reverts biasa yang terdeteksi
    if (replacerTimeout) clearTimeout(replacerTimeout);
    replacerTimeout = setTimeout(() => { jalankanPengganti(false); }, 50);
  } else if (hasNewContent) {
    // Konten baru (elemen baru dimuat): debounce normal 300ms
    if (replacerTimeout) clearTimeout(replacerTimeout);
    replacerTimeout = setTimeout(() => { jalankanPengganti(false); }, 300);
  }
});

// v52.5: IntersectionObserver — tangkap konten lazy-load yang masuk viewport
(function setupIntersectionObserver() {
  if (!("IntersectionObserver" in window)) return;
  let ioTimeout = null;
  const seenElements = new WeakSet();
  const io = new IntersectionObserver((entries) => {
    const hasNewVisible = entries.some(e => e.isIntersecting && !seenElements.has(e.target));
    if (!hasNewVisible) return;
    entries.forEach(e => { if (e.isIntersecting) seenElements.add(e.target); });
    if (ioTimeout) clearTimeout(ioTimeout);
    ioTimeout = setTimeout(() => { try { jalankanPengganti(false); } catch (ex) {} }, 400);
  }, { rootMargin: "200px 0px", threshold: 0.01 });

  function observeContentBlocks() {
    const selectors = [
      "article", "section", ".chapter-content", ".content", ".novel-content",
      ".reading-content", ".chapter-c", ".text-chapter", ".chapter-entity",
      "[class*='chapter']", "[class*='content']", "[class*='novel']", "main p"
    ];
    const visited = new WeakSet();
    selectors.forEach(sel => {
      try {
        document.querySelectorAll(sel).forEach(el => {
          if (!visited.has(el) && !el.closest("#word-replacer-host")) {
            visited.add(el);
            io.observe(el);
          }
        });
      } catch (_) {}
    });
  }

  observeContentBlocks();
  window.addEventListener("awr-urlchange", () => { setTimeout(observeContentBlocks, 800); });
})();

// SPA Navigation Support
(function patchHistoryForSPA() {
  let lastUrl = location.href;

  function onUrlChange() {
    const newUrl = location.href;
    if (newUrl === lastUrl) return;
    lastUrl = newUrl;
    setTimeout(() => { try { jalankanPengganti(true); } catch (e) {} }, 400);
    setTimeout(() => { try { jalankanPengganti(true); } catch (e) {} }, 1200);
  }

  ["pushState", "replaceState"].forEach((method) => {
    const original = history[method];
    history[method] = function () {
      const result = original.apply(this, arguments);
      window.dispatchEvent(new Event("awr-urlchange"));
      return result;
    };
  });

  window.addEventListener("popstate", onUrlChange);
  window.addEventListener("awr-urlchange", onUrlChange);
})();

async function init() {
  if (!document.body) { setTimeout(init, 50); return; }

  try { injectHighlightStyle(); } catch (e) { console.warn("Gagal inject highlight style:", e); }

  try { jalankanPengganti(true); } catch (e) { console.error("Gagal penggantian kata:", e); }
  try { bersihkanRecycleBinOtomatis(); } catch (e) { console.error("Gagal pembersihan Recycle Bin otomatis:", e); }

  const updateMode = GM_getValue("awr_update_mode_v1", "auto");
  if (updateMode === "auto") { checkForUpdates(false); }

  if (window.self === window.top) {
      try {
          const coreAPI = {
              version: SCRIPT_VERSION,
              SVGS: SVGS,
              currentHost: currentHost,
              smartSanitizeTerm: smartSanitizeTerm,
              getNovelBaseDomain: getNovelBaseDomain,
              getCachedNovelTitle: getCachedNovelTitle,
              saveCachedNovelTitle: saveCachedNovelTitle,
              getGitHubToken: getGitHubToken,
              getGistId: getGistId,
              saveGitHubCredentials: saveGitHubCredentials,
              findExistingGist: findExistingGist,
              fetchGistDetails: fetchGistDetails,
              createGist: createGist,
              simpanKeAwan: simpanKeAwan,
              sinkronisasiDariAwan: sinkronisasiDariAwan,
              getActiveNovelId: getActiveNovelId,
              setActiveNovelId: setActiveNovelId,
              getDeletedWords: getDeletedWords,
              saveDeletedWords: saveDeletedWords,
              getKamus: getKamus,
              saveKamus: saveKamus,
              getFilterMode: getFilterMode,
              saveFilterMode: saveFilterMode,
              getTargetDomains: getTargetDomains,
              saveTargetDomains: saveTargetDomains,
              getBlacklistDomains: getBlacklistDomains,
              saveBlacklistDomains: saveBlacklistDomains,
              getHighlightAktif: getHighlightAktif,
              saveHighlightAktif: saveHighlightAktif,
              isDomainAllowed: isDomainAllowed,
              jalankanPengganti: jalankanPengganti,
              getNovelContext: getNovelContext,
              checkForUpdates: checkForUpdates,
              exportCredentials: exportCredentials,
              mergeKamus: mergeKamus,
              perbaikiIdNovel: perbaikiIdNovel,
              getSiteKataUnikSelector: getSiteKataUnikSelector
          };
          window.AWR_UI_LIBRARY.init(coreAPI);
      } catch (e) { console.error("Gagal memuat Floating UI Library:", e); }

      try { await sinkronisasiDariAwan(true); } catch (e) { console.error("Gagal sinkronisasi awan Gist:", e); }
  }
}

init();
})();