AvBase Ultimate Enhancer

首頁/演員頁/作品列表頁(日期・新着・搜尋・標籤)/作品詳情頁 統一改版:全寬(高 specificity 版)、自訂欄數、大封面卡片、統一浮層(封面+預覽圖+試看+VR自動偵測+試看網址多重猜測)、sukebei 種子搜尋(含複製磁鏈)、missav 快捷、演員頁側欄精簡、標籤頁分類側欄精簡

Terá de instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

Terá de instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Terá de instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Terá de instalar uma extensão como Tampermonkey para instalar este script.

Terá de instalar uma extensão de gestão de scripts de utilizador para instalar este script.

(Já tenho um gestor de scripts de utilizador, deixe-me instalá-lo!)

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão como Stylus para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

Terá de instalar uma extensão de gestão de estilos de utilizador para instalar este estilo.

(Já tenho um gestor de estilos de utilizador, deixe-me instalá-lo!)

// ==UserScript==
// @name         AvBase Ultimate Enhancer
// @namespace    http://tampermonkey.net/
// @version      4.0.3
// @description  首頁/演員頁/作品列表頁(日期・新着・搜尋・標籤)/作品詳情頁 統一改版:全寬(高 specificity 版)、自訂欄數、大封面卡片、統一浮層(封面+預覽圖+試看+VR自動偵測+試看網址多重猜測)、sukebei 種子搜尋(含複製磁鏈)、missav 快捷、演員頁側欄精簡、標籤頁分類側欄精簡
// @match        https://www.avbase.net/*
// @match        https://avbase.net/*
// @run-at       document-idle
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      sukebei.nyaa.si
// @license      MIT
// ==/UserScript==

/*
 * ============================================================================
 * 維護須知(給未來要修這支腳本的人/AI 看)
 * ----------------------------------------------------------------------------
 * AvBase 是 Next.js 網站,class 大多是 Tailwind 自動產生,改版時很容易全部
 * 跑掉。這支腳本盡量避免死綁一長串 class,而是抓「結構特徵」:
 *
 *   - 作品卡片:辨識依據是卡片內部一定有 `.flex.min-w-0.border-y.border-light`
 *     這個「小圖 + 標題」列,以及一個 `a[href^="/works/"]` 標題連結。
 *
 *   - 全寬:目前發現網站有兩種「限制寬度」的容器 class:`.max-w-7xl`(首頁/
 *     演員頁/作品詳情頁)與 `.cl-container`(/works/date、/works/recent、
 *     /works?q=... 這類列表頁)。兩個都要蓋掉 max-width,將來如果又出現第三
 *     種,同樣加進 GM_addStyle 那段最上面的全寬區塊即可。
 *     這條規則用「重複 class 拉高 specificity」的寫法(例如
 *     `.cl-container.cl-container`),是因為 /works/date、/works/recent、
 *     /works?q=... 這類搜尋結果頁在 SPA 切頁時,Next.js 有時會晚一步才把
 *     該頁專屬的 CSS chunk 插進 <head>,順序排在我們的 GM_addStyle 後面;
 *     雙方都用 !important 時瀏覽器就會比 specificity 而不是比誰後宣告,所以
 *     故意疊 class 把分數拉高,確保不管誰先誰後都是我們贏。
 *
 *   - 封面大圖網址:DMM/FANZA 圖檔命名規則固定「資料夾名 = 檔名去掉 ps/pl/jp」,
 *     所以直接用 cid 組 pl.jpg 網址,不依賴頁面上有沒有現成的大圖連結。
 *
 *   - 試看影片網址 / 預覽圖 / 是否為 VR:全部從 Next.js 內建的
 *     `<script id="__NEXT_DATA__">` 這個 JSON 標籤直接 JSON.parse 出來,
 *     走 props.pageProps.work.products 陣列(每個 product 是不同的販售通路,
 *     各自可能有自己的 sample_movie_url / sample_image_urls)。這比起用正規
 *     表示式在整段 HTML 裡亂抓穩定非常多,是改版後最不容易失效的地方。
 *     如果 Next.js 資料結構改了(例如 work 欄位改名),要修的地方只有
 *     extractWorkInfoFromNextData() 這個函式。
 *     萬一哪天 __NEXT_DATA__ 整個消失(改用別的資料傳遞方式),才需要回頭用
 *     extractSampleVideoUrlFallback() 這類正規表示式硬找,那只是保險用的備援。
 *     另外 __NEXT_DATA__ 給的 sample_movie_url 常常是 null(尤其 duga 來源、
 *     或還沒上架的預售品),但實測發現只要照 DMM 的檔名規則自己組網址,
 *     很多時候影片其實是存在的,只是官方沒把網址寫進資料裡。
 *     buildTrailerUrlCandidates() 就是把目前已知會中的幾種猜測網址準備好,
 *     播放時依序試(跟封面圖的 fallback chain 是同一個精神),全部試過還是
 *     不行才真的代表沒有試看影片。這幾條規則是實測整理出來的,不保證每支
 *     影片都適用,DMM 那邊換路徑規則的話要重新抓包更新這裡。
 *
 *   - VR 判斷:目前用 product_id / 標題裡有沒有出現「VR」「vrkm」「savr」
 *     「urvrsp」等已知字樣來判斷,寫在 isVRProduct()。這是最土炮但最不依賴
 *     頁面結構的作法;如果之後發現誤判,調整這裡的關鍵字即可。
 *
 *   - sukebei 種子搜尋:讀 sukebei.nyaa.si 的 RSS(?page=rss&q=...),解析
 *     <item> 底下 nyaa 命名空間欄位(seeders/leechers/size/infoHash)。
 *     磁力連結是自己用 infoHash 組出來的(sukebei RSS 沒有直接給 magnet
 *     欄位)。如果 sukebei 改了 RSS 結構,要修的地方是 parseSukebeiRss()。
 *
 *   - 標籤頁 (/tags/xxx) 左側「分類」欄位跟演員頁側欄根本是同一套版面(外層
 *     都是那個 4 欄 grid,左欄是 `.lg:col-span-1`),所以用同樣的手法處理:
 *     外層 grid 疊上 avbase-talent-stack 變成上下堆疊,側欄本身再精簡成一排
 *     靠左橫向排列(分類名稱+子標籤展開鈕+編輯鈕+フォロー鈕),這樣就不
 *     會在作品列表左邊留一大塊空白。寫在 rebuildTagSidebar()。
 * ============================================================================
 */

(function () {
  'use strict';

  if (window.__avbaseReforgeLoaded) return;
  window.__avbaseReforgeLoaded = true;

  /* =========================================================
   * 0. 常數
   * ========================================================= */
  const COLS_KEY = 'avbase_reforge_cols_v1';
  const COLS_MIN = 3;
  const COLS_MAX = 12;
  const COLS_DEFAULT = 6;

  const css = `
     /* 1. 強制移除所有外層容器的最大寬度限制 */
     .max-w-7xl,
     .cl-container,
     [class*="max-w-7xl"],
     [class*="max-w-6xl"] {
         max-width: 100% !important;
         width: 100% !important;
     }

     /* 2. 縮減兩側 Margin / Padding 留白 */
     .cl-container,
     nav > div,
     header > div {
         margin-left: 0 !important;
         margin-right: 0 !important;
         padding-left: 12px !important;
         padding-right: 12px !important;
     }

     /* 3. 重新建立卡片自適應網格 (RWD Grid) */
     /* 將原先固定的 2 欄,改為依螢幕寬度自動填滿 (每張卡片最小 280px) */
     .avbase-grid2,
     div[data-avbase-grid] {
         display: grid !important;
         grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important;
         gap: 16px !important;
     }
 `;
  /* =========================================================
   * 0-1. 頁面類型判斷(全部功能的總開關,改版後路徑變了先來這裡改)
   * ========================================================= */
  function isHomePage() {
    return location.pathname === '/' || location.pathname === '';
  }

  function isTalentPage() {
    if (!/^\/talents\//.test(location.pathname)) return false;
    if (/^\/talents\/(recent|popular|edit)(\/|$)/.test(location.pathname)) return false;
    return true;
  }

  // 作品「列表」頁:日期瀏覽 /works/date(/YYYY-MM-DD)、新着 /works/recent、
  // 搜尋結果 /works(?q=...)
  function isWorksListPage() {
    const p = location.pathname;
    if (p === '/works') return true;
    if (p === '/works/date' || p.startsWith('/works/date/')) return true;
    if (p === '/works/recent') return true;
    return false;
  }

  // 標籤頁 /tags/xxx,觀察到跟其他列表頁共用同一套卡片元件
  function isTagPage() {
    return /^\/tags\//.test(location.pathname);
  }

  // 作品「詳情」頁:/works/<maker:CODE> 或 /works/<CODE>,且不是上面的列表頁
  function isWorkDetailPage() {
    const p = location.pathname;
    if (!p.startsWith('/works/')) return false;
    if (isWorksListPage()) return false;
    const rest = p.slice('/works/'.length);
    return rest.length > 0;
  }

  // 這幾種頁面共用「全寬 + 網格化 + 卡片重建」那一套邏輯
  function isCardGridPage() {
    return isHomePage() || isTalentPage() || isWorksListPage() || isTagPage();
  }

    function injectStyle() {
        if (document.getElementById('custom-fullwidth-style')) return;
        const style = document.createElement('style');
        style.id = 'custom-fullwidth-style';
        style.type = 'text/css';
        style.appendChild(document.createTextNode(css));
        (document.head || document.documentElement).appendChild(style);
    }

    // 立即注入 +  DOM 載入後二次確保(防範 SPA 頁面動態渲染覆蓋)
    injectStyle();
    document.addEventListener('DOMContentLoaded', injectStyle);

    // 監聽頁面變更 (適應 Next.js 路由切換)
    const observer = new MutationObserver(injectStyle);
    observer.observe(document.documentElement, { childList: true, subtree: true });
  /* =========================================================
   * 1. 樣式
   * ========================================================= */
  GM_addStyle(`
    :root { --avbase-cols: ${COLS_DEFAULT}; }

    /* ---- 全站全寬:去掉左右大留白(首頁/演員頁/作品詳情頁用 .max-w-7xl,
       /works/date /works/recent /works?q=... 這類列表頁用 .cl-container,
       兩種都要蓋掉。用重複 class 拉高 specificity,確保就算 Next.js 之後
       又插入一份同樣用 !important 的頁面專屬 CSS,我們還是贏 ---- */
    .max-w-7xl, .max-w-7xl.max-w-7xl,
    .cl-container, .cl-container.cl-container, .cl-container.cl-container.cl-container {
      max-width: 100% !important;
    }

    /* ---- 首頁「本日発売の作品 / 最近登録された作品」左右並排容器改成上下堆疊 ---- */
    .avbase-full-stack { display: block !important; }
    .avbase-full-stack > * { width: 100% !important; }
    .avbase-full-stack > * + * { margin-top: 12px; }

    /* ---- 作品卡片網格(首頁 / 演員頁 / 作品列表頁 / 標籤頁 共用)---- */
    .avbase-grid2 {
      display: grid !important;
      grid-template-columns: repeat(var(--avbase-cols), minmax(0, 1fr)) !important;
      gap: 14px !important;
    }
    .avbase-grid2 > .relative { width: 100%; margin: 0 !important; }

    /* ---- 卡片本體:作品代碼/日期 -> 封面(橫向為主) -> 標題(最多三行) -> 演員 ---- */
    .avbase-card2 { display: flex; flex-direction: column; height: 100%; }
    .avbase-meta2 {
      order: 1; padding: 8px 10px; font-size: 11px; color: #9ca3af;
      border-bottom: 1px dashed rgba(148,163,184,.25);
    }
    .avbase-meta2 a { color: inherit; }
    .avbase-cover2 {
      order: 2;
      position: relative;
      width: 100%;
      aspect-ratio: 16 / 10;
      background: #0b1220;
      overflow: hidden;
      cursor: zoom-in;
      display: flex; align-items: center; justify-content: center;
    }
    .avbase-cover2-img { width: 100%; height: 100%; object-fit: contain; display: block; transition: transform .25s ease; }
    .avbase-cover2:hover .avbase-cover2-img { transform: scale(1.03); }
    /* 註:故意不做放大鏡圖示,滑鼠移上去 cursor 本身就是 zoom-in 了 */
    .avbase-title2 {
      order: 3;
      display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
      overflow: hidden; padding: 10px 10px 4px; font-weight: 600; font-size: 13px; line-height: 1.4;
      color: inherit; text-decoration: none;
    }
    .avbase-title2:hover { text-decoration: underline; }
    .avbase-actors2 { order: 4; margin-top: auto; display: flex; flex-wrap: wrap; gap: 6px; padding: 4px 10px 10px; }

    /* ---- 設定面板 ---- */
    .avbase-settings-btn {
      position: fixed; right: 18px; bottom: 18px; z-index: 99998;
      width: 44px; height: 44px; border-radius: 999px;
      background: #111827; color: #fff; border: 1px solid #374151; font-size: 18px; cursor: pointer;
      box-shadow: 0 4px 14px rgba(0,0,0,.4);
    }
    .avbase-settings-panel {
      position: fixed; right: 18px; bottom: 70px; z-index: 99998;
      background: #111827; border: 1px solid #374151; border-radius: 10px;
      padding: 12px 14px; display: none; min-width: 210px;
      box-shadow: 0 8px 24px rgba(0,0,0,.5);
    }
    .avbase-settings-panel.show { display: block; }
    .avbase-settings-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
    .avbase-settings-row label { color: #cbd5e1; font-size: 13px; }
    .avbase-settings-row input {
      width: 64px; background: #0b1220; color: #e5e7eb; border: 1px solid #374151;
      border-radius: 6px; padding: 4px 8px;
    }
    .avbase-settings-hint { margin-top: 6px; color: #64748b; font-size: 11px; line-height: 1.5; }

    /* =========================================================
     * 浮層(Lightbox / Gallery)
     * 版面:左=目前作品自己的預覽圖(固定5格視窗,淡入淡出)/中=純圖片(或播放器)
     * /右=標題演員按鈕。下方=所有作品的封面列(切換「作品」用)。
     * 縮小視窗時:左側預覽圖搬頂部橫排、右側資訊搬到圖片下方。
     * ========================================================= */
    .avbase-overlay {
      position: fixed; inset: 0; background: rgba(0,0,0,.94); z-index: 100000;
      display: flex; flex-direction: column; opacity: 0; transition: opacity .2s ease;
    }
    .avbase-overlay.show { opacity: 1; }
    .avbase-ov-close {
      position: absolute; top: 12px; right: 12px; z-index: 20;
      width: 40px; height: 40px; border-radius: 999px; border: none;
      background: rgba(0,0,0,.55); color: #fff; font-size: 18px; cursor: pointer;
    }
    .avbase-ov-close:hover { background: rgba(0,0,0,.85); }

    .avbase-ov-shell { flex: 1; min-height: 0; display: flex; flex-direction: row; }

    /* 左:目前作品自己的封面+預覽圖,固定顯示 5 格,目前那張置中並放大 */
    .avbase-ov-thumbside { flex: 0 0 100px; border-right: 1px solid rgba(255,255,255,.08); background: rgba(255,255,255,.02); }
    .avbase-ov-thumbside-inner {
      height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
      gap: 10px; padding: 12px 8px; overflow: hidden;
    }
    .avbase-ov-thumb {
      flex: 0 0 auto; width: 72px; height: 72px; border-radius: 6px; overflow: hidden; cursor: pointer;
      opacity: .5; border: 2px solid transparent; background: #0b1220;
      display: flex; align-items: center; justify-content: center;
      transition: opacity .3s ease, transform .3s ease;
    }
    .avbase-ov-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .avbase-ov-thumb.active { opacity: 1; border-color: #60a5fa; transform: scale(1.15); }
    /* 換圖當下暫時套用這個 class 讓它先「淡出+往下位移」,拿掉後 CSS transition 會自動補完淡入+歸位的動畫 */
    .avbase-ov-thumb.avbase-ov-thumb-anim { opacity: 0 !important; transform: translateY(10px) scale(.85) !important; }

    /* 中:純圖片/播放器,左右鍵切換「作品」 */
    .avbase-ov-main { flex: 1; min-width: 0; position: relative; display: flex; align-items: center; justify-content: center; padding: 16px; }
    .avbase-ov-img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 6px; transition: opacity .2s ease; }
    .avbase-ov-player { display: none; width: 100%; max-width: 960px; }
    .avbase-ov-player video.avbase-trailer-video { width: 100%; max-height: 70vh; border-radius: 6px; background: #000; display: block; }
    .avbase-vr-wrap { width: 100%; position: relative; }
    .avbase-vr-wrap a-scene { width: 100%; height: 60vh; border-radius: 6px; overflow: hidden; display: block; }
    /* 來源 video 不能用 display:none,否則部分瀏覽器停止解碼,改用縮到 1px + opacity:0 */
    .avbase-vr-source-video { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }

    .avbase-ov-prev, .avbase-ov-next {
      position: absolute; top: 50%; transform: translateY(-50%);
      background: rgba(0,0,0,.5); color: #fff; border: none; border-radius: 999px;
      width: 44px; height: 44px; font-size: 20px; cursor: pointer; z-index: 3;
    }
    .avbase-ov-prev { left: 8px; }
    .avbase-ov-next { right: 8px; }
    .avbase-ov-prev:hover, .avbase-ov-next:hover { background: rgba(0,0,0,.8); }

    /* 右:標題/演員/操作按鈕 */
    .avbase-ov-side {
      flex: 0 0 320px; max-width: 320px; border-left: 1px solid rgba(255,255,255,.08);
      background: rgba(255,255,255,.02); overflow-y: auto; padding: 16px;
      display: flex; flex-direction: column; gap: 12px;
    }
    .avbase-ov-title { color: #f3f4f6; font-size: 14px; line-height: 1.5; font-weight: 600; }
    .avbase-ov-actors { display: flex; flex-wrap: wrap; gap: 6px; }
    .avbase-ov-controls { display: flex; flex-direction: column; gap: 8px; margin-top: auto; padding-top: 8px; }
    .avbase-ov-controls button, .avbase-ov-controls a {
      width: 100%; box-sizing: border-box; text-align: center;
      background: #1f2937; color: #e5e7eb; border: 1px solid #374151; border-radius: 8px;
      padding: 10px 12px; font-size: 13px; cursor: pointer; text-decoration: none;
    }
    .avbase-ov-controls button:hover, .avbase-ov-controls a:hover { background: #374151; }
    .avbase-ov-controls button:disabled { opacity: .5; cursor: default; }

    /* 下:所有「作品」的封面列,點了切換目前顯示哪一部作品(跟左側「同一部作品的預覽圖」是兩回事)*/
    .avbase-ov-bottomthumbs {
      flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 10px 16px;
      overflow-x: auto; background: rgba(255,255,255,.03); border-top: 1px solid rgba(255,255,255,.08);
    }
    .avbase-ov-bottomthumb {
      flex: 0 0 auto; width: auto; height: 86px; border-radius: 6px; overflow: hidden; cursor: pointer;
      opacity: .45; border: 2px solid transparent; background: #0b1220;
      display: flex; align-items: center; justify-content: center; transition: opacity .2s, border-color .2s;
    }
    .avbase-ov-bottomthumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .avbase-ov-bottomthumb:hover { opacity: .8; }
    .avbase-ov-bottomthumb.active { opacity: 1; border-color: #60a5fa; }

    @media (max-width: 860px) {
      .avbase-ov-shell { flex-direction: column; }
      .avbase-ov-thumbside { order: 1; flex: 0 0 auto; width: 100%; height: 100px; border-right: none; border-bottom: 1px solid rgba(255,255,255,.08); }
      .avbase-ov-thumbside-inner { flex-direction: row; }
      .avbase-ov-main { order: 2; flex: 1 1 auto; min-height: 200px; }
      .avbase-ov-side { order: 3; flex: 0 0 auto; width: 100%; max-width: none; max-height: 40vh; border-left: none; border-top: 1px solid rgba(255,255,255,.08); }
      .avbase-ov-controls { flex-direction: row; flex-wrap: wrap; margin-top: 0; }
      .avbase-ov-controls button, .avbase-ov-controls a { width: auto; flex: 1 1 120px; }
    }

    /* ---- Sukebei 種子搜尋(浮層中再開一層) ---- */
    .avbase-sukebei-overlay {
      position: fixed; inset: 0; background: rgba(0,0,0,.7); z-index: 100010;
      display: flex; align-items: center; justify-content: center; padding: 20px;
      opacity: 0; transition: opacity .2s ease;
    }
    .avbase-sukebei-overlay.show { opacity: 1; }
    .avbase-sukebei-card {
      background: #111827; border: 1px solid #374151; border-radius: 12px;
      width: 100%; max-width: 640px; max-height: 80vh; display: flex; flex-direction: column;
      box-shadow: 0 12px 40px rgba(0,0,0,.5);
    }
    .avbase-sukebei-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #374151; color: #f3f4f6; }
    .avbase-sukebei-close { width: 28px; height: 28px; border-radius: 999px; border: none; background: rgba(255,255,255,.1); color: #fff; cursor: pointer; }
    .avbase-sukebei-search-row { display: flex; gap: 8px; padding: 12px 16px; }
    .avbase-sukebei-input { flex: 1; background: #0b1220; color: #e5e7eb; border: 1px solid #374151; border-radius: 6px; padding: 8px 10px; }
    .avbase-sukebei-go { background: #2563eb; color: #fff; border: none; border-radius: 6px; padding: 8px 14px; cursor: pointer; }
    .avbase-sukebei-go:hover { background: #1d4ed8; }
    .avbase-sukebei-results { overflow-y: auto; padding: 0 16px 16px; display: flex; flex-direction: column; gap: 8px; }
    .avbase-sukebei-hint { color: #9ca3af; font-size: 13px; text-align: center; padding: 20px 0; }
    .avbase-sukebei-item { background: #1f2937; border: 1px solid #374151; border-radius: 8px; padding: 10px 12px; color: #e5e7eb; }
    .avbase-sukebei-item-title { font-size: 13px; line-height: 1.4; word-break: break-all; }
    .avbase-sukebei-item-meta { margin-top: 4px; display: flex; gap: 10px; font-size: 11px; color: #9ca3af; flex-wrap: wrap; }
    .avbase-sukebei-item-actions { margin-top: 8px; display: flex; gap: 8px; }
    .avbase-sukebei-item-actions a, .avbase-sukebei-item-actions button {
      background: #263142; color: #e5e7eb; border: 1px solid #374151; border-radius: 6px;
      padding: 6px 10px; font-size: 12px; cursor: pointer; text-decoration: none;
    }
    .avbase-sukebei-item-actions a:hover, .avbase-sukebei-item-actions button:hover { background: #33415a; }

    /* ---- 演員頁:整塊資訊改成上下堆疊(原本左欄下方留白的問題)
       標籤頁的分類側欄也共用這個 class ---- */
    .avbase-talent-stack { display: flex !important; flex-direction: column !important; gap: 16px !important; }
    .avbase-talent-stack > * { width: 100% !important; }

    /* ---- 演員頁:左側精簡資訊(置中) ---- */
    .avbase-talent-mini { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 10px; padding: 16px 8px; }
    .avbase-talent-avatar-btn {
      border: none; background: transparent; padding: 0; cursor: pointer;
      border-radius: 999px; overflow: hidden; width: 128px; height: 128px;
      transition: transform .2s ease, box-shadow .2s ease;
    }
    .avbase-talent-avatar-btn:hover { transform: scale(1.03); box-shadow: 0 4px 16px rgba(0,0,0,.25); }
    .avbase-talent-avatar-btn img { width: 100%; height: 100%; object-fit: cover; display: block; }
    .avbase-talent-name { font-size: 20px; font-weight: 700; }
    .avbase-talent-follow, .avbase-talent-social { display: flex; justify-content: center; }
    .avbase-talent-social > div { display: flex; justify-content: center; gap: 12px; }

    /* ---- 演員頁:頭像點擊後的完整資料浮卡 ---- */
    .avbase-talent-overlay {
      position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 100000;
      display: flex; align-items: center; justify-content: center; padding: 24px;
      opacity: 0; transition: opacity .2s ease;
    }
    .avbase-talent-overlay.show { opacity: 1; }
    .avbase-talent-card {
      position: relative; background: var(--background, #111827); color: inherit;
      border-radius: 12px; max-width: 480px; width: 100%; max-height: 85vh; overflow-y: auto;
      padding: 20px; box-shadow: 0 12px 40px rgba(0,0,0,.5);
    }
    .avbase-talent-card-close {
      position: absolute; top: 10px; right: 10px; width: 32px; height: 32px;
      border-radius: 999px; border: none; background: rgba(128,128,128,.25); cursor: pointer; font-size: 14px;
    }
    .avbase-talent-card-body { text-align: center; }
    .avbase-talent-card-body .text-center.grid.grid-cols-2 { grid-template-columns: 1fr; }

    /* ---- 標籤頁:分類側欄精簡(合併到作品列上方,靠左橫向排列,不像演員頁那樣置中)---- */
    .avbase-tag-mini { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 10px 4px 4px; }
    .avbase-tag-mini-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
  `);

  /* =========================================================
   * 2. 共用工具函式
   * ========================================================= */
  function escapeHtml(str) {
    return String(str).replace(/[&<>"']/g, ch => ({
      '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
    }[ch]));
  }

  function debounce(fn, wait) {
    let t;
    return (...args) => {
      clearTimeout(t);
      t = setTimeout(() => fn(...args), wait);
    };
  }

  function dedupe(arr) {
    return Array.from(new Set(arr.filter(Boolean)));
  }

  // 幫 <img> 掛上「這張失敗就換下一個網址」的重試鏈
  function withFallbackChain(imgEl, urls) {
    if (!imgEl) return;
    if (!urls || !urls.length) return;
    let i = 0;
    imgEl.onerror = () => {
      i++;
      if (i < urls.length) imgEl.src = urls[i];
      else imgEl.onerror = null;
    };
    imgEl.src = urls[0];
  }

  // AV 圖檔命名規則固定為 <資料夾>/<資料夾><ps|pl|jp|jm>.jpg,取出資料夾名稱 (cid)
  function extractCid(pathname) {
    const decoded = decodeURIComponent(pathname);
    let m = decoded.match(/\/([A-Za-z0-9_]+)\/\1(?:ps|pl|jp|jm)(?:-\d+)?\.jpg$/i);
    if (m) return m[1];
    m = decoded.match(/\/([A-Za-z0-9_]+)\/[^/]+\.jpg$/i);
    if (m) return m[1];
    return null;
  }

  function getDmmCid(url) {
    try {
      const u = new URL(url, location.href);
      if (!/(^|\.)dmm\.co\.jp$/i.test(u.hostname)) return null;
      return extractCid(u.pathname);
    } catch (e) {
      return null;
    }
  }

  // 卡片頂部大封面:優先用 pics.dmm.co.jp 的 pl.jpg
  function buildCoverCandidates(smallSrc) {
    const list = [];
    const cid = getDmmCid(smallSrc);
    if (cid) {
      list.push(`https://pics.dmm.co.jp/digital/video/${cid}/${cid}pl.jpg`);
      if (/\/mono\/movie\//i.test(smallSrc)) {
        list.push(smallSrc.replace(/ps(?:-\d+)?\.jpg(\?.*)?$/i, 'pl.jpg$1'));
      }
      list.push(`https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/${cid}/${cid}pl.jpg`);
    } else if (/ps(?:-\d+)?\.jpg(\?.*)?$/i.test(smallSrc)) {
      list.push(smallSrc.replace(/ps(?:-\d+)?\.jpg(\?.*)?$/i, 'pl.jpg$1'));
    }
    list.push(smallSrc);
    return dedupe(list);
  }

  // 浮層大圖:優先用 awsimgsrc.dmm.co.jp 的 pl.jpg
  function buildLightboxCandidates(smallSrc) {
    const list = [];
    const cid = getDmmCid(smallSrc);
    if (cid) {
      list.push(`https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/${cid}/${cid}pl.jpg`);
      list.push(`https://pics.dmm.co.jp/digital/video/${cid}/${cid}pl.jpg`);
    }
    list.push(smallSrc);
    return dedupe(list);
  }

  // 從 /works/attackers:ADN-801 這類網址取出 "ADN-801",給 sukebei / missav 搜尋用
  function extractWorkCode(workHref) {
    try {
      const last = decodeURIComponent((workHref || '').split('/').filter(Boolean).pop() || '');
      const parts = last.split(':');
      return parts[parts.length - 1] || last;
    } catch (e) {
      return workHref || '';
    }
  }

  // 統一組出「一個畫廊項目」的資料結構,卡片格、作品詳情頁封面、關聯作品都共用這個
  function buildGalleryItem({ smallSrc, workHref, title, actorsHtml, el }) {
    return {
      workHref,
      title: title || '',
      actorsHtml: actorsHtml || '',
      cid: getDmmCid(smallSrc),
      coverCandidates: buildCoverCandidates(smallSrc),
      lightboxCandidates: buildLightboxCandidates(smallSrc),
      el: el || null,
      isCurrentPage: false,
      _detailsPromise: null // fetchWorkDetails() 用的快取,見該函式
    };
  }

  /* =========================================================
   * 3. 每行顯示數量設定
   * ========================================================= */
  function getStoredCols() {
    const v = parseInt(localStorage.getItem(COLS_KEY), 10);
    return Number.isFinite(v) && v >= COLS_MIN && v <= COLS_MAX ? v : COLS_DEFAULT;
  }
  function setStoredCols(v) {
    const c = Math.max(COLS_MIN, Math.min(COLS_MAX, parseInt(v, 10) || COLS_DEFAULT));
    localStorage.setItem(COLS_KEY, String(c));
    return c;
  }
  function effectiveCols() {
    const stored = getStoredCols();
    const w = window.innerWidth;
    if (w < 640) return Math.min(stored, 2);
    if (w < 1024) return Math.min(stored, 3);
    if (w < 1440) return Math.min(stored, 5);
    return stored;
  }
  function applyColumns() {
    document.documentElement.style.setProperty('--avbase-cols', String(effectiveCols()));
  }

  function buildSettingsPanel() {
    if (document.querySelector('.avbase-settings-btn')) return;

    const btn = document.createElement('button');
    btn.type = 'button';
    btn.className = 'avbase-settings-btn';
    btn.textContent = '⚙';
    btn.title = 'AvBase 版面設定';

    const panel = document.createElement('div');
    panel.className = 'avbase-settings-panel';
    panel.innerHTML = `
      <div class="avbase-settings-row">
        <label>每行顯示數量</label>
        <input type="number" min="${COLS_MIN}" max="${COLS_MAX}" step="1" class="avbase-cols-input">
      </div>
      <div class="avbase-settings-hint">小螢幕會自動再縮減欄數(適用於首頁/演員頁/作品列表頁/標籤頁)</div>
    `;

    document.body.appendChild(btn);
    document.body.appendChild(panel);

    const input = panel.querySelector('.avbase-cols-input');
    input.value = String(getStoredCols());
    input.addEventListener('change', () => {
      setStoredCols(input.value);
      input.value = String(getStoredCols());
      applyColumns();
    });

    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      panel.classList.toggle('show');
    });
    document.addEventListener('click', (e) => {
      if (!panel.contains(e.target) && e.target !== btn) panel.classList.remove('show');
    });
  }

  /* =========================================================
   * 4. 版面重排:全寬 + 網格化(首頁 / 演員頁 / 作品列表頁 / 標籤頁)
   * ========================================================= */
  function normalizeLayout() {
    if (!isCardGridPage()) return;

    // 觀察到的卡片格 class 組合有 gap-2(首頁)跟 gap-4(其他頁)兩種,都吃
    // ※若某頁版面跟這裡假設的不一樣,卡片就不會被抓到,屬於安全失敗(原生版面照常顯示)
    const grids = document.querySelectorAll('.grid.sm\\:grid-cols-2.gap-2, .grid.sm\\:grid-cols-2.gap-4');
    grids.forEach(grid => {
      if (grid.dataset.avbaseGrid === '1') return;
      const hasCards = grid.querySelector('.bg-background.border.border-light.rounded-lg.overflow-hidden');
      if (!hasCards) return;

      grid.classList.add('avbase-grid2');
      grid.dataset.avbaseGrid = '1';

      // 首頁「本日発売の作品」「最近登録された作品」左右並排的外層改成上下堆疊、各自全寬
      const twoColWrap = grid.closest('.grid.gap-3.lg\\:grid-cols-2');
      if (twoColWrap && twoColWrap.dataset.avbaseStack !== '1') {
        twoColWrap.classList.add('avbase-full-stack');
        twoColWrap.dataset.avbaseStack = '1';
      }
    });
  }

  /* =========================================================
   * 5. 卡片重建:作品代碼/日期 -> 大封面 -> 標題 -> 演員
   * ========================================================= */
  function rebuildCard(cardEl) {
    if (cardEl.dataset.avbaseDone === '1') return;

    const metaHeader = cardEl.querySelector(':scope > .bg-muted.py-2.flex.flex-col.gap-2');
    const mediaRow = cardEl.querySelector(':scope > .flex.min-w-0.border-y.border-light');
    const actorsWrap = cardEl.querySelector(':scope > .relative.min-h-10.bg-background.p-2 .flex.flex-wrap.gap-2');
    if (!mediaRow) return;

    const smallImg = mediaRow.querySelector('img');
    const titleLink = mediaRow.querySelector('a[href^="/works/"]');
    if (!smallImg || !titleLink) return;

    const smallSrc = smallImg.getAttribute('src') || '';
    const altText = smallImg.getAttribute('alt') || titleLink.textContent.trim();
    const workHref = titleLink.getAttribute('href') || '#';
    const titleText = titleLink.textContent.trim();
    const metaHtml = metaHeader ? metaHeader.innerHTML : '';
    const actorsHtml = actorsWrap ? actorsWrap.innerHTML : '';

    const item = buildGalleryItem({ smallSrc, workHref, title: titleText, actorsHtml, el: cardEl });

    cardEl.innerHTML = `
      ${metaHtml ? `<div class="avbase-meta2">${metaHtml}</div>` : ''}
      <div class="avbase-cover2">
        <img class="avbase-cover2-img" alt="${escapeHtml(altText)}" loading="lazy">
      </div>
      <a class="avbase-title2" href="${escapeHtml(workHref)}">${escapeHtml(titleText)}</a>
      ${actorsHtml ? `<div class="avbase-actors2">${actorsHtml}</div>` : ''}
    `;

    const coverImg = cardEl.querySelector('.avbase-cover2-img');
    withFallbackChain(coverImg, item.coverCandidates);

    cardEl.classList.add('avbase-card2');
    cardEl.dataset.avbaseDone = '1';
    cardEl._avbaseItem = item;

    cardEl.querySelector('.avbase-cover2').addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      openGalleryFromCard(cardEl);
    });
  }

  function rebuildAllCards() {
    if (!isCardGridPage()) return;
    document
      .querySelectorAll('.avbase-grid2 .bg-background.border.border-light.rounded-lg.overflow-hidden')
      .forEach(card => {
        try { rebuildCard(card); } catch (e) { console.warn('[AvBase Reforge] rebuildCard failed', e); }
      });
  }

  /* =========================================================
   * 6. 從 __NEXT_DATA__ 抓試看影片 / VR 試看影片 / 預覽圖
   * ========================================================= */
  // 把整段 HTML 裡的 <script id="__NEXT_DATA__">...</script> 挖出來 JSON.parse
  function extractNextDataFromHtml(html) {
    const m = html.match(/<script id="__NEXT_DATA__"[^>]*>([\s\S]*?)<\/script>/);
    if (!m) return null;
    try { return JSON.parse(m[1]); } catch (e) { return null; }
  }

  // 判斷一個 product 是不是 VR 版本:看 product_id / 標題 / 作品標題裡有沒有出現常見 VR 關鍵字
  // ※誤判時(例如新的 VR 廠牌代碼沒列到)調整這裡的關鍵字即可
  function isVRProduct(product, work) {
    const hay = [
      product && product.product_id,
      product && product.title,
      work && work.title
    ].filter(Boolean).join(' ');
    return /【VR】|\bVR\b|vrkm|savr|urvrsp|bibivr|ipvr|atvr|juvr|3dsvr|nkkvr/i.test(hay);
  }

  // 試看影片網址「猜測」規則:__NEXT_DATA__ 給的 sample_movie_url 常常是 null,
  // 但實測發現只要照 DMM 檔名規則自己組網址,很多時候影片其實是存在的,只是
  // 官方沒把網址寫進資料裡。這裡回傳一組「候選網址陣列」,播放時依序試,
  // 全部失敗才代表真的沒有試看影片。
  // ※這是實測整理出來的規則,不保證每支影片都適用;DMM 換路徑規則的話要
  //   重新抓包更新這裡。
  function buildTrailerUrlCandidates(code, isVR) {
    if (!code) return [];
    const prefix = code.substr(0, 3);
    const list = [];

    // 部分較舊番號(1f 開頭但不是 1fsvss、或 1m 開頭)在部分鏡像站上,長影片檔名
    // 把番號中間的補零拿掉了(第 7~8 碼),所以另外組一份「去掉這兩碼」的網址
    if ((code.startsWith('1f') && !code.startsWith('1fsvss')) || code.startsWith('1m')) {
      const shortCode = code.substr(0, 6) + code.substr(8, 3);
      list.push(`https://videos.vpdmm.cc/litevideo/freepv/${code.charAt(0)}/${code.substr(0, 3)}/${shortCode}/${shortCode}4k.mp4`);
    }

    list.push(`https://cc3001.dmm.com/${isVR ? 'vrsample' : 'litevideo/freepv'}/${code.charAt(0)}/${prefix}/${code}/${code}${isVR ? 'vrlite' : 'hhb'}.mp4`);
    list.push(`https://cc3001.dmm.com/litevideo/freepv/${code.charAt(0)}/${prefix}/${code}/${code}_dmb_w.mp4`);

    return list;
  }

  // 從已經 JSON.parse 好的 __NEXT_DATA__ 物件,整理出:一般試看網址/VR試看網址/
  // 各自的候選網址陣列(官方網址 + 猜測網址)/預覽圖列表
  function extractWorkInfoFromNextData(nextData) {
    const work = nextData && nextData.props && nextData.props.pageProps && nextData.props.pageProps.work;
    const products = (work && work.products) || [];
    let normalTrailerUrl = null;
    let vrTrailerUrl = null;
    let normalTrailerCode = null;
    let vrTrailerCode = null;
    let sampleImages = [];

    products.forEach(p => {
      if (!p) return;
      const vr = isVRProduct(p, work);
      if (p.sample_movie_url) {
        if (vr && !vrTrailerUrl) vrTrailerUrl = p.sample_movie_url;
        if (!vr && !normalTrailerUrl) normalTrailerUrl = p.sample_movie_url;
      }
      // 就算這個 product 沒有現成的 sample_movie_url,也先記下它的番號,
      // 待會 buildTrailerUrlCandidates() 要用番號去猜網址
      if (vr && !vrTrailerCode && p.product_id) vrTrailerCode = p.product_id;
      if (!vr && !normalTrailerCode && p.product_id) normalTrailerCode = p.product_id;

      if (!sampleImages.length && Array.isArray(p.sample_image_urls) && p.sample_image_urls.length) {
        sampleImages = p.sample_image_urls
          .map(s => ({ s: s && s.s, l: (s && s.l) || (s && s.s) }))
          .filter(s => s.s || s.l);
      }
    });

    const normalTrailerCandidates = dedupe([normalTrailerUrl].concat(buildTrailerUrlCandidates(normalTrailerCode, false)));
    const vrTrailerCandidates = dedupe([vrTrailerUrl].concat(buildTrailerUrlCandidates(vrTrailerCode, true)));

    return { normalTrailerUrl, vrTrailerUrl, normalTrailerCandidates, vrTrailerCandidates, sampleImages };
  }

  // 備援:__NEXT_DATA__ 抓不到時,用正規表示式硬找 sample_movie_url / sample_image_urls
  // ※只在 JSON.parse 失敗時才會用到,正常情況不會走這條路
  function extractSampleVideoUrlFallback(html) {
    const patterns = [
      /"sample_movie_url"\s*:\s*"([^"]+)"/i,
      /<video[^>]+src="([^"]+?\.mp4[^"]*)"/i,
      /(https?:\\\/\\\/[^"'<>\s]+?\.mp4)/i,
      /(https?:\/\/[^"'<>\s]+?\.mp4)/i
    ];
    for (const re of patterns) {
      const m = html.match(re);
      if (m && m[1] && m[1] !== 'null') return m[1].replace(/\\\//g, '/');
    }
    return null;
  }
  function extractSampleImageUrlsFallback(html) {
    const m = html.match(/"sample_image_urls"\s*:\s*(\[[\s\S]*?\])/);
    if (!m) return [];
    try {
      const arr = JSON.parse(m[1].replace(/\\\//g, '/'));
      return arr.map(o => ({ s: o && o.s, l: (o && o.l) || (o && o.s) })).filter(o => o.s || o.l);
    } catch (e) {
      return [];
    }
  }

  // 作品詳情頁畫面上實際的縮圖列,當 __NEXT_DATA__ 抓不到預覽圖時的最後備援
  // 命名規則跟封面一樣:<cid>-<N>.jpg 或 <cid>jp-<N>.jpg
  function findWorkDetailSampleImagesFromDom(cid) {
    if (!cid) return [];
    const re = new RegExp(cid + '(?:jp)?-(\\d+)\\.jpg', 'i');
    const imgs = Array.from(document.querySelectorAll(`img[src*="${cid}"]`));
    const seen = new Map();
    imgs.forEach(img => {
      const src = img.getAttribute('src') || '';
      const m = src.match(re);
      if (m) {
        const n = parseInt(m[1], 10);
        if (!seen.has(n)) seen.set(n, src);
      }
    });
    return Array.from(seen.entries()).sort((a, b) => a[0] - b[0]).map(([, src]) => src);
  }

  // 抓單一作品的「一般試看候選網址/VR試看候選網址/預覽圖列表」,同一個作品只查一次(快取在 item 上)
  function fetchWorkDetails(it) {
    if (it._detailsPromise) return it._detailsPromise;

    it._detailsPromise = (async () => {
      let nextData = null;
      let html = null;

      if (it.isCurrentPage) {
        const el = document.getElementById('__NEXT_DATA__');
        if (el) { try { nextData = JSON.parse(el.textContent); } catch (e) { nextData = null; } }
      } else {
        try {
          const res = await fetch(it.workHref, { credentials: 'same-origin' });
          if (res.ok) {
            html = await res.text();
            nextData = extractNextDataFromHtml(html);
          }
        } catch (e) {
          console.warn('[AvBase Reforge] 抓取作品頁失敗:', it.workHref, e);
        }
      }

      let details = {
        normalTrailerUrl: null,
        vrTrailerUrl: null,
        normalTrailerCandidates: [],
        vrTrailerCandidates: [],
        sampleImages: []
      };
      if (nextData) {
        details = extractWorkInfoFromNextData(nextData);
      } else if (html) {
        // __NEXT_DATA__ 解析失敗時的備援路徑,沒有番號可以猜網址,只能用正規表示式硬找的那一條
        const fallbackUrl = extractSampleVideoUrlFallback(html);
        details.normalTrailerUrl = fallbackUrl;
        details.normalTrailerCandidates = dedupe([fallbackUrl]);
        details.vrTrailerCandidates = [];
        details.sampleImages = extractSampleImageUrlsFallback(html);
      }

      if (it.isCurrentPage && !details.sampleImages.length && it.cid) {
        details.sampleImages = findWorkDetailSampleImagesFromDom(it.cid).map(src => ({ s: src, l: src }));
      }

      return details;
    })();

    return it._detailsPromise;
  }

  /* =========================================================
   * 7. 浮層(封面/預覽圖 + 試看播放 + VR 自動偵測 + sukebei/missav 快捷)
   * ========================================================= */
  function openGalleryFromCard(cardEl) {
    // 收集同一個 avbase-grid2 網格內所有已重建的卡片,一起放進同一個浮層瀏覽
    const grid = cardEl.closest('.avbase-grid2');
    const scope = grid ? grid.querySelectorAll('.avbase-card2') : document.querySelectorAll('.avbase-grid2 .avbase-card2');
    const items = Array.from(scope).map(c => c._avbaseItem).filter(Boolean);
    const idx = items.findIndex(it => it.el === cardEl);
    openGallery(items, idx >= 0 ? idx : 0);
  }

  // items:這次浮層可以左右切換的「作品」清單
  // startIndex:一開始顯示 items 裡的第幾個作品
  // initialPreviewIndex:只有第一次顯示那個作品時,要直接跳到它自己第幾張預覽圖
  //   (目前只有「作品詳情頁點下面縮圖列」會用到這個參數,其餘情況都是 0=封面)
  function openGallery(items, startIndex, initialPreviewIndex) {
    if (!items.length) return;

    let index = startIndex;
    let vrMode = false;
    let previewList = [];   // 目前這部作品自己的圖(封面 + 預覽圖),給左側視窗 + 上下鍵切換
    let previewIndex = 0;
    let requestToken = 0;   // 防止「切作品切太快,舊的 fetch 結果蓋掉新的畫面」

    const overlay = document.createElement('div');
    overlay.className = 'avbase-overlay';
    overlay.innerHTML = `
      <button type="button" class="avbase-ov-close" aria-label="關閉">✕</button>
      <div class="avbase-ov-shell">
        <div class="avbase-ov-thumbside"><div class="avbase-ov-thumbside-inner"></div></div>
        <div class="avbase-ov-main">
          <button type="button" class="avbase-ov-prev" aria-label="上一個作品">&#10094;</button>
          <img class="avbase-ov-img" alt="">
          <div class="avbase-ov-player"></div>
          <button type="button" class="avbase-ov-next" aria-label="下一個作品">&#10095;</button>
        </div>
        <div class="avbase-ov-side">
          <div class="avbase-ov-title"></div>
          <div class="avbase-ov-actors"></div>
          <div class="avbase-ov-controls">
            <button type="button" class="avbase-ov-play">▶ 播放預告</button>
            <button type="button" class="avbase-ov-vr">🕶 VR 模式:關</button>
            <button type="button" class="avbase-ov-sukebei">🔍 搜尋種子 (sukebei)</button>
            <a class="avbase-ov-missav" target="_blank" rel="noopener">missav.ai ↗</a>
            <a class="avbase-ov-open" target="_blank" rel="noopener">前往作品頁 ↗</a>
          </div>
        </div>
      </div>
      <div class="avbase-ov-bottomthumbs"></div>
    `;
    document.body.appendChild(overlay);
    document.body.style.overflow = 'hidden';

    const shellEl = overlay.querySelector('.avbase-ov-shell');
    const thumbsideEl = overlay.querySelector('.avbase-ov-thumbside');
    const thumbsideInner = overlay.querySelector('.avbase-ov-thumbside-inner');
    const mainEl = overlay.querySelector('.avbase-ov-main');
    const sideEl = overlay.querySelector('.avbase-ov-side');
    const imgEl = overlay.querySelector('.avbase-ov-img');
    const playerEl = overlay.querySelector('.avbase-ov-player');
    const playBtn = overlay.querySelector('.avbase-ov-play');
    const vrBtn = overlay.querySelector('.avbase-ov-vr');
    const sukebeiBtn = overlay.querySelector('.avbase-ov-sukebei');
    const missavLink = overlay.querySelector('.avbase-ov-missav');
    const openLink = overlay.querySelector('.avbase-ov-open');
    const titleEl = overlay.querySelector('.avbase-ov-title');
    const actorsEl = overlay.querySelector('.avbase-ov-actors');
    const prevBtn = overlay.querySelector('.avbase-ov-prev');
    const nextBtn = overlay.querySelector('.avbase-ov-next');
    const closeBtn = overlay.querySelector('.avbase-ov-close');
    const bottomthumbsEl = overlay.querySelector('.avbase-ov-bottomthumbs');

    // ---- 左側「這部作品自己的預覽圖」:固定 5 個格子,永遠只更新內容,不整批重建 DOM ----
    const THUMBSIDE_SLOTS = 5;
    const slotEls = [];
    for (let s = 0; s < THUMBSIDE_SLOTS; s++) {
      const el = document.createElement('div');
      el.className = 'avbase-ov-thumb';
      el.innerHTML = '<img loading="lazy">';
      thumbsideInner.appendChild(el);
      slotEls.push(el);
    }

    function renderThumbside() {
      const total = previewList.length;
      if (!total) { slotEls.forEach(el => { el.style.visibility = 'hidden'; }); return; }

      const windowSize = Math.min(THUMBSIDE_SLOTS, total);
      const start = total <= THUMBSIDE_SLOTS ? 0 : Math.min(Math.max(previewIndex - 2, 0), total - THUMBSIDE_SLOTS);

      slotEls.forEach((el, slotPos) => {
        const dataIndex = start + slotPos;
        if (slotPos >= windowSize) { el.style.visibility = 'hidden'; return; }
        el.style.visibility = '';

        const entry = previewList[dataIndex];
        const img = el.querySelector('img');
        const candidates = [entry.thumbSrc].filter(Boolean).concat(entry.candidates || []);

        // 觸發「淡出+下移」再拿掉,靠 CSS transition 自動補完「淡入+歸位」的動畫
        el.classList.add('avbase-ov-thumb-anim');
        withFallbackChain(img, candidates);
        el.classList.toggle('active', dataIndex === previewIndex);
        el.onclick = () => showImage(dataIndex);
        requestAnimationFrame(() => {
          requestAnimationFrame(() => el.classList.remove('avbase-ov-thumb-anim'));
        });
      });
    }

    // ---- 中間主圖:切換「目前這張是第幾張預覽圖」,上/下鍵、點縮圖都走這裡 ----
    function showImage(i) {
      if (!previewList.length) return;
      previewIndex = (i + previewList.length) % previewList.length;
      playerEl.style.display = 'none';
      playerEl.innerHTML = '';
      imgEl.style.display = '';
      imgEl.style.opacity = '0.3';
      withFallbackChain(imgEl, previewList[previewIndex].candidates);
      imgEl.onload = () => { imgEl.style.opacity = '1'; };
      renderThumbside();
    }

    // ---- 下方「所有作品」封面列:畫一次,之後只切換 active 狀態 ----
    bottomthumbsEl.style.display = items.length > 1 ? '' : 'none';
    items.forEach((it, i) => {
      const t = document.createElement('div');
      t.className = 'avbase-ov-bottomthumb';
      t.innerHTML = '<img loading="lazy">';
      withFallbackChain(t.querySelector('img'), it.lightboxCandidates);
      t.addEventListener('click', () => render(i));
      bottomthumbsEl.appendChild(t);
    });
    const bottomThumbEls = Array.from(bottomthumbsEl.children);

    // ---- 切換「作品」:左右鍵、上/下一個作品按鈕、下方封面列都走這裡 ----
    async function render(i, forcedPreviewIndex) {
      index = (i + items.length) % items.length;
      const it = items[index];
      const myToken = ++requestToken;

      // 先用已經有的封面資料把畫面畫出來,預覽圖之後非同步補上
      previewList = [{
        thumbSrc: (it.coverCandidates && it.coverCandidates[it.coverCandidates.length - 1]) || it.lightboxCandidates[0],
        candidates: it.lightboxCandidates
      }];
      showImage(0);

      titleEl.textContent = it.title || '(無標題)';
      actorsEl.innerHTML = it.actorsHtml || '';
      if (it.isCurrentPage) { openLink.style.display = 'none'; }
      else { openLink.style.display = ''; openLink.href = it.workHref; }
      missavLink.href = `https://missav.ai/${encodeURIComponent(extractWorkCode(it.workHref))}`;
      playBtn.textContent = '▶ 播放預告';
      playBtn.disabled = false;
      vrMode = false;
      vrBtn.textContent = '🕶 VR 模式:關';
      prevBtn.style.display = items.length > 1 ? '' : 'none';
      nextBtn.style.display = items.length > 1 ? '' : 'none';
      bottomThumbEls.forEach((t, ti) => t.classList.toggle('active', ti === index));
      if (bottomThumbEls[index]) bottomThumbEls[index].scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });

      const details = await fetchWorkDetails(it);
      if (myToken !== requestToken) return; // 使用者已經切走了,這次結果作廢
      it._details = details;

      if (details.sampleImages && details.sampleImages.length) {
        const extra = details.sampleImages.map(s => ({ thumbSrc: s.s || s.l, candidates: [s.l, s.s].filter(Boolean) }));
        previewList = [previewList[0]].concat(extra);
      }
      if (typeof forcedPreviewIndex === 'number' && forcedPreviewIndex >= 0 && forcedPreviewIndex < previewList.length) {
        showImage(forcedPreviewIndex);
      } else {
        renderThumbside();
      }
    }

    // ---- 試看播放:先試一般試看候選網址(依序 fallback),全部失敗(或本來就沒有)才試 VR 候選網址;
    //      成功用 VR 網址播出來的話,自動把 VR 模式切成「開」。過程中不顯示任何提示文字。 ----
    function playSelected() {
      const it = items[index];
      const details = it._details;
      if (!details) return;

      const normalList = details.normalTrailerCandidates || [];
      const vrList = details.vrTrailerCandidates || [];
      const primaryList = vrMode ? (vrList.length ? vrList : normalList) : (normalList.length ? normalList : vrList);
      const secondaryList = vrMode ? normalList : vrList;
      if (!primaryList.length) return;
      const primaryIsVR = vrMode ? !!vrList.length : false;

      imgEl.style.display = 'none';
      playerEl.style.display = 'block';

      if (primaryIsVR) {
        mountVRPlayer(playerEl, primaryList);
      } else {
        mountFlatPlayer(playerEl, primaryList, () => {
          if (secondaryList && secondaryList.length) {
            vrMode = true;
            vrBtn.textContent = '🕶 VR 模式:開';
            mountVRPlayer(playerEl, secondaryList);
          }
        });
      }
    }

    async function handlePlay() {
      const it = items[index];
      playBtn.disabled = true;
      playBtn.textContent = '載入中…';
      if (!it._details) it._details = await fetchWorkDetails(it);
      playBtn.disabled = false;
      playBtn.textContent = '▶ 播放預告';
      const hasNormal = it._details.normalTrailerCandidates && it._details.normalTrailerCandidates.length;
      const hasVr = it._details.vrTrailerCandidates && it._details.vrTrailerCandidates.length;
      if (!hasNormal && !hasVr) return; // 安靜略過,不顯示任何訊息
      playSelected();
    }

    playBtn.addEventListener('click', handlePlay);
    vrBtn.addEventListener('click', () => {
      vrMode = !vrMode;
      vrBtn.textContent = `🕶 VR 模式:${vrMode ? '開' : '關'}`;
      if (playerEl.style.display === 'block' && playerEl.firstChild) playSelected();
    });
    sukebeiBtn.addEventListener('click', () => {
      openSukebeiSearch(extractWorkCode(items[index].workHref));
    });

    function close() {
      overlay.remove();
      document.body.style.overflow = '';
      document.removeEventListener('keydown', onKey);
    }
    function onKey(e) {
      if (e.key === 'Escape') close();
      else if (e.key === 'ArrowLeft') render(index - 1);
      else if (e.key === 'ArrowRight') render(index + 1);
      else if (e.key === 'ArrowUp') { e.preventDefault(); showImage(previewIndex - 1); }
      else if (e.key === 'ArrowDown') { e.preventDefault(); showImage(previewIndex + 1); }
    }

    // 點空白處關閉:外層背景、shell、主圖區留白、左側縮圖列背景、右側面板背景、底部封面列背景都算「空白」
    const blankEls = new Set([overlay, shellEl, mainEl, thumbsideEl, sideEl, bottomthumbsEl]);
    closeBtn.addEventListener('click', close);
    overlay.addEventListener('click', (e) => { if (blankEls.has(e.target)) close(); });
    prevBtn.addEventListener('click', () => render(index - 1));
    nextBtn.addEventListener('click', () => render(index + 1));
    document.addEventListener('keydown', onKey);

    render(index, initialPreviewIndex);
    requestAnimationFrame(() => overlay.classList.add('show'));
  }

  /* ---- 一般播放器:接受一組候選網址,依序試(跟封面圖 fallback chain 同精神),
     全部試過都失敗才呼叫 onError(播放浮層的自動 VR 備援機制靠這個)---- */
  function mountFlatPlayer(container, urls, onError) {
    container.innerHTML = '';
    const video = document.createElement('video');
    video.className = 'avbase-trailer-video';
    video.controls = true;
    video.playsInline = true;
    video.autoplay = true;
    container.appendChild(video);

    const list = dedupe(Array.isArray(urls) ? urls : [urls]);
    let idx = 0;

    function tryPlay() {
      video.play().catch(() => { video.muted = true; video.play().catch(() => {}); });
    }
    function tryNext() {
      if (idx >= list.length) {
        video.removeEventListener('error', tryNext);
        if (onError) onError();
        return;
      }
      video.src = list[idx];
      idx++;
      tryPlay();
    }

    video.addEventListener('error', tryNext);
    tryNext();
  }

  /* ---- VR 模式:只顯示 A-Frame videosphere 本身,同樣接受一組候選網址依序試 ---- */
  let aframeLoadPromise = null;
  function ensureAFrame() {
    if (window.AFRAME) return Promise.resolve();
    if (aframeLoadPromise) return aframeLoadPromise;
    aframeLoadPromise = new Promise((resolve, reject) => {
      const s = document.createElement('script');
      s.src = 'https://aframe.io/releases/1.5.0/aframe.min.js';
      s.onload = () => resolve();
      s.onerror = () => reject(new Error('aframe load failed'));
      document.head.appendChild(s);
      setTimeout(() => reject(new Error('aframe load timeout')), 8000);
    });
    return aframeLoadPromise;
  }

  async function mountVRPlayer(container, urls) {
    const list = dedupe(Array.isArray(urls) ? urls : [urls]);
    if (!list.length) return;

    try {
      await ensureAFrame();
    } catch (e) {
      mountFlatPlayer(container, list);
      return;
    }

    container.innerHTML = `
      <div class="avbase-vr-wrap">
        <video id="avbase-vr-video" class="avbase-vr-source-video" crossorigin="anonymous" playsinline loop autoplay muted></video>
        <a-scene embedded vr-mode-ui="enabled: true">
          <a-videosphere src="#avbase-vr-video" rotation="0 -90 0"></a-videosphere>
          <a-camera></a-camera>
        </a-scene>
      </div>
    `;

    const vid = container.querySelector('#avbase-vr-video');
    let idx = 0;
    function tryNext() {
      if (idx >= list.length) { vid.removeEventListener('error', tryNext); return; } // 全部候選都失敗,安靜放棄,不顯示錯誤訊息
      vid.src = list[idx];
      idx++;
      vid.play().catch(() => {});
    }
    vid.addEventListener('error', tryNext);
    tryNext();
  }

  /* =========================================================
   * 8. Sukebei (sukebei.nyaa.si) 種子搜尋
   * 用 GM_xmlhttpRequest 是因為 sukebei 不會回應一般網頁的 CORS,
   * 直接用 fetch() 會被瀏覽器擋,要靠 Tampermonkey 的跨網域權限繞過。
   * ========================================================= */
  function searchSukebei(query) {
    return new Promise((resolve, reject) => {
      if (typeof GM_xmlhttpRequest !== 'function') {
        reject(new Error('此瀏覽器/腳本管理器不支援 GM_xmlhttpRequest'));
        return;
      }
      const url = `https://sukebei.nyaa.si/?page=rss&q=${encodeURIComponent(query)}&sort=seeders&order=desc`;
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        timeout: 15000,
        onload: (res) => {
          try { resolve(parseSukebeiRss(res.responseText)); }
          catch (e) { reject(e); }
        },
        onerror: () => reject(new Error('network error')),
        ontimeout: () => reject(new Error('timeout'))
      });
    });
  }

  // 解析 sukebei 的 RSS。磁力連結 sukebei 沒有直接給,靠 nyaa:infoHash 自己組
  // magnet:?xt=urn:btih:<infoHash>&dn=<標題>。若 sukebei 改了 RSS 結構,要修的地方就是這裡。
  function parseSukebeiRss(xmlText) {
    const doc = new DOMParser().parseFromString(xmlText, 'text/xml');
    const items = Array.from(doc.querySelectorAll('item'));
    return items.map(it => {
      const title = it.querySelector('title')?.textContent || '';
      const link = it.querySelector('link')?.textContent || '';
      const pubDate = it.querySelector('pubDate')?.textContent || '';
      const size = it.getElementsByTagNameNS('*', 'size')[0]?.textContent || '';
      const seeders = it.getElementsByTagNameNS('*', 'seeders')[0]?.textContent || '';
      const leechers = it.getElementsByTagNameNS('*', 'leechers')[0]?.textContent || '';
      const infoHash = it.getElementsByTagNameNS('*', 'infoHash')[0]?.textContent || '';
      const magnet = infoHash ? `magnet:?xt=urn:btih:${infoHash}&dn=${encodeURIComponent(title)}` : '';
      return { title, link, size, seeders, leechers, pubDate, magnet };
    });
  }

  function copyTextToClipboard(text) {
    if (navigator.clipboard && navigator.clipboard.writeText) {
      return navigator.clipboard.writeText(text);
    }
    // 舊瀏覽器 / clipboard API 被擋掉時的備援
    return new Promise((resolve, reject) => {
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.opacity = '0';
      document.body.appendChild(ta);
      ta.select();
      try {
        document.execCommand('copy');
        resolve();
      } catch (e) {
        reject(e);
      } finally {
        ta.remove();
      }
    });
  }

  function openSukebeiSearch(defaultQuery) {
    const overlay = document.createElement('div');
    overlay.className = 'avbase-sukebei-overlay';
    overlay.innerHTML = `
      <div class="avbase-sukebei-card">
        <div class="avbase-sukebei-head">
          <strong>Sukebei 種子搜尋</strong>
          <button type="button" class="avbase-sukebei-close" aria-label="關閉">✕</button>
        </div>
        <div class="avbase-sukebei-search-row">
          <input type="text" class="avbase-sukebei-input" value="${escapeHtml(defaultQuery)}">
          <button type="button" class="avbase-sukebei-go">搜尋</button>
        </div>
        <div class="avbase-sukebei-results"><p class="avbase-sukebei-hint">搜尋中…</p></div>
      </div>
    `;
    document.body.appendChild(overlay);

    const input = overlay.querySelector('.avbase-sukebei-input');
    const resultsEl = overlay.querySelector('.avbase-sukebei-results');
    const closeBtn = overlay.querySelector('.avbase-sukebei-close');
    const goBtn = overlay.querySelector('.avbase-sukebei-go');

    function close() {
      overlay.remove();
      document.removeEventListener('keydown', onKey);
    }
    function onKey(e) { if (e.key === 'Escape') close(); }
    closeBtn.addEventListener('click', close);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
    document.addEventListener('keydown', onKey);

    async function runSearch() {
      const q = input.value.trim();
      if (!q) return;
      resultsEl.innerHTML = '<p class="avbase-sukebei-hint">搜尋中…</p>';
      try {
        const items = await searchSukebei(q);
        if (!items.length) {
          resultsEl.innerHTML = '<p class="avbase-sukebei-hint">找不到結果。</p>';
          return;
        }
        resultsEl.innerHTML = '';
        items.forEach(it => {
          const row = document.createElement('div');
          row.className = 'avbase-sukebei-item';
          row.innerHTML = `
            <div class="avbase-sukebei-item-title">${escapeHtml(it.title)}</div>
            <div class="avbase-sukebei-item-meta">
              <span>📦 ${escapeHtml(it.size || '-')}</span>
              <span>🌱 ${escapeHtml(it.seeders || '0')}</span>
              <span>🌊 ${escapeHtml(it.leechers || '0')}</span>
              <span>${escapeHtml(it.pubDate || '')}</span>
            </div>
            <div class="avbase-sukebei-item-actions">
              <a href="${escapeHtml(it.link || '#')}" target="_blank" rel="noopener">開啟頁面 ↗</a>
              ${it.magnet ? '<button type="button" class="avbase-sukebei-copy">複製磁鏈接</button>' : ''}
            </div>
          `;
          const copyBtn = row.querySelector('.avbase-sukebei-copy');
          if (copyBtn) {
            copyBtn.addEventListener('click', () => {
              copyTextToClipboard(it.magnet)
                .then(() => { copyBtn.textContent = '已複製 ✓'; setTimeout(() => { copyBtn.textContent = '複製磁鏈接'; }, 1500); })
                .catch(() => {});
            });
          }
          resultsEl.appendChild(row);
        });
      } catch (e) {
        resultsEl.innerHTML = '<p class="avbase-sukebei-hint">搜尋失敗,可能是網路問題或來源網站改版。</p>';
        console.warn('[AvBase Reforge] sukebei 搜尋失敗', e);
      }
    }

    goBtn.addEventListener('click', runSearch);
    input.addEventListener('keydown', (e) => { if (e.key === 'Enter') runSearch(); });

    requestAnimationFrame(() => overlay.classList.add('show'));
    runSearch();
  }

  /* =========================================================
   * 9. 演員頁:整塊資訊搬到作品列表上方 + 側欄精簡(頭像/名字/フォロー/社交)
   * ========================================================= */
  function rebuildTalentLayout() {
    if (!isTalentPage()) return;
    // 外層 4 欄 grid(左:演員資訊欄,右:作品列表欄)改成單欄上下堆疊,
    // 解決「演員資訊欄比較短、右邊作品列表比較長,左邊下方留白」的問題
    const outerGrid = document.querySelector('.grid.grid-cols-1.lg\\:grid-cols-4.lg\\:gap-8');
    if (outerGrid && outerGrid.dataset.avbaseStack !== '1') {
      outerGrid.classList.add('avbase-talent-stack');
      outerGrid.dataset.avbaseStack = '1';
    }
  }

  function rebuildTalentSidebar() {
    if (!isTalentPage()) return;

    const avatarEl = document.querySelector('.lg\\:col-span-1 span[data-slot="avatar"]');
    if (!avatarEl) return;

    const sideCol = avatarEl.closest('.lg\\:col-span-1');
    if (!sideCol || sideCol.dataset.avbaseDone === '1') return;

    const nameEl = sideCol.querySelector('h1');
    const followBtn = sideCol.querySelector('button[aria-label="フォローする"]');
    const socialWrap = sideCol.querySelector('.group\\/social');
    if (!nameEl) return; // 版面跟預期不符就不動它,避免破壞頁面

    const avatarSrc = avatarEl.querySelector('img')?.getAttribute('src') || '';
    const avatarAlt = avatarEl.querySelector('img')?.getAttribute('alt') || nameEl.textContent.trim();
    const nameHtml = nameEl.innerHTML;
    const followHtml = followBtn ? followBtn.outerHTML : '';
    const socialHtml = socialWrap ? socialWrap.innerHTML : '';

    // 其餘資訊(暱稱標籤/基本情報/プロフィール/編集履歴…)整塊原始 HTML 先存起來,
    // 收進點擊頭像才打開的浮卡
    const fullDetailHtml = sideCol.innerHTML;

    sideCol.innerHTML = `
      <div class="avbase-talent-mini">
        <button type="button" class="avbase-talent-avatar-btn" aria-label="查看完整資料">
          <img src="${escapeHtml(avatarSrc)}" alt="${escapeHtml(avatarAlt)}">
        </button>
        <div class="avbase-talent-name">${nameHtml}</div>
        <div class="avbase-talent-follow">${followHtml}</div>
        <div class="avbase-talent-social">${socialHtml}</div>
      </div>
    `;
    sideCol.dataset.avbaseDone = '1';

    sideCol.querySelector('.avbase-talent-avatar-btn').addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      openTalentDetailCard(fullDetailHtml);
    });
  }

  function openTalentDetailCard(fullDetailHtml) {
    const overlay = document.createElement('div');
    overlay.className = 'avbase-talent-overlay';
    overlay.innerHTML = `
      <div class="avbase-talent-card">
        <button type="button" class="avbase-talent-card-close" aria-label="關閉">✕</button>
        <div class="avbase-talent-card-body"></div>
      </div>
    `;
    overlay.querySelector('.avbase-talent-card-body').innerHTML = fullDetailHtml;
    document.body.appendChild(overlay);
    document.body.style.overflow = 'hidden';

    function close() {
      overlay.remove();
      document.body.style.overflow = '';
      document.removeEventListener('keydown', onKey);
    }
    function onKey(e) { if (e.key === 'Escape') close(); }

    overlay.querySelector('.avbase-talent-card-close').addEventListener('click', close);
    overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
    document.addEventListener('keydown', onKey);

    requestAnimationFrame(() => overlay.classList.add('show'));
  }

  /* =========================================================
   * 9-1. 標籤頁:分類側欄精簡(跟演員頁側欄同一套版面,同樣手法處理)
   * ========================================================= */
  function rebuildTagSidebar() {
    if (!isTagPage()) return;

    // 標籤頁左欄跟演員頁左欄是同一種 `.lg:col-span-1` 版位,用「裡面有 /tags/ 連結」
    // 這個結構特徵去找,順便排除已經處理過的
    const sideCol = Array.from(document.querySelectorAll('.lg\\:col-span-1'))
      .find(el => el.dataset.avbaseDone !== '1' && el.querySelector('a[href^="/tags/"]'));
    if (!sideCol) return;

    // 跟演員頁一樣:外層 4 欄 grid 改成上下堆疊,側欄精簡後才不會被拉開留白
    const outerGrid = sideCol.closest('.grid');
    if (outerGrid && outerGrid.dataset.avbaseStack !== '1') {
      outerGrid.classList.add('avbase-talent-stack');
      outerGrid.dataset.avbaseStack = '1';
    }

    // 結構:sideCol > 外層 wrap div > box(border 卡片) > [row1(標籤名稱+子標籤鈕+編輯鈕), followBtn]
    const box = sideCol.querySelector(':scope > div > div');
    if (!box) return;
    const row1 = box.querySelector(':scope > div');
    if (!row1) return;

    const tagInfoWrap = row1.querySelector(':scope > div'); // 圖示 + 分類連結 + 子標籤展開鈕
    const editBtn = row1.querySelector(':scope > button');   // 編輯(鉛筆)鈕
    const followBtn = box.querySelector(':scope > button');  // フォロー鈕

    const tagLink = sideCol.querySelector('a[href^="/tags/"]');
    if (!tagLink) return;

    sideCol.innerHTML = `
      <div class="avbase-tag-mini">
        <div class="avbase-tag-mini-row">
          ${tagInfoWrap ? tagInfoWrap.outerHTML : tagLink.outerHTML}
          ${editBtn ? editBtn.outerHTML : ''}
        </div>
        ${followBtn ? followBtn.outerHTML : ''}
      </div>
    `;
    sideCol.dataset.avbaseDone = '1';
  }

  /* =========================================================
   * 10. 作品詳情頁:大封面 + 下方縮圖列點擊都開浮層,關聯作品也套同一套浮層
   * ========================================================= */
  let currentPageItem = null; // 作品詳情頁自己的畫廊項目,封面跟下方縮圖列共用同一份

  // 從「出演者・メモ」區塊撈演員名單的 HTML(找標題是「出演者」的 h2,抓它同區塊底下的內容)
  function extractWorkDetailActorsHtml() {
    const headings = Array.from(document.querySelectorAll('h2'));
    const h = headings.find(el => el.textContent.trim().startsWith('出演者'));
    if (!h) return '';
    const section = h.closest('section');
    const wrap = section ? section.querySelector('.flex.flex-wrap.gap-2') : null;
    return wrap ? wrap.innerHTML : '';
  }

  function rebuildWorkDetailCover() {
    if (!isWorkDetailPage()) return;

    // 找主封面:DMM 命名規則的大圖固定叫 pl.jpg,直接用這個特徵找,比對 class 穩定
    const coverImg = document.querySelector('img[src*="pl.jpg"]');
    if (!coverImg || coverImg.dataset.avbaseDone === '1') return;
    coverImg.dataset.avbaseDone = '1';

    const smallSrc = coverImg.getAttribute('src') || '';
    const title = (document.querySelector('h1')?.textContent || '').trim();
    const actorsHtml = extractWorkDetailActorsHtml();

    const item = buildGalleryItem({ smallSrc, workHref: location.pathname, title, actorsHtml });
    item.isCurrentPage = true;
    currentPageItem = item;

    const clickTarget = coverImg.closest('a') || coverImg;
    clickTarget.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      openGallery([item], 0);
    });
    clickTarget.style.cursor = 'zoom-in';
  }

  // 封面下方那排可以左右滑動的預覽圖縮圖,點了也開同一個浮層,直接跳到那張圖
  function rebuildWorkDetailSampleStrip() {
    if (!isWorkDetailPage() || !currentPageItem || !currentPageItem.cid) return;

    const cid = currentPageItem.cid;
    const re = new RegExp(cid + '(?:jp)?-(\\d+)\\.jpg', 'i');
    const links = Array.from(document.querySelectorAll('a[target="_blank"]')).filter(a => {
      const img = a.querySelector('img');
      return img && re.test(img.getAttribute('src') || '');
    });

    links.forEach(a => {
      if (a.dataset.avbaseDone === '1') return;
      a.dataset.avbaseDone = '1';
      const img = a.querySelector('img');
      const m = (img.getAttribute('src') || '').match(re);
      const n = m ? parseInt(m[1], 10) : 1; // 第幾張預覽圖(1-based,跟 previewList 裡「封面之後」的順序對得上)
      a.addEventListener('click', (e) => {
        e.preventDefault();
        e.stopPropagation();
        openGallery([currentPageItem], 0, n);
      });
      a.style.cursor = 'zoom-in';
    });
  }

  // 「同じ女優の作品」「同じシリーズなどの作品」等側欄推薦卡片,點了也開同一套浮層
  function rebuildRelatedArticles() {
    if (!isWorkDetailPage()) return;

    const asideSections = document.querySelectorAll('aside[aria-label="関連作品"] section');
    asideSections.forEach(section => {
      if (section.dataset.avbaseDone === '1') return;
      const articles = Array.from(section.querySelectorAll('article'));
      if (!articles.length) return;

      const items = [];
      articles.forEach(article => {
        const link = article.querySelector('a[href^="/works/"]');
        const img = article.querySelector('img');
        const span = article.querySelector('span.line-clamp-3, span');
        if (!link || !img) return;
        const smallSrc = img.getAttribute('src') || '';
        if (!smallSrc) return;
        const title = (span ? span.textContent : link.textContent || '').trim();
        const item = buildGalleryItem({ smallSrc, workHref: link.getAttribute('href') || '#', title, actorsHtml: '' });
        items.push(item);

        link.addEventListener('click', (e) => {
          e.preventDefault();
          e.stopPropagation();
          const idx = items.indexOf(item);
          openGallery(items, idx >= 0 ? idx : 0);
        });
      });

      section.dataset.avbaseDone = '1';
    });
  }

  /* =========================================================
   * 11. 初始化 / 監聽 SPA 變化
   * ========================================================= */
  function runPageEnhancements() {
    if (isCardGridPage()) {
      normalizeLayout();
      applyColumns();
      rebuildAllCards();
    }
    if (isTalentPage()) {
      rebuildTalentLayout();
      rebuildTalentSidebar();
    }
    if (isTagPage()) {
      rebuildTagSidebar();
    }
    if (isWorkDetailPage()) {
      rebuildWorkDetailCover();
      rebuildWorkDetailSampleStrip();
      rebuildRelatedArticles();
    }
  }

  function init() {
    buildSettingsPanel();
    applyColumns();
    runPageEnhancements();

    const mo = new MutationObserver(debounce(runPageEnhancements, 150));
    mo.observe(document.body, { childList: true, subtree: true });

    window.addEventListener('resize', debounce(applyColumns, 200));
  }

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', init);
  } else {
    init();
  }
})();