Greasy Fork

Greasy Fork is available in English.

Discord Link Copy Utility

专为 Discord 分享设计的链接转换工具,支持 Twitter/X、Reddit、Instagram、TikTok、Threads、Pixiv、Bilibili、Facebook、Tumblr、YouTube Shorts、Bluesky 等平台。支持原始与嵌入域名互转,双击 ` 键或自定义快捷键触发:只有一个转换目标时直接复制,多个目标时开启选择面板。自动移除追踪参数,支持自定义域名规则。

当前为 2026-04-21 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              Discord  Link Copy Utility
// @namespace         https://github.com/Startanuki07?tab=repositories
// @version           1.3
// @license           MIT
// @author            Star_tanuki07
// @description       Converts social media links (Twitter/X, Reddit, Instagram, TikTok, Threads, Pixiv, Bilibili, Facebook, Tumblr, YouTube Shorts, Bluesky) into embed-friendly formats for Discord sharing. Supports mutual conversion between original and embed domains. Double-press ` (backtick) or use a custom shortcut — single target copies instantly, multiple targets open a selection panel. Strips tracking parameters and supports custom domain rules.
// @description:zh-TW 專為 Discord 分享設計的連結轉換工具,支援 Twitter/X、Reddit、Instagram、TikTok、Threads、Pixiv、Bilibili、Facebook、Tumblr、YouTube Shorts、Bluesky 等平台。支援原始與嵌入網域互轉,雙擊 ` 鍵或自訂快捷鍵觸發:只有一個轉換目標時直接複製,多個目標時開啟選擇面板。自動移除追蹤參數,支援自訂域名規則。
// @description:zh-CN 专为 Discord 分享设计的链接转换工具,支持 Twitter/X、Reddit、Instagram、TikTok、Threads、Pixiv、Bilibili、Facebook、Tumblr、YouTube Shorts、Bluesky 等平台。支持原始与嵌入域名互转,双击 ` 键或自定义快捷键触发:只有一个转换目标时直接复制,多个目标时开启选择面板。自动移除追踪参数,支持自定义域名规则。
// @description:ja    Discord シェア向けリンク変換ツール。Twitter/X・Reddit・Instagram・TikTok・Threads・Pixiv・Bilibili・Tumblr・Bluesky を埋め込み対応形式に変換。元ドメイン⇔埋め込みドメイン間の相互変換に対応。` キー連打またはカスタムショートカットで起動:変換先が1つなら即コピー、複数なら選択パネルを表示。追跡パラメータを自動除去。
// @description:ko    Discord 공유용 링크 변환 도구. Twitter/X, Reddit, Instagram, TikTok, Threads, Pixiv, Bilibili, Tumblr, Bluesky URL을 임베드 지원 형식으로 변환합니다. ` 키 두 번 또는 커스텀 단축키로 동작: 변환 대상이 하나면 즉시 복사, 여러 개면 선택 패널 표시. 추적 파라미터 자동 제거.
// @description:es    Herramienta de conversión de enlaces para Discord. Convierte URLs de Twitter/X, Reddit, Instagram, TikTok, Threads, Pixiv, Bilibili, Facebook, Tumblr y Bluesky a formatos con vista previa. Presiona ` dos veces o usa un atajo: si hay un destino, copia directo; si hay varios, abre el panel.
// @description:pt-BR Ferramenta de conversão de links para Discord. Convierte URLs do Twitter/X, Reddit, Instagram, TikTok, Threads, Pixiv, Bilibili, Facebook, Tumblr e Bluesky para formatos com pré-visualização. Pressione ` duas vezes ou use atalho: um destino copia direto, vários abre o painel.
// @description:fr    Outil de conversion de liens pour Discord. Convertit les URLs Twitter/X, Reddit, Instagram, TikTok, Threads, Pixiv, Bilibili, Facebook, Tumblr et Bluesky en formats avec aperçu intégré. ` deux fois ou raccourci : une cible = copie directe, plusieurs = panneau de sélection.
// @description:ru    Инструмент конвертации ссылок для Discord. Преобразует ссылки Twitter/X, Reddit, Instagram, TikTok, Threads, Pixiv, Bilibili, Facebook, Tumblr и Bluesky в форматы с предпросмотром. Двойное ` или ярлык: один вариант = сразу копирует, несколько = открывает панель выбора.
// @icon              https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @match             *://*/*
// @grant             GM_addStyle
// @grant             GM_setClipboard
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// ==/UserScript==

(function () {
  "use strict";

  const DEFAULT_CONFIG = {
    triggerKey: "`",
    shortcut: null,
    doublePressDelay: 300,
    lang: null,
  };

  const LANG_LABELS = {
    en: "EN",
    "zh-TW": "TW",
    "zh-CN": "CN",
    ja: "JA",
    ko: "KO",
    es: "ES",
    "pt-BR": "PT",
    fr: "FR",
    ru: "RU",
    __custom: "🌐",
  };

  const TRANSLATIONS = {
    en: {
      title: "Link Fixer",
      orig: "Original",
      orig_clean: "Original (Cleaned)",
      to: "to",
      add_rule_prompt: "➕ Add Rule for Site",
      manage_btn: "⚙️ Settings",
      settings_title: "Settings & Rules",
      sec_rules: "Custom Rules",
      sec_keys: "Key Bindings",
      ph_src: "Source Domain (e.g., youtube.com)",
      ph_tgt: "Target Domain (e.g., yewtu.be)",
      btn_add: "Add",
      btn_back: "← Back",
      lbl_trigger: "Double-Press Trigger",
      lbl_shortcut: "Global Shortcut",
      empty_rules: "No custom rules",
      rec_press: "Press any key... (Esc to cancel)",
      rec_none: "None (Click to set)",
      toast_copied: "Copied",
      toast_added: "Rule Added",
      toast_key_updated: "Key Updated",
      toast_key_cleared: "Shortcut Cleared",
      desc_trigger: "Double-press this key anywhere outside a text box. If the page has exactly one conversion target, it copies directly — no panel needed. If there are multiple targets, the panel opens so you can choose.",
      desc_shortcut: "Set a key combo (e.g. Ctrl+Shift+L) to trigger the same smart-copy logic. Press Backspace or Delete while recording to clear the shortcut.",
      desc_rules: "Rules are matched by domain. Add a source → target pair to convert any link on that domain. Your custom rules are applied on top of the built-in ones.",
      sec_prefs: "Site Preferences",
      pref_empty: "No preferences set",
      badge_last: "Last used",
      lbl_always_direct: "Always use this format for this site (skip panel)",
      pre_copied_hint: "Pre-copied (last used):",
      toast_pref_saved: "Preference saved",
      toast_pref_cleared: "Preference cleared",
      lbl_toast_jump: "Copy Toast Jump",
      desc_toast_jump: "When enabled, click or hover the copy notification to open a quick-jump list. Each entry opens in a new tab.",
      lbl_toast_action: "Trigger",
      opt_click: "Click",
      opt_hover: "Hover",
      sec_jump_links: "Jump Links",
      ph_link_name: "Site name",
      ph_link_url: "URL (https://...)",
      btn_add_link: "Add Link",
      empty_links: "No jump links",
      toast_link_added: "Link added",
      toast_link_deleted: "Deleted",
      lbl_edit: "Edit",
      lbl_done: "Done",
      lbl_cancel: "Cancel",
    },
    "zh-TW": {
      title: "連結轉換工具",
      orig: "原始連結",
      orig_clean: "原始連結 (已淨化)",
      to: "轉為",
      add_rule_prompt: "➕ 為此網站新增規則",
      manage_btn: "⚙️ 設定與規則",
      settings_title: "設定與規則管理",
      sec_rules: "自定義規則管理",
      sec_keys: "按鍵偏好設定",
      ph_src: "來源網域 (如: youtube.com)",
      ph_tgt: "目標網域 (如: yewtu.be)",
      btn_add: "新增",
      btn_back: "← 返回",
      lbl_trigger: "連點觸發鍵",
      lbl_shortcut: "快捷鍵",
      empty_rules: "無自定義規則",
      rec_press: "請按下按鍵... (Esc 取消)",
      rec_none: "無 (點擊設定)",
      toast_copied: "已複製",
      toast_added: "規則已新增",
      toast_key_updated: "按鍵已更新",
      toast_key_cleared: "快捷鍵已清除",
      desc_trigger: "在任意非輸入框的頁面上快速連按兩下此鍵。若當前頁面只有一個轉換目標,會直接複製到剪貼簿,無需開啟面板;若有多個目標則開啟面板讓你選擇。",
      desc_shortcut: "設定一組組合鍵(如 Ctrl+Shift+L)來觸發相同的智慧複製邏輯。錄製中按 Backspace 或 Delete 可清除快捷鍵設定。",
      desc_rules: "規則以網域為單位進行比對。新增來源 → 目標網域對,即可在該網域的任意頁面上進行連結轉換。自訂規則會疊加在內建規則之上。",
      sec_prefs: "網站偏好設定",
      pref_empty: "尚未設定任何偏好",
      badge_last: "上次使用",
      lbl_always_direct: "此網站預設直接複製此格式(不再彈出選單)",
      pre_copied_hint: "已預先複製(上次使用):",
      toast_pref_saved: "偏好已儲存",
      toast_pref_cleared: "偏好已清除",
      lbl_toast_jump: "複製提示跳轉",
      desc_toast_jump: "啟用後,點擊或將滑鼠移入複製通知時,會彈出已設定的跳轉連結清單,點擊可在新分頁開啟對應網站。",
      lbl_toast_action: "觸發方式",
      opt_click: "點擊",
      opt_hover: "滑鼠移入",
      sec_jump_links: "跳轉連結",
      ph_link_name: "網站名稱",
      ph_link_url: "網址 (https://...)",
      btn_add_link: "新增連結",
      empty_links: "尚無跳轉連結",
      toast_link_added: "連結已新增",
      toast_link_deleted: "已刪除",
      lbl_edit: "編輯",
      lbl_done: "完成",
      lbl_cancel: "取消",
    },
    "zh-CN": {
      title: "链接转换工具",
      orig: "原始链接",
      orig_clean: "原始链接 (已净化)",
      to: "转为",
      add_rule_prompt: "➕ 为此网站新增规则",
      manage_btn: "⚙️ 设置与规则",
      settings_title: "设置与规则管理",
      sec_rules: "自定义规则管理",
      sec_keys: "按键偏好设置",
      ph_src: "来源域名 (如: youtube.com)",
      ph_tgt: "目标域名 (如: yewtu.be)",
      btn_add: "新增",
      btn_back: "← 返回",
      lbl_trigger: "连击触发键",
      lbl_shortcut: "快捷键",
      empty_rules: "无自定义规则",
      rec_press: "请按下按键... (Esc 取消)",
      rec_none: "无 (点击设置)",
      toast_copied: "已复制",
      toast_added: "规则已新增",
      toast_key_updated: "按键已更新",
      toast_key_cleared: "快捷键已清除",
      desc_trigger: "在任意非输入框的页面上快速连按两下此键。若当前页面只有一个转换目标,将直接复制到剪贴板,无需打开面板;若有多个目标则打开面板让你选择。",
      desc_shortcut: "设置一组组合键(如 Ctrl+Shift+L)触发相同的智能复制逻辑。录制中按 Backspace 或 Delete 可清除快捷键设置。",
      desc_rules: "规则以域名为单位进行匹配。添加来源 → 目标域名对,即可在该域名的任意页面上进行链接转换。自定义规则将叠加在内置规则之上。",
      sec_prefs: "网站偏好设置",
      pref_empty: "尚未设置任何偏好",
      badge_last: "上次使用",
      lbl_always_direct: "此网站预设直接复制此格式(不再弹出选单)",
      pre_copied_hint: "已预先复制(上次使用):",
      toast_pref_saved: "偏好已保存",
      toast_pref_cleared: "偏好已清除",
      lbl_toast_jump: "复制提示跳转",
      desc_toast_jump: "启用后,点击或将鼠标移入复制通知时,会弹出已设置的跳转链接列表,点击可在新标签页打开对应网站。",
      lbl_toast_action: "触发方式",
      opt_click: "点击",
      opt_hover: "鼠标移入",
      sec_jump_links: "跳转链接",
      ph_link_name: "网站名称",
      ph_link_url: "网址 (https://...)",
      btn_add_link: "新增链接",
      empty_links: "暂无跳转链接",
      toast_link_added: "链接已新增",
      toast_link_deleted: "已删除",
      lbl_edit: "编辑",
      lbl_done: "完成",
      lbl_cancel: "取消",
    },
    ja: {
      title: "リンク変換ツール",
      orig: "元のリンク",
      orig_clean: "元のリンク (浄化済)",
      to: "変換",
      add_rule_prompt: "➕ このサイトのルールを追加",
      manage_btn: "⚙️ 設定",
      settings_title: "設定とルール管理",
      sec_rules: "カスタムルール",
      sec_keys: "キー設定",
      ph_src: "ソースドメイン (例: youtube.com)",
      ph_tgt: "ターゲットドメイン (例: yewtu.be)",
      btn_add: "追加",
      btn_back: "← 戻る",
      lbl_trigger: "連打トリガー",
      lbl_shortcut: "ショートカット",
      empty_rules: "ルールなし",
      rec_press: "キーを押してください... (Escで取消)",
      rec_none: "なし (クリック設定)",
      toast_copied: "コピーしました",
      toast_added: "ルールを追加しました",
      toast_key_updated: "キーを更新しました",
      toast_key_cleared: "ショートカットを削除しました",
      desc_trigger: "テキスト入力欄の外ならどこでも、このキーをすばやく2回押すと動作します。変換先が1つだけの場合は直接クリップボードにコピーされ、複数ある場合はパネルが開きます。",
      desc_shortcut: "キーの組み合わせ(例:Ctrl+Shift+L)を設定して、同じスマートコピー機能を呼び出せます。記録中に Backspace または Delete を押すとショートカットをクリアできます。",
      desc_rules: "ルールはドメイン単位でマッチングされます。ソース → ターゲットのドメインペアを追加すると、そのドメインの任意のページでリンク変換が適用されます。",
      sec_prefs: "サイト設定",
      pref_empty: "設定なし",
      badge_last: "最後に使用",
      lbl_always_direct: "このサイトは常にこの形式を使用(パネルを開かない)",
      pre_copied_hint: "事前コピー済み(最後に使用):",
      toast_pref_saved: "設定を保存しました",
      toast_pref_cleared: "設定をクリアしました",
      lbl_toast_jump: "コピー通知ジャンプ",
      desc_toast_jump: "有効にすると、コピー通知をクリックまたはホバーでジャンプリストを表示し、クリックで新しいタブを開きます。",
      lbl_toast_action: "トリガー",
      opt_click: "クリック",
      opt_hover: "ホバー",
      sec_jump_links: "ジャンプリンク",
      ph_link_name: "サイト名",
      ph_link_url: "URL (https://...)",
      btn_add_link: "追加",
      empty_links: "ジャンプリンクなし",
      toast_link_added: "リンクを追加しました",
      toast_link_deleted: "削除しました",
      lbl_edit: "編集",
      lbl_done: "完了",
      lbl_cancel: "キャンセル",
    },
    ko: {
      title: "링크 변환 도구",
      orig: "원본 링크",
      orig_clean: "원본 링크 (정화됨)",
      to: "변환",
      add_rule_prompt: "➕ 이 사이트에 규칙 추가",
      manage_btn: "⚙️ 설정 및 규칙",
      settings_title: "설정 및 규칙 관리",
      sec_rules: "사용자 정의 규칙",
      sec_keys: "단축키 설정",
      ph_src: "소스 도메인 (예: youtube.com)",
      ph_tgt: "타겟 도메인 (예: yewtu.be)",
      btn_add: "추가",
      btn_back: "← 뒤로",
      lbl_trigger: "연타 트리거 키",
      lbl_shortcut: "단축키",
      empty_rules: "사용자 정의 규칙 없음",
      rec_press: "키를 누르세요... (Esc 취소)",
      rec_none: "없음 (클릭하여 설정)",
      toast_copied: "복사됨",
      toast_added: "규칙이 추가됨",
      toast_key_updated: "키 설정 업데이트됨",
      toast_key_cleared: "단축키 해제됨",
      desc_trigger: "텍스트 입력창 외부에서 이 키를 빠르게 두 번 누르면 동작합니다. 변환 대상이 하나뿐이면 바로 복사되고, 여러 개면 패널이 열립니다.",
      desc_shortcut: "키 조합(예: Ctrl+Shift+L)을 설정하여 동일한 스마트 복사 기능을 사용할 수 있습니다. 녹화 중 Backspace 또는 Delete를 누르면 단축키를 초기화합니다.",
      desc_rules: "규칙은 도메인 단위로 매칭됩니다. 소스 → 대상 도메인 쌍을 추가하면 해당 도메인의 모든 페이지에서 링크 변환이 적용됩니다.",
      sec_prefs: "사이트 환경설정",
      pref_empty: "설정된 환경설정 없음",
      badge_last: "마지막 사용",
      lbl_always_direct: "이 사이트는 항상 이 형식으로 복사 (패널 생략)",
      pre_copied_hint: "사전 복사됨 (마지막 사용):",
      toast_pref_saved: "환경설정 저장됨",
      toast_pref_cleared: "환경설정 해제됨",
      lbl_toast_jump: "복사 알림 점프",
      desc_toast_jump: "활성화하면 복사 알림을 클릭하거나 호버하면 점프 링크 목록이 표시됩니다.",
      lbl_toast_action: "트리거",
      opt_click: "클릭",
      opt_hover: "호버",
      sec_jump_links: "점프 링크",
      ph_link_name: "사이트 이름",
      ph_link_url: "URL (https://...)",
      btn_add_link: "링크 추가",
      empty_links: "점프 링크 없음",
      toast_link_added: "링크 추가됨",
      toast_link_deleted: "삭제됨",
      lbl_edit: "편집",
      lbl_done: "완료",
      lbl_cancel: "취소",
    },
    es: {
      title: "Convertidor de enlaces",
      orig: "Enlace original",
      orig_clean: "Enlace original (limpio)",
      to: "a",
      add_rule_prompt: "➕ Agregar regla para este sitio",
      manage_btn: "⚙️ Ajustes",
      settings_title: "Ajustes y reglas",
      sec_rules: "Reglas personalizadas",
      sec_keys: "Teclas de acceso",
      ph_src: "Dominio origen (ej: youtube.com)",
      ph_tgt: "Dominio destino (ej: yewtu.be)",
      btn_add: "Agregar",
      btn_back: "← Volver",
      lbl_trigger: "Tecla de doble pulsación",
      lbl_shortcut: "Atajo global",
      empty_rules: "Sin reglas personalizadas",
      rec_press: "Presiona una tecla... (Esc para cancelar)",
      rec_none: "Ninguna (clic para configurar)",
      toast_copied: "Copiado",
      toast_added: "Regla agregada",
      toast_key_updated: "Tecla actualizada",
      toast_key_cleared: "Atajo eliminado",
      desc_trigger: "Presiona esta tecla dos veces rápidamente fuera de cualquier cuadro de texto. Si hay un solo destino de conversión, se copia directamente; si hay varios, se abre el panel.",
      desc_shortcut: "Configura una combinación de teclas (ej.: Ctrl+Shift+L) para la misma lógica de copia inteligente. Presiona Retroceso o Supr mientras grabas para limpiar el atajo.",
      desc_rules: "Las reglas se comparan por dominio. Agrega un par fuente → destino para convertir cualquier enlace en ese dominio. Las reglas personalizadas se suman a las integradas.",
      sec_prefs: "Preferencias de sitio",
      pref_empty: "Sin preferencias configuradas",
      badge_last: "Último uso",
      lbl_always_direct: "Usar siempre este formato para este sitio (sin panel)",
      pre_copied_hint: "Precopiado (último uso):",
      toast_pref_saved: "Preferencia guardada",
      toast_pref_cleared: "Preferencia borrada",
      lbl_toast_jump: "Salto de notificación",
      desc_toast_jump: "Al activar, clic o hover en la notificación abre la lista de saltos.",
      lbl_toast_action: "Disparador",
      opt_click: "Clic",
      opt_hover: "Hover",
      sec_jump_links: "Enlaces de salto",
      ph_link_name: "Nombre del sitio",
      ph_link_url: "URL (https://...)",
      btn_add_link: "Agregar enlace",
      empty_links: "Sin enlaces de salto",
      toast_link_added: "Enlace agregado",
      toast_link_deleted: "Eliminado",
      lbl_edit: "Editar",
      lbl_done: "Listo",
      lbl_cancel: "Cancelar",
    },
    "pt-BR": {
      title: "Conversor de links",
      orig: "Link original",
      orig_clean: "Link original (limpo)",
      to: "para",
      add_rule_prompt: "➕ Adicionar regra para este site",
      manage_btn: "⚙️ Configurações",
      settings_title: "Configurações e regras",
      sec_rules: "Regras personalizadas",
      sec_keys: "Teclas de atalho",
      ph_src: "Domínio de origem (ex: youtube.com)",
      ph_tgt: "Domínio de destino (ex: yewtu.be)",
      btn_add: "Adicionar",
      btn_back: "← Voltar",
      lbl_trigger: "Tecla de duplo toque",
      lbl_shortcut: "Atalho global",
      empty_rules: "Nenhuma regra personalizada",
      rec_press: "Pressione uma tecla... (Esc para cancelar)",
      rec_none: "Nenhuma (clique para configurar)",
      toast_copied: "Copiado",
      toast_added: "Regra adicionada",
      toast_key_updated: "Tecla atualizada",
      toast_key_cleared: "Atalho removido",
      desc_trigger: "Pressione esta tecla duas vezes rapidamente fora de qualquer caixa de texto. Se houver apenas um destino de conversão, ele é copiado diretamente; se houver vários, o painel é aberto.",
      desc_shortcut: "Configure uma combinação de teclas (ex.: Ctrl+Shift+L) para a mesma lógica de cópia inteligente. Pressione Backspace ou Delete durante a gravação para limpar o atalho.",
      desc_rules: "As regras são correspondidas por domínio. Adicione um par origem → destino para converter qualquer link nesse domínio. As regras personalizadas são aplicadas sobre as integradas.",
      sec_prefs: "Preferências de site",
      pref_empty: "Sem preferências definidas",
      badge_last: "Último uso",
      lbl_always_direct: "Usar sempre este formato (sem painel)",
      pre_copied_hint: "Pré-copiado (último uso):",
      toast_pref_saved: "Preferência salva",
      toast_pref_cleared: "Preferência removida",
      lbl_toast_jump: "Salto de notificação",
      desc_toast_jump: "Ao ativar, clicar ou passar o mouse na notificação abre a lista de saltos.",
      lbl_toast_action: "Gatilho",
      opt_click: "Clique",
      opt_hover: "Hover",
      sec_jump_links: "Links de salto",
      ph_link_name: "Nome do site",
      ph_link_url: "URL (https://...)",
      btn_add_link: "Adicionar link",
      empty_links: "Sem links de salto",
      toast_link_added: "Link adicionado",
      toast_link_deleted: "Removido",
      lbl_edit: "Editar",
      lbl_done: "Concluir",
      lbl_cancel: "Cancelar",
    },
    fr: {
      title: "Convertisseur de liens",
      orig: "Lien original",
      orig_clean: "Lien original (nettoyé)",
      to: "vers",
      add_rule_prompt: "➕ Ajouter une règle pour ce site",
      manage_btn: "⚙️ Paramètres",
      settings_title: "Paramètres et règles",
      sec_rules: "Règles personnalisées",
      sec_keys: "Raccourcis clavier",
      ph_src: "Domaine source (ex : youtube.com)",
      ph_tgt: "Domaine cible (ex : yewtu.be)",
      btn_add: "Ajouter",
      btn_back: "← Retour",
      lbl_trigger: "Touche double-pression",
      lbl_shortcut: "Raccourci global",
      empty_rules: "Aucune règle personnalisée",
      rec_press: "Appuyez sur une touche… (Échap pour annuler)",
      rec_none: "Aucun (cliquer pour configurer)",
      toast_copied: "Copié",
      toast_added: "Règle ajoutée",
      toast_key_updated: "Touche mise à jour",
      toast_key_cleared: "Raccourci supprimé",
      desc_trigger: "Appuyez deux fois rapidement sur cette touche en dehors de tout champ de texte. S'il n'y a qu'une seule cible de conversion, elle est copiée directement ; sinon, le panneau s'ouvre.",
      desc_shortcut: "Définissez une combinaison de touches (ex. : Ctrl+Shift+L) pour la même logique de copie intelligente. Appuyez sur Retour arrière ou Suppr pendant l'enregistrement pour effacer le raccourci.",
      desc_rules: "Les règles sont associées par domaine. Ajoutez une paire source → cible pour convertir tout lien de ce domaine. Les règles personnalisées s'ajoutent aux règles intégrées.",
      sec_prefs: "Préférences de site",
      pref_empty: "Aucune préférence définie",
      badge_last: "Dernier utilisé",
      lbl_always_direct: "Toujours utiliser ce format (sans panneau)",
      pre_copied_hint: "Pré-copié (dernier utilisé) :",
      toast_pref_saved: "Préférence enregistrée",
      toast_pref_cleared: "Préférence effacée",
      lbl_toast_jump: "Saut de notification",
      desc_toast_jump: "En activant, cliquez ou survolez la notification pour ouvrir la liste de sauts.",
      lbl_toast_action: "Déclencheur",
      opt_click: "Clic",
      opt_hover: "Survol",
      sec_jump_links: "Liens de saut",
      ph_link_name: "Nom du site",
      ph_link_url: "URL (https://...)",
      btn_add_link: "Ajouter un lien",
      empty_links: "Aucun lien de saut",
      toast_link_added: "Lien ajouté",
      toast_link_deleted: "Supprimé",
      lbl_edit: "Modifier",
      lbl_done: "Terminer",
      lbl_cancel: "Annuler",
    },
    ru: {
      title: "Конвертер ссылок",
      orig: "Исходная ссылка",
      orig_clean: "Исходная ссылка (очищена)",
      to: "в",
      add_rule_prompt: "➕ Добавить правило для сайта",
      manage_btn: "⚙️ Настройки",
      settings_title: "Настройки и правила",
      sec_rules: "Пользовательские правила",
      sec_keys: "Горячие клавиши",
      ph_src: "Исходный домен (напр.: youtube.com)",
      ph_tgt: "Целевой домен (напр.: yewtu.be)",
      btn_add: "Добавить",
      btn_back: "← Назад",
      lbl_trigger: "Клавиша двойного нажатия",
      lbl_shortcut: "Глобальный ярлык",
      empty_rules: "Нет пользовательских правил",
      rec_press: "Нажмите клавишу… (Esc для отмены)",
      rec_none: "Нет (нажмите для настройки)",
      toast_copied: "Скопировано",
      toast_added: "Правило добавлено",
      toast_key_updated: "Клавиша обновлена",
      toast_key_cleared: "Ярлык удалён",
      desc_trigger: "Дважды быстро нажмите эту клавишу вне текстового поля. Если есть только одна цель преобразования, ссылка копируется напрямую; если несколько — открывается панель.",
      desc_shortcut: "Задайте комбинацию клавиш (напр., Ctrl+Shift+L) для той же логики умного копирования. Нажмите Backspace или Delete во время записи, чтобы сбросить ярлык.",
      desc_rules: "Правила сопоставляются по домену. Добавьте пару источник → цель, чтобы конвертировать любые ссылки на этом домене. Пользовательские правила применяются поверх встроенных.",
      sec_prefs: "Настройки сайта",
      pref_empty: "Настройки не заданы",
      badge_last: "Последнее использование",
      lbl_always_direct: "Всегда использовать этот формат (без панели)",
      pre_copied_hint: "Предварительно скопировано (последнее использование):",
      toast_pref_saved: "Настройка сохранена",
      toast_pref_cleared: "Настройка сброшена",
      lbl_toast_jump: "Прыжок из уведомления",
      desc_toast_jump: "При активации клик или наведение на уведомление открывает список переходов.",
      lbl_toast_action: "Триггер",
      opt_click: "Клик",
      opt_hover: "Наведение",
      sec_jump_links: "Ссылки для перехода",
      ph_link_name: "Название сайта",
      ph_link_url: "URL (https://...)",
      btn_add_link: "Добавить ссылку",
      empty_links: "Нет ссылок для перехода",
      toast_link_added: "Ссылка добавлена",
      toast_link_deleted: "Удалено",
      lbl_edit: "Изменить",
      lbl_done: "Готово",
      lbl_cancel: "Отмена",
    },
  };

  const CUSTOM_LANG_INSTRUCTIONS = `1. Click 「📤 Export」 to download the JSON template.
2. Open the file and translate only the VALUES (not the keys).
3. Set the "name" field to your language name.
4. Click 「📥 Import」 to apply.

English:    Export → translate the values → Import
Deutsch:    Exportieren → Werte übersetzen → Importieren
Français:   Exporter → traduire les valeurs → Importer
Español:    Exportar → traducir los valores → Importar
Italiano:   Esporta → traduci i valori → Importa
Português:  Exportar → traduzir os valores → Importar
Русский:    Экспорт → перевести значения → Импорт
Українська: Експорт → перекласти значення → Імпорт
ภาษาไทย:   ส่งออก → แปลค่า → นำเข้า
Türkçe:     Dışa aktar → değerleri çevir → İçe aktar
Polski:     Eksportuj → przetłumacz wartości → Importuj
Čeština:    Exportovat → přeložit hodnoty → Importovat
Română:     Exportați → traduceți valorile → Importați
Magyar:     Exportálás → értékek fordítása → Importálás
Ελληνικά:   Εξαγωγή → μετάφραση τιμών → Εισαγωγή
العربية:    تصدير ← ترجمة القيم ← استيراد
עברית:      ייצוא ← תרגום הערכים ← ייבוא
فارسی:      صادر کردن ← ترجمه مقادیر ← وارد کردن
हिन्दी:     निर्यात → मान अनुवाद करें → आयात
বাংলা:      রপ্তানি → মান অনুবাদ করুন → আমদানি
Indonesia:  Ekspor → terjemahkan nilai → Impor
Melayu:     Eksport → terjemah nilai → Import
Filipino:   I-export → isalin ang mga halaga → I-import
Tiếng Việt: Xuất → dịch các giá trị → Nhập
Nederlands: Exporteren → waarden vertalen → Importeren
Svenska:    Exportera → översätt värdena → Importera
Kiswahili:  Hamisha → tafsiri maadili → Ingiza`;

  const DEFAULT_RULES = [
    { source: "twitter.com", target: "fxtwitter.com", label: "fxtwitter" },
    { source: "twitter.com", target: "vxtwitter.com", label: "vxtwitter" },
    { source: "twitter.com", target: "fixupx.com", label: "fixupx" },
    { source: "twitter.com", target: "cunnyx.com", label: "cunnyx" },
    { source: "twitter.com", target: "fixvx.com", label: "fixvx" },
    { source: "x.com", target: "fxtwitter.com", label: "fxtwitter" },
    { source: "x.com", target: "vxtwitter.com", label: "vxtwitter" },
    { source: "x.com", target: "fixupx.com", label: "fixupx" },
    { source: "x.com", target: "cunnyx.com", label: "cunnyx" },
    { source: "x.com", target: "fixvx.com", label: "fixvx" },
    { source: "fxtwitter.com", target: "x.com", label: "x.com" },
    { source: "vxtwitter.com", target: "x.com", label: "x.com" },
    { source: "fixupx.com", target: "x.com", label: "x.com" },
    { source: "cunnyx.com", target: "x.com", label: "x.com" },
    { source: "fixvx.com", target: "x.com", label: "x.com" },
    { source: "fxtwitter.com", target: "vxtwitter.com", label: "vxtwitter" },
    { source: "fxtwitter.com", target: "fixupx.com", label: "fixupx" },
    { source: "fxtwitter.com", target: "cunnyx.com", label: "cunnyx" },
    { source: "fxtwitter.com", target: "fixvx.com", label: "fixvx" },
    { source: "vxtwitter.com", target: "fxtwitter.com", label: "fxtwitter" },
    { source: "vxtwitter.com", target: "fixupx.com", label: "fixupx" },
    { source: "vxtwitter.com", target: "cunnyx.com", label: "cunnyx" },
    { source: "vxtwitter.com", target: "fixvx.com", label: "fixvx" },
    { source: "fixupx.com", target: "fxtwitter.com", label: "fxtwitter" },
    { source: "fixupx.com", target: "vxtwitter.com", label: "vxtwitter" },
    { source: "fixupx.com", target: "cunnyx.com", label: "cunnyx" },
    { source: "fixupx.com", target: "fixvx.com", label: "fixvx" },
    { source: "cunnyx.com", target: "fxtwitter.com", label: "fxtwitter" },
    { source: "cunnyx.com", target: "vxtwitter.com", label: "vxtwitter" },
    { source: "cunnyx.com", target: "fixupx.com", label: "fixupx" },
    { source: "cunnyx.com", target: "fixvx.com", label: "fixvx" },
    { source: "fixvx.com", target: "fxtwitter.com", label: "fxtwitter" },
    { source: "fixvx.com", target: "vxtwitter.com", label: "vxtwitter" },
    { source: "fixvx.com", target: "fixupx.com", label: "fixupx" },
    { source: "fixvx.com", target: "cunnyx.com", label: "cunnyx" },

    { source: "reddit.com", target: "old.reddit.com", label: "old.reddit" },
    { source: "reddit.com", target: "vxreddit.com", label: "vxreddit" },
    { source: "reddit.com", target: "rxddit.com", label: "rxddit" },
    { source: "reddit.com", target: "rxyddit.com", label: "rxyddit" },
    { source: "old.reddit.com", target: "reddit.com", label: "reddit" },
    { source: "old.reddit.com", target: "vxreddit.com", label: "vxreddit" },
    { source: "old.reddit.com", target: "rxddit.com", label: "rxddit" },
    { source: "old.reddit.com", target: "rxyddit.com", label: "rxyddit" },
    { source: "vxreddit.com", target: "reddit.com", label: "reddit" },
    { source: "vxreddit.com", target: "old.reddit.com", label: "old.reddit" },
    { source: "vxreddit.com", target: "rxddit.com", label: "rxddit" },
    { source: "vxreddit.com", target: "rxyddit.com", label: "rxyddit" },
    { source: "rxddit.com", target: "reddit.com", label: "reddit" },
    { source: "rxddit.com", target: "vxreddit.com", label: "vxreddit" },
    { source: "rxddit.com", target: "old.reddit.com", label: "old.reddit" },
    { source: "rxddit.com", target: "rxyddit.com", label: "rxyddit" },
    { source: "rxyddit.com", target: "reddit.com", label: "reddit" },
    { source: "rxyddit.com", target: "vxreddit.com", label: "vxreddit" },
    { source: "rxyddit.com", target: "rxddit.com", label: "rxddit" },
    { source: "rxyddit.com", target: "old.reddit.com", label: "old.reddit" },

    { source: "instagram.com",    target: "kkinstagram.com",  label: "kkinstagram"  },
    { source: "instagram.com",    target: "vxinstagram.com",  label: "vxinstagram"  },
    { source: "instagram.com",    target: "ddinstagram.com",  label: "ddinstagram"  },
    { source: "instagram.com",    target: "uuinstagram.com",  label: "uuinstagram"  },
    { source: "instagram.com",    target: "eeinstagram.com",  label: "eeinstagram"  },
    { source: "instagram.com",    target: "instagramez.com",  label: "InstagramEZ"  },
    { source: "kkinstagram.com",  target: "instagram.com",    label: "instagram"    },
    { source: "kkinstagram.com",  target: "vxinstagram.com",  label: "vxinstagram"  },
    { source: "kkinstagram.com",  target: "ddinstagram.com",  label: "ddinstagram"  },
    { source: "kkinstagram.com",  target: "uuinstagram.com",  label: "uuinstagram"  },
    { source: "kkinstagram.com",  target: "eeinstagram.com",  label: "eeinstagram"  },
    { source: "kkinstagram.com",  target: "instagramez.com",  label: "InstagramEZ"  },
    { source: "vxinstagram.com",  target: "instagram.com",    label: "instagram"    },
    { source: "vxinstagram.com",  target: "kkinstagram.com",  label: "kkinstagram"  },
    { source: "vxinstagram.com",  target: "ddinstagram.com",  label: "ddinstagram"  },
    { source: "vxinstagram.com",  target: "uuinstagram.com",  label: "uuinstagram"  },
    { source: "vxinstagram.com",  target: "eeinstagram.com",  label: "eeinstagram"  },
    { source: "vxinstagram.com",  target: "instagramez.com",  label: "InstagramEZ"  },
    { source: "ddinstagram.com",  target: "instagram.com",    label: "instagram"    },
    { source: "ddinstagram.com",  target: "kkinstagram.com",  label: "kkinstagram"  },
    { source: "ddinstagram.com",  target: "vxinstagram.com",  label: "vxinstagram"  },
    { source: "ddinstagram.com",  target: "uuinstagram.com",  label: "uuinstagram"  },
    { source: "ddinstagram.com",  target: "eeinstagram.com",  label: "eeinstagram"  },
    { source: "ddinstagram.com",  target: "instagramez.com",  label: "InstagramEZ"  },
    { source: "uuinstagram.com",  target: "instagram.com",    label: "instagram"    },
    { source: "uuinstagram.com",  target: "kkinstagram.com",  label: "kkinstagram"  },
    { source: "uuinstagram.com",  target: "vxinstagram.com",  label: "vxinstagram"  },
    { source: "uuinstagram.com",  target: "ddinstagram.com",  label: "ddinstagram"  },
    { source: "uuinstagram.com",  target: "eeinstagram.com",  label: "eeinstagram"  },
    { source: "uuinstagram.com",  target: "instagramez.com",  label: "InstagramEZ"  },
    { source: "eeinstagram.com",  target: "instagram.com",    label: "instagram"    },
    { source: "eeinstagram.com",  target: "kkinstagram.com",  label: "kkinstagram"  },
    { source: "eeinstagram.com",  target: "vxinstagram.com",  label: "vxinstagram"  },
    { source: "eeinstagram.com",  target: "ddinstagram.com",  label: "ddinstagram"  },
    { source: "eeinstagram.com",  target: "uuinstagram.com",  label: "uuinstagram"  },
    { source: "eeinstagram.com",  target: "instagramez.com",  label: "InstagramEZ"  },
    { source: "instagramez.com",  target: "instagram.com",    label: "instagram"    },
    { source: "instagramez.com",  target: "kkinstagram.com",  label: "kkinstagram"  },
    { source: "instagramez.com",  target: "vxinstagram.com",  label: "vxinstagram"  },
    { source: "instagramez.com",  target: "ddinstagram.com",  label: "ddinstagram"  },
    { source: "instagramez.com",  target: "uuinstagram.com",  label: "uuinstagram"  },
    { source: "instagramez.com",  target: "eeinstagram.com",  label: "eeinstagram"  },

    { source: "tiktok.com", target: "vxtiktok.com", label: "vxtiktok" },
    { source: "tiktok.com", target: "tnktok.com", label: "tnktok" },
    { source: "tiktok.com", target: "tiktxk.com", label: "tiktxk" },
    { source: "tiktok.com", target: "tfxktok.com", label: "tfxktok" },
    { source: "tiktok.com", target: "tiktokez.com", label: "TikTokEZ" },
    { source: "vxtiktok.com", target: "tiktok.com", label: "tiktok" },
    { source: "vxtiktok.com", target: "tnktok.com", label: "tnktok" },
    { source: "vxtiktok.com", target: "tiktxk.com", label: "tiktxk" },
    { source: "vxtiktok.com", target: "tfxktok.com", label: "tfxktok" },
    { source: "tnktok.com", target: "tiktok.com", label: "tiktok" },
    { source: "tnktok.com", target: "vxtiktok.com", label: "vxtiktok" },
    { source: "tnktok.com", target: "tiktxk.com", label: "tiktxk" },
    { source: "tnktok.com", target: "tfxktok.com", label: "tfxktok" },
    { source: "tiktxk.com", target: "tiktok.com", label: "tiktok" },
    { source: "tiktxk.com", target: "vxtiktok.com", label: "vxtiktok" },
    { source: "tiktxk.com", target: "tnktok.com", label: "tnktok" },
    { source: "tiktxk.com", target: "tfxktok.com", label: "tfxktok" },
    { source: "tfxktok.com", target: "tiktok.com", label: "tiktok" },
    { source: "tfxktok.com", target: "vxtiktok.com", label: "vxtiktok" },
    { source: "tfxktok.com", target: "tnktok.com", label: "tnktok" },
    { source: "tfxktok.com", target: "tiktxk.com", label: "tiktxk" },
    { source: "tiktokez.com", target: "tiktok.com", label: "tiktok" },
    { source: "tiktokez.com", target: "vxtiktok.com", label: "vxtiktok" },

    { source: "threads.com", target: "fixthreads.net", label: "fixthreads" },
    { source: "threads.com", target: "fixthreads.seria.moe", label: "fixthreads(moe)" },
    { source: "threads.net", target: "fixthreads.net", label: "fixthreads" },
    { source: "threads.net", target: "fixthreads.seria.moe", label: "fixthreads(moe)" },

    { source: "tumblr.com", target: "tpmblr.com", label: "tpmblr" },
    { source: "tpmblr.com", target: "tumblr.com", label: "tumblr" },

    { source: "pixiv.net", target: "www.phixiv.net", label: "phixiv" },
    { source: "www.phixiv.net", target: "pixiv.net", label: "pixiv" },

    { source: "bilibili.com", target: "vxbilibili.com", label: "vxbilibili" },
    { source: "b23.tv", target: "vxbilibili.com", label: "vxbilibili" },
    { source: "vxbilibili.com", target: "bilibili.com", label: "bilibili" },

    { source: "facebook.com", target: "facebed.com", label: "facebed" },
    { source: "facebed.com", target: "facebook.com", label: "facebook" },

    { source: "bsky.app", target: "bskyx.app", label: "bskyx" },
    { source: "bsky.app", target: "bsyy.app", label: "bsyy" },
    { source: "bsky.app", target: "bskye.app", label: "bskye" },
    { source: "bsky.app", target: "vxbsky.app", label: "vxbsky" },
    { source: "bskyx.app", target: "bsky.app", label: "bsky" },
    { source: "bskyx.app", target: "bsyy.app", label: "bsyy" },
    { source: "bskyx.app", target: "bskye.app", label: "bskye" },
    { source: "bskyx.app", target: "vxbsky.app", label: "vxbsky" },
    { source: "bsyy.app", target: "bsky.app", label: "bsky" },
    { source: "bsyy.app", target: "bskyx.app", label: "bskyx" },
    { source: "bsyy.app", target: "bskye.app", label: "bskye" },
    { source: "bsyy.app", target: "vxbsky.app", label: "vxbsky" },
    { source: "bskye.app", target: "bsky.app", label: "bsky" },
    { source: "bskye.app", target: "bskyx.app", label: "bskyx" },
    { source: "bskye.app", target: "bsyy.app", label: "bsyy" },
    { source: "bskye.app", target: "vxbsky.app", label: "vxbsky" },
    { source: "vxbsky.app", target: "bsky.app", label: "bsky" },
    { source: "vxbsky.app", target: "bskyx.app", label: "bskyx" },
    { source: "vxbsky.app", target: "bsyy.app", label: "bsyy" },
    { source: "vxbsky.app", target: "bskye.app", label: "bskye" },
  ];

  let lastPressTime = 0;
  let _configCache = null;
  function getConfig() {
    if (!_configCache) _configCache = GM_getValue("flcu_config", DEFAULT_CONFIG);
    return { ...DEFAULT_CONFIG, ..._configCache };
  }
  function saveConfig(c) {
    _configCache = { ...DEFAULT_CONFIG, ...c };
    GM_setValue("flcu_config", _configCache);
  }

  GM_addStyle(`
        #flcu-overlay {
            position: fixed; inset: 0;
            background: rgba(0,0,0,.65); z-index: 2147483647;
            display: flex; align-items: center; justify-content: center;
            font-family: sans-serif;
            overscroll-behavior: contain;
            animation: flcu-bg-in .22s ease-out;
        }
        #flcu-overlay.flcu-closing { animation: flcu-bg-out .18s ease-in forwards; }
        #flcu-menu {
            background: #2f3136; color: #dcddde;
            width: 400px; border-radius: 10px; border: 1px solid #202225;
            box-shadow: 0 12px 32px rgba(0,0,0,.6);
            overflow: hidden; display: flex; flex-direction: column;
            animation: flcu-menu-spring .42s cubic-bezier(.22,.68,0,1.15) forwards;
            position: relative;
        }
        #flcu-menu.flcu-closing { animation: flcu-menu-out .18s ease-in forwards; }
        #flcu-header {
            background: #202225; padding: 13px 18px;
            font-size: 15px; font-weight: bold; color: #fff;
            display: flex; justify-content: space-between; align-items: center;
            border-bottom: 1px solid #111214;
        }
        .flcu-icon-btn { cursor: pointer; padding: 4px; border-radius: 4px; transition: background .2s; }
        .flcu-icon-btn:hover { background: rgba(255,255,255,0.1); color: #fff; }

        .flcu-content {
            max-height: 65vh; overflow-y: auto; padding: 12px 0; overscroll-behavior: contain;
        }

        .flcu-item {
            width: calc(100% - 24px);
            margin: 0 12px 8px 12px;
            padding: 13px 15px;
            background: rgba(79, 84, 92, 0.16);
            border: 1px solid rgba(79, 84, 92, 0.3);
            border-radius: 7px;
            text-align: left; font-size: 14px; color: #dcddde; cursor: pointer;
            display: flex; align-items: center;
            transition: all .2s ease;
            position: relative; overflow: hidden;
            animation: flcu-item-in .24s ease-out both;
            animation-delay: var(--flcu-i, 60ms);
        }
        .flcu-rip {
            position: absolute; border-radius: 50%;
            width: 8px; height: 8px; margin: -4px;
            background: rgba(88,101,242,.35);
            animation: flcu-rip .45s ease-out forwards;
            pointer-events: none;
        }
        .flcu-item:hover {
            background: #40444b;
            border-color: #5865F2;
            color: #fff;
            transform: translateY(-1px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        .flcu-item:active { transform: translateY(0); box-shadow: none; }

        .flcu-prefix {
            font-weight: bold; color: #5865F2; margin-right: 12px;
            min-width: 24px; text-align: left; display: inline-block;
        }

        .flcu-copy-hint {
            font-size: 12px; color: #b9bbbe; margin-left: auto;
            opacity: 0; transition: opacity .2s;
            background: rgba(0, 0, 0, 0.2); padding: 4px 8px; border-radius: 4px;
        }
        .flcu-item:hover .flcu-copy-hint { opacity: 1; color: #fff; }

        .flcu-item-content {
            flex: 1; min-width: 0;
            display: flex; flex-direction: column; text-align: left;
        }
        .flcu-item-title {
            font-size: 14px; font-weight: bold; color: #dcddde;
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        }
        .flcu-item-desc {
            font-size: 12px; color: #8e9297; margin-top: 2px;
            white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
        }

        #flcu-manager {
            padding: 12px; background: #36393f;
            display: flex; flex-direction: column; gap: 8px;
            max-height: 82vh; overflow-y: auto; overscroll-behavior: contain;
        }

        .flcu-sec-title {
            font-size: 13px; font-weight: bold; color: #fff; margin-bottom: 8px;
            display: flex; align-items: center; gap: 8px;
        }
        .flcu-sec-sub {
            font-size: 11px; color: #8e9297; text-transform: uppercase;
            margin-top: 15px; border-top: 1px dashed #4f545c; padding-top: 15px;
        }

        .flcu-input-group { display: flex; gap: 8px; margin-bottom: 8px; }
        .flcu-input {
            flex: 1; background: #202225; border: 1px solid #202225;
            color: #dcddde; padding: 9px 10px; border-radius: 5px; font-size: 13px;
        }
        .flcu-input:focus { border-color: #5865F2; outline: none; }

        .flcu-rule-list {
            max-height: 130px; overflow-y: auto; background: #2f3136;
            border-radius: 5px; padding: 4px; border: 1px solid #202225;
            overscroll-behavior: contain;
        }
        .flcu-rule-item {
            display: flex; justify-content: space-between; align-items: center;
            font-size: 13px; color: #b9bbbe; padding: 7px 9px;
            border-bottom: 1px solid #36393f;
        }
        .flcu-rule-item:last-child { border-bottom: none; }
        .flcu-empty-msg { font-size: 13px; color: #72767d; text-align: center; padding: 16px; }

        .flcu-key-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
        .flcu-key-label { font-size: 13px; color: #b9bbbe; }
        .flcu-key-recorder {
            background: #202225; border: 1px solid #4f545c; color: #a1a3a7;
            padding: 5px 12px; border-radius: 4px; font-size: 12px; cursor: pointer;
            text-align: center; min-width: 80px; transition: all .2s;
        }
        .flcu-key-recorder:hover { border-color: #b9bbbe; color: #fff; }
        .flcu-key-recorder.recording {
            border-color: #ed4245; color: #ed4245; background: rgba(237, 66, 69, 0.05);
            animation: pulse 1.5s infinite;
        }

        .flcu-footer {
            background: #292b2f; padding: 9px 18px;
            display: flex; justify-content: space-between; align-items: center;
            border-top: 1px solid #202225;
        }

        .flcu-btn {
            padding: 7px 14px; border-radius: 4px; border: none; cursor: pointer;
            font-size: 13px; font-weight: bold; color: white;
        }
        .flcu-btn-primary { background: #5865F2; }
        .flcu-btn-primary:hover { background: #4752C4; }
        .flcu-btn-ghost { background: transparent; color: #b9bbbe; padding-left: 0; }
        .flcu-btn-ghost:hover { color: #fff; }

        .flcu-lang-switch { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; max-width: 200px; }
        .flcu-lang-item { font-size: 11px; color: #4f545c; cursor: pointer; font-weight: bold; transition: color .2s; }
        .flcu-lang-item:hover, .flcu-lang-item.active { color: #8e9297; }

        .flcu-content::-webkit-scrollbar,
        #flcu-manager::-webkit-scrollbar,
        .flcu-rule-list::-webkit-scrollbar,
        .flcu-custom-instructions::-webkit-scrollbar,
        #flcu-tj-panel::-webkit-scrollbar { width: 4px; }
        .flcu-content::-webkit-scrollbar-track,
        #flcu-manager::-webkit-scrollbar-track,
        .flcu-rule-list::-webkit-scrollbar-track,
        .flcu-custom-instructions::-webkit-scrollbar-track,
        #flcu-tj-panel::-webkit-scrollbar-track { background: transparent; }
        .flcu-content::-webkit-scrollbar-thumb,
        #flcu-manager::-webkit-scrollbar-thumb,
        .flcu-rule-list::-webkit-scrollbar-thumb,
        .flcu-custom-instructions::-webkit-scrollbar-thumb,
        #flcu-tj-panel::-webkit-scrollbar-thumb {
            background: #4f545c; border-radius: 2px;
        }
        .flcu-content::-webkit-scrollbar-thumb:hover,
        #flcu-manager::-webkit-scrollbar-thumb:hover,
        .flcu-rule-list::-webkit-scrollbar-thumb:hover {
            background: #72767d;
        }

        .flcu-toast {
            position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
            background: #5865F2; color: #fff; padding: 9px 18px 12px;
            border-radius: 6px; font-size: 13px; opacity: 0;
            z-index: 2147483647; pointer-events: none; overflow: hidden;
        }
        .flcu-toast.show   { animation: flcu-toast-in .38s cubic-bezier(.22,.68,0,1.2) forwards; }
        .flcu-toast.hiding { animation: flcu-toast-out .22s ease-in forwards; }
        .flcu-toast-bar {
            position: absolute; bottom: 0; left: 0;
            height: 3px; width: 100%;
            background: rgba(255,255,255,.5);
            border-radius: 0 0 6px 6px;
            transform-origin: left;
            animation: flcu-bar 2s linear forwards;
        }
        @keyframes flcu-fadein { from { opacity: 0; transform: scale(.95); } to { opacity: 1; transform: scale(1); } }
        @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
        @keyframes flcu-bg-in       { from { opacity: 0; }                                              to { opacity: 1; } }
        @keyframes flcu-bg-out      { from { opacity: 1; }                                              to { opacity: 0; } }
        @keyframes flcu-menu-spring { 0%  { opacity:0; transform:scale(.84) translateY(20px); }
                                      55% { opacity:1; transform:scale(1.04) translateY(-4px); }
                                      78% { transform:scale(.98) translateY(2px); }
                                      100%{ transform:scale(1) translateY(0); } }
        @keyframes flcu-menu-out    { to  { opacity:0; transform:scale(.92) translateY(12px); } }
        @keyframes flcu-item-in     { from { opacity:0; transform:translateY(7px); } to { opacity:1; transform:none; } }
        @keyframes flcu-rip         { to  { transform:scale(32); opacity:0; } }
        @keyframes flcu-toast-in    { 0%  { opacity:0; transform:translateX(-50%) translateY(14px) scale(.92); }
                                      60% { opacity:1; transform:translateX(-50%) translateY(-3px) scale(1.02); }
                                      100%{ opacity:1; transform:translateX(-50%) translateY(0) scale(1); } }
        @keyframes flcu-toast-out   { to  { opacity:0; transform:translateX(-50%) translateY(10px) scale(.94); } }
        @keyframes flcu-bar         { to  { transform:scaleX(0); } }

        #flcu-custom-panel {
            padding: 16px 20px; background: #36393f;
            display: flex; flex-direction: column; gap: 12px;
        }
        .flcu-custom-instructions {
            font-size: 12px; color: #b9bbbe;
            background: #2f3136; border-radius: 4px;
            padding: 10px 12px; white-space: pre-wrap;
            font-family: monospace; max-height: 200px;
            overflow-y: auto; line-height: 1.6;
            border: 1px solid #202225;
            overscroll-behavior: contain;
        }
        .flcu-custom-btns { display: flex; gap: 8px; flex-wrap: wrap; }
        .flcu-btn-import {
            background: #43b581; color: #fff;
            padding: 6px 12px; border-radius: 4px;
            border: none; cursor: pointer;
            font-size: 13px; font-weight: bold; display: inline-block;
        }
        .flcu-btn-import:hover { background: #3ca374; }
        .flcu-import-status { font-size: 12px; color: #ed4245; min-height: 16px; }
        .flcu-custom-active {
            display: flex; align-items: center; justify-content: space-between;
            background: rgba(67,181,129,0.12); border: 1px solid rgba(67,181,129,0.3);
            border-radius: 4px; padding: 6px 10px;
        }
        .flcu-custom-active-name { font-size: 13px; color: #43b581; }
        .flcu-btn-revoke {
            background: transparent; border: 1px solid #ed4245;
            color: #ed4245; border-radius: 4px; padding: 3px 8px;
            font-size: 12px; cursor: pointer; transition: background .2s;
        }
        .flcu-btn-revoke:hover { background: rgba(237,66,69,0.15); }
        .flcu-lang-item[data-l="__custom"] {
            font-size: 11px; line-height: 1;
            border: 1px solid #4f545c;
            border-radius: 10px;
            padding: 2px 6px;
            color: #8e9297;
            transition: border-color .2s, color .2s, background .2s;
        }
        .flcu-lang-item[data-l="__custom"]:hover,
        .flcu-lang-item[data-l="__custom"].active {
            border-color: #5865F2;
            color: #fff;
            background: rgba(88,101,242,0.15);
        }
        
        .flcu-desc-text {
            font-size: 12px; color: #72767d; line-height: 1.5;
            margin: 2px 0 6px 0; word-break: break-word;
        }

        .flcu-precopy-hint {
            font-size: 12px; color: #43b581; margin: 0 12px 6px 12px;
            padding: 5px 10px; background: rgba(67,181,129,0.08);
            border-radius: 4px; border-left: 2px solid #43b581;
        }
        .flcu-badge-last {
            font-size: 10px; background: rgba(88,101,242,0.25);
            color: #8ea1e1; border-radius: 10px; padding: 1px 7px;
            margin-left: 6px; vertical-align: middle;
        }
        .flcu-pref-bar {
            margin: 6px 12px 8px 12px;
            padding: 9px 11px;
            background: rgba(79,84,92,0.12);
            border: 1px dashed #4f545c;
            border-radius: 6px;
        }
        .flcu-pref-label {
            display: flex; align-items: center; gap: 8px;
            font-size: 13px; color: #b9bbbe; cursor: pointer;
            user-select: none;
        }
        .flcu-pref-check { accent-color: #5865F2; cursor: pointer; }

        .flcu-toast { cursor: default; }
        .flcu-toast.flcu-toast-jumpable { cursor: pointer; pointer-events: auto; }
        .flcu-toast-jump-hint { opacity: 0.7; }

        #flcu-jump-popup {
            position: fixed; bottom: 70px; left: 50%;
            transform: translateX(-50%);
            background: #2f3136; border: 1px solid #202225;
            border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,.5);
            min-width: 220px; max-width: 320px;
            z-index: 2147483647; overflow: hidden;
            animation: flcu-fadein .15s ease-out;
        }
        .flcu-jump-item {
            display: flex; align-items: center; gap: 10px;
            padding: 11px 15px; cursor: pointer;
            transition: background .15s;
            border-bottom: 1px solid #202225;
        }
        .flcu-jump-item:last-child { border-bottom: none; }
        .flcu-jump-item:hover { background: #40444b; }
        .flcu-jump-icon { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
        .flcu-jump-name { flex: 1; font-size: 13px; color: #dcddde; }
        .flcu-jump-arrow { font-size: 12px; color: #72767d; }

        .flcu-toggle-wrap {
            position: relative; display: inline-block;
            width: 36px; height: 20px; flex-shrink: 0;
            cursor: pointer; border-radius: 20px;
        }
        .flcu-toggle-wrap input { display: none; }
        .flcu-toggle-slider {
            position: absolute; inset: 0;
            background: #4f545c; border-radius: 20px; transition: .2s;
            pointer-events: none;
        }
        .flcu-toggle-slider::before {
            content: ""; position: absolute; height: 14px; width: 14px;
            left: 3px; bottom: 3px; background: #fff;
            border-radius: 50%; transition: .2s;
        }
        .flcu-toggle-wrap.flcu-on .flcu-toggle-slider { background: #5865F2; }
        .flcu-toggle-wrap.flcu-on .flcu-toggle-slider::before { transform: translateX(16px); }

        .flcu-action-group { display: flex; gap: 4px; }
        .flcu-action-btn {
            padding: 4px 12px; border-radius: 4px; border: 1px solid #4f545c;
            background: transparent; color: #8e9297; font-size: 12px;
            cursor: pointer; transition: all .15s;
        }
        .flcu-action-btn.active, .flcu-action-btn:hover {
            background: rgba(88,101,242,0.2); border-color: #5865F2; color: #fff;
        }

        .flcu-jump-item { cursor: default; }
        .flcu-jump-btn {
            display: flex; align-items: center; justify-content: center;
            width: 28px; height: 28px; border-radius: 5px; border: 1px solid #4f545c;
            background: transparent; color: #8e9297; cursor: pointer; flex-shrink: 0;
            transition: all .15s;
        }
        .flcu-jump-btn:hover { background: rgba(88,101,242,0.2); border-color: #5865F2; color: #fff; }
        .flcu-jump-btn-spa { border-color: #3a3d44; color: #72767d; }
        .flcu-jump-btn-spa:hover { background: rgba(67,181,129,0.2); border-color: #43b581; color: #43b581; }

        .flcu-section-advanced > .flcu-section-hdr { background: rgba(79,84,92,0.10); }
        .flcu-section-advanced > .flcu-section-hdr:hover { background: rgba(79,84,92,0.20); }
        .flcu-adv-badge {
            font-size: 10px; font-weight: normal; color: #72767d;
            background: rgba(79,84,92,0.3); border-radius: 4px;
            padding: 1px 6px; margin-left: 8px; letter-spacing: 0.3px;
        }

        .flcu-nav-btn {
            display: flex; justify-content: space-between; align-items: center;
            padding: 9px 12px; background: rgba(79,84,92,0.15);
            border: 1px solid rgba(79,84,92,0.3); border-radius: 6px;
            cursor: pointer; font-size: 13px; color: #dcddde; transition: all .2s;
        }
        .flcu-nav-btn:hover { background: #40444b; border-color: #5865F2; }

        .flcu-jl-row { gap: 6px; flex-wrap: nowrap; }
        .flcu-jl-icon { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }

        .flcu-section { border: 1px solid #3a3d44; border-radius: 8px; overflow: hidden; flex-shrink: 0; }
        .flcu-section-hdr {
            display: flex; justify-content: space-between; align-items: center;
            padding: 11px 14px; cursor: pointer; user-select: none;
            background: rgba(79,84,92,0.18); transition: background .15s;
        }
        .flcu-section-hdr:hover { background: rgba(79,84,92,0.32); }
        .flcu-section-hdr-title {
            font-size: 13px; font-weight: bold; color: #dcddde;
            display: flex; align-items: center; gap: 8px;
        }
        .flcu-section-chevron {
            font-size: 11px; color: #72767d;
            transition: transform .2s; display: inline-block;
        }
        .flcu-section.open > .flcu-section-hdr .flcu-section-chevron { transform: rotate(90deg); }
        .flcu-section-body {
            display: none; padding: 14px;
            border-top: 1px solid #3a3d44;
            background: #36393f;
            display: none; flex-direction: column; gap: 10px;
        }
        .flcu-section.open > .flcu-section-body { display: flex; }
    `);

  function getUserRules() {
    return GM_getValue("user_rules", []);
  }
  function addUserRule(s, tgt) {
    const rules = getUserRules();
    if (rules.some((r) => r.source === s && r.target === tgt)) return false;
    const label = tgt.replace(/^www\./, "").split(".")[0];
    rules.push({ source: s, target: tgt, label });
    GM_setValue("user_rules", rules);
    return true;
  }
  function removeUserRule(i) {
    const rules = getUserRules();
    rules.splice(i, 1);
    GM_setValue("user_rules", rules);
  }

  function getLastUsed() {
    return GM_getValue("flcu_last_used", {});
  }
  function setLastUsedForHost(host, targetHostname) {
    const lu = getLastUsed();
    lu[host] = targetHostname;
    GM_setValue("flcu_last_used", lu);
  }
  function getSitePrefs() {
    return GM_getValue("flcu_site_pref", {});
  }
  function setSitePref(host, targetHostname) {
    const p = getSitePrefs();
    p[host] = targetHostname;
    GM_setValue("flcu_site_pref", p);
  }
  function removeSitePref(host) {
    const p = getSitePrefs();
    delete p[host];
    GM_setValue("flcu_site_pref", p);
  }

  function getToastJumpCfg() {
    return GM_getValue("flcu_toast_jump", { enabled: false, action: "click" });
  }
  function saveToastJumpCfg(cfg) {
    GM_setValue("flcu_toast_jump", cfg);
  }
  function getJumpLinks() {
    return GM_getValue("flcu_jump_links", []);
  }
  function saveJumpLinks(links) {
    GM_setValue("flcu_jump_links", links);
  }
  function genId() {
    return Math.random().toString(36).slice(2, 9);
  }

  function getSectionStates() {
    return GM_getValue("flcu_sections", {});
  }
  function setSectionState(id, open) {
    const s = getSectionStates();
    s[id] = open;
    GM_setValue("flcu_sections", s);
  }

  function escHtml(str) {
    return String(str)
      .replace(/&/g, "&")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;")
      .replace(/"/g, "&quot;");
  }

  function cleanUrl(urlStr) {
    try {
      const url = new URL(urlStr);
      const paramsToRemove = [
        "utm_source",
        "utm_medium",
        "utm_campaign",
        "utm_term",
        "utm_content",
        "utm_id",
        "utm_referrer",
        "fbclid",
        "gclid",
        "igsh",
        "twclid",
        "msclkid",
        "dclid",
        "yclid",
        "gbraid",
        "wbraid",
        "ref",
        "ref_src",
        "sp_ref",
        "sp_url",
        "sxsrf",
        "ved",
        "ei",
        "gs_lcp",
        "oq",
        "aqs",
        "sourceid",
        "ie",
        "client",
        "feature",
        "pp",
        "pbjreload",
        "annotation_id",
        "si",
        "s",
        "openExternalBrowser",
      ];
      paramsToRemove.forEach((p) => url.searchParams.delete(p));
      return url.toString();
    } catch (e) {
      return urlStr;
    }
  }

  function getLang() {
    const cfg = getConfig();
    if (cfg.lang) return cfg.lang;
    const navLang = navigator.language.toLowerCase();
    if (
      navLang.startsWith("zh-tw") ||
      navLang.startsWith("zh-hk") ||
      navLang.startsWith("zh-mo")
    )
      return "zh-TW";
    if (navLang.startsWith("zh")) return "zh-CN";
    if (navLang.startsWith("ja")) return "ja";
    if (navLang.startsWith("ko")) return "ko";
    if (navLang.startsWith("pt")) return "pt-BR";
    if (navLang.startsWith("es")) return "es";
    if (navLang.startsWith("fr")) return "fr";
    if (navLang.startsWith("ru")) return "ru";
    return "en";
  }
  function t(key) {
    const lang = getLang();
    if (lang === "__custom") {
      const custom = GM_getValue("custom_lang", null);
      if (custom && custom[key] !== undefined) return custom[key];
      return TRANSLATIONS["en"][key] ?? key;
    }
    return TRANSLATIONS[lang]?.[key] ?? TRANSLATIONS["en"][key] ?? key;
  }
  function setLang(langCode) {
    const c = getConfig();
    c.lang = langCode;
    saveConfig(c);
  }

  function getUrlInfo() {
    const u = new URL(location.href);
    return { host: u.hostname };
  }
  function getMatchedRules(host) {
    const userRules = getUserRules();
    const seen = new Set();
    return [...DEFAULT_RULES, ...userRules].filter((r) => {
      const matches = host === r.source || host.endsWith("." + r.source);
      if (!matches) return false;
      if (r.target === host) return false;
      if (seen.has(r.target)) return false;
      seen.add(r.target);
      return true;
    });
  }
  function shortcutToString(s) {
    if (!s) return t("rec_none");
    const p = [];
    if (s.ctrl) p.push("Ctrl");
    if (s.alt) p.push("Alt");
    if (s.shift) p.push("Shift");
    if (s.meta) p.push("Win");
    p.push(s.key);
    return p.join(" + ");
  }
  function isInput(el) {
    return ["INPUT", "TEXTAREA"].includes(el.tagName) || el.isContentEditable;
  }

  function createMenu() {
    if (document.getElementById("flcu-overlay")) return;

    const rawUrl = location.href;
    const cleanedUrl = cleanUrl(rawUrl);
    const isCleaned = rawUrl !== cleanedUrl;

    const { host } = getUrlInfo();
    const rules = getMatchedRules(host);

    const overlay = document.createElement("div");
    overlay.id = "flcu-overlay";
    overlay.innerHTML = `
            <div id="flcu-menu">
                <div id="flcu-header">
                    <span>${t("title")}</span>
                    <span class="flcu-icon-btn" id="flcu-close" style="font-size:18px">×</span>
                </div>
                <div class="flcu-content"></div>
                <div class="flcu-footer">
                    <span style="font-size:10px;color:#4f545c">v${typeof GM_info !== "undefined" ? GM_info.script.version : "0.3"}</span>
                    <span class="flcu-icon-btn" id="flcu-settings" style="font-size:12px;color:#b9bbbe">${t("manage_btn")}</span>
                </div>
            </div>
        `;

    document.body.appendChild(overlay);

    overlay._cleanupFI = null;
    const closeOverlay = () => {
      overlay._cleanupFI?.();
      overlay.classList.add("flcu-closing");
      overlay.querySelector("#flcu-menu")?.classList.add("flcu-closing");
      setTimeout(() => overlay.remove(), 190);
    };

    const stopScroll = (e) => {
      const inScrollable = e.target.closest(
        ".flcu-content, .flcu-rule-list, .flcu-custom-instructions, #flcu-manager",
      );
      if (!inScrollable) {
        e.preventDefault();
      }
      e.stopPropagation();
    };
    overlay.addEventListener("wheel", stopScroll, { passive: false });
    overlay.addEventListener("touchmove", stopScroll, { passive: false });

    const menu = overlay.querySelector("#flcu-menu");
    const content = menu.querySelector(".flcu-content");

    let _ripIdx = 0;
    content.addEventListener("mousedown", (e) => {
      const item = e.target.closest(".flcu-item");
      if (!item) return;
      const r = item.getBoundingClientRect();
      const rip = document.createElement("span");
      rip.className = "flcu-rip";
      rip.style.left = (e.clientX - r.left) + "px";
      rip.style.top  = (e.clientY - r.top)  + "px";
      item.appendChild(rip);
      rip.addEventListener("animationend", () => rip.remove());
    });

    const _origAppend = content.appendChild.bind(content);
    content.appendChild = (el) => {
      if (el?.classList?.contains("flcu-item")) {
        el.style.setProperty("--flcu-i", `${60 + _ripIdx * 48}ms`);
        _ripIdx++;
      }
      return _origAppend(el);
    };

    const origBtn = document.createElement("button");
    origBtn.className = "flcu-item";
    origBtn.style.backgroundColor = "rgba(88, 101, 242, 0.1)";
    origBtn.style.borderColor = "rgba(88, 101, 242, 0.3)";

    const origLabel = isCleaned ? `✨ ${t("orig_clean")}` : t("orig");

    origBtn.innerHTML = `
    <span class="flcu-prefix" style="color: #5865F2; min-width: 28px;">🔗</span>
    <div class="flcu-item-content">
        <div class="flcu-item-title" style="${isCleaned ? "color:#43b581" : ""}">${origLabel}</div>
        <div class="flcu-item-desc">${escHtml(cleanedUrl)}</div>
    </div>
    <span class="flcu-copy-hint" style="background: rgba(88,101,242,0.8);">📋</span>
`;
    origBtn.onclick = () => copy(cleanedUrl);
    content.appendChild(origBtn);

    const shortsMatch = rawUrl.match(
      /^https?:\/\/(?:www\.)?youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})([?&].*)?$/,
    );
    if (shortsMatch) {
      content.appendChild(
        Object.assign(document.createElement("div"), {
          style: "height:1px;background:#3f4147;margin:0",
        }),
      );
      const videoId = shortsMatch[1];
      const extraParams = shortsMatch[2]
        ? shortsMatch[2].replace(/^[?&]/, "&")
        : "";
      const watchUrl = `https://www.youtube.com/watch?v=${videoId}${extraParams}`;
      const shortsBtn = document.createElement("button");
      shortsBtn.className = "flcu-item";
      shortsBtn.innerHTML = `
        <span class="flcu-prefix">▶️ WATCH</span>
        <span style="flex: 1; text-align: left;">${t("to")} youtube.com/watch</span>
        <span class="flcu-copy-hint">📋</span>
    `;
      shortsBtn.onclick = () => copy(watchUrl);
      content.appendChild(shortsBtn);
    }

    if (rules.length > 0) {
      content.appendChild(
        Object.assign(document.createElement("div"), {
          style: "height:1px;background:#3f4147;margin:0",
        }),
      );

      const lastUsedMap = getLastUsed();
      const lastUsedHostname = lastUsedMap[host] || null;
      let preCopied = false;

      if (lastUsedHostname) {
        const lastItem = rules.find((r) => r.target === lastUsedHostname);
        if (lastItem) {
          try {
            const u = new URL(cleanedUrl);
            u.hostname = lastItem.target;
            GM_setClipboard(u.toString(), "text");
            preCopied = true;
          } catch (_) {}
        }
      }

      if (preCopied) {
        const hint = document.createElement("div");
        hint.className = "flcu-precopy-hint";
        hint.textContent = `${t("pre_copied_hint")} ${lastUsedHostname}`;
        content.appendChild(hint);
      }

      rules.forEach((r) => {
        const isLast = r.target === lastUsedHostname;
        const btn = document.createElement("button");
        btn.className = "flcu-item" + (isLast ? " flcu-item-last" : "");
        btn.innerHTML = `
            <span class="prefix">${escHtml(r.label.toUpperCase())}</span>
            <div style="flex:1;text-align:left;min-width:0">
              <span>${t("to")} ${escHtml(r.label)}</span>
              ${isLast ? `<span class="flcu-badge-last">${escHtml(t("badge_last"))}</span>` : ""}
            </div>
            <span class="copy-hint">📋</span>
        `;

        let targetUrl = cleanedUrl;
        try {
          const u = new URL(cleanedUrl);
          u.hostname = r.target;
          targetUrl = u.toString();
        } catch (_) {}

        btn.onclick = () => {
          const prefCheckbox = content.querySelector("#flcu-pref-check");
          if (prefCheckbox?.checked) {
            setSitePref(host, r.target);
            toast(`${t("toast_pref_saved")}: ${r.target}`);
            const ov = document.getElementById("flcu-overlay");
            ov?._cleanupFI?.();
            if (ov) {
              ov.classList.add("flcu-closing");
              ov.querySelector("#flcu-menu")?.classList.add("flcu-closing");
              setTimeout(() => ov.remove(), 190);
            }
            GM_setClipboard(targetUrl, "text");
          } else {
            copy(targetUrl, r.target);
          }
        };
        content.appendChild(btn);
      });

      const prefBar = document.createElement("div");
      prefBar.className = "flcu-pref-bar";
      prefBar.innerHTML = `
        <label class="flcu-pref-label">
          <input type="checkbox" id="flcu-pref-check" class="flcu-pref-check">
          <span>${escHtml(t("lbl_always_direct"))}</span>
        </label>
      `;
      content.appendChild(prefBar);
    }

    overlay.onclick = (e) => e.target === overlay && closeOverlay();
    overlay.querySelector("#flcu-close").onclick = () => closeOverlay();
    overlay.querySelector("#flcu-settings").onclick = () =>
      switchToManager(menu, false);
  }

  function openTjSidePanel(menu, autoFill) {
    const manager = menu.querySelector("#flcu-manager");
    const headerTitle = menu.querySelector("#flcu-header span");
    if (!manager) return;
    manager.style.display = "none";
    headerTitle.textContent = t("lbl_toast_jump");

    const panel = document.createElement("div");
    panel.id = "flcu-tj-panel";
    panel.style.cssText = "padding:20px;background:#36393f;display:flex;flex-direction:column;gap:14px;max-height:82vh;overflow-y:auto;overscroll-behavior:contain;";

    const buildJumpLinkRow = (link, container, rerender) => {
      const domain = (() => { try { return new URL(link.url).hostname; } catch (_) { return ""; } })();
      const row = document.createElement("div");
      row.className = "flcu-rule-item flcu-jl-row";
      row.innerHTML = `
        <img class="flcu-jl-icon" src="https://www.google.com/s2/favicons?sz=32&domain=${encodeURIComponent(domain)}" alt="">
        <div style="flex:1;min-width:0;overflow:hidden">
          <div style="font-size:13px;color:#fff;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(link.name)}</div>
          <div style="font-size:11px;color:#72767d;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${escHtml(link.url)}</div>
        </div>
        <div class="flcu-toggle-wrap${link.enabled ? " flcu-on" : ""}" data-jl-toggle style="margin-left:6px">
          <span class="flcu-toggle-slider"></span>
        </div>
        <span class="flcu-icon-btn" data-jl-edit style="color:#b9bbbe;font-size:13px;margin-left:6px" title="${escHtml(t("lbl_edit"))}">✏️</span>
        <span class="flcu-icon-btn" data-jl-del style="color:#ed4245;font-weight:bold;margin-left:4px" title="${escHtml(t("toast_link_deleted"))}">×</span>
      `;
      row.querySelector("[data-jl-toggle]").onclick = () => {
        const ls = getJumpLinks(); const idx = ls.findIndex((l) => l.id === link.id);
        if (idx !== -1) { ls[idx].enabled = !ls[idx].enabled; saveJumpLinks(ls); rerender(); }
      };
      row.querySelector("[data-jl-del]").onclick = () => {
        saveJumpLinks(getJumpLinks().filter((l) => l.id !== link.id));
        toast(t("toast_link_deleted")); rerender();
      };
      row.querySelector("[data-jl-edit]").onclick = () => {
        const ls = getJumpLinks(); const target = ls.find((l) => l.id === link.id);
        if (!target) return;
        row.innerHTML = `
          <div style="display:flex;gap:6px;flex:1;flex-wrap:wrap;align-items:center">
            <input class="flcu-input" id="flcu-jl-en" value="${escHtml(target.name)}" placeholder="${escHtml(t("ph_link_name"))}" style="flex:1;min-width:0">
            <input class="flcu-input" id="flcu-jl-eu" value="${escHtml(target.url)}" placeholder="${escHtml(t("ph_link_url"))}" style="flex:2;min-width:0">
            <button class="flcu-btn flcu-btn-primary" id="flcu-jl-eok">${t("lbl_done")}</button>
            <button class="flcu-btn flcu-btn-ghost" id="flcu-jl-ecancel">${t("lbl_cancel")}</button>
          </div>`;
        row.querySelector("#flcu-jl-eok").onclick = () => {
          const n = row.querySelector("#flcu-jl-en").value.trim();
          const u = row.querySelector("#flcu-jl-eu").value.trim();
          if (!n || !u) return;
          const ls2 = getJumpLinks(); const idx = ls2.findIndex((l) => l.id === link.id);
          if (idx !== -1) { ls2[idx].name = n; ls2[idx].url = u; saveJumpLinks(ls2); }
          rerender();
        };
        row.querySelector("#flcu-jl-ecancel").onclick = rerender;
      };
      container.appendChild(row);
    };

    const renderPanel = () => {
      panel.innerHTML = "";
      const cfg = getToastJumpCfg();

      const toggleRow = document.createElement("div");
      toggleRow.className = "flcu-key-row";
      toggleRow.innerHTML = `
        <span class="flcu-key-label" style="font-size:13px">${t("lbl_toast_jump")}</span>
        <div class="flcu-toggle-wrap${cfg.enabled ? " flcu-on" : ""}" id="flcu-tj-toggle" role="switch" tabindex="0">
          <span class="flcu-toggle-slider"></span>
        </div>`;
      const tog = toggleRow.querySelector("#flcu-tj-toggle");
      tog.onclick = () => {
        const c = getToastJumpCfg(); c.enabled = !tog.classList.contains("flcu-on");
        saveToastJumpCfg(c); renderPanel();
      };
      panel.appendChild(toggleRow);

      const desc = document.createElement("div");
      desc.className = "flcu-desc-text";
      desc.textContent = t("desc_toast_jump");
      panel.appendChild(desc);

      if (cfg.enabled) {
        const actRow = document.createElement("div");
        actRow.className = "flcu-key-row";
        actRow.innerHTML = `
          <span class="flcu-key-label">${t("lbl_toast_action")}</span>
          <div class="flcu-action-group">
            <button class="flcu-action-btn${cfg.action === "click" ? " active" : ""}" data-act="click">${t("opt_click")}</button>
            <button class="flcu-action-btn${cfg.action === "hover" ? " active" : ""}" data-act="hover">${t("opt_hover")}</button>
          </div>`;
        actRow.querySelectorAll(".flcu-action-btn").forEach((btn) => {
          btn.onclick = () => {
            const c = getToastJumpCfg(); c.action = btn.dataset.act; saveToastJumpCfg(c);
            actRow.querySelectorAll(".flcu-action-btn").forEach((b) => b.classList.toggle("active", b === btn));
          };
        });
        panel.appendChild(actRow);

        const jlTitle = document.createElement("div");
        jlTitle.className = "flcu-sec-title";
        jlTitle.textContent = t("sec_jump_links");
        panel.appendChild(jlTitle);

        const nameInput = document.createElement("input");
        nameInput.type = "text"; nameInput.id = "flcu-jl-name";
        nameInput.className = "flcu-input"; nameInput.placeholder = t("ph_link_name");
        nameInput.style.marginBottom = "6px";

        const urlRow = document.createElement("div");
        urlRow.className = "flcu-input-group";
        urlRow.style.marginBottom = "0";
        const urlInput = document.createElement("input");
        urlInput.type = "text"; urlInput.id = "flcu-jl-url";
        urlInput.className = "flcu-input"; urlInput.placeholder = t("ph_link_url");
        const addBtn = document.createElement("button");
        addBtn.id = "flcu-jl-add"; addBtn.className = "flcu-btn flcu-btn-primary";
        addBtn.style.whiteSpace = "nowrap"; addBtn.textContent = t("btn_add_link");
        urlRow.appendChild(urlInput); urlRow.appendChild(addBtn);
        panel.appendChild(nameInput); panel.appendChild(urlRow);

        const jlContainer = document.createElement("div");
        jlContainer.className = "flcu-rule-list";
        jlContainer.style.maxHeight = "200px";
        panel.appendChild(jlContainer);

        const rerender = () => renderPanel();
        const renderLinks = () => {
          jlContainer.innerHTML = "";
          const links = getJumpLinks();
          if (links.length === 0) {
            jlContainer.innerHTML = `<div class="flcu-empty-msg">${t("empty_links")}</div>`;
            return;
          }
          links.forEach((link) => buildJumpLinkRow(link, jlContainer, rerender));
        };
        renderLinks();

        const doAdd = () => {
          const n = nameInput.value.trim(); const u = urlInput.value.trim();
          if (!n || !u) return;
          const ls = getJumpLinks(); ls.push({ id: genId(), name: n, url: u, enabled: true });
          saveJumpLinks(ls); nameInput.value = ""; urlInput.value = "";
          toast(t("toast_link_added")); renderLinks();
        };
        addBtn.onclick = doAdd;
        urlInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); doAdd(); } });
        nameInput.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); urlInput.focus(); } });
      }

      const backBtn = document.createElement("button");
      backBtn.className = "flcu-btn flcu-btn-ghost"; backBtn.textContent = t("btn_back");
      backBtn.style.alignSelf = "flex-start";
      backBtn.onclick = () => {
        panel.remove();
        manager.style.display = "";
        headerTitle.textContent = t("settings_title");
      };
      panel.appendChild(backBtn);
    };

    renderPanel();
    menu.appendChild(panel);
  }

  function switchToManager(menu, autoFill = false) {
    const { host } = getUrlInfo();
    const config = getConfig();
    const content = menu.querySelector(".flcu-content");
    const footer = menu.querySelector(".flcu-footer");
    const headerTitle = menu.querySelector("#flcu-header span");

    content.style.display = "none";
    footer.style.display = "none";
    headerTitle.textContent = t("settings_title");

    const manager = document.createElement("div");
    manager.id = "flcu-manager";

    const langSwitcherHtml = Object.entries(LANG_LABELS)
      .map(
        ([code, label]) =>
          `<span class="flcu-lang-item" data-l="${code}">${label}</span>`,
      )
      .join("");

    const secStates = getSectionStates();
    const isOpen = (id, def) => (id in secStates ? secStates[id] : def);

    manager.innerHTML = `
            <!-- ① 按鍵偏好設定 -->
            <div class="flcu-section${isOpen("keys", true) ? " open" : ""}" data-sec="keys">
                <div class="flcu-section-hdr">
                    <span class="flcu-section-hdr-title">${t("sec_keys")}</span>
                    <span class="flcu-section-chevron">›</span>
                </div>
                <div class="flcu-section-body">
                    <div class="flcu-key-row" style="margin:0">
                        <span class="flcu-key-label">${t("lbl_trigger")}</span>
                        <div id="rec-trigger" class="flcu-key-recorder">${escHtml(config.triggerKey)}</div>
                    </div>
                    <div class="flcu-desc-text" style="margin:0">${escHtml(t("desc_trigger"))}</div>
                    <div class="flcu-key-row" style="margin:0">
                        <span class="flcu-key-label">${t("lbl_shortcut")}</span>
                        <div id="rec-shortcut" class="flcu-key-recorder">${escHtml(shortcutToString(config.shortcut))}</div>
                    </div>
                    <div class="flcu-desc-text" style="margin:0">${escHtml(t("desc_shortcut"))}</div>
                </div>
            </div>

            <!-- ② 複製提示跳轉(toggle + config in one section) -->
            <div class="flcu-section${isOpen("tj", false) ? " open" : ""}" data-sec="tj">
                <div class="flcu-section-hdr" id="flcu-tj-section-hdr">
                    <span class="flcu-section-hdr-title">
                        ${t("lbl_toast_jump")}
                    </span>
                    <div style="display:flex;align-items:center;gap:8px">
                        <div class="flcu-toggle-wrap${getToastJumpCfg().enabled ? " flcu-on" : ""}" id="flcu-tj-hdr-toggle" role="switch" tabindex="0">
                            <span class="flcu-toggle-slider"></span>
                        </div>
                        <span class="flcu-section-chevron">›</span>
                    </div>
                </div>
                <div class="flcu-section-body">
                    <div class="flcu-desc-text" style="margin:0">${escHtml(t("desc_toast_jump"))}</div>
                    <div class="flcu-key-row" style="margin:0;display:${getToastJumpCfg().enabled ? "flex" : "none"}" id="flcu-tj-action-row">
                        <span class="flcu-key-label">${t("lbl_toast_action")}</span>
                        <div class="flcu-action-group">
                            <button class="flcu-action-btn${getToastJumpCfg().action === "click" ? " active" : ""}" data-act="click">${t("opt_click")}</button>
                            <button class="flcu-action-btn${getToastJumpCfg().action === "hover" ? " active" : ""}" data-act="hover">${t("opt_hover")}</button>
                        </div>
                    </div>
                    <div id="flcu-tj-entry" class="flcu-nav-btn" style="display:${getToastJumpCfg().enabled ? "flex" : "none"}">
                        <span>${t("sec_jump_links")}</span>
                        <span style="color:#72767d">›</span>
                    </div>
                </div>
            </div>

            <!-- ③ 進階功能(自定義規則 + 網站偏好)-->
            <div class="flcu-section flcu-section-advanced${isOpen("advanced", false) ? " open" : ""}" data-sec="advanced">
                <div class="flcu-section-hdr">
                    <span class="flcu-section-hdr-title" style="color:#8e9297">
                        ${t("sec_rules")} / ${t("sec_prefs")}
                        <span class="flcu-adv-badge">⚙ Advanced</span>
                    </span>
                    <span class="flcu-section-chevron">›</span>
                </div>
                <div class="flcu-section-body">
                    <div class="flcu-desc-text" style="margin:0">${escHtml(t("desc_rules"))}</div>
                    <div class="flcu-input-group" style="margin:0">
                        <input type="text" id="flcu-src" class="flcu-input" placeholder="${escHtml(t("ph_src"))}" value="${autoFill ? escHtml(host) : ""}">
                    </div>
                    <div class="flcu-input-group" style="margin:0">
                        <input type="text" id="flcu-tgt" class="flcu-input" placeholder="${escHtml(t("ph_tgt"))}">
                        <button id="flcu-add" class="flcu-btn flcu-btn-primary" style="white-space:nowrap">${t("btn_add")}</button>
                    </div>
                    <div class="flcu-rule-list" id="flcu-list-container"></div>
                    <div class="flcu-desc-text" style="margin:4px 0 0 0;color:#4f545c">${t("sec_prefs")}</div>
                    <div class="flcu-rule-list" id="flcu-prefs-container"></div>
                </div>
            </div>

            <div style="display:flex; justify-content:space-between; align-items:flex-end; margin-top:4px;">
                <button id="flcu-back" class="flcu-btn flcu-btn-ghost">${t("btn_back")}</button>
                <div class="flcu-lang-switch">${langSwitcherHtml}</div>
            </div>
        `;

    menu.appendChild(manager);

    manager.querySelectorAll(".flcu-section").forEach((sec) => {
      const secId = sec.dataset.sec;
      sec.querySelector(".flcu-section-hdr").addEventListener("click", (e) => {
        if (e.target.closest("#flcu-tj-hdr-toggle")) return;
        const nowOpen = sec.classList.toggle("open");
        setSectionState(secId, nowOpen);
      });
    });

    const tjHdrToggle = manager.querySelector("#flcu-tj-hdr-toggle");
    if (tjHdrToggle) {
      const tjActionRow = manager.querySelector("#flcu-tj-action-row");
      const tjEntryBtn  = manager.querySelector("#flcu-tj-entry");
      tjHdrToggle.onclick = (e) => {
        e.stopPropagation();
        const c = getToastJumpCfg(); c.enabled = !tjHdrToggle.classList.contains("flcu-on");
        saveToastJumpCfg(c);
        tjHdrToggle.classList.toggle("flcu-on", c.enabled);
        if (tjActionRow) tjActionRow.style.display = c.enabled ? "flex" : "none";
        if (tjEntryBtn)  tjEntryBtn.style.display  = c.enabled ? "flex" : "none";
        const tjSec = manager.querySelector('[data-sec="tj"]');
        if (c.enabled && tjSec && !tjSec.classList.contains("open")) {
          tjSec.classList.add("open"); setSectionState("tj", true);
        }
      };
      tjHdrToggle.onkeydown = (e) => {
        if (e.key === " " || e.key === "Enter") { e.preventDefault(); tjHdrToggle.click(); }
      };
    }

    manager.querySelectorAll(".flcu-action-btn").forEach((btn) => {
      btn.onclick = () => {
        manager.querySelectorAll(".flcu-action-btn").forEach((b) => b.classList.remove("active"));
        btn.classList.add("active");
        const cfg = getToastJumpCfg(); cfg.action = btn.dataset.act; saveToastJumpCfg(cfg);
      };
    });

    if (autoFill) {
      const advSec = manager.querySelector('[data-sec="advanced"]');
      if (advSec && !advSec.classList.contains("open")) {
        advSec.classList.add("open"); setSectionState("advanced", true);
      }
      setTimeout(() => manager.querySelector("#flcu-tgt")?.focus(), 50);
    }

    const renderRules = () => {
      const container = manager.querySelector("#flcu-list-container");
      container.innerHTML = "";
      const rules = getUserRules();
      if (rules.length === 0) {
        container.innerHTML = `<div class="flcu-empty-msg">${t("empty_rules")}</div>`;
        return;
      }
      rules.forEach((r, i) => {
        const row = document.createElement("div");
        row.className = "flcu-rule-item";
        row.innerHTML = `
                    <div style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:240px;">
                        <span style="color:#fff">${escHtml(r.source)}</span> → <span style="color:#5865F2">${escHtml(r.target)}</span>
                    </div>
                    <span class="flcu-icon-btn del-btn" style="color:#ed4245;font-weight:bold">×</span>
                `;
        row.querySelector(".del-btn").onclick = () => {
          removeUserRule(i);
          renderRules();
        };
        container.appendChild(row);
      });
    };
    renderRules();

    const doAddRule = () => {
      const src = manager.querySelector("#flcu-src").value.trim();
      const tgt = manager.querySelector("#flcu-tgt").value.trim();
      if (src && tgt) {
        const added = addUserRule(src, tgt);
        if (added === false) {
          toast("⚠️ " + src + " → " + tgt);
          return;
        }
        renderRules();
        manager.querySelector("#flcu-src").value = "";
        manager.querySelector("#flcu-tgt").value = "";
        toast(t("toast_added"));
      }
    };
    manager.querySelector("#flcu-add").onclick = doAddRule;
    manager.querySelector("#flcu-tgt").addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        doAddRule();
      }
    });
    manager.querySelector("#flcu-src").addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        manager.querySelector("#flcu-tgt").focus();
      }
    });

    const bindRecorder = (el, type) => {
      el.onclick = () => {
        if (el.classList.contains("recording")) return;
        el.classList.add("recording");
        el.innerText = t("rec_press");

        const handler = (e) => {
          e.preventDefault();
          e.stopPropagation();
          if (e.key === "Escape") {
            el.classList.remove("recording");
            el.innerText =
              type === "trigger"
                ? config.triggerKey
                : shortcutToString(config.shortcut);
          } else if (type === "trigger") {
            config.triggerKey = e.key;
            saveConfig(config);
            el.innerText = e.key;
            el.classList.remove("recording");
            toast(t("toast_key_updated"));
          } else if (type === "shortcut") {
            if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) return;
            if (e.key === "Backspace" || e.key === "Delete") {
              config.shortcut = null;
              toast(t("toast_key_cleared"));
            } else {
              config.shortcut = {
                ctrl: e.ctrlKey,
                alt: e.altKey,
                shift: e.shiftKey,
                meta: e.metaKey,
                key: e.key.length === 1 ? e.key.toUpperCase() : e.key,
              };
              toast(t("toast_key_updated"));
            }
            el.innerText = shortcutToString(config.shortcut);
            saveConfig(config);
            el.classList.remove("recording");
          }
          document.removeEventListener("keydown", handler, true);
        };
        document.addEventListener("keydown", handler, true);
      };
    };
    bindRecorder(manager.querySelector("#rec-trigger"), "trigger");
    bindRecorder(manager.querySelector("#rec-shortcut"), "shortcut");

    const renderPrefs = () => {
      const container = manager.querySelector("#flcu-prefs-container");
      container.innerHTML = "";
      const prefs = getSitePrefs();
      const entries = Object.entries(prefs);
      if (entries.length === 0) {
        container.innerHTML = `<div class="flcu-empty-msg">${t("pref_empty")}</div>`;
        return;
      }
      entries.forEach(([h, tgt]) => {
        const row = document.createElement("div");
        row.className = "flcu-rule-item";
        row.innerHTML = `
          <div style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:220px;flex:1">
            <span style="color:#b9bbbe">${escHtml(h)}</span>
            <span style="color:#4f545c"> → </span>
            <span style="color:#5865F2">${escHtml(tgt)}</span>
          </div>
          <span class="flcu-icon-btn del-btn" style="color:#ed4245;font-weight:bold" title="${escHtml(t("toast_pref_cleared"))}">×</span>
        `;
        row.querySelector(".del-btn").onclick = () => {
          removeSitePref(h);
          toast(t("toast_pref_cleared"));
          renderPrefs();
        };
        container.appendChild(row);
      });
    };
    renderPrefs();

    manager.querySelector("#flcu-tj-entry").onclick = () => {
      openTjSidePanel(menu, autoFill);
    };

    manager.querySelector("#flcu-back").onclick = () => {
      manager.remove();
      content.style.display = "block";
      footer.style.display = "flex";
      headerTitle.textContent = t("title");
    };

    manager.querySelectorAll(".flcu-lang-item").forEach((btn) => {
      if (btn.dataset.l === getLang()) btn.classList.add("active");
      btn.onclick = () => {
        if (btn.dataset.l === "__custom") {
          manager.remove();
          switchToCustomLang(menu, autoFill);
        } else {
          setLang(btn.dataset.l);
          manager.remove();
          menu.querySelector("#flcu-header span").textContent = t("title");
          switchToManager(menu, autoFill);
        }
      };
    });
  }

  function getCustomLang() {
    return GM_getValue("custom_lang", null);
  }
  function setCustomLang(obj) {
    GM_setValue("custom_lang", obj);
  }

  function buildExportJson() {
    const template = {
      _instructions: CUSTOM_LANG_INSTRUCTIONS,
      _note:
        "Translate the VALUES only. Do NOT change the KEYS. " +
        "Keep {placeholders} like {n} {s} {t} untouched. " +
        "Preserve HTML tags and class='...' attributes as-is. " +
        "The 'name' field will be shown in the language selector.",
      name: "My Custom Language",
      ...TRANSLATIONS["en"],
    };
    return JSON.stringify(template, null, 2);
  }

  function switchToCustomLang(menu, autoFill = false) {
    const content = menu.querySelector(".flcu-content");
    const footer = menu.querySelector(".flcu-footer");
    const headerTitle = menu.querySelector("#flcu-header span");

    content.style.display = "none";
    footer.style.display = "none";
    headerTitle.textContent =
      "🌐 " +
      (getLang() === "__custom"
        ? getCustomLang()?._name || "Custom Language"
        : "Custom Language");

    const panel = document.createElement("div");
    panel.id = "flcu-custom-panel";
    const existingCustom = getCustomLang();
    panel.innerHTML = `
            <div class="flcu-custom-instructions">${escHtml(CUSTOM_LANG_INSTRUCTIONS)}</div>
            ${
              existingCustom
                ? `
            <div class="flcu-custom-active">
                <span class="flcu-custom-active-name">✅ ${escHtml(existingCustom._name || "Custom Language")} active</span>
                <button id="flcu-revoke-lang" class="flcu-btn-revoke">🗑 Remove</button>
            </div>`
                : ""
            }
            <div class="flcu-custom-btns">
                <button id="flcu-export-lang" class="flcu-btn flcu-btn-primary">📤 Export</button>
                <button id="flcu-import-btn" class="flcu-btn-import">📥 Import</button>
            </div>
            <div id="flcu-import-status" class="flcu-import-status"></div>
            <div style="margin-top:4px">
                <button id="flcu-back-custom" class="flcu-btn flcu-btn-ghost">${t("btn_back")}</button>
            </div>
        `;
    menu.appendChild(panel);

    const revokeBtn = panel.querySelector("#flcu-revoke-lang");
    if (revokeBtn) {
      revokeBtn.onclick = () => {
        GM_setValue("custom_lang", null);
        setLang(null);
        toast("🗑 Custom language removed");
        cleanupInput();
        panel.remove();
        content.style.display = "block";
        footer.style.display = "flex";
        headerTitle.textContent = t("title");
      };
    }

    panel.querySelector("#flcu-export-lang").onclick = () => {
      const json = buildExportJson();
      const dataUrl =
        "data:application/json;charset=utf-8," + encodeURIComponent(json);
      const a = document.createElement("a");
      a.href = dataUrl;
      a.download = "flcu_custom_lang.json";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    };

    const fileInput = document.createElement("input");
    fileInput.type = "file";
    fileInput.accept = ".json";
    fileInput.style.cssText =
      "position:fixed;opacity:0;pointer-events:none;width:0;height:0;top:0;left:0;z-index:-1";
    document.body.appendChild(fileInput);

    panel.querySelector("#flcu-import-btn").onclick = (e) => {
      e.stopPropagation();
      fileInput.click();
    };

    const cleanupInput = () => {
      fileInput.remove();
      const ov = document.getElementById("flcu-overlay");
      if (ov) ov._cleanupFI = null;
    };
    const ov = document.getElementById("flcu-overlay");
    if (ov) ov._cleanupFI = cleanupInput;

    fileInput.onchange = (e) => {
      const file = e.target.files[0];
      if (!file) return;
      const statusEl = panel.querySelector("#flcu-import-status");
      const reader = new FileReader();
      reader.onload = (ev) => {
        try {
          const raw = JSON.parse(ev.target.result);
          const validKeys = Object.keys(TRANSLATIONS["en"]);
          const filtered = {};
          for (const k of validKeys) {
            if (raw[k] !== undefined) filtered[k] = String(raw[k]);
          }
          if (raw.name) filtered._name = String(raw.name);
          setCustomLang(filtered);
          setLang("__custom");
          toast("✅ " + (filtered._name || "Custom language") + " loaded!");
          cleanupInput();
          panel.remove();
          content.style.display = "block";
          footer.style.display = "flex";
          headerTitle.textContent = t("title");
        } catch (err) {
          statusEl.textContent = "❌ Invalid JSON: " + err.message;
        }
      };
      reader.readAsText(file);
    };

    panel.querySelector("#flcu-back-custom").onclick = () => {
      cleanupInput();
      panel.remove();
      switchToManager(menu, autoFill);
    };
  }

  function resolveLinks() {
    const rawUrl = location.href;
    const cleanedUrl = cleanUrl(rawUrl);
    const { host } = getUrlInfo();
    const rules = getMatchedRules(host);
    const targets = [];

    const shortsMatch = rawUrl.match(
      /^https?:\/\/(?:www\.)?youtube\.com\/shorts\/([a-zA-Z0-9_-]{11})([?&].*)?$/,
    );
    if (shortsMatch) {
      const videoId = shortsMatch[1];
      const extraParams = shortsMatch[2]
        ? shortsMatch[2].replace(/^[?&]/, "&")
        : "";
      targets.push({
        label: "YouTube Watch",
        url: `https://www.youtube.com/watch?v=${videoId}${extraParams}`,
      });
    }

    rules.forEach((r) => {
      try {
        const u = new URL(cleanedUrl);
        u.hostname = r.target;
        targets.push({ label: r.label, url: u.toString() });
      } catch (_) {}
    });

    return { cleanedUrl, targets };
  }

  function triggerSmartAction() {
    const { cleanedUrl, targets } = resolveLinks();
    const { host } = getUrlInfo();

    if (targets.length === 0) {
      GM_setClipboard(cleanedUrl, "text");
      const disp = cleanedUrl.length > 60 ? cleanedUrl.slice(0, 57) + "..." : cleanedUrl;
      toast(`${t("toast_copied")}: ${disp}`);
    } else if (targets.length === 1) {
      const url = targets[0].url;
      try { setLastUsedForHost(host, new URL(url).hostname); } catch (_) {}
      GM_setClipboard(url, "text");
      const disp = url.length > 60 ? url.slice(0, 57) + "..." : url;
      toast(`${t("toast_copied")}: ${disp}`);
    } else {
      const prefs = getSitePrefs();
      if (prefs[host]) {
        const prefItem = targets.find((item) => {
          try { return new URL(item.url).hostname === prefs[host]; } catch (_) { return false; }
        });
        if (prefItem) {
          try { setLastUsedForHost(host, new URL(prefItem.url).hostname); } catch (_) {}
          GM_setClipboard(prefItem.url, "text");
          const disp = prefItem.url.length > 60 ? prefItem.url.slice(0, 57) + "..." : prefItem.url;
          toast(`${t("toast_copied")}: ${disp}`);
          return;
        }
        removeSitePref(host);
      }
      createMenu();
    }
  }

  function copy(text, targetHostname) {
    GM_setClipboard(text, "text");
    if (targetHostname) {
      const { host } = getUrlInfo();
      setLastUsedForHost(host, targetHostname);
    }
    const displayText = text.length > 60 ? text.slice(0, 57) + "..." : text;
    toast(`${t("toast_copied")}: ${displayText}`);
    const ov = document.getElementById("flcu-overlay");
    if (ov) {
      ov._cleanupFI?.();
      ov.classList.add("flcu-closing");
      ov.querySelector("#flcu-menu")?.classList.add("flcu-closing");
      setTimeout(() => ov.remove(), 190);
    }
  }

  let _toastHideTimer = null;

  function toast(msg) {
    clearTimeout(_toastHideTimer);
    let toastEl = document.querySelector(".flcu-toast");
    if (!toastEl) {
      toastEl = document.createElement("div");
      toastEl.className = "flcu-toast";
      document.body.appendChild(toastEl);
    }

    const cfg = getToastJumpCfg();
    const links = cfg.enabled ? getJumpLinks().filter((l) => l.enabled) : [];

    const oldPopup = document.getElementById("flcu-jump-popup");
    if (oldPopup) oldPopup.remove();
    toastEl._cleanJump?.();

    toastEl.innerHTML = "";
    const msgSpan = document.createElement("span");
    msgSpan.className = "flcu-toast-msg";
    msgSpan.textContent = msg;
    toastEl.appendChild(msgSpan);

    const bar = document.createElement("div");
    bar.className = "flcu-toast-bar";
    toastEl.appendChild(bar);

    if (links.length > 0) {
      const hint = document.createElement("span");
      hint.className = "flcu-toast-jump-hint";
      hint.textContent = " ↗";
      toastEl.insertBefore(hint, bar);
      toastEl.classList.add("flcu-toast-jumpable");
    } else {
      toastEl.classList.remove("flcu-toast-jumpable");
    }

    toastEl.classList.remove("show", "hiding");
    void toastEl.offsetWidth;
    toastEl.classList.add("show");

    const hideToast = () => {
      toastEl.classList.add("hiding");
      setTimeout(() => toastEl.classList.remove("show", "hiding"), 230);
    };
    _toastHideTimer = setTimeout(hideToast, 2000);

    const resetTimer = () => {
      clearTimeout(_toastHideTimer);
      _toastHideTimer = setTimeout(hideToast, 2000);
    };

    if (links.length > 0) {
      let popup = null;

      const buildPopup = () => {
        if (popup) return;
        popup = document.createElement("div");
        popup.id = "flcu-jump-popup";
        links.forEach((link) => {
          const item = document.createElement("div");
          item.className = "flcu-jump-item";
          const domain = (() => { try { return new URL(link.url).hostname; } catch (_) { return ""; } })();
          item.innerHTML = `
            <img class="flcu-jump-icon" src="https://www.google.com/s2/favicons?sz=32&domain=${encodeURIComponent(domain)}" alt="">
            <span class="flcu-jump-name">${escHtml(link.name)}</span>
            <button class="flcu-jump-btn" data-action="newtab" title="另開新分頁">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" 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>
            </button>
            <button class="flcu-jump-btn flcu-jump-btn-spa" data-action="spa" title="嘗試強制SPA跳轉進入連結">
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
                <circle cx="12" cy="12" r="10"/>
                <polyline points="12 8 16 12 12 16"/>
                <line x1="8" y1="12" x2="16" y2="12"/>
              </svg>
            </button>
          `;
          item.querySelector("[data-action='newtab']").onclick = (e) => {
            e.stopPropagation();
            window.open(link.url, "_blank", "noopener");
          };
          item.querySelector("[data-action='spa']").onclick = (e) => {
            e.stopPropagation();
            try {
              history.pushState(null, "", link.url);
              window.dispatchEvent(new PopStateEvent("popstate", { state: null }));
            } catch (_) {}
            location.href = link.url;
          };
          popup.appendChild(item);
        });
        document.body.appendChild(popup);
      };

      const destroyPopup = () => {
        popup?.remove();
        popup = null;
      };

      if (cfg.action === "hover") {
        let leaveTimer = null;
        const clearLeave = () => clearTimeout(leaveTimer);

        const onToastEnter = () => {
          clearLeave();
          clearTimeout(_toastHideTimer);
          buildPopup();
          const p = document.getElementById("flcu-jump-popup");
          if (p && !p._hovered) {
            p._hovered = true;
            p.addEventListener("mouseenter", clearLeave);
            p.addEventListener("mouseleave", () => {
              leaveTimer = setTimeout(() => { destroyPopup(); resetTimer(); }, 300);
            });
          }
        };
        const onToastLeave = () => {
          leaveTimer = setTimeout(() => { destroyPopup(); resetTimer(); }, 300);
        };

        toastEl.addEventListener("mouseenter", onToastEnter);
        toastEl.addEventListener("mouseleave", onToastLeave);

        toastEl._cleanJump = () => {
          destroyPopup();
          clearLeave();
          toastEl.removeEventListener("mouseenter", onToastEnter);
          toastEl.removeEventListener("mouseleave", onToastLeave);
        };
      } else {
        const onClick = (e) => {
          e.stopPropagation();
          if (popup) { destroyPopup(); return; }
          clearTimeout(_toastHideTimer);
          buildPopup();
          const onOutside = (ev) => {
            if (!popup?.contains(ev.target) && ev.target !== toastEl) {
              destroyPopup();
              document.removeEventListener("click", onOutside, true);
              resetTimer();
            }
          };
          setTimeout(() => document.addEventListener("click", onOutside, true), 0);
        };
        toastEl.addEventListener("click", onClick);
        toastEl._cleanJump = () => {
          destroyPopup();
          toastEl.removeEventListener("click", onClick);
        };
      }
    }
  }

  document.addEventListener("keydown", (e) => {
    if (isInput(e.target)) return;
    if (e.repeat) return;
    const config = getConfig();

    if (config.shortcut) {
      const s = config.shortcut;
      if (
        e.ctrlKey === !!s.ctrl &&
        e.altKey === !!s.alt &&
        e.shiftKey === !!s.shift &&
        e.metaKey === !!s.meta &&
        (e.key.toUpperCase() === s.key || e.key === s.key)
      ) {
        e.preventDefault();
        triggerSmartAction();
        return;
      }
    }
    if (e.key === config.triggerKey) {
      const now = Date.now();
      if (now - lastPressTime < config.doublePressDelay) {
        e.preventDefault();
        triggerSmartAction();
        lastPressTime = 0;
      } else {
        lastPressTime = now;
      }
    } else if (!["Control", "Alt", "Shift", "Meta"].includes(e.key)) {
      lastPressTime = 0;
    }
  });

  if (typeof GM_registerMenuCommand !== "undefined") {
    GM_registerMenuCommand("🛠️ Open Link Conversion Panel", () => {
      createMenu();
    });
  }
})();