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

})();