YouTube Channel Filter

Filter YouTube videos by channel, keyword, duration, language, live streams and members-only content. Full blocklist/whitelist management with quick action buttons on video cards.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         YouTube Channel Filter
// @namespace    http://tampermonkey.net/
// @version      1.1.1-rc1
// @description  Filter YouTube videos by channel, keyword, duration, language, live streams and members-only content. Full blocklist/whitelist management with quick action buttons on video cards.
// @author       MayoHu
// @match        https://www.youtube.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  // ============================================================
  // DEBUG
  // ============================================================
  // 使用方式(任選一種):
  //  A. Tampermonkey 圖示選單:點擊「🔍 切換 Debug 模式」
  //  B. Console 輸入:window.__YTF_DEBUG = true  然後 window.__ytfReport()
  //
  // 腳本在 sandbox 執行(因 @grant),必須透過 unsafeWindow 共享頁面變數,
  // 否則用戶在頁面 Console 輸入的 window.xxx 無法被腳本讀到。
  var _pageWin = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
  var _dbgOn = false;  // 腳本內部旗標,同步於 _pageWin.__YTF_DEBUG
  var _ytfStats = {
    obsTotalRuns: 0,
    obsCardsSelected: 0,
    processOneCalls: 0,
    processOneSkipped_noInfo: 0,
    processOneSkipped_memberWait: 0,
    processOneHidden: 0,
    processOnePass: 0,
    qaCalls: 0,
    qaSkipped_alreadyMarked: 0,
    qaSkipped_noInfo: 0,
    qaInjected: 0,
    selectorSamples: {},
    checkReasons: {}
  };
  // 暴露到頁面 window(Console 可直接存取)
  try {
    _pageWin.__ytfStats = _ytfStats;
    _pageWin.__ytfReport = function() {
      try { console.table(_ytfStats); } catch(e){ console.log('[YTF]', _ytfStats); }
    };
    // 讓用戶可以在 console 切換 debug:透過 getter/setter 同步腳本內的 _dbgOn
    Object.defineProperty(_pageWin, '__YTF_DEBUG', {
      configurable: true,
      get: function(){ return _dbgOn; },
      set: function(v){ _dbgOn = !!v; console.log('[YTF] Debug mode:', _dbgOn); }
    });
  } catch(e){ console.warn('[YTF] unsafeWindow bridge failed:', e); }
  function ftDbg(){
    if(!_dbgOn) return;
    try { console.log.apply(console, ['[YTF]'].concat(Array.prototype.slice.call(arguments))); } catch(e){}
  }
  function ftCount(key){
    if(typeof _ytfStats[key] === 'number') _ytfStats[key]++;
  }
  // Tampermonkey 選單按鈕
  try {
    if(typeof GM_registerMenuCommand === 'function') {
      GM_registerMenuCommand(t('tm.debugToggle'), function(){
        _dbgOn = !_dbgOn;
        try { _pageWin.__YTF_DEBUG = _dbgOn; } catch(e){}
        console.log('[YTF] Debug mode:', _dbgOn, t('debug.hint'));
      });
      GM_registerMenuCommand(t('tm.debugStats'), function(){
        try { console.table(_ytfStats); } catch(e){ console.log('[YTF]', _ytfStats); }
      });
    }
  } catch(e){}

  // ============================================================
  // DOM HELPER — 完全不使用 innerHTML,CSP 相容
  // ============================================================
  // h(tag, {attr:val, cls:'...', on:{click:fn}}, [...children | 'text'])
  function h(tag, attrs, children) {
    var el = document.createElement(tag);
    attrs = attrs || {};
    Object.keys(attrs).forEach(function(k) {
      if (k === 'cls') {
        el.className = attrs[k];
      } else if (k === 'on') {
        Object.keys(attrs[k]).forEach(function(ev) {
          el.addEventListener(ev, attrs[k][ev]);
        });
      } else if (k === 'style') {
        el.style.cssText = attrs[k];
      } else {
        el.setAttribute(k, attrs[k]);
      }
    });
    var childArr = Array.isArray(children) ? children : (children !== null && children !== undefined ? [children] : []);
    childArr.forEach(function(c) {
      if (c === null || c === undefined) return;
      if (typeof c === 'string' || typeof c === 'number') {
        el.appendChild(document.createTextNode(String(c)));
      } else if (c.nodeType) {
        el.appendChild(c);
      }
    });
    return el;
  }

  // 清空並重新填入子元素
  function setChildren(parent, children) {
    while (parent.firstChild) parent.removeChild(parent.firstChild);
    var childArr = Array.isArray(children) ? children : (children !== null && children !== undefined ? [children] : []);
    childArr.forEach(function(c) {
      if (!c) return;
      if (typeof c === 'string') parent.appendChild(document.createTextNode(c));
      else if (c.nodeType) parent.appendChild(c);
    });
  }

  // ============================================================
  // I18N (介面多語言)
  // ============================================================
  // 字串對照表:i18n.zh / i18n.en 之下以語意化 key 儲存
  // t(key) 讀取當前 LANG 對應值,找不到時回退英文,再找不到回傳 key 本身
  var i18n = { zh: {}, en: {} };

  // ── C-2: 核心訊息(Notif / QA / FAB) ─────────────────────
  i18n.zh['reason.unknown']       = '未知';
  i18n.zh['reason.keyword']       = '關鍵字「';          // 後綴 + 值 + 」
  i18n.zh['reason.keyword.suffix']= '」';
  i18n.zh['reason.channel']       = '頻道隱藏';
  i18n.zh['reason.language']      = '語言(';            // 後綴 + 值 + )
  i18n.zh['reason.language.suffix']= ')';
  i18n.zh['reason.duration']      = '時長過濾';
  i18n.zh['reason.views']         = '觀看數過濾';
  i18n.zh['reason.date']          = '日期過濾';
  i18n.zh['reason.live']          = '直播過濾';
  i18n.zh['reason.upcoming']      = '首播過濾';
  i18n.zh['reason.members_only']  = '會員專屬';
  i18n.zh['notif.hidden.prefix']  = '已隱藏 ';
  i18n.zh['notif.hidden.suffix']  = ' 部影片,觸發:';
  i18n.zh['qa.channelTip']        = '頻道:';
  i18n.zh['qa.block']             = '🚫 隱藏此頻道';
  i18n.zh['qa.whitelist']         = '⭐ 加入白名單';
  i18n.zh['qa.blockTitle']        = '隱藏頻道:';
  i18n.zh['qa.whitelistTitle']    = '加入白名單:';
  i18n.zh['qa.hidden']            = '✅ 已隱藏';
  i18n.zh['qa.whitelisted']       = '✅ 已加入白名單';
  i18n.zh['qa.already.block']     = '已隱藏頻道:';
  i18n.zh['qa.already.white']     = '已加入白名單:';
  i18n.zh['qa.conflict.blockToWL']= '」已在白名單';
  i18n.zh['qa.conflict.WLToBlock']= '」已在隱藏名單';
  i18n.zh['qa.quote.open']       = '「';
  i18n.zh['qa.hiddenPrefix']      = '已隱藏:';
  i18n.zh['fab.title']            = 'YouTube 頻道過濾器 - 點擊開啟 / 右鍵切換';
  i18n.zh['fab.tip.enabled']      = 'YouTube 頻道過濾器(啟用中)\n左鍵:開啟面板 | 右鍵:暫停';
  i18n.zh['fab.tip.disabled']     = 'YouTube 頻道過濾器(已暫停)\n左鍵:開啟面板 | 右鍵:啟用';
  i18n.zh['filter.enabled']       = '過濾器已啟用';
  i18n.zh['filter.disabled']      = '過濾器已暫停';
  i18n.zh['notif.separator']     = '、';
  // ── C-3: Panel UI ─────────────────────────────────────
  i18n.zh['panel.title'] = 'YouTube 頻道過濾器';
  i18n.zh['tab.blocklist'] = '🚫 隱藏名單';
  i18n.zh['tab.whitelist'] = '⭐ 白名單';
  i18n.zh['tab.keywords'] = '🔤 關鍵字';
  i18n.zh['tab.filters'] = '⚙️ 設定';
  i18n.zh['tab.stats'] = '📊 統計';
  i18n.zh['tab.history'] = '📋 歷史';
  i18n.zh['tab.io'] = '💾 匯出入';
  i18n.zh['tab.about'] = '📖 使用說明';
  i18n.zh['panel.close'] = '關閉';
  i18n.zh['common.confirm'] = '確認';
  i18n.zh['common.cancel'] = '取消';
  i18n.zh['common.save'] = '儲存';
  i18n.zh['common.remove'] = '移除';
  i18n.zh['common.edit'] = '編輯';
  i18n.zh['common.add'] = '新增';
  i18n.zh['common.create'] = '建立';
  i18n.zh['common.copy'] = '複製';
  i18n.zh['common.skip'] = '略過';
  i18n.zh['common.all'] = '全部';
  i18n.zh['common.up'] = '上移';
  i18n.zh['common.down'] = '下移';
  i18n.zh['common.mode'] = '模式';
  i18n.zh['common.total'] = '共 ';
  i18n.zh['common.items'] = ' 筆';
  i18n.zh['common.times'] = ' 次';
  i18n.zh['common.groups'] = ' 群組';
  i18n.zh['common.channels'] = '個頻道';
  i18n.zh['channel.searchPlaceholder'] = '搜尋頻道...';
  i18n.zh['channel.name'] = '頻道名稱';
  i18n.zh['channel.handle'] = '頻道 ID(選填,如 @handle)';
  i18n.zh['channel.handleShort'] = '@handle(選填)';
  i18n.zh['channel.addPrompt'] = '請輸入頻道名稱';
  i18n.zh['channel.empty.blocklist'] = '尚無頻道';
  i18n.zh['channel.tagsHint'] = '標籤(逗號分隔,選填)';
  i18n.zh['channel.clearAll'] = '🗑 清空全部';
  i18n.zh['kw.newGroup'] = '+ 新增群組';
  i18n.zh['kw.delGroup'] = '🗑 刪除群組';
  i18n.zh['kw.groupName'] = '請輸入群組名稱';
  i18n.zh['kw.groupNamePh'] = '群組名稱';
  i18n.zh['kw.kwPlaceholder'] = '關鍵字';
  i18n.zh['kw.addKwPrompt'] = '請輸入關鍵字';
  i18n.zh['kw.scope.title'] = ' 標題';
  i18n.zh['kw.scope.description'] = ' 描述';
  i18n.zh['kw.noGroups'] = '尚無群組';
  i18n.zh['filter.wlOnly'] = '⭐ 只顯示白名單頻道';
  i18n.zh['filter.wlOnly.hint'] = '啟用後,僅顯示白名單內的頻道,其餘全部隱藏';
  i18n.zh['filter.duration'] = '⏱ 影片時長';
  i18n.zh['filter.duration.min'] = '最短(秒)';
  i18n.zh['filter.duration.max'] = '最長(秒,0=不限)';
  i18n.zh['filter.live'] = '📡 直播 / 首播';
  i18n.zh['filter.live.hideLive'] = ' 隱藏直播中';
  i18n.zh['filter.live.hideUpcoming'] = ' 隱藏即將首播';
  i18n.zh['filter.member'] = '👑 會員專屬';
  i18n.zh['filter.views'] = '👁 觀看數';
  i18n.zh['filter.views.min'] = '最少觀看';
  i18n.zh['filter.views.max'] = '最多觀看(0=不限)';
  i18n.zh['filter.views.hint'] = '直接輸入數字:10000(一萬)/ 1000000(百萬)。直播的「N 人正在觀看」不計入;部分卡片無觀看數時略過';
  i18n.zh['filter.date'] = '📅 上傳日期';
  i18n.zh['filter.date.hide'] = '隱藏';
  i18n.zh['filter.date.hint'] = '首頁顯示相對時間(精度:天),觀看頁側邊欄可取得精確日期';
  i18n.zh['filter.lang'] = '🌐 語言過濾';
  i18n.zh['filter.lang.modeBlock'] = '過濾指定語言';
  i18n.zh['filter.lang.modeAllow'] = '僅顯示指定語言';
  i18n.zh['filter.lang.custom'] = '其他語言代碼(逗號分隔,如 nl, pl, sv)';
  i18n.zh['filter.lang.ref'] = '🔗 BCP-47 語言代碼參考';
  i18n.zh['filter.lang.refTip'] = '開啟 IANA 語言代碼查詢';
  i18n.zh['filter.saveBtn'] = '💾 儲存設定';
  i18n.zh['filter.savedToast'] = '設定已儲存';
  i18n.zh['lang.zh'] = '中文(含所有變體)';
  i18n.zh['lang.en'] = '🇺🇸 英文';
  i18n.zh['lang.ja'] = '🇯🇵 日文';
  i18n.zh['lang.ko'] = '🇰🇷 韓文';
  i18n.zh['lang.th'] = '🇹🇭 泰文';
  i18n.zh['lang.vi'] = '🇻🇳 越南文';
  i18n.zh['lang.id'] = '🇮🇩 印尼文';
  i18n.zh['lang.es'] = '🇪🇸 西班牙文';
  i18n.zh['lang.pt'] = '🇵🇹 葡萄牙文';
  i18n.zh['lang.fr'] = '🇫🇷 法文';
  i18n.zh['lang.de'] = '🇩🇪 德文';
  i18n.zh['lang.ar'] = '🇸🇦 阿拉伯文';
  i18n.zh['lang.ru'] = '🇷🇺 俄文';
  i18n.zh['lang.hi'] = '🇮🇳 印地文';
  i18n.zh['lang.tr'] = '🇹🇷 土耳其文';
  i18n.zh['lang.it'] = '🇮🇹 義大利文';
  i18n.zh['set.global'] = '⚙️ 全域設定';
  i18n.zh['set.toast'] = ' 顯示操作通知(Toast)';
  i18n.zh['set.toastHint'] = '關閉後,操作通知仍會寫入面板頂部記錄,但不會彈出';
  i18n.zh['set.stats'] = ' 顯示統計與歷史 Tab';
  i18n.zh['set.statsHint'] = '關閉後 Tab 隱藏,但後台繼續記錄,可在匯出入頁獨立匯出';
  i18n.zh['set.lang'] = '🌐 介面語言 / Interface';
  i18n.zh['set.lang.zh'] = '中文(繁體)';
  i18n.zh['set.lang.switched'] = '語言已切換 / Language changed';
  i18n.zh['stat.totalHidden'] = '累計隱藏';
  i18n.zh['stat.topKw'] = '🔤 關鍵字命中排行';
  i18n.zh['stat.topCh'] = '🚫 頻道命中排行';
  i18n.zh['stat.noData'] = '尚無資料';
  i18n.zh['stat.clear'] = '🗑 清空統計';
  i18n.zh['stat.clearConfirm'] = '確定清空所有統計資料?';
  i18n.zh['stat.cleared'] = '統計已清空';
  i18n.zh['hist.empty'] = '尚無記錄';
  i18n.zh['hist.clear'] = '🗑 清空歷史';
  i18n.zh['hist.clearConfirm'] = '確定清空所有歷史記錄?';
  i18n.zh['hist.cleared'] = '歷史已清空';
  i18n.zh['io.backup'] = '💾 設定備份';
  i18n.zh['io.backupDesc'] = '備份隱藏名單、白名單、關鍵字群組、設定等完整資料。';
  i18n.zh['io.stat.section'] = '📊 統計與歷史獨立備份';
  i18n.zh['io.statHint'] = '統計與歷史不納入完整設定備份,請使用下方按鈕獨立匯出。';
  i18n.zh['io.exportJson'] = '📥 匯出 JSON';
  i18n.zh['io.importJson'] = '📤 匯入 JSON';
  i18n.zh['io.csvHint'] = 'CSV 格式可用試算表軟體開啟編輯,再匯入回腳本。';
  i18n.zh['io.mergeMode'] = '匯入模式:';
  i18n.zh['io.merge'] = '合併';
  i18n.zh['io.replace'] = '覆蓋';
  i18n.zh['io.share'] = '🔗 分享清單';
  i18n.zh['io.shareDesc'] = '產生分享連結,讓他人直接匯入你的過濾設定。';
  i18n.zh['io.shareBlock'] = '分享隱藏名單';
  i18n.zh['io.shareWhite'] = '分享白名單';
  i18n.zh['io.shareKw'] = '分享關鍵字';
  i18n.zh['io.copied'] = '已複製到剪貼簿';
  i18n.zh['io.genLinkFail'] = '產生連結失敗';
  i18n.zh['io.linkTooLong'] = '⚠️ 連結過長(';
  i18n.zh['io.linkTooLongSuffix'] = ' 字元),建議改用下方 JSON 備份匯出';
  i18n.zh['io.sharedImported'] = '分享清單已匯入';
  i18n.zh['io.backupFilename'] = '備份檔名(選填)';
  i18n.zh['io.backupFilenameHint'] = '留空則使用 yt-filter-backup(固定檔名覆蓋)';
  i18n.zh['about.software'] = '📋 軟體資訊';
  i18n.zh['about.name'] = '名稱';
  i18n.zh['about.version'] = '版本';
  i18n.zh['about.compat'] = '相容性';
  i18n.zh['about.author'] = '作者';
  i18n.zh['about.source'] = '原始碼';
  i18n.zh['about.donate'] = '☕ 支持開發者';
  i18n.zh['about.donateText'] = '本專案採用自願訂閱模式。如果您認為這個工具有價值,並且每月能輕鬆捐贈幾枚金幣,歡迎支持我持續維護與開發。您的支持是最大的動力!';
  i18n.zh['toast.channelEmpty'] = '名稱不可為空';
  i18n.zh['channel.manageTags'] = '管理標籤';
  i18n.zh['channel.removeTag'] = '移除標籤';
  i18n.zh['channel.confirmRemove'] = '確認移除';
  i18n.zh['channel.regexMode'] = ' ⌥R.* Regex 模式(比對頻道名稱)';
  i18n.zh['channel.regexHint'] = '輸入 pattern 即可,不需加 / 或 /i,預設不分大小寫';
  i18n.zh['channel.cleared.block'] = '已清空隱藏名單';
  i18n.zh['channel.cleared.white'] = '已清空白名單';
  i18n.zh['io.shareHint2'] = '對方在 YouTube 頁面開啟此連結後,腳本會自動偵測並提示匯入';
  i18n.zh['io.autoBackup'] = ' 啟用自動備份';
  i18n.zh['io.backupInterval'] = '備份間隔(天)';
  i18n.zh['io.backupPrefixHint'] = '指定前綴後,檔名為「前綴-日期.json」;留空則每次覆蓋同一檔案';
  i18n.zh['help.language2'] = '偵測方式:根據標題字元集自動判斷語系。拉丁語系(越南文、法文等)因字元重疊偵測精度較低,建議搭配關鍵字過濾補充。';
  i18n.zh['channel.clearBlockConfirm'] = '確定清空所有隱藏頻道(共 ';
  i18n.zh['channel.clearWhiteConfirm'] = '確定清空所有白名單頻道(共 ';
  i18n.zh['common.countSuffix'] = ' 個)?';
  i18n.zh['channel.confirmRemovePrefix'] = '確定移除「';
  i18n.zh['channel.confirmRemoveSuffix'] = '」?';
  i18n.zh['kw.confirmDelPrefix'] = '確定刪除群組「';
  i18n.zh['kw.confirmDelSuffix2'] = '」?';
  i18n.zh['toast.added'] = '已新增:';
  i18n.zh['toast.updated'] = '已更新:';
  i18n.zh['io.label.blocklist'] = '隱藏名單:';
  i18n.zh['io.label.whitelist'] = '白名單:';
  i18n.zh['io.label.keywords'] = '關鍵字:';
  i18n.zh['kw.countSuffix'] = '個關鍵字';
  i18n.zh['channel.tagFilter.all'] = '全部(';
  i18n.zh['channel.tagFilter.allSuffix'] = ')';
  i18n.zh['hist.unknownTitle'] = '未知標題';
  i18n.zh['channel.noResults'] = '無符合結果';
  i18n.zh['kw.addKwBtn'] = '+ 關鍵字';
  i18n.zh['channel.newTagPh'] = '新標籤名稱,Enter 確認';
  i18n.zh['channel.editRegexMode'] = ' ⌥R.* Regex 模式';
  i18n.zh['filter.lang.customLabel'] = '自訂其他語言代碼';

  // ── D-5: 重寫 makeAbout 新增 key ─────────────────
  i18n.zh['help.quickStart.title'] = '🚀 快速入門';
  i18n.zh['help.quickStart.step1'] = '① 隱藏頻道:滑鼠移到影片卡片上,點擊底部「🚫 隱藏此頻道」即可永久隱藏該頻道所有影片';
  i18n.zh['help.quickStart.step2'] = '② 白名單:點擊「⭐ 加入白名單」,該頻道將永遠顯示(不受任何過濾規則影響)';
  i18n.zh['help.quickStart.step3'] = '③ 關鍵字過濾:在 🔤 關鍵字 Tab 建立群組並加入字詞,標題或描述包含該字詞的影片會被隱藏';
  i18n.zh['help.quickStart.step4'] = '④ 過濾器開關:右鍵點擊頂欄 🔍 按鈕可暫停 / 啟用整個過濾器(臨時查看所有影片時很方便)';
  i18n.zh['help.regex.title'] = '🔍 Regex 模式完整指南';
  i18n.zh['help.regex.intro'] = '在「頻道隱藏/白名單」和「關鍵字群組」勾選「⌥R.* Regex 模式」後,可用正規表達式精確比對。預設不分大小寫。';
  i18n.zh['help.regex.syntax.title'] = '常用語法';
  i18n.zh['help.regex.syntax.1'] = '• ^abc  — 以 abc 開頭';
  i18n.zh['help.regex.syntax.2'] = '• abc$  — 以 abc 結尾';
  i18n.zh['help.regex.syntax.3'] = '• a|b  — a 或 b(擇一)';
  i18n.zh['help.regex.syntax.4'] = '• \\d+  — 一個或多個數字';
  i18n.zh['help.regex.syntax.5'] = '• \\b  — 單詞邊界(避免部分匹配)';
  i18n.zh['help.regex.syntax.6'] = '• .*  — 任意字元(零或多個)';
  i18n.zh['help.regex.syntax.7'] = '• [0-9]  — 字元範圍(如 0 到 9)';
  i18n.zh['help.regex.examples.title'] = '實用範例';
  i18n.zh['help.regex.examples.1'] = '• ^\\[業配\\]  — 所有「[業配]」開頭的影片';
  i18n.zh['help.regex.examples.2'] = '• \\bNASA\\b  — 獨立出現 NASA 的影片(避開 "NASA-related")';
  i18n.zh['help.regex.examples.3'] = '• (股票|投資|理財)  — 提到股票、投資、理財任一關鍵字';
  i18n.zh['help.regex.examples.4'] = '• EP\\.?\\d+  — EP1, EP.1, EP 1 等格式的集數(頻道名用於過濾特定系列)';
  i18n.zh['help.regex.examples.5'] = '• ^(?!.*官方).*$  — 標題不包含「官方」的所有影片(負向先行斷言)';
  i18n.zh['help.regex.tip'] = '💡 提示:不需要加 / 或 /i 標記,直接輸入 pattern 即可;反斜線在 JS 需要跳脫 \\\\,但本介面會自動處理。';
  i18n.zh['help.kids.title'] = '🛡️ 兒童保護設定範例';
  i18n.zh['help.kids.intro'] = '以下組合利用腳本現有功能,打造一個「白名單專享 + 危險內容自動過濾」的兒童友善瀏覽環境。';
  i18n.zh['help.kids.step1'] = '第 1 步|白名單:到 ⭐ 白名單 Tab 加入孩子可看的頻道(例如 @PBSKids、@SesameStreet、@CocomelonOfficial)';
  i18n.zh['help.kids.step2'] = '第 2 步|白名單獨享:到 ⚙️ 設定 最頂部,勾選「⭐ 只顯示白名單頻道」— 其他所有頻道都會被隱藏';
  i18n.zh['help.kids.step3'] = '第 3 步|禁直播:勾選「📡 直播 / 首播」區塊內的「隱藏直播中」和「隱藏即將首播」— 避免直播聊天室風險';
  i18n.zh['help.kids.step4'] = '第 4 步|關鍵字黑名單:到 🔤 關鍵字 建立「危險內容」群組,加入如 暴力、血腥、恐怖、成人、賭博 等字詞並啟用(可用 Regex:(暴力|血腥|恐怖|成人))';
  i18n.zh['help.kids.step5'] = '第 5 步(可選)|限制日期:到 📅 上傳日期 設定「隱藏 30 天前上傳的影片」— 避免演算法推送陳年爭議內容';
  i18n.zh['help.kids.step6'] = '第 6 步(可選)|語言過濾:到 🌐 語言過濾 選擇「僅顯示指定語言」並只勾選孩子的母語';
  i18n.zh['help.kids.warning'] = '⚠️ 重要警語:此腳本僅過濾首頁 / 搜尋 / 推薦等列表頁,無法阻止直接輸入影片 URL,也無法偵測影片實際內容。強烈建議搭配 YouTube Kids、家長監護軟體(Google Family Link 等)、或路由器層級的 DNS 過濾同時使用。';
  i18n.zh['help.shortcuts.title'] = '⌨️ 快捷操作';
  i18n.zh['help.shortcuts.fab'] = '頂欄 🔍 按鈕:左鍵開啟面板,右鍵切換過濾器開關';
  i18n.zh['help.shortcuts.cardBar'] = '影片卡片快捷列:滑鼠懸停時底部滑出「🚫 隱藏」和「⭐ 白名單」按鈕';
  i18n.zh['help.shortcuts.tmMenu'] = 'Tampermonkey 選單:瀏覽器擴充圖示也提供「開啟面板」「切換過濾器」選項';
  i18n.zh['io.toast.exported.json'] = '已匯出設定 JSON';
  i18n.zh['io.toast.exported.bl'] = '已匯出隱藏名單 CSV';
  i18n.zh['io.toast.exported.wl'] = '已匯出白名單 CSV';
  i18n.zh['io.toast.exported.kw'] = '已匯出關鍵字 CSV';
  i18n.zh['io.toast.exported.stats'] = '已匯出統計資料 JSON';
  i18n.zh['io.toast.exported.hist'] = '已匯出歷史記錄 CSV';
  i18n.zh['io.toast.importFailed'] = '匯入失敗:';
  i18n.zh['io.err.format'] = '格式不符';
  i18n.zh['io.toast.imported.overwrite'] = '已覆蓋匯入設定';
  i18n.zh['io.toast.imported.prefix'] = '已匯入設定(新增 ';
  i18n.zh['io.toast.imported.blPrefix'] = '已匯入隱藏名單:新增 ';
  i18n.zh['io.toast.imported.kwPrefix'] = '已匯入關鍵字:新增 ';
  i18n.zh['io.toast.imported.suffixDup'] = ' 筆(重複)';
  i18n.zh['io.toast.imported.skip'] = ' / 略過 ';
  i18n.zh['io.toast.imported.suffixEnd'] = ' 筆)';
  i18n.zh['io.toast.autoBackupDone'] = '自動備份完成:';
  i18n.zh['io.toast.shareReceived'] = '收到分享清單(';
  i18n.zh['io.toast.shareReceivedEnd'] = '),請在管理面板 > 匯出入 中匯入';
  i18n.zh['io.label.groups'] = '關鍵字群組:';
  i18n.zh['common.groupSuffix'] = '個群組';
  i18n.zh['common.countOnly'] = '個';
  i18n.zh['tm.togglePanel'] = '⚙️ 開啟管理面板';
  i18n.zh['tm.toggleFilter'] = '🔄 切換過濾器';
  i18n.zh['tm.backupNow'] = '💾 立即備份設定';
  i18n.zh['tm.debugToggle'] = '🔍 切換 Debug 模式';
  i18n.zh['tm.debugStats'] = '📊 顯示 Debug 統計';
  i18n.zh['debug.hint'] = '— 執行 __ytfReport() 查看統計';
  i18n.zh['csv.header.channelId'] = '頻道ID';
  i18n.zh['csv.header.channelName'] = '頻道名稱';
  i18n.zh['csv.header.tags'] = '標籤';
  i18n.zh['csv.header.groupName'] = '群組名稱';
  i18n.zh['csv.header.keyword'] = '關鍵字';
  i18n.zh['csv.header.regex'] = 'Regex';
  i18n.zh['csv.header.scope'] = '範圍';
  i18n.zh['csv.header.enabled'] = '啟用';
  i18n.zh['csv.header.title'] = '標題';
  i18n.zh['csv.header.reason'] = '觸發原因';
  i18n.zh['csv.header.time'] = '時間';
  i18n.zh['kw.searchPh'] = '搜尋群組名或關鍵字...';
  i18n.zh['channel.noExistingTags'] = '目前沒有可選的標籤';
  i18n.zh['channel.addTagLabel'] = '新增標籤';
  i18n.zh['channel.existingTagsLabel'] = '所有標籤(✓已擁有、點擊切換)';
  i18n.zh['kw.groupNameLabel'] = '群組名稱';
  i18n.zh['kw.scopeLabel'] = '套用範圍';
  i18n.zh['kw.scopeRequired'] = '至少需要選一個範圍(標題或描述)';
  i18n.zh['filter.date.hideOlder'] = '比設定值老的(舊片)';
  i18n.zh['filter.date.hideNewer'] = '比設定值新的(新片)';
  i18n.zh['filter.date.unit.day'] = '天';
  i18n.zh['filter.date.unit.year'] = '年';


  i18n.en['reason.unknown']       = 'Unknown';
  i18n.en['reason.keyword']       = 'Keyword: ';
  i18n.en['reason.keyword.suffix']= '';
  i18n.en['reason.channel']       = 'Channel blocked';
  i18n.en['reason.language']      = 'Language (';
  i18n.en['reason.language.suffix']= ')';
  i18n.en['reason.duration']      = 'Duration filter';
  i18n.en['reason.views']         = 'View count filter';
  i18n.en['reason.date']          = 'Upload date filter';
  i18n.en['reason.live']          = 'Live stream';
  i18n.en['reason.upcoming']      = 'Upcoming premiere';
  i18n.en['reason.members_only']  = 'Members only';
  i18n.en['notif.hidden.prefix']  = 'Hidden ';
  i18n.en['notif.hidden.suffix']  = ' videos — reason: ';
  i18n.en['qa.channelTip']        = 'Channel: ';
  i18n.en['qa.block']             = '🚫 Block channel';
  i18n.en['qa.whitelist']         = '⭐ Whitelist';
  i18n.en['qa.blockTitle']        = 'Block channel: ';
  i18n.en['qa.whitelistTitle']    = 'Whitelist: ';
  i18n.en['qa.hidden']            = '✅ Hidden';
  i18n.en['qa.whitelisted']       = '✅ Whitelisted';
  i18n.en['qa.already.block']     = 'Already blocked: ';
  i18n.en['qa.already.white']     = 'Already whitelisted: ';
  i18n.en['qa.conflict.blockToWL']= '» is already in blocklist';
  i18n.en['qa.conflict.WLToBlock']= '» is already whitelisted';
  i18n.en['qa.hiddenPrefix']      = 'Hidden: ';
  i18n.en['fab.title']            = 'YouTube Channel Filter — click to open / right-click to toggle';
  i18n.en['fab.tip.enabled']      = 'YouTube Channel Filter (active)\nLeft-click: open panel | Right-click: pause';
  i18n.en['fab.tip.disabled']     = 'YouTube Channel Filter (paused)\nLeft-click: open panel | Right-click: resume';
  i18n.en['filter.enabled']       = 'Filter enabled';
  i18n.en['filter.disabled']      = 'Filter paused';
  i18n.en['notif.separator']     = ', ';
  i18n.en['qa.quote.open']       = '«';
  // ── C-3: Panel UI (EN) ────────────────────────────────
  i18n.en['panel.title'] = 'YouTube Channel Filter';
  i18n.en['tab.blocklist'] = '🚫 Blocklist';
  i18n.en['tab.whitelist'] = '⭐ Whitelist';
  i18n.en['tab.keywords'] = '🔤 Keywords';
  i18n.en['tab.filters'] = '⚙️ Settings';
  i18n.en['tab.stats'] = '📊 Stats';
  i18n.en['tab.history'] = '📋 History';
  i18n.en['tab.io'] = '💾 I/O';
  i18n.en['tab.about'] = '📖 Help';
  i18n.en['panel.close'] = 'Close';
  i18n.en['common.confirm'] = 'Confirm';
  i18n.en['common.cancel'] = 'Cancel';
  i18n.en['common.save'] = 'Save';
  i18n.en['common.remove'] = 'Remove';
  i18n.en['common.edit'] = 'Edit';
  i18n.en['common.add'] = 'Add';
  i18n.en['common.create'] = 'Create';
  i18n.en['common.copy'] = 'Copy';
  i18n.en['common.skip'] = 'Skip';
  i18n.en['common.all'] = 'All';
  i18n.en['common.up'] = 'Up';
  i18n.en['common.down'] = 'Down';
  i18n.en['common.mode'] = 'Mode';
  i18n.en['common.total'] = 'Total ';
  i18n.en['common.items'] = ' items';
  i18n.en['common.times'] = ' times';
  i18n.en['common.groups'] = ' groups';
  i18n.en['common.channels'] = ' channels';
  i18n.en['channel.searchPlaceholder'] = 'Search channels...';
  i18n.en['channel.name'] = 'Channel name';
  i18n.en['channel.handle'] = 'Channel ID (optional, e.g. @handle)';
  i18n.en['channel.handleShort'] = '@handle (optional)';
  i18n.en['channel.addPrompt'] = 'Please enter a channel name';
  i18n.en['channel.regexMode'] = ' ⌥R.* Regex mode (match channel name)';
  i18n.en['channel.regexHint'] = 'Enter pattern only, no slashes or flags. Case-insensitive by default.';
  i18n.en['channel.empty.blocklist'] = 'No channels yet';
  i18n.en['channel.tagsHint'] = 'Tags (comma-separated, optional)';
  i18n.en['channel.manageTags'] = 'Manage tags';
  i18n.en['channel.removeTag'] = 'Remove tag';
  i18n.en['channel.confirmRemove'] = 'Confirm removal';
  i18n.en['channel.clearAll'] = '🗑 Clear all';
  i18n.en['channel.cleared.block'] = 'Blocklist cleared';
  i18n.en['channel.cleared.white'] = 'Whitelist cleared';
  i18n.en['kw.newGroup'] = '+ New group';
  i18n.en['kw.delGroup'] = '🗑 Delete group';
  i18n.en['kw.groupName'] = 'Please enter a group name';
  i18n.en['kw.groupNamePh'] = 'Group name';
  i18n.en['kw.kwPlaceholder'] = 'Keyword';
  i18n.en['kw.addKwPrompt'] = 'Please enter a keyword';
  i18n.en['kw.scope.title'] = ' Title';
  i18n.en['kw.scope.description'] = ' Description';
  i18n.en['kw.noGroups'] = 'No groups yet';
  i18n.en['filter.wlOnly'] = '⭐ Show only whitelisted channels';
  i18n.en['filter.wlOnly.hint'] = 'When enabled, only whitelisted channels are shown; all others are hidden.';
  i18n.en['filter.duration'] = '⏱ Video duration';
  i18n.en['filter.duration.min'] = 'Minimum (seconds)';
  i18n.en['filter.duration.max'] = 'Maximum (seconds, 0 = unlimited)';
  i18n.en['filter.live'] = '📡 Live / Premieres';
  i18n.en['filter.live.hideLive'] = ' Hide live streams';
  i18n.en['filter.live.hideUpcoming'] = ' Hide upcoming premieres';
  i18n.en['filter.member'] = '👑 Members-only';
  i18n.en['filter.views'] = '👁 View count';
  i18n.en['filter.views.min'] = 'Minimum views';
  i18n.en['filter.views.max'] = 'Maximum views (0 = unlimited)';
  i18n.en['filter.views.hint'] = 'Enter numbers directly: 10000 (10K) / 1000000 (1M). Live-stream viewer counts are not counted; cards without a view count are skipped.';
  i18n.en['filter.date'] = '📅 Upload date';
  i18n.en['filter.date.hide'] = 'Hide';
  i18n.en['filter.date.hint'] = 'Example: choose «Older than» + 4 years = hide videos older than 4 years.';
  i18n.en['filter.lang'] = '🌐 Language filter';
  i18n.en['filter.lang.modeBlock'] = 'Block selected languages';
  i18n.en['filter.lang.modeAllow'] = 'Show only selected languages';
  i18n.en['filter.lang.custom'] = 'Other language codes (comma-separated, e.g. nl, pl, sv)';
  i18n.en['filter.lang.ref'] = '🔗 BCP-47 language code reference';
  i18n.en['filter.lang.refTip'] = 'Open IANA language subtag lookup';
  i18n.en['filter.saveBtn'] = '💾 Save settings';
  i18n.en['filter.savedToast'] = 'Settings saved';
  i18n.en['lang.zh'] = 'Chinese (all variants)';
  i18n.en['lang.en'] = '🇺🇸 English';
  i18n.en['lang.ja'] = '🇯🇵 Japanese';
  i18n.en['lang.ko'] = '🇰🇷 Korean';
  i18n.en['lang.th'] = '🇹🇭 Thai';
  i18n.en['lang.vi'] = '🇻🇳 Vietnamese';
  i18n.en['lang.id'] = '🇮🇩 Indonesian';
  i18n.en['lang.es'] = '🇪🇸 Spanish';
  i18n.en['lang.pt'] = '🇵🇹 Portuguese';
  i18n.en['lang.fr'] = '🇫🇷 French';
  i18n.en['lang.de'] = '🇩🇪 German';
  i18n.en['lang.ar'] = '🇸🇦 Arabic';
  i18n.en['lang.ru'] = '🇷🇺 Russian';
  i18n.en['lang.hi'] = '🇮🇳 Hindi';
  i18n.en['lang.tr'] = '🇹🇷 Turkish';
  i18n.en['lang.it'] = '🇮🇹 Italian';
  i18n.en['set.global'] = '⚙️ Global settings';
  i18n.en['set.toast'] = ' Show operation toasts';
  i18n.en['set.toastHint'] = 'When disabled, toasts are silent but still logged in the panel.';
  i18n.en['set.stats'] = ' Show Stats & History tabs';
  i18n.en['set.statsHint'] = 'When disabled, tabs are hidden but tracking continues. Export separately in Import/Export.';
  i18n.en['set.lang'] = '🌐 Interface language / 介面語言';
  i18n.en['set.lang.zh'] = 'Chinese (Traditional)';
  i18n.en['set.lang.switched'] = 'Language changed / 語言已切換';
  i18n.en['stat.totalHidden'] = 'Total hidden';
  i18n.en['stat.topKw'] = '🔤 Top keywords';
  i18n.en['stat.topCh'] = '🚫 Top channels';
  i18n.en['stat.noData'] = 'No data yet';
  i18n.en['stat.clear'] = '🗑 Clear stats';
  i18n.en['stat.clearConfirm'] = 'Clear all stats?';
  i18n.en['stat.cleared'] = 'Stats cleared';
  i18n.en['hist.empty'] = 'No entries yet';
  i18n.en['hist.clear'] = '🗑 Clear history';
  i18n.en['hist.clearConfirm'] = 'Clear all history?';
  i18n.en['hist.cleared'] = 'History cleared';
  i18n.en['io.backup'] = '💾 Settings backup';
  i18n.en['io.backupDesc'] = 'Backup blocklist, whitelist, keyword groups, filter settings, and preferences.';
  i18n.en['io.stat.section'] = '📊 Stats & History standalone backup';
  i18n.en['io.statHint'] = 'Stats and history are not included in the full backup. Use the buttons below to export them separately.';
  i18n.en['io.exportJson'] = '📥 Export JSON';
  i18n.en['io.importJson'] = '📤 Import JSON';
  i18n.en['io.csvHint'] = 'CSV can be edited in spreadsheet software and imported back.';
  i18n.en['io.mergeMode'] = 'Import mode: ';
  i18n.en['io.merge'] = 'Merge';
  i18n.en['io.replace'] = 'Replace';
  i18n.en['io.share'] = '🔗 Share list';
  i18n.en['io.shareDesc'] = 'Generate a share link so others can import your filter config directly.';
  i18n.en['io.shareBlock'] = 'Share blocklist';
  i18n.en['io.shareWhite'] = 'Share whitelist';
  i18n.en['io.shareKw'] = 'Share keywords';
  i18n.en['io.copied'] = 'Copied to clipboard';
  i18n.en['io.genLinkFail'] = 'Failed to generate link';
  i18n.en['io.linkTooLong'] = '⚠️ Link too long (';
  i18n.en['io.linkTooLongSuffix'] = ' chars). Use JSON backup instead.';
  i18n.en['io.sharedImported'] = 'Shared list imported';
  i18n.en['io.backupFilename'] = 'Backup filename (optional)';
  i18n.en['io.backupFilenameHint'] = 'Leave blank to use yt-filter-backup (overwrites same file).';
  i18n.en['about.software'] = '📋 Software info';
  i18n.en['about.name'] = 'Name';
  i18n.en['about.version'] = 'Version';
  i18n.en['about.compat'] = 'Compatibility';
  i18n.en['about.author'] = 'Author';
  i18n.en['about.source'] = 'Source code';
  i18n.en['about.donate'] = '☕ Support the developer';
  i18n.en['about.donateText'] = 'This project runs on voluntary support. If you find this tool valuable and can spare a few coins each month, your sponsorship helps me keep maintaining and improving it. Thank you!';
  i18n.en['toast.channelEmpty'] = 'Name cannot be empty';
  i18n.en['io.shareHint2'] = 'When the recipient opens this link on YouTube, the script will detect and prompt for import.';
  i18n.en['io.autoBackup'] = ' Enable auto-backup';
  i18n.en['io.backupInterval'] = 'Backup interval (days)';
  i18n.en['io.backupPrefixHint'] = 'With a prefix, filename becomes «prefix-date.json». Blank overwrites the same file.';
  i18n.en['help.language2'] = 'Detection uses title charset. Latin-based languages (Vietnamese, French etc.) have lower detection accuracy due to character overlap—combine with keyword filters for better coverage.';
  i18n.en['channel.clearBlockConfirm'] = 'Clear all blocked channels (total ';
  i18n.en['channel.clearWhiteConfirm'] = 'Clear all whitelisted channels (total ';
  i18n.en['common.countSuffix'] = ' items)?';
  i18n.en['channel.confirmRemovePrefix'] = 'Remove «';
  i18n.en['channel.confirmRemoveSuffix'] = '» ?';
  i18n.en['kw.confirmDelPrefix'] = 'Delete group «';
  i18n.en['kw.confirmDelSuffix2'] = '» ?';
  i18n.en['toast.added'] = 'Added: ';
  i18n.en['toast.updated'] = 'Updated: ';
  i18n.en['io.label.blocklist'] = 'Blocklist: ';
  i18n.en['io.label.whitelist'] = 'Whitelist: ';
  i18n.en['io.label.keywords'] = 'Keywords: ';
  i18n.en['kw.countSuffix'] = ' keywords';
  i18n.en['channel.tagFilter.all'] = 'All (';
  i18n.en['channel.tagFilter.allSuffix'] = ')';
  i18n.en['hist.unknownTitle'] = 'Untitled';
  i18n.en['channel.noResults'] = 'No matching results';
  i18n.en['kw.addKwBtn'] = '+ Keyword';
  i18n.en['channel.newTagPh'] = 'New tag, press Enter to confirm';
  i18n.en['channel.editRegexMode'] = ' ⌥R.* Regex mode';
  i18n.en['filter.lang.customLabel'] = 'Custom language codes';

  // ── D-5: makeAbout new keys (EN) ──────────────────
  i18n.en['help.quickStart.title'] = '🚀 Quick Start';
  i18n.en['help.quickStart.step1'] = '① Block a channel: Hover a video card and click «🚫 Block channel» at the bottom to permanently hide all videos from that channel.';
  i18n.en['help.quickStart.step2'] = '② Whitelist: Click «⭐ Whitelist» to always show that channel (bypassing all filter rules).';
  i18n.en['help.quickStart.step3'] = '③ Keyword filter: Go to 🔤 Keywords, create a group, and add terms. Videos whose title or description contains those terms will be hidden.';
  i18n.en['help.quickStart.step4'] = '④ Toggle filter: Right-click the 🔍 button in the top bar to pause/resume the whole filter (handy when you want to browse without filters).';
  i18n.en['help.regex.title'] = '🔍 Regex Mode Complete Guide';
  i18n.en['help.regex.intro'] = 'Enable «⌥R.* Regex mode» in Block/Whitelist entries or Keyword groups to use regular expressions for precise matching. Case-insensitive by default.';
  i18n.en['help.regex.syntax.title'] = 'Common syntax';
  i18n.en['help.regex.syntax.1'] = '• ^abc  — starts with abc';
  i18n.en['help.regex.syntax.2'] = '• abc$  — ends with abc';
  i18n.en['help.regex.syntax.3'] = '• a|b  — a OR b';
  i18n.en['help.regex.syntax.4'] = '• \\d+  — one or more digits';
  i18n.en['help.regex.syntax.5'] = '• \\b  — word boundary (avoids partial match)';
  i18n.en['help.regex.syntax.6'] = '• .*  — any characters (zero or more)';
  i18n.en['help.regex.syntax.7'] = '• [0-9]  — character range (e.g. 0 to 9)';
  i18n.en['help.regex.examples.title'] = 'Practical examples';
  i18n.en['help.regex.examples.1'] = '• ^\\[Ad\\]  — all videos starting with «[Ad]»';
  i18n.en['help.regex.examples.2'] = '• \\bNASA\\b  — standalone NASA (avoids matches like «NASA-related»)';
  i18n.en['help.regex.examples.3'] = '• (stocks|investing|finance)  — mentions any of these keywords';
  i18n.en['help.regex.examples.4'] = '• EP\\.?\\d+  — matches EP1, EP.1, EP 1 etc. (useful for filtering specific series)';
  i18n.en['help.regex.examples.5'] = '• ^(?!.*official).*$  — titles NOT containing «official» (negative lookahead)';
  i18n.en['help.regex.tip'] = '💡 Tip: no need for / slashes or /i flags — just enter the pattern. Backslashes are handled automatically by the UI.';
  i18n.en['help.kids.title'] = '🛡️ Child Safety Setup Example';
  i18n.en['help.kids.intro'] = 'The following combo uses the script\'s existing features to build a «whitelist-only + auto-filter harmful content» child-friendly browsing setup.';
  i18n.en['help.kids.step1'] = 'Step 1 | Whitelist: Go to the ⭐ Whitelist tab and add child-safe channels (e.g. @PBSKids, @SesameStreet, @CocomelonOfficial).';
  i18n.en['help.kids.step2'] = 'Step 2 | Whitelist-only: Go to ⚙️ Settings (top) and tick «⭐ Show only whitelisted channels» — all other channels will be hidden.';
  i18n.en['help.kids.step3'] = 'Step 3 | No live streams: In «📡 Live / Premieres», enable «Hide live streams» and «Hide upcoming premieres» — avoids live-chat risks.';
  i18n.en['help.kids.step4'] = 'Step 4 | Keyword blocklist: In 🔤 Keywords, create a «Harmful content» group with words like violence, gore, horror, adult, gambling and enable it (regex: (violence|gore|horror|adult|gambling)).';
  i18n.en['help.kids.step5'] = 'Step 5 (optional) | Date limit: In 📅 Upload date, set «Hide videos older than 30 days» — avoids the algorithm pushing old controversial content.';
  i18n.en['help.kids.step6'] = 'Step 6 (optional) | Language filter: In 🌐 Language filter, choose «Show only selected languages» and tick only the child\'s native language.';
  i18n.en['help.kids.warning'] = '⚠️ Important disclaimer: This script only filters listing pages (home / search / recommendations). It cannot block direct URL access, nor detect actual video content. Strongly recommend combining with YouTube Kids, parental control software (Google Family Link, etc.), or router-level DNS filtering.';
  i18n.en['help.shortcuts.title'] = '⌨️ Quick Actions';
  i18n.en['help.shortcuts.fab'] = 'Top bar 🔍 button: left-click opens panel, right-click toggles filter on/off.';
  i18n.en['help.shortcuts.cardBar'] = 'Card quick-bar: hover any video card to reveal «🚫 Block» and «⭐ Whitelist» buttons at the bottom.';
  i18n.en['help.shortcuts.tmMenu'] = 'Tampermonkey menu: the browser extension icon also provides «Open panel» and «Toggle filter» options.';
  i18n.en['io.toast.exported.json'] = 'Exported settings JSON';
  i18n.en['io.toast.exported.bl'] = 'Exported blocklist CSV';
  i18n.en['io.toast.exported.wl'] = 'Exported whitelist CSV';
  i18n.en['io.toast.exported.kw'] = 'Exported keywords CSV';
  i18n.en['io.toast.exported.stats'] = 'Exported stats JSON';
  i18n.en['io.toast.exported.hist'] = 'Exported history CSV';
  i18n.en['io.toast.importFailed'] = 'Import failed: ';
  i18n.en['io.err.format'] = 'Format mismatch';
  i18n.en['io.toast.imported.overwrite'] = 'Settings imported (overwritten)';
  i18n.en['io.toast.imported.prefix'] = 'Settings imported (added ';
  i18n.en['io.toast.imported.blPrefix'] = 'Blocklist imported: added ';
  i18n.en['io.toast.imported.kwPrefix'] = 'Keywords imported: added ';
  i18n.en['io.toast.imported.suffixDup'] = ' items (duplicates)';
  i18n.en['io.toast.imported.skip'] = ' / skipped ';
  i18n.en['io.toast.imported.suffixEnd'] = ' items)';
  i18n.en['io.toast.autoBackupDone'] = 'Auto-backup complete: ';
  i18n.en['io.toast.shareReceived'] = 'Received shared list (';
  i18n.en['io.toast.shareReceivedEnd'] = '), please import from Panel > Import/Export';
  i18n.en['io.label.groups'] = 'Keyword groups: ';
  i18n.en['common.groupSuffix'] = ' groups';
  i18n.en['common.countOnly'] = ' items';
  i18n.en['tm.togglePanel'] = '⚙️ Open management panel';
  i18n.en['tm.toggleFilter'] = '🔄 Toggle filter';
  i18n.en['tm.backupNow'] = '💾 Backup settings now';
  i18n.en['tm.debugToggle'] = '🔍 Toggle debug mode';
  i18n.en['tm.debugStats'] = '📊 Show debug stats';
  i18n.en['debug.hint'] = '— run __ytfReport() to view stats';
  i18n.en['csv.header.channelId'] = 'Channel ID';
  i18n.en['csv.header.channelName'] = 'Channel Name';
  i18n.en['csv.header.tags'] = 'Tags';
  i18n.en['csv.header.groupName'] = 'Group Name';
  i18n.en['csv.header.keyword'] = 'Keyword';
  i18n.en['csv.header.regex'] = 'Regex';
  i18n.en['csv.header.scope'] = 'Scope';
  i18n.en['csv.header.enabled'] = 'Enabled';
  i18n.en['csv.header.title'] = 'Title';
  i18n.en['csv.header.reason'] = 'Trigger Reason';
  i18n.en['csv.header.time'] = 'Time';
  i18n.en['kw.searchPh'] = 'Search groups or keywords...';
  i18n.en['channel.noExistingTags'] = 'No existing tags available';
  i18n.en['channel.addTagLabel'] = 'Add tag';
  i18n.en['channel.existingTagsLabel'] = 'All tags (✓owned, click to toggle)';
  i18n.en['kw.groupNameLabel'] = 'Group name';
  i18n.en['kw.scopeLabel'] = 'Apply to';
  i18n.en['kw.scopeRequired'] = 'At least one scope is required (title or description)';
  i18n.en['filter.date.hideOlder'] = 'Older than (old videos)';
  i18n.en['filter.date.hideNewer'] = 'Newer than (recent videos)';
  i18n.en['filter.date.unit.day'] = 'days';
  i18n.en['filter.date.unit.year'] = 'years';


  var LANG = (function() {
    try {
      var raw = GM_getValue('ytf_lang', null);
      if (raw === 'zh' || raw === 'en') return raw;
    } catch(e) {}
    // v1.1.0 預設英文(使用者可透過設定或 Tampermonkey 選單切換為中文)
    return 'en';
  })();
  function t(key) {
    var tbl = i18n[LANG] || i18n.en || {};
    if (tbl[key] !== undefined) return tbl[key];
    if (i18n.en && i18n.en[key] !== undefined) return i18n.en[key];
    return key;
  }
  function setLang(newLang) {
    if (newLang !== 'zh' && newLang !== 'en') return;
    LANG = newLang;
    try { GM_setValue('ytf_lang', newLang); } catch(e) {}
  }

  // ============================================================
  // STORAGE
  // ============================================================
  // ── Module-level Regex cache(S 和 Filter 共用,設定更新時由 Filter.reset() 清除)──
  var _reCache = new Map();
  function compiledRe(pat) {
    if(!_reCache.has(pat)){
      try{_reCache.set(pat,new RegExp(pat,'i'));}
      catch(e){_reCache.set(pat,null);}
    }
    return _reCache.get(pat);
  }

  var S = (function() {
    function jget(k, def) {
      try { var v = GM_getValue(k, null); return v === null ? def : JSON.parse(v); } catch(e) { return def; }
    }
    function jset(k, v) { try { GM_setValue(k, JSON.stringify(v)); } catch(e) {} }

    // ── 批次寫入(addStat / addHistory 高頻呼叫時 debounce)──
    var _pendingStat=null, _pendingHist=null;
    var _statTimer=null, _histTimer=null;
    function _flushStat(){ if(_pendingStat!==null){jset('ytf_st',_pendingStat);_pendingStat=null;} }
    function _flushHist(){ if(_pendingHist!==null){jset('ytf_hi',_pendingHist);_pendingHist=null;} }
    // 頁面隱藏時強制 flush,避免資料遺失
    document.addEventListener('visibilitychange',function(){ if(document.hidden){_flushStat();_flushHist();} });

    var defFilters = { duration:{enabled:false,min:0,max:0}, live:{enabled:false,hideLive:false,hideUpcoming:false}, member:{enabled:false}, language:{enabled:false,mode:'block',langs:[]}, whitelistOnly:false, uploadDate:{enabled:false,condition:'older',days:365}, views:{enabled:false,min:0,max:0} };

    function getBlocklist()  { return jget('ytf_bl', []); }
    function setBlocklist(v) { jset('ytf_bl', v); }
    function getWhitelist()  { return jget('ytf_wl', []); }
    function setWhitelist(v) { jset('ytf_wl', v); }
    function getKeyGroups()  { return jget('ytf_kg', []); }
    function setKeyGroups(v) { jset('ytf_kg', v); }
    function getFilters()    { return jget('ytf_fs', defFilters); }
    function setFilters(v)   { jset('ytf_fs', v); }
    function getStats()      { return jget('ytf_st', {totalHidden:0,keywordHits:{},channelHits:{}}); }
    function setStats(v)     { jset('ytf_st', v); }
    function getHistory()    { return jget('ytf_hi', []); }
    function setHistory(v)   { jset('ytf_hi', v); }
    function getGlobal()     { return jget('ytf_gl', {enabled:true,autoBackup:false,backupInterval:7,lastBackup:null,showToast:true,enableStats:true}); }
    function setGlobal(v)    { jset('ytf_gl', v); }

    function isEnabled()     { return getGlobal().enabled !== false; }
    function setEnabled(v)   { var g=getGlobal(); g.enabled=v; setGlobal(g); }

    function addToBlocklist(ch) {
      var l=getBlocklist();
      if (l.find(function(c){ return (ch.id&&c.id===ch.id)||(ch.name&&c.name===ch.name&&!ch.id); })) return false;
      l.push({id:ch.id||'',name:ch.name||'',isRegex:!!ch.isRegex,tags:ch.tags||[],addedAt:Date.now()});
      setBlocklist(l); return true;
    }
    function removeFromBlocklist(key) { setBlocklist(getBlocklist().filter(function(c){ return c.id!==key&&c.name!==key; })); }
    function updateBlocklistEntry(key, upd) {
      setBlocklist(getBlocklist().map(function(c){
        if(c.id===key||c.name===key) return Object.assign({},c,upd);
        return c;
      }));
    }
    function isBlocked(id,name) {
      return getBlocklist().some(function(c){
        if(c.isRegex&&c.name&&name){var _re=compiledRe(c.name);return _re?_re.test(name):false;}
        return (id&&c.id&&c.id===id)||(name&&c.name&&c.name===name);
      });
    }

    function addToWhitelist(ch) {
      var l=getWhitelist();
      if (l.find(function(c){ return (ch.id&&c.id===ch.id)||(ch.name&&c.name===ch.name); })) return false;
      // v1.1.0: 支援 tags 欄位(選填)
      l.push({id:ch.id||'',name:ch.name||'',isRegex:!!ch.isRegex,tags:ch.tags||[],addedAt:Date.now()});
      setWhitelist(l); return true;
    }
    function removeFromWhitelist(key) { setWhitelist(getWhitelist().filter(function(c){ return c.id!==key&&c.name!==key; })); }
    function updateWhitelistEntry(key, upd) {
      setWhitelist(getWhitelist().map(function(c){
        if(c.id===key||c.name===key) return Object.assign({},c,upd);
        return c;
      }));
    }
    function isWhitelisted(id,name) {
      return getWhitelist().some(function(c){
        if(c.isRegex&&c.name&&name){var _re=compiledRe(c.name);return _re?_re.test(name):false;}
        return (id&&c.id&&c.id===id)||(name&&c.name&&c.name===name);
      });
    }

    function addKeyGroup(g) {
      var groups=getKeyGroups();
      var ng={id:'g'+Date.now(),name:g.name||'未命名',enabled:true,keywords:g.keywords||[],scope:g.scope||['title']};
      groups.push(ng); setKeyGroups(groups); return ng;
    }
    function updateKeyGroup(id,upd) { setKeyGroups(getKeyGroups().map(function(g){ return g.id===id?Object.assign({},g,upd):g; })); }
    function removeKeyGroup(id)     { setKeyGroups(getKeyGroups().filter(function(g){ return g.id!==id; })); }
    function moveKeyGroup(id, dir) {
      var gs = getKeyGroups();
      var idx = gs.findIndex(function(g){ return g.id===id; });
      if(idx<0) return;
      var newIdx = dir==='up' ? idx-1 : idx+1;
      if(newIdx<0||newIdx>=gs.length) return;
      var tmp = gs[idx]; gs[idx] = gs[newIdx]; gs[newIdx] = tmp;
      setKeyGroups(gs);
    }

    function addStat(reason,chId,chName) {
      if(_pendingStat===null) _pendingStat=getStats();
      var s=_pendingStat; s.totalHidden=(s.totalHidden||0)+1;
      if (reason&&reason.indexOf('keyword:')===0){var kw=reason.slice(8);s.keywordHits[kw]=(s.keywordHits[kw]||0)+1;}
      if (chId) {
        s.channelHits[chId]=(s.channelHits[chId]||0)+1;
        if(chName){if(!s.channelNames)s.channelNames={};s.channelNames[chId]=chName;}
      }
      clearTimeout(_statTimer); _statTimer=setTimeout(_flushStat,1000);
    }
    function resetStats() { setStats({totalHidden:0,keywordHits:{},channelHits:{}}); }
    function addHistory(entry) {
      if(_pendingHist===null) _pendingHist=getHistory();
      var hh=_pendingHist; hh.unshift(Object.assign({},entry,{hiddenAt:Date.now()}));
      if(hh.length>500) hh.pop();
      clearTimeout(_histTimer); _histTimer=setTimeout(_flushHist,1000);
    }
    function clearHistory() { setHistory([]); }

    function exportAll() {
      // 統計與歷史不納入完整備份,只能透過獨立按鈕匯出
      return {version:'2.1',exportedAt:Date.now(),blocklist:getBlocklist(),whitelist:getWhitelist(),keyGroups:getKeyGroups(),filters:getFilters(),global:getGlobal()};
    }
    function exportStats() { return {version:'1.0',exportedAt:Date.now(),stats:getStats()}; }
    function exportHistoryCSV() {
      var rows=[[t('csv.header.title'),t('csv.header.channelName'),t('csv.header.channelId'),t('csv.header.reason'),t('csv.header.time')]];
      getHistory().forEach(function(r){
        rows.push([
          (r.title||'').replace(/,/g,','),
          (r.channelName||'').replace(/,/g,','),
          (r.channelId||'').replace(/,/g,','),
          (r.reason||'').replace(/,/g,','),
          r.hiddenAt?new Date(r.hiddenAt).toLocaleString():''
        ]);
      });
      return rows.map(function(r){return r.join(',');}).join('\n');
    }
    function importAll(data,mode) {
      if (!data||(data.version!=='2.1'&&data.version!=='2.0')) throw new Error(t('io.err.format'));
      if (mode==='overwrite'){
        if(data.blocklist)setBlocklist(data.blocklist);
        if(data.whitelist)setWhitelist(data.whitelist);
        if(data.keyGroups)setKeyGroups(data.keyGroups);
        if(data.filters)setFilters(data.filters);
      } else {
        if(data.blocklist){var bl=getBlocklist();data.blocklist.forEach(function(c){if(!bl.find(function(x){return x.id===c.id;}))bl.push(c);});setBlocklist(bl);}
        if(data.whitelist){var wl=getWhitelist();data.whitelist.forEach(function(c){if(!wl.find(function(x){return x.id===c.id;}))wl.push(c);});setWhitelist(wl);}
        if(data.keyGroups){var kg=getKeyGroups();data.keyGroups.forEach(function(g){if(!kg.find(function(x){return x.id===g.id;}))kg.push(g);});setKeyGroups(kg);}
      }
    }

    // 標籤管理:取得不重複標籤(v1.1.0: 修訂:支援 type 隔離 blocklist/whitelist)
    // type: 'block' 只掃隱藏名單 / 'white' 只掃白名單 / 不傳 = 兩者合計(向後相容)
    function getAllTags(type) {
      var tags = {};
      if(type==='block' || !type) {
        getBlocklist().forEach(function(c){
          (c.tags||[]).forEach(function(t){ if(t) tags[t] = (tags[t]||0)+1; });
        });
      }
      if(type==='white' || !type) {
        getWhitelist().forEach(function(c){
          (c.tags||[]).forEach(function(t){ if(t) tags[t] = (tags[t]||0)+1; });
        });
      }
      return Object.keys(tags).sort().map(function(t){ return {name:t, count:tags[t]}; });
    }
    // 更新某頻道的標籤
    function updateBlocklistTags(key, tags) {
      var l = getBlocklist();
      var found = false;
      l = l.map(function(c){
        if(c.id===key || c.name===key){ found=true; return Object.assign({},c,{tags:tags}); }
        return c;
      });
      if(found){ setBlocklist(l); return true; }
      return false;
    }
    // v1.1.0: 更新白名單某頻道的標籤
    function updateWhitelistTags(key, tags) {
      var l = getWhitelist();
      var found = false;
      l = l.map(function(c){
        if(c.id===key || c.name===key){ found=true; return Object.assign({},c,{tags:tags}); }
        return c;
      });
      if(found){ setWhitelist(l); return true; }
      return false;
    }

    return {
      getBlocklist,setBlocklist,addToBlocklist,removeFromBlocklist,updateBlocklistEntry,isBlocked,
      getWhitelist,setWhitelist,addToWhitelist,removeFromWhitelist,updateWhitelistEntry,isWhitelisted,
      getKeyGroups,setKeyGroups,addKeyGroup,updateKeyGroup,removeKeyGroup,moveKeyGroup,
      getFilters,setFilters,getStats,addStat,resetStats,
      getHistory,addHistory,clearHistory,
      getGlobal,setGlobal,isEnabled,setEnabled,
      exportAll,importAll,exportStats,exportHistoryCSV,
      getAllTags,updateBlocklistTags,updateWhitelistTags,
    };
  })();

  // ============================================================
  // NOTIFICATION
  // ============================================================
  var Notif = (function() {
    var wrap = null;
    var log = [];

    function ensureWrap() {
      if (wrap && document.body.contains(wrap)) return;
      wrap = h('div', {id:'ytf-nw'});
      document.body.appendChild(wrap);
    }

    function show(msg, type, dur) {
      type = type || 'info';
      dur  = (dur === undefined) ? 3500 : dur;

      // 寫入 Log(不受 showToast 影響)
      var entry = {msg:msg, type:type, time:Date.now()};
      log.unshift(entry);
      if (log.length > 50) log.pop();
      document.dispatchEvent(new CustomEvent('ytf:log', {detail:entry}));

      // showToast 關閉時只寫 Log,不彈出 Toast
      var gs = S.getGlobal();
      if (gs.showToast === false) return;

      ensureWrap();
      var msgSpan = h('span', {cls:'ytf-tm'}, [msg]);
      var closeBtn = h('button', {cls:'ytf-tx'}, ['✕']);
      var toast = h('div', {cls:'ytf-toast ytf-t-'+type}, [msgSpan, closeBtn]);
      closeBtn.addEventListener('click', function() { dismiss(toast); });
      wrap.appendChild(toast);
      setTimeout(function() { toast.classList.add('ytf-ti'); }, 10);
      if (dur > 0) setTimeout(function() { dismiss(toast); }, dur);
    }

    function dismiss(t) {
      t.classList.remove('ytf-ti');
      setTimeout(function() { if (t.parentNode) t.parentNode.removeChild(t); }, 300);
    }

    // showForce:強制彈出 Toast(不受 showToast 開關影響,用於快捷操作通知)
    function showForce(msg, type, dur) {
      type = type || 'success';
      dur  = (dur === undefined) ? 3500 : dur;
      var entry = {msg:msg, type:type, time:Date.now()};
      log.unshift(entry);
      if (log.length > 50) log.pop();
      document.dispatchEvent(new CustomEvent('ytf:log', {detail:entry}));
      ensureWrap();
      var msgSpan = h('span', {cls:'ytf-tm'}, [msg]);
      var closeBtn = h('button', {cls:'ytf-tx'}, ['✕']);
      var toast = h('div', {cls:'ytf-toast ytf-t-'+type}, [msgSpan, closeBtn]);
      closeBtn.addEventListener('click', function() { dismiss(toast); });
      wrap.appendChild(toast);
      setTimeout(function() { toast.classList.add('ytf-ti'); }, 10);
      if (dur > 0) setTimeout(function() { dismiss(toast); }, dur);
    }

    function reasonText(r) {
      if (!r) return t('reason.unknown');
      if (r.indexOf('keyword:')===0) return t('reason.keyword')+r.slice(8)+t('reason.keyword.suffix');
      if (r.indexOf('channel:')===0) return t('reason.channel');
      if (r.indexOf('language:')===0) return t('reason.language')+r.slice(9)+t('reason.language.suffix');
      if (r==='duration') return t('reason.duration');
      if (r==='views') return t('reason.views');
      if (r==='date') return t('reason.date');
      if (r==='live') return t('reason.live');
      if (r==='upcoming') return t('reason.upcoming');
      if (r==='members_only') return t('reason.members_only');
      return r;
    }

    function summary(items) {
      if (!items||!items.length) return;
      var rs={};
      items.forEach(function(it){ var r=it.reason||'?'; rs[r]=(rs[r]||0)+1; });
      var txt=Object.keys(rs).map(function(r){ return reasonText(r)+(rs[r]>1?' ×'+rs[r]:''); }).join(t('notif.separator'));
      show(t('notif.hidden.prefix')+items.length+t('notif.hidden.suffix')+txt);
    }

    return {show,showForce,dismiss,reasonText,summary,getLog:function(){return log.slice();}};
  })();

  // ============================================================
  // FILTER
  // ============================================================
  var Filter = (function() {
    var PROC='data-ytf-p', HIDE='data-ytf-h';

    // ── Regex 編譯快取(避免每次 new RegExp,設定更新時清除)──
    // ── videoId → {id, name} 快取(補強 Shorts / compact 卡片的頻道識別)──
    var _vcm = (function(){
      var cache = {};
      var dirty = false;
      var flushTimer = null;
      // 載入持久化快取
      try{ var saved=GM_getValue('ytf_vcm',null); if(saved) cache=JSON.parse(saved)||{}; }catch(e){}
      function flush(){
        flushTimer=null; if(!dirty) return;
        // 只保留最近 500 筆
        var keys=Object.keys(cache);
        if(keys.length>500){var trim={};keys.slice(-400).forEach(function(k){trim[k]=cache[k];});cache=trim;}
        try{GM_setValue('ytf_vcm',JSON.stringify(cache));}catch(e){}
        dirty=false;
      }
      return {
        set:function(vId,chId,chName){
          if(!vId) return;
          cache[vId]={id:chId||'',name:chName||''};
          dirty=true;
          clearTimeout(flushTimer);
          flushTimer=setTimeout(flush,2000);
        },
        get:function(vId){ return vId?cache[vId]:null; }
      };
    })();
    function getInfo(card) {
      var info={channelId:'',channelName:'',title:'',description:'',dur:null,isLive:false,isUpcoming:false,isMember:false,videoId:'',publishDate:null,viewCount:null,isCollection:false};
      try {
        var tSels=[
          // 新版 camelCase class
          'a.ytLockupMetadataViewModelTitle span[role="text"]',
          'h3.ytLockupMetadataViewModelHeadingReset a span',
          // 舊版 kebab-case class
          'a.yt-lockup-metadata-view-model__title span[role="text"]',
          'h3.yt-lockup-metadata-view-model__heading-reset a span',
          // 更舊版 / 其他頁面
          'yt-formatted-string#video-title',
          '#video-title-link yt-formatted-string',
          '#video-title',
          'h3 a'
        ];
        for (var i=0;i<tSels.length;i++){var te=card.querySelector(tSels[i]);if(te&&te.textContent.trim()){info.title=te.textContent.trim();break;}}

        var chSels=['a.yt-core-attributed-string__link[href^="/@"]','a.yt-core-attributed-string__link[href^="/channel/"]','ytd-channel-name a','#channel-name a','a[href^="/@"]'];
        for (var j=0;j<chSels.length;j++){
          var ca=card.querySelector(chSels[j]); if(!ca) continue;
          var name='';
          ca.childNodes.forEach(function(n){
            if(n.nodeType===3) name+=n.textContent;
            else if(n.nodeType===1&&n.tagName==='SPAN'&&!n.querySelector('svg')){n.childNodes.forEach(function(nn){if(nn.nodeType===3)name+=nn.textContent;});}
          });
          if(!name.trim()) name=ca.textContent;
          if(name.trim()){info.channelName=name.trim();info.channelId=(ca.getAttribute('href')||'').replace(/^\//,'').split('?')[0];break;}
        }
        // 新版 ViewModel:頻道名在 span 而非 <a>,從 avatar aria-label 取得
        // 首頁:yt-spec-avatar-shape__button(kebab)
        // 側邊欄 compact:ytSpecAvatarShapeButton(camelCase)
        if(!info.channelName) {
          var avatarBtn=card.querySelector(
            '.yt-spec-avatar-shape__button[aria-label],' +
            '.ytSpecAvatarShapeButton[aria-label]'
          );
          if(avatarBtn){
            var al=avatarBtn.getAttribute('aria-label')||'';
            // aria-label 格式:「前往頻道:頻道名稱」或「Go to channel: Name」
            var m=al.match(/(?:前往頻道[::]\s*|Go to channel:\s*)(.+)/i);
            if(m&&m[1]) info.channelName=m[1].trim();
          }
        }
        // 再嘗試從 metadata span 取頻道名(第一個 metadata row)
        if(!info.channelName) {
          var metaRow=card.querySelector('.ytContentMetadataViewModelMetadataRow .ytContentMetadataViewModelMetadataText');
          if(metaRow){
            var txt=metaRow.textContent.trim();
            // 排除「觀看次數」「X年前」等統計資訊
            if(txt&&!/^[0-9,.]|次$|前$|前$/.test(txt)) info.channelName=txt;
          }
        }

        // 時長:從縮圖底部的 badge 取得(格式 M:SS 或 H:MM:SS)
        // 同時支援新版 camelCase (ytBadgeShapeText) 和舊版 kebab-case
        var durBadges=card.querySelectorAll(
          '.yt-badge-shape__text,badge-shape .yt-badge-shape__text,' +
          '.ytBadgeShapeText,badge-shape .ytBadgeShapeText'
        );
        for(var b=0;b<durBadges.length;b++){
          var bt=durBadges[b].textContent.trim();
          if(/^\d{1,2}:\d{2}(:\d{2})?$/.test(bt)){info.dur=parseDur(bt);break;}
        }
        // aria-label fallback:從 <a> 標題的 aria-label 取時長(如:「...47 分鐘」)
        if(info.dur===null){
          var titleLink=card.querySelector('.ytLockupMetadataViewModelTitle[aria-label],.ytLockupViewModelTitle[aria-label]');
          if(titleLink){
            var al2=titleLink.getAttribute('aria-label')||'';
            var mh=al2.match(/(\d+)\s*小時(?:[^分]*?(\d+)\s*分)?/);
            var mm=al2.match(/(\d+)\s*分(?:[^秒]*?(\d+)\s*秒)?/);
            var ms=al2.match(/(\d+)\s*秒/);
            if(mh) info.dur=(parseInt(mh[1])*3600)+(mh[2]?parseInt(mh[2])*60:0);
            else if(mm) info.dur=(parseInt(mm[1])*60)+(mm[2]?parseInt(mm[2]):0);
            else if(ms) info.dur=parseInt(ms[1]);
          }
        }

        // ── 直播/首播偵測(只看縮圖 overlay,不看頻道頭像)──
        // 方式A:舊版 ytd-thumbnail-overlay-time-status-renderer 的 overlay-style 屬性
        var ov=card.querySelector('ytd-thumbnail-overlay-time-status-renderer');
        if(ov){
          var os=ov.getAttribute('overlay-style')||'';
          if(os==='LIVE') info.isLive=true;
          if(os==='UPCOMING') info.isUpcoming=true;
        }
        // 方式B:新版 ViewModel 縮圖 badge(只看縮圖區塊內的 badge,排除頻道頭像)
        // 縮圖區塊:a.ytLockupViewModelContentImage 或 yt-thumbnail-view-model
        var thumbArea = card.querySelector(
          'a.ytLockupViewModelContentImage, a.yt-lockup-view-model__content-image, yt-thumbnail-view-model, #thumbnail'
        );
        if(thumbArea) {
          // 同時支援新版 camelCase(ytBadgeShapeText)和舊版 kebab-case(yt-badge-shape__text)
          var thumbBadges = thumbArea.querySelectorAll(
            '.yt-badge-shape__text,badge-shape .yt-badge-shape__text,' +
            '.ytBadgeShapeText,badge-shape .ytBadgeShapeText'
          );
          for(var bx=0; bx<thumbBadges.length; bx++){
            var bval = thumbBadges[bx].textContent.trim().toUpperCase();
            // 只有純文字 badge 才是直播/首播,時長格式(數字:數字)已在 duration 讀取
            if(!(/^\d/.test(bval))) {
              if(bval==='LIVE'||bval==='直播') info.isLive=true;
              if(bval==='UPCOMING'||bval==='即將首播'||bval==='首播'||bval==='PREMIERES') info.isUpcoming=true;
            }
          }
          // 備援:直接用 class 識別直播 badge(ytBadgeShapeThumbnailLive)
          if(!info.isLive && thumbArea.querySelector('.ytBadgeShapeThumbnailLive')) info.isLive=true;
        }
        // 舊版:badge-style 屬性(ytd-badge-supported-renderer)
        if(card.querySelector('[badge-style="MEMBERS_ONLY"]')) info.isMember=true;
        // 新版 ViewModel:yt-badge-shape--commerce class(metadata row 內,非縮圖)
        if(!info.isMember && card.querySelector('.yt-badge-shape--commerce')) info.isMember=true;
        // 雙重保障:badge 文字比對(含新版 camelCase)
        if(!info.isMember){
          var allBadgeTxt=card.querySelectorAll(
            '.yt-badge-shape__text,.ytBadgeShapeText'
          );
          for(var bi2=0;bi2<allBadgeTxt.length;bi2++){
            if(/會員|members.only/i.test(allBadgeTxt[bi2].textContent)){info.isMember=true;break;}
          }
        }
        var de=card.querySelector('#description-text'); if(de) info.description=de.textContent.trim();

        // ── 上傳日期:優先從觀看頁 meta tag 取精確日期;首頁從相對時間估算 ──
        var dateMeta=document.querySelector('meta[itemprop="datePublished"]');
        if(dateMeta){
          var dv=dateMeta.getAttribute('content');
          if(dv&&/^\d{4}-\d{2}-\d{2}/.test(dv)) info.publishDate=dv;
        }
        // 首頁卡片:從 metadata row 解析「1 天前 / 3 週前」等相對時間
        // 一次迴圈解析 metadata row:上傳日期(相對時間)+ 觀看數
        var allMetaTexts=card.querySelectorAll('.ytContentMetadataViewModelMetadataText');
        for(var mi=0;mi<allMetaTexts.length;mi++){
          var mt=allMetaTexts[mi].textContent.trim();
          if(!mt) continue;

          // 上傳日期:優先比對相對時間(含「前」或「ago」),若 publishDate 已從觀看頁 meta 取得則跳過
          if(!info.publishDate){
            // 排除「正在觀看」等直播文字
            if(!/watching|正在觀看/i.test(mt)){
              var relTs=parseRelativeTime(mt);
              if(relTs!==null){ info.publishDate='__rel__'+relTs; }
            }
          }

          // 觀看數:比對「觀看次數:N萬次」/「N.NM views」格式
          if(info.viewCount===null){
            // 中文:「觀看次數:2萬次」/「觀看次數:361萬次」
            var mZh=mt.match(/觀看次數[::]\s*([\d.,]+)\s*([萬億万]?)\s*次/);
            // 英文:「1.2M views」/「500 views」(排除「N watching」直播格式)
            var mEn=!mZh && !/watching/i.test(mt) ? mt.match(/^([\d.,]+)\s*([KMBkmb])?\s*views?$/i) : null;
            if(mZh) info.viewCount=parseViews(mZh[1], mZh[2]);
            else if(mEn) info.viewCount=parseViews(mEn[1], mEn[2]);
          }
        }

        // ── videoId 提取(從縮圖連結或標題連結)──
        var vLink=card.querySelector('a[href*="watch?v="],a[href*="/shorts/"]');
        if(vLink){
          var vh=vLink.getAttribute('href')||'';
          var vm=vh.match(/(?:watch\?v=|\/shorts\/)([a-zA-Z0-9_-]{11})/);
          if(vm) info.videoId=vm[1];
          // 合輯偵測:YouTube Mix(list=RD...)/ Radio 類卡片,非單一頻道影片
          if(/[?&]list=(RD|OL)/.test(vh)) info.isCollection=true;
        }
        // 合輯偵測:badge 文字是「合輯」/「Mix」
        if(!info.isCollection){
          var cBadges=card.querySelectorAll('.ytBadgeShapeText,badge-shape .ytBadgeShapeText');
          for(var cbi=0;cbi<cBadges.length;cbi++){
            var cbt=cBadges[cbi].textContent.trim();
            if(cbt==='合輯'||/^mix$/i.test(cbt)){ info.isCollection=true; break; }
          }
        }
        // vcm 雙向同步:識別成功時存入快取;失敗時嘗試從快取補全
        if(info.videoId){
          if(info.channelName||info.channelId){
            _vcm.set(info.videoId,info.channelId,info.channelName);
          } else {
            var cached=_vcm.get(info.videoId);
            if(cached){ info.channelId=cached.id; info.channelName=cached.name; }
          }
        }
      } catch(e){console.warn('[YTFilter] getInfo',e);}
      return info;
    }

    function parseDur(t){var p=t.split(':').map(Number);if(p.some(isNaN))return null;return p.length===3?p[0]*3600+p[1]*60+p[2]:p[0]*60+p[1];}

    // 解析 YouTube 相對時間文字 → 估算的毫秒時間戳(首頁卡片無精確日期)
    // 中文:1 天前 / 3 週前 / 2 個月前 / 1 年前
    // 英文:1 day ago / 3 weeks ago / 2 months ago / 1 year ago
    function parseRelativeTime(txt) {
      if(!txt) return null;
      txt = txt.trim();
      var now = Date.now();
      if(/剛剛|just\s*now/i.test(txt)) return now;
      var m = txt.match(/(\d+)\s*(天|day|days|週|week|weeks|個月|month|months|年|year|years|小時|hour|hours|分鐘|minute|minutes)/i);
      if(!m) return null;
      var n = parseInt(m[1]);
      var u = m[2].toLowerCase();
      var mult = {
        '天':86400000,'day':86400000,'days':86400000,
        '週':604800000,'week':604800000,'weeks':604800000,
        '個月':2592000000,'month':2592000000,'months':2592000000,
        '年':31536000000,'year':31536000000,'years':31536000000,
        '小時':3600000,'hour':3600000,'hours':3600000,
        '分鐘':60000,'minute':60000,'minutes':60000
      };
      var ms = mult[u];
      return ms ? now - n * ms : null;
    }

    // 解析觀看數字串 → 整數(中文:萬 / 億;英文:K / M / B)
    function parseViews(num, unit) {
      var n = parseFloat(String(num).replace(/,/g, ''));
      if (isNaN(n)) return null;
      unit = (unit || '').trim();
      if (unit === '萬' || unit === '万') n *= 10000;
      else if (unit === '億')             n *= 100000000;
      else if (/^k$/i.test(unit))         n *= 1000;
      else if (/^m$/i.test(unit))         n *= 1000000;
      else if (/^b$/i.test(unit))         n *= 1000000000;
      return Math.round(n);
    }
    function detectLang(t) {
      // 依 Unicode block 偵測語言,回傳 BCP-47 代碼
      if (/[฀-๿]/.test(t)) return 'th';       // 泰文 Thai
      if (/[ऀ-ॿ]/.test(t)) return 'hi';       // 印地文 Hindi (Devanagari)
      if (/[ঀ-৿]/.test(t)) return 'bn';       // 孟加拉文 Bengali
      if (/[؀-ۿݐ-ݿ]/.test(t)) return 'ar'; // 阿拉伯文 Arabic
      if (/[Ѐ-ӿ]/.test(t)) return 'ru';       // 俄文 (Cyrillic)
      if (/[぀-ゟ゠-ヿ]/.test(t)) return 'ja'; // 日文 (Hiragana/Katakana)
      if (/[가-힯]/.test(t)) return 'ko';       // 韓文 Korean
      if (/[一-鿿㐀-䶿]/.test(t)) return 'zh'; // 中文 CJK
      if (/[က-႟]/.test(t)) return 'my';       // 緬甸文 Myanmar/Burmese
      if (/[඀-෿]/.test(t)) return 'si';       // 僧伽羅文 Sinhala
      if (/[ఀ-౿]/.test(t)) return 'te';       // 泰盧固文 Telugu
      if (/[஀-௿]/.test(t)) return 'ta';       // 泰米爾文 Tamil
      if (/[਀-੿]/.test(t)) return 'pa';       // 旁遮普文 Punjabi
      if (/[Ⴀ-ჿ]/.test(t)) return 'ka';       // 喬治亞文 Georgian
      if (/[԰-֏]/.test(t)) return 'hy';       // 亞美尼亞文 Armenian
      if (/[ሀ-፿]/.test(t)) return 'am';       // 阿姆哈拉文 Amharic (Ethiopic)
      if (/[֐-׿]/.test(t)) return 'he';       // 希伯來文 Hebrew
      // 拉丁字母類(根據常見特殊字元區分)
      // 越南文優先偵測(有獨有聲調組合字元,且與其他拉丁語系字元集重疊最少)
      if (/[ắặẩẫầảấẻẽẹếềểễệỉịọốồổỗộớờởỡợụứừửữựỳỵ]/i.test(t)) return 'vi'; // 越南文
      if (/[äöüßÄÖÜ]/.test(t)) return 'de';             // 德文
      if (/[àâçéèêëîïôùûüÿÀÂÇÉÈÊËÎÏÔÙÛÜŸœæ]/.test(t)) return 'fr';  // 法文
      if (/[áéíóúüñ¿¡]/.test(t)) return 'es';          // 西班牙文
      if (/[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/.test(t)) return 'it';       // 義大利文
      if (/[ãõáéíóúâêôç]/.test(t)) return 'pt';        // 葡萄牙文
      if (/[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/.test(t)) return 'pl'; // 波蘭文
      if (/[áéíóöőúüűÁÉÍÓÖŐÚÜŰ]/.test(t)) return 'hu'; // 匈牙利文
      if (/[蚞ȊŽ]/.test(t)) return 'cs';             // 捷克/斯洛伐克文
      if (/[øæå]/.test(t)) return 'no';                 // 北歐文(挪威/丹麥/瑞典)
      // 純 ASCII 拉丁文 → 英文(預設)
      return 'en';
    }

    function check(info) {
      var fs=S.getFilters();

      // ══ 第一優先:白名單保護 ══════════════════════════════════
      // 白名單頻道不受任何過濾條件拘束(直播/會員/關鍵字等全部跳過)
      if(fs.whitelistOnly) {
        // 白名單獨享模式:只保留白名單頻道
        if(S.isWhitelisted(info.channelId, info.channelName)) return null;
        // 頻道資訊無法識別時保守顯示,避免誤殺正常卡片
        if(!info.channelId && !info.channelName) return null;
        return 'channel:'+(info.channelName||info.channelId);
      }
      // 一般白名單保護
      if(S.isWhitelisted(info.channelId, info.channelName)) return null;

      // ══ 第二層:頻道隱藏名單 ══════════════════════════════════
      if(S.isBlocked(info.channelId, info.channelName))
        return 'channel:'+(info.channelName||info.channelId);

      // ══ 第三層:關鍵字過濾(標題/描述)══════════════════════
      var groups=S.getKeyGroups().filter(function(g){return g.enabled;});
      for(var gi=0;gi<groups.length;gi++){
        var g=groups[gi];
        for(var ki=0;ki<g.keywords.length;ki++){
          var kw=g.keywords[ki];
          var targets=[];
          if(g.scope.indexOf('title')>=0&&info.title) targets.push(info.title);
          if(g.scope.indexOf('description')>=0&&info.description) targets.push(info.description);
          for(var ti=0;ti<targets.length;ti++){
            var _kre=kw.isRegex?compiledRe(kw.value):null;
            var hit=_kre?_kre.test(targets[ti]):targets[ti].toLowerCase().indexOf(kw.value.toLowerCase())>=0;
            if(hit) return 'keyword:'+kw.value;
          }
        }
      }

      // ══ 第四層:影片屬性過濾 ══════════════════════════════════
      // 直播 / 首播:根據影片縮圖 overlay 的屬性判斷,不根據頻道屬性
      if(fs.live.enabled){
        if(fs.live.hideLive && info.isLive) return 'live';
        if(fs.live.hideUpcoming && info.isUpcoming) return 'upcoming';
      }
      // 會員專屬:影片有付費 badge 才觸發
      if(fs.member.enabled && info.isMember) return 'members_only';
      // 時長:影片時長不在範圍內
      if(fs.duration.enabled && info.dur!==null){
        var mn=fs.duration.min||0, mx=fs.duration.max||0;
        if(info.dur<mn || (mx>0 && info.dur>mx)) return 'duration';
      }
      // 觀看數:超出 min / max 範圍(直播「N 人正在觀看」格式不會寫入 viewCount)
      if(fs.views && fs.views.enabled && info.viewCount!==null){
        var vmn=fs.views.min||0, vmx=fs.views.max||0;
        if(vmn>0 && info.viewCount<vmn) return 'views';
        if(vmx>0 && info.viewCount>vmx) return 'views';
      }

      // ══ 第五層:上傳日期過濾(觀看頁精確日期 或 首頁相對時間估算)══════
      if(fs.uploadDate && fs.uploadDate.enabled && info.publishDate){
        var _pubMs;
        if(info.publishDate.indexOf('__rel__')===0){
          // 首頁相對時間:直接使用估算時間戳
          _pubMs=parseInt(info.publishDate.slice(7));
        } else {
          _pubMs=new Date(info.publishDate).getTime();
        }
        if(!isNaN(_pubMs)){
          var _nowMs=Date.now();
          var _limitMs=(fs.uploadDate.days||365)*86400000;
          // v1.1.1 修正:語義清楚化
          // 'older' = 隱藏比 N 天/年老的(舊片) → (now-pub) > limit 則 hide
          // 'newer' = 隱藏比 N 天/年新的(新片) → (now-pub) < limit 則 hide
          if(fs.uploadDate.condition==='older' && (_nowMs-_pubMs)>_limitMs) return 'date';
          if(fs.uploadDate.condition==='newer' && (_nowMs-_pubMs)<_limitMs) return 'date';
        }
      }

      // ══ 第六層:語言過濾 ══════════════════════════════════════
      if(fs.language.enabled && info.title){
        var lang=detectLang(info.title);
        var allLangs=(fs.language.langs||[]).concat(fs.language.customLangs||[]);
        if(allLangs.length){
          var inList=allLangs.some(function(l){return lang&&(lang===l||lang.indexOf(l+'-')===0);});
          if(fs.language.mode==='block' && inList) return 'language:'+lang;
          if(fs.language.mode==='allow' && !inList) return 'language:'+lang;
        }
      }
      return null;
    }

    function processOne(card) {
      if(card.getAttribute(PROC)) return null;
      if(!S.isEnabled()) return null;
      ftCount('processOneCalls');
      // v1.1.0: 起由 Observer 的 :has() SEL 從源頭避免父子重複選取,
      // 不再需要 innerLockup 去重補丁
      var info=getInfo(card);
      // metadata 尚未載入(無標題且無頻道名),不打標記,等下次 poll 重新處理
      if(!info.title && !info.channelName) {
        ftCount('processOneSkipped_noInfo');
        if(_dbgOn){
          ftDbg('processOne skip (no info):', card.tagName,
            'children=', card.children.length,
            'hasTitle=', !!card.querySelector('h3,yt-formatted-string,.ytLockupMetadataViewModelTitle'),
            'hasChannel=', !!card.querySelector('a[href^="/@"]'));
        }
        return null;
      }
      // 合輯(YouTube Mix / Radio):非單一頻道內容,不套用任何過濾規則,打標記避免重複處理
      if(info.isCollection){
        card.setAttribute(PROC,'1');
        if(_dbgOn) ftDbg('SKIP collection:', (info.title||'').slice(0,40));
        return null;
      }
      // 會員過濾啟用時:metadata row 存在但 commerce badge 尚未渲染,等候最多 3 個 poll
      var fs0=S.getFilters();
      if(fs0.member && fs0.member.enabled) {
        var hasMetaRow = card.querySelector('.ytContentMetadataViewModelMetadataRowMetadataRowWrap');
        var hasCommerce = card.querySelector('.yt-badge-shape--commerce');
        if(hasMetaRow && !hasCommerce) {
          card._ytfMemberWait = (card._ytfMemberWait||0) + 1;
          if(card._ytfMemberWait <= 3) {
            ftCount('processOneSkipped_memberWait');
            return null;
          }
        }
      }
      card.setAttribute(PROC,'1');

      var reason=check(info);
      if(reason){
        ftCount('processOneHidden');
        _ytfStats.checkReasons[reason]=(_ytfStats.checkReasons[reason]||0)+1;
        if(_dbgOn) ftDbg('HIDE', info.channelName||'?', '| reason=', reason, '| title=', (info.title||'').slice(0,40));
        card.setAttribute(HIDE,'1');
        // 隱藏目標:若 card 在 ytd-rich-item-renderer 裡(首頁格子),hide 外層避免留白
        // 否則 hide card 本身
        var hideTarget = card.closest('ytd-rich-item-renderer') || card;
        hideTarget.setAttribute(HIDE,'1');
        hideTarget.style.setProperty('display','none','important');
        S.addStat(reason,info.channelId,info.channelName);
        S.addHistory({title:info.title,channelId:info.channelId,channelName:info.channelName,reason:reason});
        return{info,reason};
      }
      ftCount('processOnePass');
      if(_dbgOn) ftDbg('PASS', info.channelName||'?', '| title=', (info.title||'').slice(0,40));
      return null;
    }

    function processAll(cards){var hidden=[];Array.prototype.slice.call(cards).forEach(function(c){var r=processOne(c);if(r)hidden.push(r);});return hidden;}
    function reset(){
      _reCache.clear(); // 設定可能已更新,清除 Regex 快取
      var procCount=document.querySelectorAll('['+PROC+']').length;
      var qaCount=document.querySelectorAll('[data-ytf-qa]').length;
      // 清除過濾標記與隱藏樣式(包含祖先 ytd-rich-item-renderer 可能被加上 HIDE)
      document.querySelectorAll('['+PROC+'],['+HIDE+']').forEach(function(el){
        el.removeAttribute(PROC);
        el.removeAttribute(HIDE);
        el.style.removeProperty('display');
      });
      // 清除 QA 注入的快捷按鈕標記和殘留元素
      document.querySelectorAll('[data-ytf-qa]').forEach(function(el){
        el.removeAttribute('data-ytf-qa');
        el.style.removeProperty('position');
        el.querySelectorAll('.ytf-qa-bar,.ytf-qa-icons').forEach(function(b){
          if(b.parentNode) b.parentNode.style.removeProperty('position');
          if(b.parentNode) b.parentNode.removeChild(b);
        });
      });
    }

    return {processAll,processOne,reset,getInfo};
  })();

  // ============================================================
  // OBSERVER
  // ============================================================
  var Obs = (function() {
    var obs=null,poll=null,deb=null,cb=null;
    // SEL 由陣列組成,每個 selector 個別附加 :not() 才能正確篩選
    // 首頁的 ytd-rich-item-renderer 與內層 yt-lockup-view-model 有父子關係,
    // 用 :has() 精確選取:有 lockup 時只選內層,無 lockup 時選外層
    var SEL_ARR = [
      // 新版 ViewModel 卡片(首頁、觀看頁側邊欄 compact 等)
      // 不加 ytd-rich-item-renderer:not(:has()) fallback,避免選到
      // YouTube 還沒載入內容的空殼 placeholder 導致頁面缺損
      'yt-lockup-view-model',
      // 其他頁面(搜尋、頻道頁等舊版 DOM)
      'ytd-video-renderer',
      'ytd-compact-video-renderer',
      'ytd-grid-video-renderer',
      'ytd-rich-grid-media'
    ];
    var SEL = SEL_ARR.join(',');
    function cards(){return document.querySelectorAll(SEL);}
    function unprocessed(){
      // 個別附加 :not([data-ytf-p]),避免只套用到最後一個 selector
      var sel = SEL_ARR.map(function(s){return s+':not([data-ytf-p])';}).join(',');
      return document.querySelectorAll(sel);
    }
    function runCb(all){
      if(!cb) return;
      var els=Array.prototype.slice.call(all?cards():unprocessed());
      if(_dbgOn){
        ftCount('obsTotalRuns');
        _ytfStats.obsCardsSelected += els.length;
        // 統計各 tagName
        els.forEach(function(el){
          var tag=el.tagName;
          _ytfStats.selectorSamples[tag]=(_ytfStats.selectorSamples[tag]||0)+1;
        });
        ftDbg('Obs.runCb all='+all+' selected='+els.length+' tags=',
          els.slice(0,3).map(function(e){return e.tagName;}));
      }
      if(els.length) cb(els);
    }
    function start(callback){
      cb=callback;
      if(obs)obs.disconnect();
      obs=new MutationObserver(function(muts){if(muts.some(function(m){return m.addedNodes.length>0;})){clearTimeout(deb);deb=setTimeout(function(){runCb(false);},400);}});
      obs.observe(document.body,{childList:true,subtree:true});
      setTimeout(function(){runCb(true);},1000);
      setTimeout(function(){runCb(true);},2500);
      clearInterval(poll);
      poll=setInterval(function(){runCb(false);},2000);
    }
    function stop(){if(obs){obs.disconnect();obs=null;}clearTimeout(deb);clearInterval(poll);}
    function watchNav(fn){
      var last=location.href;
      var debNav=null;
      function trigger(){clearTimeout(debNav);debNav=setTimeout(fn,800);}
      // yt-navigate-finish:YouTube SPA 任何導航完成時觸發(含 Logo 點擊回首頁)
      window.addEventListener('yt-navigate-finish',function(){
        setTimeout(function(){last=location.href;fn();},600);
      });
      // URL polling 保底:防止 yt-navigate-finish 不觸發的邊角情況
      setInterval(function(){
        if(location.href!==last){last=location.href;trigger();}
      },600);
    }
    return {start,stop,restart:function(c){stop();start(c);},watchNav,cards};
  })();

  // ============================================================
  // IO
  // ============================================================
  var IO = (function() {
    // UTF-8 BOM bytes(EF BB BF),確保 Excel 在 Windows 系統開啟時正確識別編碼避免亂碼
    var UTF8_BOM = new Uint8Array([0xEF, 0xBB, 0xBF]);
    function dl(blob,name){var url=URL.createObjectURL(blob),a=document.createElement('a');a.href=url;a.download=name;a.click();URL.revokeObjectURL(url);}
    function csv(rows,name){var c=rows.map(function(r){return r.map(function(x){return'"'+String(x).replace(/"/g,'""')+'"';}).join(',');}).join('\n');dl(new Blob([UTF8_BOM, c],{type:'text/csv;charset=utf-8;'}),name);}
    function ds(){var d=new Date();return d.getFullYear()+pad2(d.getMonth()+1)+pad2(d.getDate());}
    function pad2(n){return String(n).padStart(2,'0');}
    function readFile(file,cb){var r=new FileReader();r.onload=function(e){cb(e.target.result);};r.readAsText(file,'UTF-8');}
    function parseCSV(file){return new Promise(function(res){readFile(file,function(txt){res(txt.replace(/^\uFEFF/,'').split('\n').filter(function(l){return l.trim();}).map(function(l){return l.split(',').map(function(c){return c.trim().replace(/^"|"$/g,'').replace(/""/g,'"');});}));});});}

    function exportJSON(){dl(new Blob([JSON.stringify(S.exportAll(),null,2)],{type:'application/json'}),'yt-filter-'+ds()+'.json');Notif.show(t('io.toast.exported.json'),'success');}
    function exportBlocklistCSV(){var rows=[[t('csv.header.channelId'),t('csv.header.channelName'),t('csv.header.tags')]];S.getBlocklist().forEach(function(c){rows.push([c.id,c.name,(c.tags||[]).join(';')]);});csv(rows,'yt-blocklist-'+ds()+'.csv');Notif.show(t('io.toast.exported.bl'),'success');}
    function exportWhitelistCSV(){var rows=[[t('csv.header.channelId'),t('csv.header.channelName'),t('csv.header.tags')]];S.getWhitelist().forEach(function(c){rows.push([c.id,c.name,(c.tags||[]).join(';')]);});csv(rows,'yt-whitelist-'+ds()+'.csv');Notif.show(t('io.toast.exported.wl'),'success');}
    function exportKeywordsCSV(){var rows=[[t('csv.header.groupName'),t('csv.header.keyword'),t('csv.header.regex'),t('csv.header.scope'),t('csv.header.enabled')]];S.getKeyGroups().forEach(function(g){g.keywords.forEach(function(kw){rows.push([g.name,kw.value,kw.isRegex?'Y':'N',g.scope.join(';'),g.enabled?'Y':'N']);});});csv(rows,'yt-keywords-'+ds()+'.csv');Notif.show(t('io.toast.exported.kw'),'success');}
    function importJSON(file,mode){return new Promise(function(res,rej){readFile(file,function(txt){try{
        var d=JSON.parse(txt);
        // 記錄匯入前數量
        var before={bl:S.getBlocklist().length,wl:S.getWhitelist().length,kg:S.getKeyGroups().length};
        S.importAll(d,mode);
        Filter.reset();
        var after={bl:S.getBlocklist().length,wl:S.getWhitelist().length,kg:S.getKeyGroups().length};
        var added=(after.bl-before.bl)+(after.wl-before.wl)+(after.kg-before.kg);
        var msg=mode==='overwrite'?t('io.toast.imported.overwrite'):t('io.toast.imported.prefix')+added+t('io.toast.imported.suffixEnd');
        Notif.show(msg,'success');
        res(d);
      }catch(e){Notif.show(t('io.toast.importFailed')+e.message,'error');rej(e);}});});}
    function importBlocklistCSV(file){return parseCSV(file).then(function(rows){var added=0;rows.slice(1).forEach(function(r){if(r[0]||r[1]){if(S.addToBlocklist({id:r[0],name:r[1],tags:r[2]?r[2].split(';'):[]}))added++;}});var total=rows.slice(1).filter(function(r){return r[0]||r[1];}).length;
        var skipped=total-added;
        Filter.reset();
        Notif.show(t('io.toast.imported.blPrefix')+added+t('common.items')+(skipped?t('io.toast.imported.skip')+skipped+t('io.toast.imported.suffixDup'):''),'success');});}
    function importKeywordsCSV(file,mode){return parseCSV(file).then(function(rows){var map={};rows.slice(1).forEach(function(r){if(!r[0]||!r[1])return;if(!map[r[0]])map[r[0]]={name:r[0],enabled:r[4]!=='N',keywords:[],scope:r[3]?r[3].split(';'):['title']};map[r[0]].keywords.push({value:r[1],isRegex:r[2]==='Y'});});if(mode==='overwrite')S.setKeyGroups([]);Object.values(map).forEach(function(g){S.addKeyGroup(g);});var addedKg=Object.keys(map).length; Filter.reset();Notif.show(t('io.toast.imported.kwPrefix')+addedKg+t('common.groupSuffix'),'success');});}

    function autoBackup(){
      // 自動備份:使用固定檔名 yt-filter-backup.json(同名會覆蓋)
      var gs = S.getGlobal();
      var filename = 'yt-filter-backup.json';
      // 如果使用者有設定前綴,用前綴_日期
      if (gs.backupPrefix && gs.backupPrefix.trim()) {
        filename = gs.backupPrefix.trim() + '-' + ds() + '.json';
      }
      dl(new Blob([JSON.stringify(S.exportAll(),null,2)],{type:'application/json'}), filename);
      var g = S.getGlobal(); g.lastBackup = Date.now(); S.setGlobal(g);
      Notif.show(t('io.toast.autoBackupDone') + filename,'success');
    }

    // 分享過濾清單:產生分享連結
    function generateShareLink(what) {
      // what: 'blocklist'|'whitelist'|'keywords'|'all'
      var payload = {v:'1',t:what,d:Date.now()};
      if(what==='blocklist'||what==='all') payload.bl=S.getBlocklist().map(function(c){return{id:c.id,name:c.name,tags:c.tags};});
      if(what==='whitelist'||what==='all') payload.wl=S.getWhitelist().map(function(c){return{id:c.id,name:c.name};});
      if(what==='keywords'||what==='all')  payload.kg=S.getKeyGroups();
      try {
        var encoded = btoa(unescape(encodeURIComponent(JSON.stringify(payload))));
        return 'https://www.youtube.com/#ytf-import=' + encoded;
      } catch(e) { return null; }
    }

    // 從 URL hash 偵測並匯入
    function checkImportFromHash() {
      var hash = location.hash || '';
      var m = hash.match(/#ytf-import=([A-Za-z0-9+/=]+)/);
      if(!m) return;
      try {
        var payload = JSON.parse(decodeURIComponent(escape(atob(m[1]))));
        if(payload.v!=='1') return;
        // 清除 hash,避免重複匯入
        history.replaceState(null,'',location.pathname+location.search);
        // 顯示預覽並詢問匯入
        var items=[];
        if(payload.bl) items.push(t('io.label.blocklist')+payload.bl.length+t('common.channels'));
        if(payload.wl) items.push(t('io.label.whitelist')+payload.wl.length+t('common.channels'));
        if(payload.kg) items.push(t('io.label.groups')+payload.kg.length+t('common.countOnly'));
        Notif.show(t('io.toast.shareReceived')+items.join('、')+t('io.toast.shareReceivedEnd'),'info',6000);
        // 存到暫存供面板讀取
        try { sessionStorage.setItem('ytf_pending_share', JSON.stringify(payload)); } catch(e){}
      } catch(e) { /* 格式不合,靜默忽略 */ }
    }

    function exportStatsJSON(){
      dl(new Blob([JSON.stringify(S.exportStats(),null,2)],{type:'application/json'}),
        'ytf-stats-'+ds()+'.json');
      Notif.show(t('io.toast.exported.stats'),'success');
    }
    // UTF-8 BOM:在 IO 模組最頂部已宣告,此處直接使用

    function exportHistoryCSV(){
      // D-5 修正:把 reason key 換成本地化描述文字,並加 UTF-8 BOM
      var raw = S.exportHistoryCSV();
      var lines = raw.split('\n');
      var header = lines.shift();
      var localized = [header].concat(lines.map(function(line){
        // CSV 第 4 欄是 reason key(如 keyword:xxx, date, live 等),轉為本地化描述
        var cols = line.split(',');
        if(cols.length >= 4 && cols[3]){
          cols[3] = Notif.reasonText(cols[3]);
        }
        return cols.join(',');
      }));
      var csvText = localized.join('\n');
      // 用 Uint8Array 明確輸出 BOM + UTF-8 bytes,讓 Excel 正確識別編碼
      dl(new Blob([UTF8_BOM, csvText], {type:'text/csv;charset=utf-8;'}),
        'ytf-history-'+ds()+'.csv');
      Notif.show(t('io.toast.exported.hist'),'success');
    }

    return {exportJSON,exportBlocklistCSV,exportWhitelistCSV,exportKeywordsCSV,
            importJSON,importBlocklistCSV,importKeywordsCSV,autoBackup,
            generateShareLink,checkImportFromHash,
            exportStatsJSON,exportHistoryCSV};
  })();

  // ============================================================
  // PANEL — 全部用 createElement,零 innerHTML
  // ============================================================
  var Panel = (function() {
    var root=null, bodyEl=null, ctEl=null, navEl=null, lbEl=null;
    var isOpen=false, curTab='blocklist';

    function fmtTime(ts){if(!ts)return'';var d=new Date(ts);return(d.getMonth()+1)+'/'+d.getDate()+' '+pad2(d.getHours())+':'+pad2(d.getMinutes());}
    function pad2(n){return String(n).padStart(2,'0');}

    // ── Switch element ──
    function makeSwitch(id, checked, small) {
      var input = h('input',{type:'checkbox',id:id});
      if (checked) input.checked = true;
      var span = h('span',{cls:'ytf-sl'});
      var label = h('label',{cls:'ytf-sw'+(small?' ytf-sw-sm':''),'for':id},[input,span]);
      return {label,input};
    }

    // ── Build panel shell ──
    function build(forceRebuild) {
      // YouTube SPA reload 後 DOM 可能已被清除,需重置
      if (root && !document.body.contains(root)) {
        root = null; bodyEl = null; ctEl = null; navEl = null; lbEl = null;
      }
      // 強制重建(例如切換語言後):移除舊 DOM 並重置狀態
      if (forceRebuild && root) {
        if (root.parentNode) root.parentNode.removeChild(root);
        root = null; bodyEl = null; ctEl = null; navEl = null; lbEl = null;
      }
      if (root) return;

      // Global switch
      var gswInput = h('input',{type:'checkbox',id:'ytf-gsw'});
      gswInput.checked = S.isEnabled();
      gswInput.addEventListener('change', function() {
        S.setEnabled(gswInput.checked);
        Filter.reset(); FAB.update();
        Notif.show(gswInput.checked?t('filter.enabled'):t('filter.disabled'));
        if (gswInput.checked) doFilter(Obs.cards());
      });

      var xBtn = h('button',{id:'ytf-x',title:t('panel.close')},['✕']);
      xBtn.addEventListener('click', hide);

      var hd = h('div',{id:'ytf-hd'},[
        h('span',{id:'ytf-logo'},['🔍 ' + t('panel.title'), h('small',{id:'ytf-ver'},['v1.1.1-rc1'])]),
        h('div',{style:'display:flex;align-items:center;gap:12px'},[
          h('label',{cls:'ytf-sw',style:'margin:0'},[gswInput, h('span',{cls:'ytf-sl'})]),
          xBtn
        ])
      ]);

      lbEl = h('div',{id:'ytf-lb'});
      navEl = h('nav',{id:'ytf-nav'});
      ctEl  = h('div',{id:'ytf-ct'});
      bodyEl = h('div',{id:'ytf-bd'},[hd, lbEl, navEl, ctEl]);

      var ov = h('div',{id:'ytf-ov'});
      ov.addEventListener('click', hide);

      root = h('div',{id:'ytf-panel'},[ov, bodyEl]);
      document.body.appendChild(root);

      document.addEventListener('ytf:log', function(ev) {
        if (!isOpen) return;
        var item = h('div',{cls:'ytf-l ytf-l-'+ev.detail.type},'['+fmtTime(ev.detail.time)+'] '+ev.detail.msg);
        lbEl.insertBefore(item, lbEl.firstChild);
        while (lbEl.children.length > 4) lbEl.removeChild(lbEl.lastChild);
      });

      buildNav();
      renderTab(curTab);
    }

    function buildNav() {
      var gs = S.getGlobal();
      var tabs=[
        {id:'blocklist',l:t('tab.blocklist')},
        {id:'whitelist',l:t('tab.whitelist')},
        {id:'keywords',l:t('tab.keywords')},
        {id:'filters',l:t('tab.filters')},
      ];
      if(gs.enableStats!==false){
        tabs.push({id:'stats',l:t('tab.stats')});
        tabs.push({id:'history',l:t('tab.history')});
      }
      tabs.push({id:'io',l:t('tab.io')});
      tabs.push({id:'about',l:t('tab.about')});
      setChildren(navEl, tabs.map(function(t){
        return h('button',{cls:'ytf-tab','data-tab':t.id},[t.l]);
      }));
      // 用事件委派,避免重複綁定問題
      navEl.addEventListener('click',function(ev){
        var tabBtn=ev.target.closest('.ytf-tab');
        if(tabBtn && tabBtn.getAttribute('data-tab')) {
          renderTab(tabBtn.getAttribute('data-tab'));
        }
      });
    }

    function renderTab(id) {
      curTab=id;
      navEl.querySelectorAll('.ytf-tab').forEach(function(b){ b.classList.toggle('ytf-tab-on',b.getAttribute('data-tab')===id); });
      setChildren(ctEl, [makeTabContent(id)]);
      bindTabEvents(id);
    }

    function show() {
      if(!root)build();
      root.querySelector('#ytf-gsw').checked=S.isEnabled();
      root.classList.add('ytf-open');
      isOpen=true;
      // 自動 focus 當前 Tab 的搜尋欄
      setTimeout(function(){
        var searchEl=root.querySelector('#ytf-bs,#ytf-ws');
        if(searchEl) searchEl.focus();
      },180);
    }
    function hide() { if(root)root.classList.remove('ytf-open'); isOpen=false; }
    function toggle() { isOpen?hide():show(); }

    // ── Helper: input ──
    function inp(id, placeholder, small) {
      return h('input',{cls:'ytf-in'+(small?' ytf-ins':''),type:'text',id:id||'',placeholder:placeholder||''});
    }
    function numInp(id, val, min2) {
      var el=h('input',{cls:'ytf-in ytf-ins',type:'number',id:id,min:String(min2||0)});
      el.value=String(val||0); return el;
    }
    function btn(cls, id, text) { return h('button',{cls:cls,id:id||''},[text]); }
    function row() { return h('div',{cls:'ytf-row'},Array.prototype.slice.call(arguments)); }

    // ── Channel list DOM ──
    // tagFilter: 當前選取的標籤篩選(空字串=全部)
    // onTagsChanged: callback 供上層 tag bar 收到「標籤有變動」時刷新
    function makeChList(list, type, filter, tagFilter, onTagsChanged) {
      filter = filter||''; tagFilter = tagFilter||'';
      var fl = list;
      if(filter) fl = fl.filter(function(c){
        return (c.name||'').toLowerCase().indexOf(filter.toLowerCase())>=0 ||
               (c.id||'').toLowerCase().indexOf(filter.toLowerCase())>=0;
      });
      if(tagFilter) fl = fl.filter(function(c){
        return (c.tags||[]).indexOf(tagFilter) >= 0;
      });
      if(!fl.length) return h('div',{cls:'ytf-mt'},[list.length?t('channel.noResults'):t('channel.empty.blocklist')]);

      // datalist 用於標籤建議(僅顯示同類型的標籤)
      var allTagNames = S.getAllTags(type).map(function(t){return t.name;});
      var dl = h('datalist',{id:'ytf-tags-dl'});
      allTagNames.forEach(function(t){ dl.appendChild(h('option',{value:t},[])); });

      var ul = h('ul',{cls:'ytf-cl'});
      ul.appendChild(dl);

      fl.forEach(function(c){
        var key = c.id||c.name;

        // 確認移除列
        var yesBtn = h('button',{cls:'ytf-del-yes'},[t('channel.confirmRemove')]);
        var noBtn  = h('button',{cls:'ytf-del-no'},[t('common.cancel')]);
        var confirmRow = h('div',{cls:'ytf-del-confirm ytf-hid'},[
          h('span',{cls:'ytf-del-confirm-txt'},[t('channel.confirmRemovePrefix')+(c.name||c.id)+t('channel.confirmRemoveSuffix')]),
          yesBtn, noBtn
        ]);
        yesBtn.addEventListener('click',function(ev){
          ev.stopPropagation();
          if(type==='block') S.removeFromBlocklist(key);
          else S.removeFromWhitelist(key);
          Filter.reset(); renderTab(curTab);
        });
        noBtn.addEventListener('click',function(ev){
          ev.stopPropagation();
          confirmRow.classList.add('ytf-hid');
        });

        // ── 標籤區(v1.1.0: 隱藏名單與白名單都支援) ──
        var tagsRow = null;
        var tagInputRow = null;
        // 根據 type 選擇對應的 Storage 操作函式
        var getList     = type==='block' ? S.getBlocklist   : S.getWhitelist;
        var updateTags  = type==='block' ? S.updateBlocklistTags : S.updateWhitelistTags;
        {
          var tagChips = h('div',{cls:'ytf-tag-chips'});
          function refreshTagChips(tags) {
            while(tagChips.firstChild) tagChips.removeChild(tagChips.firstChild);
            (tags||[]).forEach(function(tg){
              var rmBtn = h('button',{cls:'ytf-tag-rm',title:t('channel.removeTag')},['×']);
              rmBtn.addEventListener('click',function(ev){
                ev.stopPropagation();
                var cur = getList().find(function(x){return x.id===key||x.name===key;});
                var newTags = (cur&&cur.tags||[]).filter(function(x){return x!==tg;});
                updateTags(key, newTags);
                // 局部更新:tag chips + 已有標籤快選列 + 上層 tag bar
                refreshTagChips(newTags);
                if(typeof refreshExistingChips === 'function') refreshExistingChips();
                var rmIdx=list.findIndex(function(x){return x.id===key||x.name===key;});
                if(rmIdx>=0) list[rmIdx]=Object.assign({},list[rmIdx],{tags:newTags});
                if(typeof onTagsChanged === 'function') onTagsChanged();
              });
              var chip = h('span',{cls:'ytf-tag ytf-tag-edit'},[tg, rmBtn]);
              tagChips.appendChild(chip);
            });
          }
          refreshTagChips(c.tags||[]);

          // v1.1.0: 修訂:標籤 UI 重新設計,邏輯分區清楚
          // ┌──────────────────────────────────────┐
          // │ 📝 新增標籤                           │
          // │ [輸入新標籤.........]  [+ 新增]      │
          // │ ─────────────────────────────────── │
          // │ 📌 已有標籤(快選,點即加入)         │
          // │ [tag1(3)] [tag2(1)] [tag3(5)] ...   │
          // └──────────────────────────────────────┘

          var tagIn = h('input',{type:'text',cls:'ytf-in',placeholder:t('channel.newTagPh')});
          var addTagBtn = h('button',{cls:'ytf-tag-add-btn',title:t('common.add')},['+ '+t('common.add')]);
          var existingTagsWrap = h('div',{cls:'ytf-tag-existing-chips'});

          // 刷新「已有標籤」chip 列
          // v1.1.0: 修訂:顯示所有系統標籤,已擁有的以「已選」狀態呈現(點擊移除)
          // 讓使用者一眼看出所有可用標籤以及該頻道的擁有狀態
          function refreshExistingChips() {
            while(existingTagsWrap.firstChild) existingTagsWrap.removeChild(existingTagsWrap.firstChild);
            var cur = getList().find(function(x){return x.id===key||x.name===key;});
            var have = (cur&&cur.tags)||[];
            var all = S.getAllTags(type);
            if(!all.length){
              existingTagsWrap.appendChild(h('span',{cls:'ytf-tag-pick-empty'},[t('channel.noExistingTags')]));
              return;
            }
            all.forEach(function(tg){
              var owned = have.indexOf(tg.name) >= 0;
              var item = h('button',{cls:'ytf-tag-pick-chip'+(owned?' ytf-tag-pick-chip-on':'')},
                [tg.name, h('small',{},[' ('+tg.count+')'])]);
              item.addEventListener('click',function(ev){
                ev.stopPropagation();
                if(owned){
                  // 已擁有 → 點擊移除
                  var newTags = have.filter(function(x){return x!==tg.name;});
                  updateTags(key, newTags);
                  refreshTagChips(newTags);
                  refreshExistingChips();
                  var idx=list.findIndex(function(x){return x.id===key||x.name===key;});
                  if(idx>=0) list[idx]=Object.assign({},list[idx],{tags:newTags});
                  if(typeof onTagsChanged === 'function') onTagsChanged();
                } else {
                  // 未擁有 → 點擊加入
                  tagIn.value = tg.name;
                  doAddTag();
                }
              });
              existingTagsWrap.appendChild(item);
            });
          }

          function doAddTag() {
            var val = tagIn.value.trim();
            if(!val) return;
            var cur = getList().find(function(x){return x.id===key||x.name===key;});
            var curTags = cur&&cur.tags ? cur.tags.slice() : [];
            if(curTags.indexOf(val)<0){
              curTags.push(val);
              updateTags(key,curTags);
              // 清空輸入框但保持面板打開,讓使用者可連續新增多個 tag
              tagIn.value='';
              // 局部更新:已有 chip 列 + 現有標籤快選 + 上層 tag bar
              refreshTagChips(curTags);
              refreshExistingChips();
              // 同步 list 快照,再通知上層 tag bar 刷新
              var idx=list.findIndex(function(x){return x.id===key||x.name===key;});
              if(idx>=0) list[idx]=Object.assign({},list[idx],{tags:curTags});
              if(typeof onTagsChanged === 'function') onTagsChanged();
              tagIn.focus();
            } else {
              // 重複則清空輸入,保持打開
              tagIn.value='';
              tagIn.focus();
            }
          }
          tagIn.addEventListener('keydown',function(ev){ if(ev.key==='Enter'){ev.preventDefault();doAddTag();} });
          addTagBtn.addEventListener('click',function(ev){ ev.stopPropagation(); doAddTag(); });

          var addTagToggle = h('button',{cls:'ytf-tag-toggle',title:t('channel.manageTags')},['🏷']);

          // 標籤新增面板(分區:新增輸入 + 現有 chip 快選)
          var addSection = h('div',{cls:'ytf-tag-section'},[
            h('div',{cls:'ytf-tag-section-label'},['📝 '+t('channel.addTagLabel')]),
            h('div',{cls:'ytf-tag-input-flex'},[tagIn, addTagBtn])
          ]);
          var pickSection = h('div',{cls:'ytf-tag-section'},[
            h('div',{cls:'ytf-tag-section-label'},['📌 '+t('channel.existingTagsLabel')]),
            existingTagsWrap
          ]);
          tagInputRow = h('div',{cls:'ytf-tag-input-row ytf-hid'},[addSection, pickSection]);

          addTagToggle.addEventListener('click',function(ev){
            ev.stopPropagation();
            var willOpen = tagInputRow.classList.contains('ytf-hid');
            tagInputRow.classList.toggle('ytf-hid');
            if(willOpen){
              refreshExistingChips();  // 每次打開都刷新可選項
              tagIn.focus();
            }
          });

          tagsRow = h('div',{cls:'ytf-tags-area'},[tagChips, addTagToggle]);
        }

        // 移除按鈕
        var delBtn = h('button',{cls:'ytf-del',title:t('common.remove')},['✕']);
        delBtn.addEventListener('click',function(ev){
          ev.stopPropagation();
          ul.querySelectorAll('.ytf-del-confirm:not(.ytf-hid)').forEach(function(r){
            if(r!==confirmRow) r.classList.add('ytf-hid');
          });
          confirmRow.classList.toggle('ytf-hid');
        });

        var nameInfo = [];
        if(c.isRegex) nameInfo.push(h('span',{cls:'ytf-re-badge'},['⌥R.*']));
        nameInfo.push(h('b',{cls:'ytf-cn'},[c.name||c.id]));
        if(c.id&&c.name&&!c.isRegex) nameInfo.push(h('small',{cls:'ytf-sid'},[c.id]));

        // ── 編輯功能 ──
        var editBtn = h('button',{cls:'ytf-edit-btn',title:t('common.edit')},['✏️']);
        var editRow = h('div',{cls:'ytf-edit-row ytf-hid'});
        // v1.1.0: 修正:移除 IIFE 讓 editBtn handler 可存取 eTagsIn 等編輯元件
        var eNameIn  = h('input',{type:'text',cls:'ytf-in',placeholder:t('channel.name')});
        var eIdIn    = h('input',{type:'text',cls:'ytf-in',placeholder:t('channel.handleShort')});
        var eReChk   = h('input',{type:'checkbox',id:'ytf-er-'+key});
        eNameIn.value = c.name||'';
        eIdIn.value   = c.id||'';
        eReChk.checked = !!c.isRegex;
        var eSaveBtn   = h('button',{cls:'ytf-bp',style:'font-size:12px;padding:5px 10px'},[t('common.save')]);
        var eCancelBtn = h('button',{cls:'ytf-bg',style:'font-size:12px;padding:5px 10px'},[t('common.cancel')]);

        eSaveBtn.addEventListener('click',function(ev){
          ev.stopPropagation();
          var newName = eNameIn.value.trim();
          var newId   = eIdIn.value.trim();
            if(!newName&&!newId){ Notif.show(t('toast.channelEmpty'),'warning'); return; }
            // v1.1.0: 同時儲存 tags
            var newTags = (eTagsIn.value||'').split(',').map(function(x){return x.trim();}).filter(Boolean);
            var upd = {name:newName, id:newId, isRegex:eReChk.checked, tags:newTags};
            if(type==='block') S.updateBlocklistEntry(key, upd);
            else S.updateWhitelistEntry(key, upd);
            Filter.reset();
            // v1.1.0: 修正:編輯儲存後需重建整個 Tab 以同步標籤篩選 bar
            // (refreshList 只重繪 listWrap,不會刷新上方的 tag bar)
            renderTab(type==='block'?'blocklist':'whitelist');
            Notif.show(t('toast.updated')+newName,'success');
          });
          eCancelBtn.addEventListener('click',function(ev){
            ev.stopPropagation();
            editRow.classList.add('ytf-hid');
          });

          // v1.1.0: 白名單也支援標籤編輯
          var eTagsIn = h('input',{type:'text',cls:'ytf-in',placeholder:t('channel.tagsHint'),list:'ytf-tags-dl'});
          eTagsIn.value = (c.tags||[]).join(', ');

          // v1.1.0: 任務 2: 編輯表單也提供已有標籤快選
          var eTagPickWrap = h('div',{cls:'ytf-tag-existing-chips'});
          function refreshEditTagPick(){
            while(eTagPickWrap.firstChild) eTagPickWrap.removeChild(eTagPickWrap.firstChild);
            // 當前輸入框中的 tags(逗號分隔、去除空白)
            var currentTags = (eTagsIn.value||'').split(',').map(function(x){return x.trim();}).filter(Boolean);
            var all = S.getAllTags(type);
            if(!all.length){
              eTagPickWrap.appendChild(h('span',{cls:'ytf-tag-pick-empty'},[t('channel.noExistingTags')]));
              return;
            }
            all.forEach(function(tg){
              var owned = currentTags.indexOf(tg.name) >= 0;
              var item = h('button',{cls:'ytf-tag-pick-chip'+(owned?' ytf-tag-pick-chip-on':'')},
                [tg.name, h('small',{},[' ('+tg.count+')'])]);
              item.addEventListener('click',function(ev){
                ev.preventDefault();
                ev.stopPropagation();
                var cur = currentTags.slice();
                if(owned){
                  // 已擁有 → 移除
                  cur = cur.filter(function(x){return x!==tg.name;});
                } else {
                  // 未擁有 → 加入
                  cur.push(tg.name);
                }
                eTagsIn.value = cur.join(', ');
                refreshEditTagPick();
              });
              eTagPickWrap.appendChild(item);
            });
          }
          // 輸入框內容變動時也刷新候選(使用者手打/刪除)
          eTagsIn.addEventListener('input', refreshEditTagPick);
          refreshEditTagPick();

          var editFields = [
            h('div',{cls:'ytf-edit-fields'},[
              eNameIn, eIdIn,
              h('label',{cls:'ytf-cb',style:'font-size:12px'},[eReChk,t('channel.editRegexMode')])
            ])
          ];
          editFields[0].appendChild(eTagsIn);
          // 已有標籤快選區(放在 tag 輸入框下方)
          editFields[0].appendChild(h('div',{cls:'ytf-tag-section',style:'margin-top:4px'},[
            h('div',{cls:'ytf-tag-section-label'},['📌 '+t('channel.existingTagsLabel')]),
            eTagPickWrap
          ]));
          editFields[0].appendChild(h('div',{cls:'ytf-row'},[eSaveBtn,eCancelBtn]));
          setChildren(editRow, editFields);

        editBtn.addEventListener('click',function(ev){
          ev.stopPropagation();
          // 關閉其他開啟的編輯列
          ul.querySelectorAll('.ytf-edit-row:not(.ytf-hid)').forEach(function(r){
            if(r!==editRow) r.classList.add('ytf-hid');
          });
          // v1.1.0: 修正:打開編輯表單前,重新從 Storage 讀取最新的 tags
          // (使用者可能透過 🏷 區已經改過 tags,而 c 是 IIFE 當時的快照)
          var latest = getList().find(function(x){return x.id===key||x.name===key;});
          if(latest){
            eNameIn.value = latest.name || '';
            eIdIn.value   = latest.id   || '';
            eReChk.checked = !!latest.isRegex;
            eTagsIn.value = (latest.tags||[]).join(', ');
            refreshEditTagPick();
          }
          editRow.classList.toggle('ytf-hid');
        });

        var actionBtns = h('div',{cls:'ytf-ci-actions'},[editBtn, delBtn]);
        // D-4: 把 channelName 和 tagChips 放在同一個 flex-wrap 容器
        // 原本 nameInfo 獨佔 flex:1、tagsRow 另起 flex:1,導致標籤被擠到名稱右邊的狹小空間
        // 新做法:name + sid + chips 全部進 .ytf-ci-nametags(flex-wrap),空間不足時自動換行
        var nameAndTags = h('div',{cls:'ytf-ci-nametags'},[]);
        nameInfo.forEach(function(n){ nameAndTags.appendChild(n); });
        if(tagsRow) nameAndTags.appendChild(tagsRow);
        var liChildren = [
          h('div',{cls:'ytf-ci-main'},[nameAndTags, actionBtns]),
          editRow,
          confirmRow
        ];
        if(tagInputRow) liChildren.push(tagInputRow);
        ul.appendChild(h('li',{cls:'ytf-ci ytf-ci-wrap'},liChildren));
      });
      return ul;
    }

    // ── Tabs content ──
    function makeTabContent(id) {
      switch(id) {
        case 'blocklist': return makeBlocklist();
        case 'whitelist': return makeWhitelist();
        case 'keywords':  return makeKeywords();
        case 'filters':   return makeFilters();
        case 'stats':     return makeStats();
        case 'history':   return makeHistory();
        case 'io':        return makeIO();
        case 'about':     return makeAbout();
      }
      return h('div');
    }

    // ── Blocklist/Whitelist 共用:標籤篩選 bar(v1.1.0: 修正:白名單也支援) ──
    // 回傳 { render(): 重繪清單與 bar, getFilter(): 取得目前 filter, pane: DOM 框架 }
    function buildTagBarWrapper(type, list, searchIn, listContainerId) {
      var allTags = S.getAllTags(type);
      var curTagFilter = '';
      var tagBar = h('div',{cls:'ytf-tag-bar'});

      // 當條目的 tags 變動時,重新計算 allTags 並刷新 bar
      // v1.1.0: 任務 3 修正:若當前篩選的 tag 已不存在(被刪除/條目移除),自動回到「全部」並重繪清單
      function onTagsChanged() {
        allTags = S.getAllTags(type);
        var stillExists = allTags.some(function(tg){ return tg.name===curTagFilter; });
        if(curTagFilter && !stillExists){
          curTagFilter = '';
          setChildren(listWrap,[makeChList(list, type, searchIn?searchIn.value:'', curTagFilter, onTagsChanged)]);
        }
        renderBar();
      }

      var listWrap = h('div',{id:listContainerId},[makeChList(list, type, '', curTagFilter, onTagsChanged)]);

      function renderBar() {
        while(tagBar.firstChild) tagBar.removeChild(tagBar.firstChild);
        if(!allTags.length) { tagBar.style.display='none'; return; }
        tagBar.style.display='';
        var allChip = h('button',{cls:'ytf-tagf'+(curTagFilter===''?' ytf-tagf-on':'')},
          [t('channel.tagFilter.all')+list.length+t('channel.tagFilter.allSuffix')]);
        allChip.addEventListener('click',function(){
          curTagFilter='';
          renderBar();
          setChildren(listWrap,[makeChList(list, type, searchIn?searchIn.value:'', curTagFilter, onTagsChanged)]);
        });
        tagBar.appendChild(allChip);
        allTags.forEach(function(tg){
          var chip = h('button',{cls:'ytf-tagf'+(curTagFilter===tg.name?' ytf-tagf-on':'')},
            [tg.name+' ('+tg.count+')']);
          chip.addEventListener('click',function(){
            curTagFilter = curTagFilter===tg.name ? '' : tg.name;
            renderBar();
            setChildren(listWrap,[makeChList(list, type, searchIn?searchIn.value:'', curTagFilter, onTagsChanged)]);
          });
          tagBar.appendChild(chip);
        });
      }

      return {
        tagBar: tagBar,
        listWrap: listWrap,
        render: renderBar,
        refreshTags: function(){ allTags = S.getAllTags(type); renderBar(); },
        getFilter: function(){ return curTagFilter; }
      };
    }

    // ── Blocklist ──
    function makeBlocklist() {
      var list=S.getBlocklist();
      var searchIn=inp('ytf-bs',t('channel.searchPlaceholder'));
      var addBtn=btn('ytf-bp','ytf-ba',t('common.add'));
      var clearBtn=btn('ytf-bg','ytf-blk-clr',t('channel.clearAll'));
      var nameIn=inp('ytf-bn',t('channel.name'));
      var idIn=inp('ytf-bi',t('channel.handle'));
      var tagsIn=inp('ytf-bt',t('channel.tagsHint'));
      var blReChk=h('input',{type:'checkbox',id:'ytf-bl-re'});
      var okBtn=btn('ytf-bp','ytf-bok',t('common.confirm'));
      var cancelBtn=btn('ytf-bg','',t('common.cancel'));
      var form=h('div',{cls:'ytf-form ytf-hid',id:'ytf-bf'},[
        nameIn,idIn,tagsIn,
        h('label',{cls:'ytf-cb',style:'font-size:12px'},[blReChk,t('channel.regexMode')]),
        h('small',{cls:'ytf-sid',style:'padding-left:22px'},[t('channel.regexHint')]),
        row(okBtn,cancelBtn)
      ]);

      var tbw = buildTagBarWrapper('block', list, searchIn, 'ytf-blist');
      tbw.render();

      var countEl = h('small',{cls:'ytf-sid',id:'ytf-bl-count'},[t('common.total')+list.length+t('common.channels')]);

      return h('div',{cls:'ytf-pane'},[
        row(searchIn,addBtn,list.length?clearBtn:null),
        form,
        tbw.tagBar,
        tbw.listWrap,
        countEl
      ]);
    }
    function bindTabEvents(id) {
      if (!ctEl) return;
      var qs=function(s){return ctEl.querySelector(s);};

      if (id==='blocklist') {
        var addBtn=qs('#ytf-ba'),form=qs('#ytf-bf');
        if(addBtn&&form) addBtn.addEventListener('click',function(){form.classList.toggle('ytf-hid');});
        var cancelBtn=form?form.querySelectorAll('button')[1]:null;
        if(cancelBtn) cancelBtn.addEventListener('click',function(){form.classList.add('ytf-hid');});
        var okBtn=qs('#ytf-bok');
        if(okBtn) okBtn.addEventListener('click',function(){
          var name=(qs('#ytf-bn').value||'').trim(),id2=(qs('#ytf-bi').value||'').trim();
          var tags=(qs('#ytf-bt').value||'').split(',').map(function(t){return t.trim();}).filter(Boolean);
          if(!name&&!id2){Notif.show(t('channel.addPrompt'),'warning');return;}
          var isRe=!!(qs('#ytf-bl-re')&&qs('#ytf-bl-re').checked);
          S.addToBlocklist({id:isRe?'':id2||name,name:name||id2,isRegex:isRe,tags});
          Filter.reset();Notif.show(t('toast.added')+(name||id2),'success');renderTab('blocklist');
        });
        var searchIn=qs('#ytf-bs');
        if(searchIn) searchIn.addEventListener('input',function(){
          var lw=qs('#ytf-blist');
          if(lw){
            var tagOn=ctEl.querySelector('.ytf-tagf-on');
            var tf=(tagOn&&tagOn.textContent.indexOf(t('common.all'))===0)?'':
                   (tagOn?tagOn.textContent.replace(/\s*\(\d+\)$/,''):'');
            setChildren(lw,[makeChList(S.getBlocklist(),'block',searchIn.value,tf)]);
          }
        });
        var clrBtn=qs('#ytf-blk-clr');
        if(clrBtn) clrBtn.addEventListener('click',function(){
          if(!confirm(t('channel.clearBlockConfirm')+S.getBlocklist().length+t('common.countSuffix'))) return;
          S.setBlocklist([]); Filter.reset();
          Notif.show(t('channel.cleared.block'),'success'); renderTab('blocklist');
        });
      }

      if (id==='whitelist') {
        var wa=qs('#ytf-wa'),wf=qs('#ytf-wf');
        if(wa&&wf) wa.addEventListener('click',function(){wf.classList.toggle('ytf-hid');});
        var wcancels=wf?wf.querySelectorAll('button'):[];
        if(wcancels[1]) wcancels[1].addEventListener('click',function(){wf.classList.add('ytf-hid');});
        var wok=qs('#ytf-wok');
        if(wok) wok.addEventListener('click',function(){
          var name=(qs('#ytf-wn').value||'').trim(),id2=(qs('#ytf-wi').value||'').trim();
          if(!name&&!id2){Notif.show(t('channel.addPrompt'),'warning');return;}
          var isReW=!!(qs('#ytf-wl-re')&&qs('#ytf-wl-re').checked);
          // v1.1.0: 讀取白名單 tags 輸入
          var wtagsEl=qs('#ytf-wt');
          var tags=wtagsEl ? (wtagsEl.value||'').split(',').map(function(x){return x.trim();}).filter(Boolean) : [];
          S.addToWhitelist({id:isReW?'':id2||name,name:name||id2,isRegex:isReW,tags:tags});
          Filter.reset();Notif.show(t('qa.already.white')+(name||id2),'success');renderTab('whitelist');
        });
        var ws=qs('#ytf-ws');
        // v1.1.0: 為了跟標籤篩選 bar 的 filter 保持一致,搜尋時重建整個 Tab
        // 但要保留輸入框的值與游標位置(避免打字時失焦)
        if(ws) ws.addEventListener('input',function(){
          var lw=qs('#ytf-wlist');
          if(lw) setChildren(lw,[makeChList(S.getWhitelist(),'white',ws.value)]);
        });
        var wclrBtn=qs('#ytf-wh-clr');
        if(wclrBtn) wclrBtn.addEventListener('click',function(){
          if(!confirm(t('channel.clearWhiteConfirm')+S.getWhitelist().length+t('common.countSuffix'))) return;
          S.setWhitelist([]); Filter.reset();
          Notif.show(t('channel.cleared.white'),'success'); renderTab('whitelist');
        });
      }

      if (id==='keywords') {
        var ga=qs('#ytf-ga'),gf=qs('#ytf-gf');
        if(ga&&gf) ga.addEventListener('click',function(){gf.classList.toggle('ytf-hid');});
        var gcancels=gf?gf.querySelectorAll('button'):[];
        if(gcancels[1]) gcancels[1].addEventListener('click',function(){gf.classList.add('ytf-hid');});
        var gok=qs('#ytf-gok');
        if(gok) gok.addEventListener('click',function(){
          var name=(qs('#ytf-gn').value||'').trim();if(!name){Notif.show(t('kw.groupName'),'warning');return;}
          var scope=[]; var sct=qs('#ytf-sct'),scd=qs('#ytf-scd');
          if(sct&&sct.checked)scope.push('title');if(scd&&scd.checked)scope.push('description');
          S.addKeyGroup({name,scope:scope.length?scope:['title']});renderTab('keywords');
        });
        // 使用事件委派,避免多次渲染造成的重複綁定問題
        var glistEl = ctEl.querySelector('#ytf-glist');
        if (glistEl) {
          glistEl.addEventListener('change', function(ev) {
            var tog = ev.target.closest('.ytf-gt');
            if (tog) {
              S.updateKeyGroup(tog.getAttribute('data-id'), {enabled: tog.checked});
              Filter.reset();
            }
          });

          glistEl.addEventListener('click', function(ev) {
            // 刪除群組
            var gdBtn = ev.target.closest('.ytf-gd');
            if (gdBtn) {
              var gid = gdBtn.getAttribute('data-id');
              var gs2 = S.getKeyGroups();
              var grp = gs2.find(function(x){return x.id===gid;});
              var gname = grp ? grp.name : gid;
              if (!confirm(t('kw.confirmDelPrefix')+gname+t('kw.confirmDelSuffix2'))) return;
              S.removeKeyGroup(gid); Filter.reset(); renderTab('keywords');
              return;
            }
            // 展開關鍵字輸入列
            var kaBtn = ev.target.closest('.ytf-ka');
            if (kaBtn) {
              var r = ctEl.querySelector('#ytf-ki-'+kaBtn.getAttribute('data-gid'));
              if (r) r.classList.toggle('ytf-hid');
              return;
            }
            // 新增關鍵字
            var koBtn = ev.target.closest('.ytf-ko');
            if (koBtn) {
              var row2 = ctEl.querySelector('#ytf-ki-'+koBtn.getAttribute('data-gid'));
              if (!row2) return;
              var val = row2.querySelector('.ytf-kv').value.trim();
              var isRe = row2.querySelector('.ytf-rc').checked;
              if (!val) { Notif.show(t('kw.addKwPrompt'),'warning'); return; }
              var gs3=S.getKeyGroups(), g3=gs3.find(function(x){return x.id===koBtn.getAttribute('data-gid');});
              if (g3) { g3.keywords.push({value:val,isRegex:isRe}); S.setKeyGroups(gs3); Filter.reset(); renderTab('keywords'); }
              return;
            }
            // 移除關鍵字
            var kxBtn = ev.target.closest('.ytf-kx');
            if (kxBtn) {
              var gs4=S.getKeyGroups(), g4=gs4.find(function(x){return x.id===kxBtn.getAttribute('data-gid');});
              if (g4) { g4.keywords.splice(Number(kxBtn.getAttribute('data-i')),1); S.setKeyGroups(gs4); Filter.reset(); renderTab('keywords'); }
              return;
            }
          });
        }
      }

      if (id==='filters') {
        [['#ytf-dur','#ytf-dur-bd'],['#ytf-live','#ytf-live-bd'],['#ytf-views','#ytf-views-bd'],['#ytf-ud','#ytf-ud-bd'],['#ytf-lang','#ytf-lang-bd']].forEach(function(p){
          var chk=qs(p[0]),bd=qs(p[1]);
          if(chk&&bd) chk.addEventListener('change',function(){bd.classList.toggle('ytf-dis',!chk.checked);});
        });
        var saveBtn=qs('#ytf-fsave');
        if(saveBtn) saveBtn.addEventListener('click',function(){
          var langs=Array.prototype.slice.call(ctEl.querySelectorAll('.ytf-lk:checked')).map(function(el){return el.value;});
          var lm=qs('#ytf-lm');
          var customLangsRaw = (qs('#ytf-lcustom')||{}).value||'';
          var customLangsParsed = customLangsRaw.split(',').map(function(s){return s.trim().toLowerCase();}).filter(function(s){return s.length>0;});
          var udCondEl=qs('#ytf-ud-cond');
          var udAmountEl=qs('#ytf-ud-amount');
          var udUnitEl=qs('#ytf-ud-unit');
          var udAmount=Number((udAmountEl||{}).value)||1;
          var udUnit=(udUnitEl&&udUnitEl.value)||'day';
          var udDays = udUnit==='year' ? udAmount*365 : udAmount;
          S.setFilters({
            whitelistOnly:(qs('#ytf-wlo')||{}).checked||false,
            duration:{enabled:qs('#ytf-dur').checked,min:Number(qs('#ytf-dmin').value)||0,max:Number(qs('#ytf-dmax').value)||0},
            live:{enabled:qs('#ytf-live').checked,hideLive:qs('#ytf-ll').checked,hideUpcoming:qs('#ytf-lu').checked},
            member:{enabled:qs('#ytf-mem').checked},
            uploadDate:{enabled:(qs('#ytf-ud')||{}).checked||false,condition:udCondEl?udCondEl.value:'older',amount:udAmount,unit:udUnit,days:udDays},
            views:{enabled:(qs('#ytf-views')||{}).checked||false,min:Number((qs('#ytf-vmin')||{}).value)||0,max:Number((qs('#ytf-vmax')||{}).value)||0},
            language:{enabled:qs('#ytf-lang').checked,mode:lm?lm.value:'block',langs:langs,customLangs:customLangsParsed},
          });
          // 語言切換(即時生效:重建面板、重建頁面上所有快捷按鈕)
          var langSelEl=qs('#ytf-lang-sel');
          if(langSelEl && langSelEl.value !== LANG){
            setLang(langSelEl.value);
            Panel.build(true);
            Panel.show();
            // 重跑過濾:Filter.reset() 清除所有 PROC/HIDE/QA 標記與殘留 bar,
            // 然後重新 doFilter() 讓所有卡片用新語言重建快捷按鈕與 notif
            Filter.reset();
            doFilter(Obs.cards());
            FAB.update(); // FAB 的 title/tip 也要用新語言
            Notif.show(t('set.lang.switched'),'success');
            return;
          }
          // 全域設定開關
        var toastEl=qs('#ytf-toast'), statsEl=qs('#ytf-estats');
        if(toastEl||statsEl){
          var g2=S.getGlobal();
          if(toastEl) g2.showToast=toastEl.checked;
          if(statsEl){
            var wasStats=g2.enableStats!==false;
            g2.enableStats=statsEl.checked;
            if(wasStats!==statsEl.checked){S.setGlobal(g2);buildNav();if(!statsEl.checked&&(curTab==='stats'||curTab==='history'))renderTab('blocklist');Filter.reset();Notif.show(t('filter.savedToast'),'success');return;}
          }
          S.setGlobal(g2);
        }
        Filter.reset();doFilter(Obs.cards());Notif.show(t('filter.savedToast'),'success');
        });
      }

      if (id==='stats') {
        var rst=qs('#ytf-rst');
        if(rst) rst.addEventListener('click',function(){if(!confirm(t('stat.clearConfirm')))return;S.resetStats();renderTab('stats');Notif.show(t('stat.cleared'),'success');});
      }
      if (id==='history') {
        var hclr=qs('#ytf-hclr');
        if(hclr) hclr.addEventListener('click',function(){if(!confirm(t('hist.clearConfirm')))return;S.clearHistory();renderTab('history');Notif.show(t('hist.cleared'),'success');});
      }
      if (id==='about') {
        // 關於頁面無需事件綁定
      }
      if (id==='io') {
        var gm=function(){var el=qs('#ytf-im');return el?el.value:'merge';};
        var fev=function(elId,fn){var el=qs(elId);if(el)el.addEventListener('click',fn);};
        fev('#ytf-ej',IO.exportJSON); fev('#ytf-eb',IO.exportBlocklistCSV);
        fev('#ytf-ew',IO.exportWhitelistCSV); fev('#ytf-ek',IO.exportKeywordsCSV);
        fev('#ytf-est',IO.exportStatsJSON);
        fev('#ytf-ehi',IO.exportHistoryCSV);
        // 分享連結按鈕
        function showShareLink(what) {
          var url = IO.generateShareLink(what);
          if(!url){ Notif.show(t('io.genLinkFail'),'error'); return; }
          // 連結過長警告(超過 1800 字元可能在部分瀏覽器截斷)
          var warn = qs('#ytf-share-warn');
          if(warn) {
            if(url.length > 1800) {
              warn.textContent = t('io.linkTooLong')+url.length+t('io.linkTooLongSuffix');
              warn.classList.remove('ytf-hid');
            } else {
              warn.classList.add('ytf-hid');
            }
          }
          var out = qs('#ytf-share-out'), inp2 = qs('#ytf-share-url');
          if(out&&inp2){ out.classList.remove('ytf-hid'); inp2.value=url; inp2.select(); }
        }
        fev('#ytf-sh-bl',function(){showShareLink('blocklist');});
        fev('#ytf-sh-wl',function(){showShareLink('whitelist');});
        fev('#ytf-sh-kw',function(){showShareLink('keywords');});
        fev('#ytf-sh-all',function(){showShareLink('all');});
        fev('#ytf-sh-copy',function(){
          var inp2=qs('#ytf-share-url');
          if(!inp2) return;
          if(navigator.clipboard){ navigator.clipboard.writeText(inp2.value).then(function(){Notif.show(t('io.copied'),'success');}); }
          else { inp2.select(); document.execCommand('copy'); Notif.show(t('io.copied'),'success'); }
        });
        // 檢查是否有待匯入的分享清單
        try {
          var pending = sessionStorage.getItem('ytf_pending_share');
          if(pending) {
            var pdata = JSON.parse(pending);
            var pItems=[];
            if(pdata.bl) pItems.push(t('io.label.blocklist')+pdata.bl.length+t('common.channels'));
            if(pdata.wl) pItems.push(t('io.label.whitelist')+pdata.wl.length+t('common.channels'));
            if(pdata.kg) pItems.push(t('io.label.keywords')+pdata.kg.length+t('common.groups'));
            var pNote = h('div',{cls:'ytf-iob',style:'border-color:var(--gold)'},[
              h('h4',{style:'margin:0 0 6px;color:var(--gold)'},[t('io.sharedImported')]),
              h('div',{style:'font-size:13px;margin-bottom:8px'},[pItems.join('、')]),
              h('div',{style:'display:flex;gap:8px'},[
                btn('ytf-bp','ytf-sh-imp',t('io.importJson')),
                btn('ytf-bg','ytf-sh-dis',t('common.skip'))
              ])
            ]);
            var ct2=qs('#ytf-ct'); if(ct2) ct2.insertBefore(pNote, ct2.firstChild);
            fev('#ytf-sh-imp',function(){
              if(pdata.bl){ var bl=S.getBlocklist(); pdata.bl.forEach(function(c){if(!bl.find(function(x){return x.id===c.id;}))bl.push(Object.assign({tags:[]},c));});S.setBlocklist(bl);}
              if(pdata.wl){ var wl=S.getWhitelist(); pdata.wl.forEach(function(c){if(!wl.find(function(x){return x.id===c.id;}))wl.push(c);});S.setWhitelist(wl);}
              if(pdata.kg){ var kg=S.getKeyGroups(); pdata.kg.forEach(function(g){if(!kg.find(function(x){return x.id===g.id;}))kg.push(g);});S.setKeyGroups(kg);}
              sessionStorage.removeItem('ytf_pending_share');
              Filter.reset(); Notif.show(t('io.sharedImported'),'success'); renderTab('io');
            });
            fev('#ytf-sh-dis',function(){ sessionStorage.removeItem('ytf_pending_share'); if(pNote.parentNode)pNote.parentNode.removeChild(pNote); });
          }
        } catch(e){}
        function fon(sel,fn){var el=qs(sel);if(el)el.addEventListener('change',function(ev){if(ev.target.files[0])fn(ev.target.files[0]);});}
        fon('#ytf-ij',function(f){IO.importJSON(f,gm()).then(function(){renderTab('io');});});
        fon('#ytf-ib',function(f){IO.importBlocklistCSV(f).then(function(){renderTab('io');});});
        fon('#ytf-ik',function(f){IO.importKeywordsCSV(f,gm()).then(function(){renderTab('io');});});
        var ab=qs('#ytf-ab'); if(ab) ab.addEventListener('change',function(){var g=S.getGlobal();g.autoBackup=ab.checked;S.setGlobal(g);});
        var bi=qs('#ytf-bi2'); if(bi) bi.addEventListener('change',function(){var g=S.getGlobal();g.backupInterval=Number(bi.value);S.setGlobal(g);});
        var bkpfx=qs('#ytf-bkpfx'); if(bkpfx) bkpfx.addEventListener('change',function(){var g=S.getGlobal();g.backupPrefix=bkpfx.value.trim();S.setGlobal(g);});
      }
    }

    // ── Whitelist ──
    function makeWhitelist() {
      var list=S.getWhitelist();
      var ws=inp('ytf-ws',t('channel.searchPlaceholder'));
      var wa=btn('ytf-bp','ytf-wa',t('common.add'));
      var wclearBtn=btn('ytf-bg','ytf-wh-clr',t('channel.clearAll'));
      var wn=inp('ytf-wn',t('channel.name'));
      var wi=inp('ytf-wi',t('channel.handleShort'));
      var wlReChk=h('input',{type:'checkbox',id:'ytf-wl-re'});
      // v1.1.0: 白名單新增表單加入 tags 輸入
      var wtagsIn=inp('ytf-wt',t('channel.tagsHint'));
      var wok=btn('ytf-bp','ytf-wok',t('common.confirm'));
      var wcl=btn('ytf-bg','',t('common.cancel'));
      var wf=h('div',{cls:'ytf-form ytf-hid',id:'ytf-wf'},[
        wn,wi,wtagsIn,
        h('label',{cls:'ytf-cb',style:'font-size:12px'},[wlReChk,t('channel.regexMode')]),
        h('small',{cls:'ytf-sid',style:'padding-left:22px'},[t('channel.regexHint')]),
        row(wok,wcl)
      ]);
      // v1.1.0: 白名單也加上標籤篩選 bar(與隱藏名單同架構)
      var tbw = buildTagBarWrapper('white', list, ws, 'ytf-wlist');
      tbw.render();
      return h('div',{cls:'ytf-pane'},[
        row(ws,wa,list.length?wclearBtn:null),
        wf,
        tbw.tagBar,
        tbw.listWrap,
        h('small',{cls:'ytf-sid'},[t('common.total')+list.length+t('common.channels')])
      ]);
    }

    // ── Keywords ──
    function makeKeywords() {
      var groups=S.getKeyGroups();
      var ga=btn('ytf-bp','ytf-ga',t('kw.newGroup'));
      var gn=inp('ytf-gn',t('kw.groupNamePh'));
      var sct=h('input',{type:'checkbox',id:'ytf-sct'}); sct.checked=true;
      var scd=h('input',{type:'checkbox',id:'ytf-scd'});
      var gok=btn('ytf-bp','ytf-gok',t('common.create'));
      var gcl=btn('ytf-bg','',t('common.cancel'));
      var gf=h('div',{cls:'ytf-form ytf-hid',id:'ytf-gf'},[
        gn,
        row(h('label',{cls:'ytf-cb'},[sct,t('kw.scope.title')]),h('label',{cls:'ytf-cb'},[scd,t('kw.scope.description')])),
        row(gok,gcl)
      ]);

      // v1.1.0: 關鍵字搜尋框(搜尋群組名、關鍵字本體)
      var kwSearch = h('input',{type:'text',cls:'ytf-in',id:'ytf-kw-search',placeholder:t('kw.searchPh')});
      var glist=h('div',{id:'ytf-glist'});

      // 依搜尋字串過濾群組 + 標記命中的關鍵字
      function renderGroups(q) {
        var query = (q||'').trim().toLowerCase();
        while(glist.firstChild) glist.removeChild(glist.firstChild);
        var curGroups = S.getKeyGroups();
        if(!curGroups.length){
          glist.appendChild(h('div',{cls:'ytf-mt'},[t('kw.noGroups')]));
          return;
        }
        var matchedCount = 0;
        curGroups.forEach(function(g){
          if(!query){
            glist.appendChild(makeGroupCard(g));
            matchedCount++;
            return;
          }
          // 群組命中條件:群組名含 query 或任一關鍵字含 query
          var groupNameHit = g.name.toLowerCase().indexOf(query) !== -1;
          var matchedKws = g.keywords.filter(function(kw){ return kw.value.toLowerCase().indexOf(query) !== -1; });
          if(groupNameHit || matchedKws.length){
            // 傳入 query 讓 makeGroupCard 高亮命中的關鍵字
            glist.appendChild(makeGroupCard(g, query));
            matchedCount++;
          }
        });
        if(!matchedCount){
          glist.appendChild(h('div',{cls:'ytf-mt'},[t('channel.noResults')]));
        }
      }
      // 綁定搜尋框 input 事件(即時搜尋)
      var searchDeb;
      kwSearch.addEventListener('input',function(){
        clearTimeout(searchDeb);
        searchDeb = setTimeout(function(){ renderGroups(kwSearch.value); }, 150);
      });
      // 初次渲染
      renderGroups('');

      return h('div',{cls:'ytf-pane'},[
        ga,
        gf,
        h('div',{cls:'ytf-kw-search-wrap'},[kwSearch]),
        glist
      ]);
    }

    function makeGroupCard(g, query) {
      var tog=h('input',{type:'checkbox',cls:'ytf-gt'}); tog.checked=g.enabled; tog.setAttribute('data-id',g.id);
      var delBtn=btn('ytf-bg ytf-gd','',t('kw.delGroup')); delBtn.setAttribute('data-id',g.id); delBtn.style.fontSize='12px';
      var kws=h('div',{cls:'ytf-kws'});
      var q = (query||'').toLowerCase();
      g.keywords.forEach(function(kw,i){
        var xBtn=h('button',{cls:'ytf-kx'},['✕']); xBtn.setAttribute('data-gid',g.id); xBtn.setAttribute('data-i',String(i));
        var isHit = q && kw.value.toLowerCase().indexOf(q) !== -1;
        var chipCls = 'ytf-kw' + (kw.isRegex?' ytf-kwr':'') + (isHit?' ytf-kw-hit':'');
        var chip=h('span',{cls:chipCls},[]);
        if(kw.isRegex) chip.appendChild(h('span',{cls:'ytf-re'},['Re']));
        chip.appendChild(document.createTextNode(kw.value));
        chip.appendChild(xBtn);
        kws.appendChild(chip);
      });
      var kaBtn=btn('ytf-ka','',t('kw.addKwBtn')); kaBtn.setAttribute('data-gid',g.id);
      kws.appendChild(kaBtn);
      var kvIn=h('input',{type:'text',cls:'ytf-in ytf-kv',placeholder:t('kw.kwPlaceholder')});
      var rcChk=h('input',{type:'checkbox',cls:'ytf-rc'});
      var koBtn=btn('ytf-bp ytf-ko','',t('common.add')); koBtn.setAttribute('data-gid',g.id);
      var kiRow=h('div',{cls:'ytf-kr ytf-hid',id:'ytf-ki-'+g.id},[kvIn,h('label',{cls:'ytf-cb'},[rcChk,' Regex']),koBtn]);
      var groups2 = S.getKeyGroups();
      var gIdx = groups2.findIndex(function(x){ return x.id===g.id; });
      var upBtn  = h('button',{cls:'ytf-sort-btn',title:t('common.up')},['↑']);
      var dnBtn  = h('button',{cls:'ytf-sort-btn',title:t('common.down')},['↓']);
      if(gIdx===0) upBtn.disabled = true;
      if(gIdx===groups2.length-1) dnBtn.disabled = true;
      upBtn.addEventListener('click',function(ev){ ev.stopPropagation(); S.moveKeyGroup(g.id,'up'); Filter.reset(); renderTab('keywords'); });
      dnBtn.addEventListener('click',function(ev){ ev.stopPropagation(); S.moveKeyGroup(g.id,'down'); Filter.reset(); renderTab('keywords'); });
      // 群組標題高亮(若群組名命中搜尋)
      var groupNameHit = q && g.name.toLowerCase().indexOf(q) !== -1;
      var groupNameEl = h('b', groupNameHit?{cls:'ytf-grp-name-hit'}:{}, [g.name]);

      // v1.1.0: 新增:群組編輯功能(群組名、套用範圍)
      var editBtn = h('button',{cls:'ytf-grp-edit-btn',title:t('common.edit')},['✏️']);
      var editRow = h('div',{cls:'ytf-grp-edit-row ytf-hid'});
      (function(){
        var eNameIn = h('input',{type:'text',cls:'ytf-in',placeholder:t('kw.groupNamePh')});
        eNameIn.value = g.name || '';
        var eScTitle = h('input',{type:'checkbox'});
        var eScDesc  = h('input',{type:'checkbox'});
        eScTitle.checked = (g.scope||[]).indexOf('title') >= 0;
        eScDesc.checked  = (g.scope||[]).indexOf('description') >= 0;
        var eSave = h('button',{cls:'ytf-bp',style:'font-size:12px;padding:5px 10px'},[t('common.save')]);
        var eCancel = h('button',{cls:'ytf-bg',style:'font-size:12px;padding:5px 10px'},[t('common.cancel')]);
        eSave.addEventListener('click',function(ev){
          ev.stopPropagation();
          var newName = eNameIn.value.trim();
          if(!newName){ Notif.show(t('toast.channelEmpty'),'warning'); return; }
          var newScope = [];
          if(eScTitle.checked) newScope.push('title');
          if(eScDesc.checked)  newScope.push('description');
          if(!newScope.length){ Notif.show(t('kw.scopeRequired'),'warning'); return; }
          S.updateKeyGroup(g.id, {name:newName, scope:newScope});
          Filter.reset();
          renderTab('keywords');
          Notif.show(t('toast.updated')+newName,'success');
        });
        eCancel.addEventListener('click',function(ev){
          ev.stopPropagation();
          editRow.classList.add('ytf-hid');
        });
        setChildren(editRow, [
          h('div',{cls:'ytf-grp-edit-fields'},[
            h('label',{style:'font-size:11px;color:var(--t2);font-weight:600'},[t('kw.groupNameLabel')]),
            eNameIn,
            h('label',{style:'font-size:11px;color:var(--t2);font-weight:600;margin-top:6px;display:block'},[t('kw.scopeLabel')]),
            h('div',{style:'display:flex;gap:16px;margin-top:4px'},[
              h('label',{cls:'ytf-cb',style:'font-size:12px'},[eScTitle, t('kw.scope.title')]),
              h('label',{cls:'ytf-cb',style:'font-size:12px'},[eScDesc,  t('kw.scope.description')])
            ]),
            h('div',{cls:'ytf-row',style:'margin-top:8px'},[eSave, eCancel])
          ])
        ]);
      })();
      editBtn.addEventListener('click',function(ev){
        ev.stopPropagation();
        // 關閉其他開啟的編輯列
        ctEl && ctEl.querySelectorAll('.ytf-grp-edit-row:not(.ytf-hid)').forEach(function(r){
          if(r!==editRow) r.classList.add('ytf-hid');
        });
        editRow.classList.toggle('ytf-hid');
      });

      return h('div',{cls:'ytf-grp'},[
        h('div',{style:'display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px'},[
          h('div',{style:'display:flex;align-items:center;gap:8px'},[
            h('label',{cls:'ytf-sw ytf-sw-sm'},[tog,h('span',{cls:'ytf-sl'})]),
            groupNameEl,
            h('small',{cls:'ytf-sid'},[g.scope.join('+')+' · '+g.keywords.length+t('kw.countSuffix')])
          ]),
          h('div',{style:'display:flex;align-items:center;gap:4px'},[upBtn, dnBtn, editBtn, delBtn])
        ]),
        editRow,
        kws, kiRow
      ]);
    }

    // ── Filters ──
    function makeFilters() {
      var fs=S.getFilters();

      // 白名單獨享模式開關(置頂,最顯眼)
      var wlOnlyChk=h('input',{type:'checkbox',id:'ytf-wlo'}); wlOnlyChk.checked=!!fs.whitelistOnly;
      var wlOnlyCard=h('div',{cls:'ytf-fs'},[
        h('div',{cls:'ytf-fh',style:'background:'+(fs.whitelistOnly?'rgba(255,193,7,.12)':'')},[
          h('div',{},[
            h('span',{},[t('filter.wlOnly')]),
            h('small',{cls:'ytf-sid',style:'margin-top:3px'},[t('filter.wlOnly.hint')])
          ]),
          h('label',{cls:'ytf-sw ytf-sw-sm'},[wlOnlyChk,h('span',{cls:'ytf-sl'})])
        ])
      ]);

      function sec(title, togId, enabled, bodyChildren) {
        var tog=h('input',{type:'checkbox',id:togId}); tog.checked=enabled;
        var hd=h('div',{cls:'ytf-fh'},[h('span',{},[title]),h('label',{cls:'ytf-sw ytf-sw-sm'},[tog,h('span',{cls:'ytf-sl'})])]);
        var bd=bodyChildren?h('div',{cls:'ytf-fb'+(enabled?'':' ytf-dis'),id:togId+'-bd'},bodyChildren):null;
        return h('div',{cls:'ytf-fs'},bd?[hd,bd]:[hd]);
      }
      var dmin=numInp('ytf-dmin',fs.duration.min,0), dmax=numInp('ytf-dmax',fs.duration.max,0);
      var ll=h('input',{type:'checkbox',id:'ytf-ll'}); ll.checked=fs.live.hideLive;
      var lu=h('input',{type:'checkbox',id:'ytf-lu'}); lu.checked=fs.live.hideUpcoming;
      var lm=h('select',{id:'ytf-lm',cls:'ytf-sel'},[
        h('option',{value:'block'},[t('filter.lang.modeBlock')]),h('option',{value:'allow'},[t('filter.lang.modeAllow')])
      ]);
      lm.value=fs.language.mode;
      // 常用語言清單(勾選框)
      var COMMON_LANGS=[
        ['ja',t('lang.ja')],['ko',t('lang.ko')],['zh',t('lang.zh')],['en',t('lang.en')],
        ['ru',t('lang.ru')],['ar',t('lang.ar')],['es',t('lang.es')],['fr',t('lang.fr')],
        ['de',t('lang.de')],['pt',t('lang.pt')],['hi',t('lang.hi')],['th',t('lang.th')],
        ['vi',t('lang.vi')],['id',t('lang.id')],['tr',t('lang.tr')],['it',t('lang.it')]
      ];
      var langCks=COMMON_LANGS.map(function(l){
        var ck=h('input',{type:'checkbox',cls:'ytf-lk',value:l[0]});
        if(fs.language.langs.indexOf(l[0])>=0) ck.checked=true;
        return h('label',{cls:'ytf-cb ytf-lbl-lang'},[ck,' '+l[1]]);
      });

      // 自訂語言代碼輸入
      var customLangs = (fs.language.customLangs||[]).join(', ');
      var customIn = h('input',{type:'text',cls:'ytf-in',id:'ytf-lcustom',placeholder:t('filter.lang.custom')});
      customIn.value = customLangs;

      // BCP-47 查詢連結(用 GM_openInTab 或 window.open 避免 CSP 限制)
      var refLink = h('button',{cls:'ytf-lang-ref',id:'ytf-lang-ref-btn',title:t('filter.lang.refTip')},[t('filter.lang.ref')]);
      refLink.addEventListener('click',function(){
        window.open('https://r12a.github.io/app-subtags/', '_blank', 'noopener');
      });

      var saveBtn=btn('ytf-bp','ytf-fsave',t('filter.saveBtn')); saveBtn.style.width='100%';

      // 上傳日期 section(v1.1.1:修正語義歧義 + 新增單位選項)
      // 舊版用「Uploaded more than N days ago / Uploaded within N days」label 易讓人誤解
      // 新版改為「隱藏 __ 的影片」明確句式,並提供 天/年 雙單位(不用計算 1461 這種數字)
      var dateSec = (function(){
        var ud = fs.uploadDate || {};
        // 將舊的 days 值換算為顯示用的 amount + unit
        var storedDays = ud.days || 365;
        var storedUnit = ud.unit; // 如果已經是新格式則用新格式
        var displayAmount, displayUnit;
        if(storedUnit==='year' || storedUnit==='day'){
          displayUnit = storedUnit;
          displayAmount = ud.amount || (storedUnit==='year' ? Math.round(storedDays/365) : storedDays);
        } else {
          // 舊版資料:若 days 是 365 的倍數,顯示為年
          if(storedDays % 365 === 0 && storedDays >= 365){
            displayUnit = 'year';
            displayAmount = storedDays / 365;
          } else {
            displayUnit = 'day';
            displayAmount = storedDays;
          }
        }

        var udAmountIn = numInp('ytf-ud-amount', displayAmount, 1);
        var udUnitSel = h('select',{id:'ytf-ud-unit',cls:'ytf-sel'},[
          h('option',{value:'day'},[t('filter.date.unit.day')]),
          h('option',{value:'year'},[t('filter.date.unit.year')])
        ]);
        udUnitSel.value = displayUnit;
        var udCond = h('select',{id:'ytf-ud-cond',cls:'ytf-sel'},[
          h('option',{value:'older'},[t('filter.date.hideOlder')]),
          h('option',{value:'newer'},[t('filter.date.hideNewer')])
        ]);
        udCond.value = ud.condition || 'older';
        return sec(t('filter.date'),'ytf-ud', ud.enabled||false, [
          row(
            h('label',{style:'flex:1'},[t('filter.date.hide')]),
            udCond, udAmountIn, udUnitSel
          ),
          h('small',{cls:'ytf-sid'},[t('filter.date.hint')])
        ]);
      })();

      // 全域設定 section(原本 IIFE 抽出)
      var globalSec = (function(){
        var gs2=S.getGlobal();
        var toastChk=h('input',{type:'checkbox',id:'ytf-toast'}); toastChk.checked=gs2.showToast!==false;
        var statsChk=h('input',{type:'checkbox',id:'ytf-estats'}); statsChk.checked=gs2.enableStats!==false;
        var langSel=h('select',{id:'ytf-lang-sel',cls:'ytf-sel',style:'min-width:120px'},[
          h('option',{value:'zh'},[t('set.lang.zh')]),
          h('option',{value:'en'},['English'])
        ]);
        langSel.value = LANG;
        return h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 10px;font-size:13px'},[t('set.global')]),
          h('div',{style:'display:flex;flex-direction:column;gap:10px'},[
            // 語言選擇置於全域設定頂部(高優先級、使用者最常調整)
            h('div',{style:'display:flex;align-items:center;gap:8px;padding-bottom:10px;border-bottom:1px dashed var(--bd)'},[
              h('span',{style:'flex:1;font-size:13px'},[t('set.lang')]),
              langSel
            ]),
            h('label',{cls:'ytf-cb'},[toastChk,t('set.toast')]),
            h('small',{cls:'ytf-sid',style:'margin-top:-6px;padding-left:22px'},[t('set.toastHint')]),
            h('label',{cls:'ytf-cb'},[statsChk,t('set.stats')]),
            h('small',{cls:'ytf-sid',style:'margin-top:-6px;padding-left:22px'},[t('set.statsHint')])
          ])
        ]);
      })();

      // D-5 修訂:設定 Tab 最終分群(儲存置頂免捲動、全域設定次位、過濾器分群)
      // (1) 💾 儲存設定(置頂,修改後立即可存,不用往下拉)
      // (2) ⚙️ 全域設定(含介面語言切換)
      // (3) ⭐ 白名單獨享(頻道類)
      // (4) ⏱📡👑📅👁 影片屬性類
      // (5) 🌐 語言過濾(內容類)
      return h('div',{cls:'ytf-pane'},[
        // ═ 儲存(置頂)═
        saveBtn,
        h('hr',{cls:'ytf-divider'}),
        // ═ 系統類 ═
        globalSec,
        h('hr',{cls:'ytf-divider'}),
        // ═ 頻道類 ═
        wlOnlyCard,
        h('hr',{cls:'ytf-divider'}),
        // ═ 影片屬性類 ═
        sec(t('filter.duration'),'ytf-dur',fs.duration.enabled,[row(h('label',{style:'flex:1'},[t('filter.duration.min')]),dmin),row(h('label',{style:'flex:1'},[t('filter.duration.max')]),dmax)]),
        sec(t('filter.live'),'ytf-live',fs.live.enabled,[h('label',{cls:'ytf-cb'},[ll,t('filter.live.hideLive')]),h('label',{cls:'ytf-cb'},[lu,t('filter.live.hideUpcoming')])]),
        sec(t('filter.member'),'ytf-mem',fs.member.enabled,null),
        dateSec,
        sec(t('filter.views'),'ytf-views',(fs.views&&fs.views.enabled)||false,[
          row(h('label',{style:'flex:1'},[t('filter.views.min')]),numInp('ytf-vmin',(fs.views&&fs.views.min)||0,0)),
          row(h('label',{style:'flex:1'},[t('filter.views.max')]),numInp('ytf-vmax',(fs.views&&fs.views.max)||0,0)),
          h('small',{cls:'ytf-sid'},[t('filter.views.hint')])
        ]),
        h('hr',{cls:'ytf-divider'}),
        // ═ 內容類 ═
        sec(t('filter.lang'),'ytf-lang',fs.language.enabled,[
          row(h('label',{style:'flex:1'},[t('common.mode')]),lm),
          h('div',{cls:'ytf-langs'},langCks),
          h('div',{style:'margin-top:10px;display:flex;flex-direction:column;gap:6px'},[
            h('label',{style:'font-size:12px;color:var(--t2)'},[t('filter.lang.customLabel')]),
            customIn,
            refLink,
            h('small',{cls:'ytf-sid'},[t('help.language2')])
          ])
        ])
      ]);
    }

    // ── Stats ──
    function makeStats() {
      var st=S.getStats();
      var kws=Object.entries(st.keywordHits||{}).sort(function(a,b){return b[1]-a[1];}).slice(0,15);
      var chs=Object.entries(st.channelHits||{}).sort(function(a,b){return b[1]-a[1];}).slice(0,15);

      // 解碼頻道 ID(可能是 URL encode 的中文路徑)
      function decodeChId(raw) {
        try {
          // 優先從統計的 channelNames 快取查詢(即使頻道已從隱藏名單移除也能顯示)
          var st2=S.getStats();
          if(st2.channelNames&&st2.channelNames[raw]) return st2.channelNames[raw];
          var decoded = decodeURIComponent(raw);
          if(st2.channelNames&&st2.channelNames[decoded]) return st2.channelNames[decoded];
          // Fallback:從隱藏名單 / 白名單查詢
          var bl = S.getBlocklist().concat(S.getWhitelist());
          var found = bl.find(function(c){ return c.id === raw || c.id === decoded; });
          if (found && found.name) return found.name;
          return decoded;
        } catch(e) { return raw; }
      }

      function makeStatRow(label, count, cls) {
        return h('div',{cls:'ytf-sr2'},[
          h('div',{cls:'ytf-sr2-bar-wrap'},[
            h('div',{cls:'ytf-sr2-bar'+(cls?' '+cls:''),style:'width:'+count+'%'})
          ]),
          h('span',{cls:'ytf-sr2-label',title:label},[label]),
          h('span',{cls:'ytf-sr2-count'},[String(count)])
        ]);
      }

      // 計算最大值(用於百分比 bar)
      var kwMax = kws.length ? kws[0][1] : 1;
      var chMax = chs.length ? chs[0][1] : 1;

      var kwSection = h('div',{cls:'ytf-stat-sec'},[
        h('div',{cls:'ytf-stat-sec-hd'},[
          h('span',{},[t('stat.topKw')]),
          h('span',{cls:'ytf-stat-total'},[kws.length ? t('common.total')+kws.reduce(function(s,e){return s+e[1];},0)+t('common.times') : ''])
        ])
      ]);
      if (kws.length) {
        kws.forEach(function(e){
          kwSection.appendChild(h('div',{cls:'ytf-sr2'},[
            h('div',{cls:'ytf-sr2-row'},[
              h('span',{cls:'ytf-sr2-label',title:e[0]},[e[0]]),
              h('span',{cls:'ytf-sr2-count'},[String(e[1])+t('common.times')])
            ]),
            h('div',{cls:'ytf-sr2-bar-wrap'},[
              h('div',{cls:'ytf-sr2-bar ytf-sr2-kw',style:'width:'+Math.round(e[1]/kwMax*100)+'%'})
            ])
          ]));
        });
      } else {
        kwSection.appendChild(h('div',{cls:'ytf-mt'},[t('stat.noData')]));
      }

      var chSection = h('div',{cls:'ytf-stat-sec'},[
        h('div',{cls:'ytf-stat-sec-hd'},[
          h('span',{},[t('stat.topCh')]),
          h('span',{cls:'ytf-stat-total'},[chs.length ? t('common.total')+chs.reduce(function(s,e){return s+e[1];},0)+t('common.times') : ''])
        ])
      ]);
      if (chs.length) {
        chs.forEach(function(e){
          var label = decodeChId(e[0]);
          chSection.appendChild(h('div',{cls:'ytf-sr2'},[
            h('div',{cls:'ytf-sr2-row'},[
              h('span',{cls:'ytf-sr2-label',title:label},[label]),
              h('span',{cls:'ytf-sr2-count'},[String(e[1])+t('common.times')])
            ]),
            h('div',{cls:'ytf-sr2-bar-wrap'},[
              h('div',{cls:'ytf-sr2-bar ytf-sr2-ch',style:'width:'+Math.round(e[1]/chMax*100)+'%'})
            ])
          ]));
        });
      } else {
        chSection.appendChild(h('div',{cls:'ytf-mt'},[t('stat.noData')]));
      }

      var rst=btn('ytf-bg','ytf-rst',t('stat.clear'));
      return h('div',{cls:'ytf-pane'},[
        h('div',{cls:'ytf-sn'},[
          h('span',{style:'font-size:48px;font-weight:800;display:block;line-height:1'},[String(st.totalHidden||0)]),
          h('span',{style:'font-size:13px;opacity:.85'},[t('stat.totalHidden')])
        ]),
        // D-3: 關鍵字與頻道排行兩欄並排
        h('div',{cls:'ytf-stat-grid'},[kwSection, chSection]),
        rst
      ]);
    }

    // ── History ──
    function makeHistory() {
      var hist=S.getHistory();
      var hclr=btn('ytf-bg','ytf-hclr',t('hist.clear'));
      var items;
      if (hist.length) {
        var ul=h('ul',{cls:'ytf-hl'});
        hist.slice(0,100).forEach(function(r){
          ul.appendChild(h('li',{cls:'ytf-hi'},[
            h('div',{},[h('b',{cls:'ytf-ht'},[r.title||t('hist.unknownTitle')]),h('small',{cls:'ytf-sid'},[r.channelName||(function(){try{return decodeURIComponent(r.channelId||'');}catch(e){return r.channelId||'';}})()])]),
            h('div',{style:'display:flex;align-items:center;gap:8px;white-space:nowrap'},[
              h('span',{cls:'ytf-badge'},[Notif.reasonText(r.reason)]),
              h('small',{cls:'ytf-sid'},[fmtTime(r.hiddenAt)])
            ])
          ]));
        });
        items=ul;
      } else {
        items=h('div',{cls:'ytf-mt'},[t('hist.empty')]);
      }
      return h('div',{cls:'ytf-pane'},[
        h('div',{style:'display:flex;align-items:center;gap:10px'},[h('small',{cls:'ytf-sid'},[t('common.total')+hist.length+t('common.items')]),hclr]),
        items
      ]);
    }

    // ── IO ──
    function makeIO() {
      var gs=S.getGlobal();
      var imSel=h('select',{id:'ytf-im',cls:'ytf-sel'},[h('option',{value:'merge'},[t('io.merge')]),h('option',{value:'overwrite'},[t('io.replace')])]);
      function fileLbl(text, inputId, accept) {
        var fi=h('input',{type:'file',id:inputId,accept:accept,style:'display:none'});
        var lbl=h('label',{cls:'ytf-bg',style:'cursor:pointer'},[text,fi]);
        return lbl;
      }
      var ab=h('input',{type:'checkbox',id:'ytf-ab'}); ab.checked=gs.autoBackup;
      var bi=numInp('ytf-bi2',gs.backupInterval||7,1); bi.max='30';
      var gs2=S.getGlobal();
      var pfxIn=h('input',{type:'text',cls:'ytf-in',id:'ytf-bkpfx',placeholder:t('io.backupFilenameHint')});
      pfxIn.value = gs2.backupPrefix||'';
      return h('div',{cls:'ytf-pane'},[

        // ── 1. 設定備份(完整 JSON + 自動備份)────────────────
        h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 8px'},[t('io.backup')]),
          h('small',{cls:'ytf-sid',style:'margin-bottom:8px;display:block'},
            [t('io.backupDesc')]),
          h('div',{style:'display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px'},[
            btn('ytf-bp','ytf-ej','📥 ' + t('io.exportJson'))
          ]),
          h('div',{style:'border-top:1px solid var(--bd);padding-top:10px;margin-top:4px'},[
            h('label',{cls:'ytf-cb',style:'margin-bottom:6px'},[ab,t('io.autoBackup')]),
            h('div',{style:'display:flex;align-items:center;gap:8px;margin-top:6px'},[
              h('label',{style:'flex:1;font-size:13px'},[t('io.backupInterval')]),bi
            ]),
            h('div',{style:'display:flex;flex-direction:column;gap:4px;margin-top:8px'},[
              h('label',{style:'font-size:12px;color:var(--t2)'},[t('io.backupFilename')]),
              pfxIn,
              h('small',{cls:'ytf-sid'},[t('io.backupPrefixHint')])
            ])
          ])
        ]),

        // ── 2. 個別資料匯出 ────────────────────────────────────
        h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 8px'},[t('io.stat.section')]),
          h('small',{cls:'ytf-sid',style:'margin-bottom:8px;display:block'},
            [t('io.csvHint')]),
          h('div',{style:'display:flex;flex-wrap:wrap;gap:8px'},[
            btn('ytf-bg','ytf-eb',t('tab.blocklist') + ' CSV'),
            btn('ytf-bg','ytf-ew',t('tab.whitelist') + ' CSV'),
            btn('ytf-bg','ytf-ek',t('tab.keywords') + ' CSV')
          ])
        ]),

        // ── 3. 統計與歷史匯出 ──────────────────────────────────
        h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 8px'},[t('io.stat.section')]),
          h('small',{cls:'ytf-sid',style:'margin-bottom:8px;display:block'},
            [t('io.statHint')]),
          h('div',{style:'display:flex;flex-wrap:wrap;gap:8px'},[
            btn('ytf-bg','ytf-est',t('tab.stats') + ' JSON'),
            btn('ytf-bg','ytf-ehi',t('tab.history') + ' CSV')
          ])
        ]),

        // ── 4. 匯入 ────────────────────────────────────────────
        h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 8px'},['📥 ' + t('common.add')]),
          h('div',{style:'display:flex;align-items:center;gap:8px;margin-bottom:10px'},[
            h('label',{style:'flex:1'},[t('io.mergeMode')]),imSel
          ]),
          h('div',{style:'display:flex;flex-wrap:wrap;gap:8px'},[
            fileLbl(t('io.backup'),'ytf-ij','.json'),
            fileLbl(t('tab.blocklist') + ' CSV','ytf-ib','.csv'),
            fileLbl(t('tab.keywords') + ' CSV','ytf-ik','.csv')
          ])
        ]),

        // ── 5. 分享過濾清單 ────────────────────────────────────
        h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 8px'},[t('io.share')]),
          h('small',{cls:'ytf-sid',style:'margin-bottom:8px;display:block'},
            [t('io.shareDesc')]),
          h('div',{style:'display:flex;flex-wrap:wrap;gap:8px'},[
            btn('ytf-bg','ytf-sh-bl',t('io.shareBlock')),
            btn('ytf-bg','ytf-sh-wl',t('io.shareWhite')),
            btn('ytf-bg','ytf-sh-kw',t('io.shareKw')),
            btn('ytf-bp','ytf-sh-all',t('common.all'))
          ]),
          h('div',{id:'ytf-share-warn',cls:'ytf-hid',style:'font-size:12px;color:var(--gold);margin-top:4px'},['']),
          h('div',{id:'ytf-share-out',cls:'ytf-hid',style:'margin-top:8px'},[
            h('div',{style:'display:flex;gap:6px'},[
              h('input',{type:'text',cls:'ytf-in',id:'ytf-share-url',readonly:'',placeholder:t('io.share')}),
              btn('ytf-bg','ytf-sh-copy',t('common.copy'))
            ]),
            h('small',{cls:'ytf-sid'},[t('io.shareHint2')])
          ])
        ])
      ]);
    }


    // ── About ──
    function makeAbout() {
      function sec(title, items) {
        var itemEls = items.map(function(item) {
          return h('li',{style:'margin-bottom:6px'},[item]);
        });
        var ul = h('ul',{style:'margin:8px 0 0 0;padding-left:20px;display:flex;flex-direction:column;gap:4px'},itemEls);
        return h('div',{cls:'ytf-iob'},[h('h4',{style:'margin:0 0 8px;font-size:13px'},[title]),ul]);
      }
      function secWithIntro(title, intro, items) {
        var ul = h('ul',{style:'margin:8px 0 0 0;padding-left:20px;display:flex;flex-direction:column;gap:4px'},
          items.map(function(x){return h('li',{style:'margin-bottom:4px'},[x]);})
        );
        return h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 6px;font-size:13px'},[title]),
          intro ? h('p',{style:'margin:0 0 4px;font-size:12px;color:var(--t2);line-height:1.5'},[intro]) : null,
          ul
        ]);
      }
      function kv(key, val) {
        return h('div',{style:'display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--bd);font-size:13px'},[
          h('span',{style:'color:var(--t2)'},[key]),
          h('span',{style:'font-weight:600'},[val])
        ]);
      }
      // Donate 按鈕(Liberapay + Ko-fi 並排)
      var donateLiberapay = h('a',{
        href:'https://liberapay.com/MayoHu_xu4fu/donate',
        target:'_blank',
        rel:'noopener noreferrer',
        cls:'ytf-donate-btn'
      },['❤️ Liberapay']);
      var donateKofi = h('a',{
        href:'https://ko-fi.com/L4L41XPRRX',
        target:'_blank',
        rel:'noopener noreferrer',
        cls:'ytf-donate-btn ytf-kofi-btn'
      },['☕ Ko-fi']);
      var donateBtns = h('div',{cls:'ytf-donate-row'},[donateLiberapay, donateKofi]);

      return h('div',{cls:'ytf-pane'},[
        // 軟體資訊
        h('div',{cls:'ytf-iob'},[
          h('h4',{style:'margin:0 0 10px;font-size:14px'},[t('about.software')]),
          kv(t('about.name'),t('panel.title')),
          kv(t('about.version'),'v1.1.1-rc1'),
          kv(t('about.compat'),'Tampermonkey (Firefox / Chrome / Edge)'),
          kv(t('about.author'),'MayoHu'),
          h('div',{style:'display:flex;justify-content:space-between;align-items:center;padding:6px 0 0;font-size:13px'},[
            h('span',{style:'color:var(--t2)'},[t('about.source')]),
            h('a',{href:'https://github.com/hubig21/YouTube-Gatekeeper',target:'_blank',rel:'noopener noreferrer',cls:'ytf-gh-link'},['🔗 GitHub'])
          ])
        ]),

        // 快速入門(精簡取代原本 4 大段落)
        sec(t('help.quickStart.title'),[
          t('help.quickStart.step1'),
          t('help.quickStart.step2'),
          t('help.quickStart.step3'),
          t('help.quickStart.step4'),
        ]),

        // Regex 完整指南
        secWithIntro(t('help.regex.title'), t('help.regex.intro'), [
          h('strong',{style:'color:var(--t);font-size:12px'},[t('help.regex.syntax.title')]),
          t('help.regex.syntax.1'),
          t('help.regex.syntax.2'),
          t('help.regex.syntax.3'),
          t('help.regex.syntax.4'),
          t('help.regex.syntax.5'),
          t('help.regex.syntax.6'),
          t('help.regex.syntax.7'),
          h('strong',{style:'color:var(--t);font-size:12px;display:inline-block;margin-top:6px'},[t('help.regex.examples.title')]),
          t('help.regex.examples.1'),
          t('help.regex.examples.2'),
          t('help.regex.examples.3'),
          t('help.regex.examples.4'),
          t('help.regex.examples.5'),
          h('em',{style:'color:var(--t2);font-size:12px;display:inline-block;margin-top:4px'},[t('help.regex.tip')]),
        ]),

        // 兒童保護設定範例
        secWithIntro(t('help.kids.title'), t('help.kids.intro'), [
          t('help.kids.step1'),
          t('help.kids.step2'),
          t('help.kids.step3'),
          t('help.kids.step4'),
          t('help.kids.step5'),
          t('help.kids.step6'),
          h('strong',{cls:'ytf-wlo-warn',style:'display:inline-block;margin-top:6px;line-height:1.5;font-weight:600'},[t('help.kids.warning')]),
        ]),

        // 快捷操作(精簡)
        sec(t('help.shortcuts.title'),[
          t('help.shortcuts.fab'),
          t('help.shortcuts.cardBar'),
          t('help.shortcuts.tmMenu'),
        ]),

        // Donate
        h('div',{cls:'ytf-iob ytf-donate-box'},[
          h('h4',{style:'margin:0 0 8px;font-size:13px'},[t('about.donate')]),
          h('p',{cls:'ytf-donate-txt'},[t('about.donateText')]),
          donateBtns,
        ]),
      ]);
    }

    // refreshList:直接更新隱藏名單或白名單的清單 DOM,不重建整個 Tab
    function refreshList(type) {
      if(type==='blocklist'||type==='block') {
        var bl=document.querySelector('#ytf-blist');
        if(bl) setChildren(bl,[makeChList(S.getBlocklist(),'block','','')]);
        var bc=document.querySelector('#ytf-bl-count');
        if(bc) bc.textContent=t('common.total')+S.getBlocklist().length+t('common.channels');
      } else if(type==='whitelist'||type==='white') {
        var wl=document.querySelector('#ytf-wlist');
        if(wl) setChildren(wl,[makeChList(S.getWhitelist(),'white','')]);
      }
    }
    return {build,show,hide,toggle,renderTab,refreshList,getIsOpen:function(){return isOpen;},getCurTab:function(){return curTab;}};
  })();

  // ============================================================
  // FAB
  // ============================================================
  var FAB = (function() {
    var headerBtn = null;

    function build() {
      // 不建立浮動按鈕,只注入頂欄按鈕
      injectHeaderBtn();
    }

    function injectHeaderBtn() {
      var tryInject = function() {
        if (headerBtn && document.body.contains(headerBtn)) return;
        headerBtn = null; // reset so we can re-inject
        // YouTube 頂欄右側 #end 或 #buttons
        var end = document.querySelector('#end, #buttons.ytd-masthead');
        if (!end) { setTimeout(tryInject, 1500); return; }

        // 主按鈕:點擊開啟面板
        headerBtn = h('button', {id:'ytf-hb', title:t('fab.title')}, ['🔍']);
        headerBtn.addEventListener('click', function() { Panel.toggle(); });

        // 右鍵切換過濾器開關
        headerBtn.addEventListener('contextmenu', function(ev) {
          ev.preventDefault();
          var cur = S.isEnabled(); S.setEnabled(!cur);
          Filter.reset(); update();
          Notif.show(cur ? t('filter.disabled') : t('filter.enabled'));
          if (!cur) doFilter(Obs.cards());
        });

        end.insertBefore(headerBtn, end.firstChild);
        update();
      };
      setTimeout(tryInject, 1200);
    }

    function update() {
      if (!headerBtn) return;
      var on = S.isEnabled();
      headerBtn.textContent = on ? '🔍' : '⏸';
      headerBtn.title = on
        ? t('fab.tip.enabled')
        : t('fab.tip.disabled');
      headerBtn.style.opacity = on ? '1' : '0.55';
    }

    return {build, update, injectHeaderBtn};
  })();

  // ============================================================
  // CSS
  // ============================================================
  GM_addStyle([
    ':root{--bg:#0f0f0f;--bg2:#1c1c1c;--bg3:#282828;--bd:#363636;--t:#e8e8e8;--t2:#999;--ac:#ff4444;--gold:#ffc107;--r:10px;--font:"Segoe UI","PingFang TC","Microsoft JhengHei",sans-serif}',
    'html:not([dark]){--bg:#fff;--bg2:#f5f5f5;--bg3:#eee;--bd:#ddd;--t:#111;--t2:#666}',

    '#ytf-panel{position:fixed;inset:0;z-index:2147483646;pointer-events:none;font-family:var(--font)}',
    '#ytf-panel.ytf-open{pointer-events:all}',
    '#ytf-ov{position:absolute;inset:0;background:rgba(0,0,0,.55);opacity:0;transition:.25s}',
    '#ytf-panel.ytf-open #ytf-ov{opacity:1}',
    '#ytf-bd{position:absolute;top:0;right:0;width:620px;max-width:96vw;height:100vh;background:var(--bg);border-left:1px solid var(--bd);display:flex;flex-direction:column;color:var(--t);font-size:14px;transform:translateX(100%);transition:transform .28s cubic-bezier(.4,0,.2,1);overflow:hidden}',
    '#ytf-panel.ytf-open #ytf-bd{transform:translateX(0)}',
    '#ytf-hd{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid var(--bd);background:var(--bg2);flex-shrink:0}',
    '#ytf-logo{font-size:15px;font-weight:700;display:inline-flex;align-items:baseline;gap:8px}',
    '#ytf-ver{font-size:10px;font-weight:400;color:var(--t2);opacity:.7;letter-spacing:.5px}',
    '#ytf-x{background:none;border:none;color:var(--t2);font-size:18px;cursor:pointer;padding:4px 8px;border-radius:6px}#ytf-x:hover{background:var(--bg3)}',
    '#ytf-lb{padding:0 14px;flex-shrink:0;height:44px;overflow:hidden;border-bottom:1px solid var(--bd)}',
    '.ytf-l{font-size:11px;padding:2px 0;color:var(--t2)}.ytf-l-success{color:#4caf50}.ytf-l-error{color:var(--ac)}.ytf-l-warning{color:var(--gold)}',
    '#ytf-nav{display:flex;overflow-x:auto;border-bottom:1px solid var(--bd);flex-shrink:0;background:var(--bg2);scrollbar-width:none;position:relative;mask-image:linear-gradient(to right,#000 calc(100% - 20px),transparent 100%);-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 20px),transparent 100%)}#ytf-nav::-webkit-scrollbar{display:none}',
    '.ytf-tab{padding:7px 8px;background:none;border:none;border-bottom:2px solid transparent;color:var(--t2);cursor:pointer;white-space:nowrap;font-size:12px;font-family:var(--font);transition:.15s}',
    '.ytf-tab:hover{color:var(--t);background:var(--bg3)}.ytf-tab-on{color:var(--ac)!important;border-bottom-color:var(--ac)!important}',
    '#ytf-ct{flex:1;overflow-y:auto;padding:clamp(8px,2.2%,14px);padding-bottom:24px;scrollbar-width:thin;scrollbar-color:var(--bd) transparent}',
    '.ytf-pane{display:flex;flex-direction:column;gap:clamp(8px,1.8%,12px)}',
    '.ytf-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
    '.ytf-in{flex:1;padding:8px 10px;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;color:var(--t);font-size:13px;font-family:var(--font);outline:none;min-width:0;box-sizing:border-box}.ytf-in:focus{border-color:var(--ac)}',
    '.ytf-ins{max-width:90px;flex:none}',
    '.ytf-sel{padding:7px 10px;background:var(--bg2);border:1px solid var(--bd);border-radius:6px;color:var(--t);font-size:13px;cursor:pointer}',
    '.ytf-bp{padding:8px 12px;background:var(--ac);border:none;border-radius:6px;color:#fff;font-size:13px;font-family:var(--font);cursor:pointer;white-space:nowrap}.ytf-bp:hover{filter:brightness(1.15)}',
    '.ytf-bg{padding:8px 12px;background:none;border:1px solid var(--bd);border-radius:6px;color:var(--t);font-size:13px;font-family:var(--font);cursor:pointer;white-space:nowrap}.ytf-bg:hover{background:var(--bg3)}',
    '.ytf-hid{display:none!important}',
    '.ytf-mt{text-align:center;color:var(--t2);padding:20px;font-size:13px}',
    '.ytf-sid{font-size:11px;color:var(--t2);display:block;margin-top:2px}',
    '.ytf-form{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:12px;display:flex;flex-direction:column;gap:10px}',
    '.ytf-cb{display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px}',
    '.ytf-tag{display:inline-block;padding:1px 6px;background:var(--bg3);border-radius:10px;font-size:11px;color:var(--t2);margin:1px}',
    '.ytf-badge{font-size:11px;padding:2px 7px;background:rgba(255,68,68,.15);color:var(--ac);border-radius:10px;white-space:nowrap}',
    '.ytf-cl{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px}',
    '.ytf-ci{display:flex;align-items:center;justify-content:space-between;padding:9px 11px;background:var(--bg2);border:1px solid var(--bd);border-radius:8px;gap:8px}.ytf-ci:hover{border-color:var(--ac)}',
    // 頻道列表 & 移除確認列
    '.ytf-ci-wrap{flex-direction:column;align-items:stretch;padding:0}',
    '.ytf-cn{font-size:13px;display:block}',
    '.ytf-del{background:none;border:none;cursor:pointer;font-size:14px;padding:3px 6px;color:var(--t2);flex-shrink:0;border-radius:4px;transition:.15s;font-weight:600}',
    '.ytf-del:hover{background:rgba(255,68,68,.15);color:var(--ac)}',
    '.ytf-del-confirm{display:flex;align-items:center;gap:6px;padding:6px 11px 8px;border-top:1px solid var(--bd);background:rgba(255,68,68,.06)}',
    '.ytf-del-confirm-txt{font-size:12px;color:var(--t2);flex:1}',
    '.ytf-del-yes{padding:4px 12px;background:var(--ac);border:none;border-radius:5px;color:#fff;font-size:12px;cursor:pointer;font-weight:600}',
    '.ytf-del-yes:hover{filter:brightness(1.15)}',
    '.ytf-del-no{padding:4px 10px;background:none;border:1px solid var(--bd);border-radius:5px;color:var(--t2);font-size:12px;cursor:pointer}',
    '.ytf-del-no:hover{background:var(--bg3)}',
    '.ytf-grp{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:12px;display:flex;flex-direction:column;gap:10px}',
    '.ytf-kws{display:flex;flex-wrap:wrap;gap:5px;align-items:center}',
    '.ytf-kw{display:inline-flex;align-items:center;gap:3px;padding:3px 8px;background:var(--bg3);border:1px solid var(--bd);border-radius:20px;font-size:12px}.ytf-kwr{border-color:#2196f3}',
    // v1.1.0: 關鍵字搜尋命中高亮
    '.ytf-kw-hit{background:rgba(255,193,7,.3);border-color:var(--gold);box-shadow:0 0 0 1px var(--gold)}',
    '.ytf-grp-name-hit{color:var(--gold);text-shadow:0 0 8px rgba(255,193,7,.4)}',
    '.ytf-kw-search-wrap{margin:8px 0;padding:0}',
    '#ytf-kw-search{width:100%}',
    '.ytf-re{font-size:10px;background:#2196f3;color:#fff;padding:1px 4px;border-radius:4px}',
    '.ytf-kx{background:none;border:none;cursor:pointer;font-size:11px;color:var(--t2);padding:0 2px}.ytf-kx:hover{color:var(--ac)}',
    '.ytf-ka{padding:3px 10px;background:none;border:1px dashed var(--bd);border-radius:20px;color:var(--t2);font-size:12px;cursor:pointer}.ytf-ka:hover{border-color:var(--ac);color:var(--ac)}',
    '.ytf-kr{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding-top:4px}.ytf-kv{flex:1;min-width:120px}',
    '.ytf-fs{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);overflow:hidden}',
    '.ytf-fh{display:flex;align-items:center;justify-content:space-between;padding:9px 13px;font-weight:600}',
    '.ytf-fb{padding:9px 13px;border-top:1px solid var(--bd);display:flex;flex-direction:column;gap:8px}.ytf-fb.ytf-dis{opacity:.4;pointer-events:none}',
    '.ytf-divider{border:none;border-top:1px solid var(--bd);margin:4px 0}',
    '.ytf-stat-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}',
    '.ytf-stat-grid>*{min-width:0}',
    '.ytf-sn{background:linear-gradient(135deg,var(--ac),#b00);border-radius:var(--r);padding:16px;text-align:center;color:#fff}',
    '.ytf-sb{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:11px}',
    '.ytf-sr{display:flex;justify-content:space-between;padding:3px 0;border-bottom:1px solid var(--bd);font-size:12px}.ytf-sr:last-child{border:none}',
    '.ytf-hl{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:6px}',
    '.ytf-hi{display:flex;align-items:flex-start;justify-content:space-between;gap:8px;padding:9px 11px;background:var(--bg2);border:1px solid var(--bd);border-radius:8px}',
    '.ytf-ht{font-size:13px;font-weight:500;display:block;margin-bottom:2px}',
    '.ytf-iob{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:clamp(8px,2%,13px);display:flex;flex-direction:column;gap:clamp(6px,1.5%,10px)}',
    '.ytf-sw{position:relative;display:inline-block;width:40px;height:22px;flex-shrink:0;margin:0}',
    '.ytf-sw input{opacity:0;width:0;height:0;position:absolute}',
    '.ytf-sl{position:absolute;inset:0;background:var(--bd);border-radius:22px;cursor:pointer;transition:.2s}',
    '.ytf-sl:before{content:"";position:absolute;width:16px;height:16px;left:3px;top:3px;background:#fff;border-radius:50%;transition:.2s}',
    '.ytf-sw input:checked+.ytf-sl{background:var(--ac)}.ytf-sw input:checked+.ytf-sl:before{transform:translateX(18px)}',
    '.ytf-sw-sm{width:32px;height:18px}.ytf-sw-sm .ytf-sl:before{width:12px;height:12px}.ytf-sw-sm input:checked+.ytf-sl:before{transform:translateX(14px)}',
    // Compact 卡片(側邊推薦):小圓形圖示
    '.ytf-qa-icons{position:absolute;top:4px;right:4px;display:flex;flex-direction:column;gap:3px;z-index:9999;opacity:0;transition:opacity .15s;pointer-events:none}',
    '.ytf-qa-icons.ytf-qa-show{opacity:1;pointer-events:all}',
    '.ytf-qa-ic{width:26px;height:26px;border-radius:50%;border:none;cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(6px);transition:transform .12s}',
    '.ytf-qa-ic:hover{transform:scale(1.18)}',
    '.ytf-qa-ic-b{background:rgba(220,50,50,.82)}.ytf-qa-ic-w{background:rgba(255,193,7,.82)}',
    // 影片卡片 hover action bar(JS mouseenter 控制顯示)
    '.ytf-qa-bar{position:absolute;height:36px;background:rgba(0,0,0,.78);backdrop-filter:blur(8px);display:flex;align-items:stretch;z-index:9999;opacity:0;transition:opacity .15s;pointer-events:none;border-radius:0 0 6px 6px;overflow:hidden}',
    '.ytf-qa-bar.ytf-qa-show{opacity:1;pointer-events:all}',
    '.ytf-qa-bar button{flex:1;border:none;background:none;cursor:pointer;font-size:12px;color:#fff;display:flex;align-items:center;justify-content:center;gap:5px;transition:background .12s;white-space:nowrap;padding:0 8px}',
    '.ytf-qa-b:hover{background:rgba(220,50,50,.6)}.ytf-qa-w:hover{background:rgba(255,193,7,.3)}',
    '.ytf-qa-bar button+button{border-left:1px solid rgba(255,255,255,.18)}',
    '#ytf-nw{position:fixed;top:70px;right:16px;display:flex;flex-direction:column;gap:8px;z-index:2147483647;pointer-events:none}',
    '.ytf-toast{display:flex;align-items:center;gap:8px;padding:9px 12px;background:var(--bg);border:1px solid var(--bd);border-radius:10px;font-size:13px;font-family:var(--font);color:var(--t);box-shadow:0 4px 14px rgba(0,0,0,.35);max-width:290px;pointer-events:all;opacity:0;transform:translateX(20px);transition:.25s}',
    '.ytf-toast.ytf-ti{opacity:1;transform:translateX(0)}.ytf-tm{flex:1}.ytf-tx{background:none;border:none;cursor:pointer;color:var(--t2);font-size:11px;flex-shrink:0}',
    // 頂欄注入按鈕
    '#ytf-hb{background:none;border:none;cursor:pointer;font-size:18px;padding:6px 8px;border-radius:50%;color:var(--t);transition:.2s;flex-shrink:0}',
    '#ytf-hb:hover{background:rgba(255,255,255,.1)}',
    // 統計區塊 - 橫條圖設計
    '.ytf-stat-sec{background:var(--bg2);border:1px solid var(--bd);border-radius:var(--r);padding:12px;display:flex;flex-direction:column;gap:6px}',
    '.ytf-stat-sec-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;font-weight:600;font-size:13px}',
    '.ytf-stat-total{font-size:11px;color:var(--t2);font-weight:400}',
    '.ytf-sr2{display:flex;flex-direction:column;padding:5px 0;border-bottom:1px solid var(--bd);font-size:12px;min-width:0;gap:4px}',
    '.ytf-sr2:last-child{border-bottom:none}',
    '.ytf-sr2-bar-wrap{grid-column:1/-1;height:4px;background:var(--bg3);border-radius:2px;overflow:hidden;margin-bottom:2px}',
    '.ytf-sr2-bar{height:100%;border-radius:2px;transition:width .4s ease;min-width:2px}',
    '.ytf-sr2-kw{background:var(--ac)}',
    '.ytf-sr2-ch{background:#2196f3}',
    '.ytf-sr2-row{display:flex;justify-content:space-between;align-items:center;gap:4px;min-width:0}',
    '.ytf-sr2-label{color:var(--t);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}',
    '.ytf-sr2-count{color:var(--ac);font-weight:700;flex-shrink:0;white-space:nowrap}',

    // 關於頁面列表
    '.ytf-pane ul li{font-size:13px;color:var(--t);line-height:1.6}',
    // Regex 頻道條目 badge
    '.ytf-re-badge{display:inline-flex;align-items:center;padding:1px 5px;background:rgba(100,149,237,.2);border:1px solid rgba(100,149,237,.5);border-radius:4px;font-size:10px;font-weight:700;color:#6495ed;font-family:monospace;margin-right:5px;flex-shrink:0}',
    // Donate 區塊
    '.ytf-donate-box{border-color:var(--gold)!important;background:rgba(255,193,7,.05)!important}',
    '.ytf-donate-txt{font-size:13px;color:var(--t);line-height:1.6;margin:0 0 10px}',
    '.ytf-donate-btn{display:inline-flex;align-items:center;gap:6px;padding:9px 18px;background:#f6c915;color:#1a1a1a;border-radius:8px;font-weight:700;font-size:13px;text-decoration:none;transition:.15s}',
    '.ytf-donate-btn:hover{filter:brightness(1.1);transform:translateY(-1px)}',
    '.ytf-donate-row{display:flex;gap:8px;flex-wrap:wrap}',
    '.ytf-kofi-btn{background:#29abe0;color:#fff}',
    '.ytf-gh-link{color:var(--p);text-decoration:none;font-weight:600}',
    '.ytf-gh-link:hover{text-decoration:underline}',
    // 標籤篩選 bar(隱藏名單頁頂部)
    '.ytf-tag-bar{display:flex;flex-wrap:wrap;gap:6px;padding:4px 0}',
    '.ytf-tagf{padding:4px 10px;background:var(--bg3);border:1px solid var(--bd);border-radius:20px;color:var(--t2);font-size:11px;cursor:pointer;transition:.15s;white-space:nowrap}',
    '.ytf-tagf:hover{border-color:var(--ac);color:var(--t)}',
    '.ytf-tagf-on{background:var(--ac);border-color:var(--ac);color:#fff!important}',
    // 頻道列表標籤區
    // D-4: .ytf-ci-nametags 讓 name、sid、re-badge、tags chips 同在一個 flex-wrap 容器
    '.ytf-ci-nametags{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:center;gap:4px 8px}',
    '.ytf-tags-area{display:inline-flex;align-items:center;gap:4px;flex-wrap:wrap}',
    '.ytf-tag-chips{display:inline-flex;flex-wrap:wrap;gap:3px}',
    '.ytf-tag-edit{padding:2px 6px;cursor:default;display:inline-flex;align-items:center;gap:3px}',
    '.ytf-tag-rm{background:none;border:none;cursor:pointer;font-size:14px;color:var(--t2);padding:0 3px;line-height:1;margin-left:2px}.ytf-tag-rm:hover{color:var(--ac)}',
    '.ytf-tag-toggle{background:none;border:1px dashed var(--bd);border-radius:50%;width:20px;height:20px;font-size:11px;cursor:pointer;color:var(--t2);display:flex;align-items:center;justify-content:center;flex-shrink:0;padding:0;transition:.15s}',
    '.ytf-tag-toggle:hover{border-color:var(--ac);color:var(--ac)}',
    // v1.1.0: 任務 1-3:標籤輸入 UI 重新設計(清楚分區 + 自動收起)
    '.ytf-tag-input-row{display:flex;flex-direction:column;gap:10px;padding:10px 11px 12px;border-top:1px solid var(--bd);background:var(--bg2)}',
    '.ytf-tag-section{display:flex;flex-direction:column;gap:6px}',
    '.ytf-tag-section-label{font-size:11px;color:var(--t2);font-weight:600;letter-spacing:.3px}',
    '.ytf-tag-input-flex{display:flex;gap:6px}',
    '.ytf-tag-input-flex .ytf-in{flex:1}',
    '.ytf-tag-add-btn{padding:6px 12px;background:var(--ac);border:none;border-radius:6px;color:#fff;font-size:12px;font-weight:600;cursor:pointer;flex-shrink:0;transition:.12s}',
    '.ytf-tag-add-btn:hover{background:#d32f2f}',
    '.ytf-tag-existing-chips{display:flex;flex-wrap:wrap;gap:4px;max-height:100px;overflow-y:auto;padding:4px 2px}',
    '.ytf-tag-pick-chip{background:var(--bg3);border:1px solid var(--bd);border-radius:12px;padding:3px 9px;cursor:pointer;font-size:12px;color:var(--t);display:inline-flex;align-items:center;gap:3px;transition:.12s}',
    '.ytf-tag-pick-chip:hover{background:var(--ac);border-color:var(--ac);color:#fff}',
    '.ytf-tag-pick-chip small{color:var(--t2);font-size:10px}',
    '.ytf-tag-pick-chip:hover small{color:#fff;opacity:.85}',
    // 已擁有狀態:淡紅底表示這個 tag 該頻道已擁有(點擊可移除)
    '.ytf-tag-pick-chip-on{background:rgba(255,68,68,.18);border-color:var(--ac);color:var(--ac)}',
    '.ytf-tag-pick-chip-on:before{content:"✓";margin-right:2px;font-weight:bold}',
    '.ytf-tag-pick-chip-on:hover{background:rgba(255,68,68,.35)}',
    '.ytf-tag-pick-chip-on small{color:var(--ac);opacity:.7}',
    '.ytf-tag-pick-empty{font-size:11px;color:var(--t2);padding:4px;font-style:italic}',
    // ytf-ci-main layout fix for tags
    '.ytf-ci-main{display:flex;align-items:center;gap:8px;padding:7px 11px;min-width:0}',
    // D-4: first-child 的 flex:1 已移到 .ytf-ci-nametags
    // 語言過濾相關
    '.ytf-langs{display:flex;flex-wrap:wrap;gap:6px 10px}',
    '.ytf-lbl-lang{font-size:12px;padding:3px 0}',
    '.ytf-lang-ref{background:none;border:1px solid var(--bd);border-radius:6px;padding:6px 10px;color:var(--t2);font-size:12px;cursor:pointer;text-align:left;width:100%;transition:.15s}',
    '.ytf-lang-ref:hover{border-color:var(--ac);color:var(--ac)}',
    // 白名單獨享模式
    '.ytf-wlo-warn{font-size:12px;color:var(--gold);margin-top:4px}',
    // 編輯按鈕與 inline 編輯列
    '.ytf-edit-btn{background:none;border:none;cursor:pointer;font-size:12px;padding:3px 5px;color:var(--t2);border-radius:4px;transition:.15s;flex-shrink:0}.ytf-edit-btn:hover{background:var(--bg3);color:var(--t)}',
    '.ytf-ci-actions{display:flex;align-items:center;gap:2px;flex-shrink:0}',
    '.ytf-edit-row{padding:10px 11px;border-top:1px solid var(--bd);background:var(--bg3)}',
    '.ytf-edit-fields{display:flex;flex-direction:column;gap:8px}',
    '.ytf-sort-btn{background:none;border:1px solid var(--bd);border-radius:4px;padding:2px 6px;font-size:12px;cursor:pointer;color:var(--t2);transition:.15s}.ytf-sort-btn:hover:not(:disabled){background:var(--bg3);border-color:var(--ac);color:var(--t)}.ytf-sort-btn:disabled{opacity:.3;cursor:default}',
    // 關鍵字群組編輯
    '.ytf-grp-edit-btn{background:none;border:1px solid var(--bd);border-radius:4px;padding:2px 6px;font-size:12px;cursor:pointer;transition:.15s}.ytf-grp-edit-btn:hover{background:var(--bg3);border-color:var(--ac)}',
    '.ytf-grp-edit-row{padding:10px 0 4px;border-top:1px dashed var(--bd);margin-top:2px}',
    '.ytf-grp-edit-fields{display:flex;flex-direction:column;gap:4px}',
  ].join(''));

  // ============================================================
  // QUICK ADD
  // ============================================================
  var QA = (function() {
    var A='data-ytf-qa';
    function injectCards(cards) {
      Array.prototype.slice.call(cards).forEach(function(card){
        ftCount('qaCalls');
        if(card.getAttribute(A)){ ftCount('qaSkipped_alreadyMarked'); return; }
        // v1.1.0: 起 Observer SEL 已避免父子重複選取,不再需要父層檢查
        // 先取 info,若卡片 metadata 尚未渲染則不打標記,下次 poll 重試
        // (不能先標記再 return,否則卡片會永遠失去注入機會)
        var info=Filter.getInfo(card);
        if(!info.channelName&&!info.channelId){
          ftCount('qaSkipped_noInfo');
          if(_dbgOn){
            ftDbg('QA skip (no info):', card.tagName,
              'children=', card.children.length,
              'hasThumb=', !!card.querySelector('a[href*="watch?v="]'),
              'hasChLink=', !!card.querySelector('a[href^="/@"]'),
              'hasAvatar=', !!card.querySelector('.yt-spec-avatar-shape__button,.ytSpecAvatarShapeButton'));
          }
          return;
        }
        // 合輯(YouTube Mix / Radio):非單一頻道內容,不注入快捷按鈕
        if(info.isCollection){
          card.setAttribute(A,'1');
          if(_dbgOn) ftDbg('QA skip collection:', (info.title||'').slice(0,40));
          return;
        }
        // v1.1.0: 任務 5:已白名單頻道不注入快捷按鈕(使用者已信任這些頻道)
        if(S.isWhitelisted(info.channelId, info.channelName)){
          card.setAttribute(A,'1');
          if(_dbgOn) ftDbg('QA skip whitelisted:', info.channelName);
          return;
        }
        card.setAttribute(A,'1');
        ftCount('qaInjected');
        if(_dbgOn) ftDbg('QA inject:', info.channelName, '| tag=', card.tagName);

        var chName = info.channelName||info.channelId||'';
        var chTitle = t('qa.channelTip')+chName;

        // 判斷是否為側邊推薦的 compact 卡片
        // 新版 ViewModel:yt-lockup-view-model 下的 div 有 ytLockupViewModelHorizontal class
        // 舊版:ytd-compact-video-renderer tag
        var isCompact = card.tagName==='YTD-COMPACT-VIDEO-RENDERER' ||
                        card.classList.contains('ytd-compact-video-renderer') ||
                        card.tagName==='YT-LOCKUP-VIEW-MODEL' && (function(){
                          var inner = card.querySelector('.ytLockupViewModelHost');
                          return inner && inner.classList.contains('ytLockupViewModelHorizontal');
                        })();

        // 找縮圖區塊(新舊版 class 都支援)
        var thumbLink = card.querySelector(
          'a.ytLockupViewModelContentImage,' +
          'a.yt-lockup-view-model__content-image,' +
          '#thumbnail,ytd-thumbnail'
        );

        function doBlock(ev) {
          ev.preventDefault(); ev.stopPropagation();
          var ok=S.addToBlocklist({id:info.channelId,name:info.channelName});
          Filter.reset();
          doFilter(Obs.cards());
          // 若面板開著且在 blocklist/whitelist Tab,同步更新清單 DOM
          Panel.refreshList('blocklist');
          Notif.showForce(ok?t('qa.hiddenPrefix')+chName:t('qa.quote.open')+chName+t('qa.conflict.WLToBlock'));
        }
        function doWhite(ev) {
          ev.preventDefault(); ev.stopPropagation();
          var ok=S.addToWhitelist({id:info.channelId,name:info.channelName});
          Filter.reset();
          doFilter(Obs.cards());
          // 若面板開著且在 blocklist/whitelist Tab,同步更新清單 DOM
          Panel.refreshList('whitelist');
          Notif.showForce(ok?t('qa.already.white')+chName:t('qa.quote.open')+chName+t('qa.conflict.blockToWL'));
        }

        if (isCompact) {
          // ── Compact 模式(側邊推薦):小圓形圖示 ──
          var bBtn=h('button',{cls:'ytf-qa-ic ytf-qa-ic-b',title:t('qa.blockTitle')+chName},['🚫']);
          var wBtn=h('button',{cls:'ytf-qa-ic ytf-qa-ic-w',title:t('qa.whitelistTitle')+chName},['⭐']);
          bBtn.addEventListener('click',doBlock);
          wBtn.addEventListener('click',doWhite);
          var icons=h('div',{cls:'ytf-qa-icons'},[bBtn,wBtn]);

          // 掛在 thumbLink 的父元素(避免 a 元素的 overflow:hidden 裁切)
          // 新版 ViewModel:thumbLink(a) 的父是 ytLockupViewModelHost div
          var thumbParent = thumbLink ? thumbLink.parentElement : null;
          var mountEl = thumbParent || card;
          mountEl.style.position = 'relative';
          // 如果 mountEl 是縮圖 a 本身(無父)也能用
          if(thumbLink && mountEl === card) {
            thumbLink.style.overflow = 'visible';
          }
          mountEl.appendChild(icons);

          // hover card 時顯示圖示
          card.addEventListener('mouseenter',function(){ icons.classList.add('ytf-qa-show'); });
          card.addEventListener('mouseleave',function(){ icons.classList.remove('ytf-qa-show'); });

        } else {
          // ── Normal 模式(首頁/搜尋/訂閱):底部 bar ──
          var bBtn2=h('button',{cls:'ytf-qa-b',title:t('qa.blockTitle')+chName},[t('qa.block')]);
          var wBtn2=h('button',{cls:'ytf-qa-w',title:t('qa.whitelistTitle')+chName},[t('qa.whitelist')]);
          bBtn2.addEventListener('click',doBlock);
          wBtn2.addEventListener('click',doWhite);

          var bar=h('div',{cls:'ytf-qa-bar'},[bBtn2,wBtn2]);

          // bar 掛在 thumbLink 的直接父元素,而非 card
          // 避免 card.position:relative 殘留在 SPA 複用的 DOM 上造成黑色空白
          var mountEl = thumbLink ? (thumbLink.parentElement || card) : card;
          mountEl.style.position='relative';
          mountEl.appendChild(bar);

          function positionBar() {
            if(!thumbLink) return;
            var rect=thumbLink.getBoundingClientRect();
            var mountRect=mountEl.getBoundingClientRect();
            // 縮圖 lazy load 未完成(rect.width=0),等待 img load 後重試一次
            if(!rect.width) {
              var img=thumbLink.querySelector('img');
              if(img && !img._ytfPosListener) {
                img._ytfPosListener=true;
                img.addEventListener('load',function(){positionBar();},{once:true});
              }
              return;
            }
            bar.style.top=(rect.bottom-mountRect.top-36)+'px';
            bar.style.left=(rect.left-mountRect.left)+'px';
            bar.style.width=rect.width+'px';
            bar.style.bottom='auto';
          }
          card.addEventListener('mouseenter',function(){ positionBar(); bar.classList.add('ytf-qa-show'); });
          card.addEventListener('mouseleave',function(){ bar.classList.remove('ytf-qa-show'); });
        }
      });
    }
    function injectChannelPage() {
      if(!location.pathname.startsWith('/@')&&!location.pathname.startsWith('/channel/')) return;
      if(document.querySelector('.ytf-cpb')) return;
      var header=document.querySelector('#channel-header-container,ytd-channel-header-renderer'); if(!header) return;
      var nameEl=header.querySelector('#channel-name yt-formatted-string,#channel-name');
      var cName=nameEl?nameEl.textContent.trim():'';
      var cId=location.pathname.replace(/^\//,'');
      var bBtn=h('button',{cls:'ytf-cbb'},[t('qa.block')]);
      var wBtn=h('button',{cls:'ytf-cbw'},[t('qa.whitelist')]);
      bBtn.addEventListener('click',function(){
        var ok=S.addToBlocklist({id:cId,name:cName});
        if(ok){
          bBtn.textContent=t('qa.hidden');
          bBtn.disabled=true;
          bBtn.style.opacity='0.6';
        }
        Notif.showForce(ok?t('qa.already.block')+cName:t('qa.quote.open')+cName+t('qa.conflict.WLToBlock'),'success');
      });
      wBtn.addEventListener('click',function(){
        var ok=S.addToWhitelist({id:cId,name:cName});
        if(ok){
          wBtn.textContent=t('qa.whitelisted');
          wBtn.disabled=true;
          wBtn.style.opacity='0.6';
        }
        Notif.showForce(ok?t('qa.already.white')+cName:t('qa.quote.open')+cName+t('qa.conflict.blockToWL'),'success');
      });
      var bar=h('div',{cls:'ytf-cpb'},[bBtn,wBtn]);
      header.appendChild(bar);
    }
    return {injectCards,injectChannelPage};
  })();

  // ============================================================
  // MAIN
  // ============================================================

  function doFilter(cards) {
    var arr=Array.prototype.slice.call(cards); if(!arr.length) return;
    if(_dbgOn) ftDbg('doFilter', arr.length, 'cards');
    var hidden=Filter.processAll(arr); if(hidden.length) Notif.summary(hidden);
    QA.injectCards(arr);
  }

  function init() {
    Panel.build();
    FAB.build(); // 只注入頂欄按鈕
    Obs.start(doFilter);
    Obs.watchNav(function(){
      Filter.reset();
      Obs.restart(doFilter);
      Panel.build();  // 確保 SPA 導航後面板仍掛在 DOM 上
      setTimeout(function(){
        QA.injectChannelPage();
        FAB.injectHeaderBtn();
      },1200);
    });
    GM_registerMenuCommand(t('tm.togglePanel'),function(){Panel.show();});
    GM_registerMenuCommand(t('tm.toggleFilter'),function(){S.setEnabled(!S.isEnabled());Filter.reset();FAB.update();if(S.isEnabled())doFilter(Obs.cards());});
    GM_registerMenuCommand(t('tm.backupNow'),function(){IO.autoBackup();});
    setTimeout(function(){QA.injectChannelPage();},1500);
    // 偵測分享連結匯入
    IO.checkImportFromHash();
    // 自動備份檢查
    setTimeout(function(){
      var gs=S.getGlobal();
      if(!gs.autoBackup) return;
      var interval=(gs.backupInterval||7)*86400000;
      if(!gs.lastBackup||Date.now()-gs.lastBackup>interval) IO.autoBackup();
    }, 3000);
  }

  if (document.body) { setTimeout(init,600); }
  else { document.addEventListener('DOMContentLoaded',function(){setTimeout(init,600);}); }

})();