Enhanced_Media_Helper

Code Manager Panel with javgg site support (Preact + htm) + magnet screenshot preview + Linear UI

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           Enhanced_Media_Helper
// @version        3.6.0
// @description    Code Manager Panel with javgg site support (Preact + htm) + magnet screenshot preview + Linear UI
// @author         cores
// @match          https://javgg.net/tag/to-be-release/*
// @match          https://javgg.net/featured/*
// @match          https://javgg.net/
// @match          https://javgg.net/new-post/*
// @match          https://javgg.net/jav/*
// @match          https://javgg.net/star/*
// @match          https://javgg.net/trending/*
// @include        *://*jav*/*
// @include        https://1cili.com/*
// @include        https://btnets.net/*
// @include        *://*av*/*
// @include        *://*fc2*/*
// @include        *://*missav*/*
// @include        *://*javdb*/*
// @include        *://*javlibrary*/*
// @include        *://*dmm.co.jp/*
// @include        *://*dmm.com/*
// @include        *://*heyzo*/*
// @include        *://*caribbeancom*/*
// @include        *://*1pondo*/*
// @include        *://*pacopaco*/*
// @include        *://*xvideos*/*
// @include        *://*pornhub*/*
// @include        *://*xhamster*/*
// @include        *://*reddit.com/*
// @include        *://*redditmedia.com/*
// @include        *://*t66y.com/*
// @include        *://*sis001.com/*
// @include        *://*cl*forum*/*
// @include        *://*91porn*/*
// @include        *://*jable*/*
// @include        *://*avple*/*
// @include        *://*.cbox.ws/*
// @include        *://cbox.ws/*
// @include        *://my.cbox.ws/*
// @require        https://cdn.jsdelivr.net/npm/[email protected]/dist/preact.umd.js
// @require        https://cdn.jsdelivr.net/npm/[email protected]/hooks/dist/hooks.umd.js
// @require        https://cdn.jsdelivr.net/npm/[email protected]/dist/htm.umd.js
// @icon           data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_addValueChangeListener
// @grant          GM_xmlhttpRequest
// @grant          GM_openInTab
// @connect        1cili.com
// @connect        whatslink.info
// @connect        avwikidb.com
// @run-at         document-start
// @noframes
// @license        MPL
// @namespace      cdn.bootcss.com
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        codeManager: {
            storageKey: 'emh_code_library',
            trashStorageKey: 'emh_code_trash',
            trashRetentionDays: 7,
            statusColors: {
                unmarked: '#6b7280',
                favorite: '#f87171',
                watched: '#34d399'
            }
        },
        magnetPreview: {
            cacheTtlMs: 7 * 24 * 60 * 60 * 1000,
            errorTtlMs: 5 * 60 * 1000
        },
        alternateUrl: {
            av123: 'https://123av.com/cn/v/',
            jable: 'https://jable.tv/videos/',
            cili1: 'https://1cili.com/search?q='
        }
    };

    const CODE_LIBRARY = {
        data: null,
        trash: null,
        initialized: false,

        init: function(force) {
            if (this.initialized && !force) return true;
            try {
                const savedData = GM_getValue(CONFIG.codeManager.storageKey);
                this.data = savedData ? JSON.parse(savedData) : {
                    items: [],
                    lastUpdated: new Date().toISOString()
                };
                const savedTrash = GM_getValue(CONFIG.codeManager.trashStorageKey);
                this.trash = savedTrash ? JSON.parse(savedTrash) : {
                    items: [],
                    lastUpdated: new Date().toISOString()
                };
                this.cleanupTrash();
                // 自愈:清理历史遗留的非字符串 remarks(如误写入的 [object Object])
                let dataFixed = false;
                const fixRemarks = (list) => {
                    for (const it of list) {
                        if (it && it.remarks != null && typeof it.remarks !== 'string') {
                            it.remarks = '';
                            dataFixed = true;
                        }
                    }
                };
                if (Array.isArray(this.data.items)) fixRemarks(this.data.items);
                if (Array.isArray(this.trash.items)) fixRemarks(this.trash.items);
                if (dataFixed) {
                    this.data.lastUpdated = new Date().toISOString();
                    GM_setValue(CONFIG.codeManager.storageKey, JSON.stringify(this.data));
                    this.trash.lastUpdated = new Date().toISOString();
                    GM_setValue(CONFIG.codeManager.trashStorageKey, JSON.stringify(this.trash));
                }
                this.initialized = true;
                return true;
            } catch (e) {
                console.error('番号库初始化失败:', e);
                this.data = { items: [], lastUpdated: new Date().toISOString() };
                this.trash = { items: [], lastUpdated: new Date().toISOString() };
                this.initialized = true;
                return false;
            }
        },

        save: function() {
            try {
                this.data.lastUpdated = new Date().toISOString();
                GM_setValue(CONFIG.codeManager.storageKey, JSON.stringify(this.data));
                this.trash.lastUpdated = new Date().toISOString();
                GM_setValue(CONFIG.codeManager.trashStorageKey, JSON.stringify(this.trash));
                window.dispatchEvent(new CustomEvent('emh_library_updated', {
                    detail: { type: 'library_update', data: this.data }
                }));
                if (typeof GM_setValue !== 'undefined') {
                    GM_setValue('emh_sync_timestamp', Date.now().toString());
                }
                return true;
            } catch (e) {
                console.error('保存番号库失败:', e);
                UTILS.showToast('保存番号库失败', 'error');
                return false;
            }
        },

        getAll: function() {
            if (!this.initialized) this.init();
            return [...this.data.items];
        },

        getFavorites: function() {
            if (!this.initialized) this.init();
            return this.data.items.filter(item => item.status === 'favorite');
        },

        getWatched: function() {
            if (!this.initialized) this.init();
            return this.data.items.filter(item => item.status === 'watched');
        },

        getTrash: function() {
            if (!this.initialized) this.init();
            return [...this.trash.items];
        },

        add: function(code, title = '', remarks = '') {
            if (!this.initialized) this.init();
            if (!code) return false;
            const normalizedCode = code.toUpperCase();
            if (this.getItem(normalizedCode)) {
                UTILS.showToast(`番号 ${normalizedCode} 已存在于番号库中`, 'warning');
                return false;
            }
            this.data.items.unshift({
                code: normalizedCode,
                title: title || normalizedCode,
                status: 'unmarked',
                remarks: remarks || '',
                tags: [],
                createdDate: new Date().toISOString(),
                modifiedDate: new Date().toISOString()
            });
            this.save();
            return true;
        },

        delete: function(code) {
            if (!this.initialized) this.init();
            if (!code) return false;
            const normalizedCode = code.toUpperCase();
            const itemIndex = this.data.items.findIndex(item => item.code.toUpperCase() === normalizedCode);
            if (itemIndex === -1) return false;
            const item = this.data.items[itemIndex];
            item.deleteDate = new Date().toISOString();
            this.data.items.splice(itemIndex, 1);
            this.trash.items.unshift(item);
            return this.save();
        },

        cleanupTrash: function() {
            if (!this.trash || !this.trash.items || !this.trash.items.length) return;
            const now = new Date();
            const retentionPeriod = CONFIG.codeManager.trashRetentionDays * 24 * 60 * 60 * 1000;
            const before = this.trash.items.length;
            this.trash.items = this.trash.items.filter(item => {
                return (now - new Date(item.deleteDate)) < retentionPeriod;
            });
            if (this.trash.items.length !== before) {
                this.data.lastUpdated = new Date().toISOString();
                GM_setValue(CONFIG.codeManager.storageKey, JSON.stringify(this.data));
                this.trash.lastUpdated = new Date().toISOString();
                GM_setValue(CONFIG.codeManager.trashStorageKey, JSON.stringify(this.trash));
            }
        },

        getItem: function(code) {
            if (!this.initialized) this.init();
            if (!code) return null;
            const normalizedCode = code.toUpperCase();
            return this.data.items.find(item => item.code.toUpperCase() === normalizedCode);
        },

        getStatus: function(code) {
            const item = this.getItem(code);
            return item ? item.status : 'unmarked';
        },

        // 归一化 magnet 字段:兼容三种形态(单值字符串 / 旧字符串数组 / 新对象数组),
        // 统一返回对象数组 [{ id, value, preview? }]。旧数据自动生成 id(数据迁移)。
        // preview 为 whatslink 缓存:{ name, type, fileType, size, count, screenshots, fetchedAt, error? }
        normMagnets: function(m) {
            let arr = [];
            if (!m) {
                arr = [];
            } else if (Array.isArray(m)) {
                arr = m.slice();
            } else {
                arr = [m];
            }
            // 统一为 { id, value, preview? } 对象
            return arr.map((entry, i) => {
                if (entry && typeof entry === 'object' && entry.value) {
                    const out = { id: entry.id || ('m-' + Date.now().toString(36) + '-' + i), value: String(entry.value).trim() };
                    const pv = this.sanitizeMagnetPreview(entry.preview);
                    if (pv) out.preview = pv;
                    return out;
                }
                const v = entry == null ? '' : String(entry).trim();
                if (!v) return null;
                return { id: 'm-' + Date.now().toString(36) + '-' + i, value: v };
            }).filter(Boolean);
        },

        sanitizeMagnetPreview: function(preview) {
            if (!preview || typeof preview !== 'object' || Array.isArray(preview)) return null;
            const isSafeHttpUrl = (url) => {
                if (typeof url !== 'string') return false;
                try {
                    const u = new URL(url.trim());
                    return u.protocol === 'https:' || u.protocol === 'http:';
                } catch (_) { return false; }
            };
            const shotsRaw = Array.isArray(preview.screenshots) ? preview.screenshots : [];
            const screenshots = [];
            const seen = new Set();
            for (let i = 0; i < shotsRaw.length && screenshots.length < 48; i++) {
                const item = shotsRaw[i];
                if (item == null) continue;
                let url = '';
                let time = null;
                if (typeof item === 'string') url = item;
                else if (typeof item === 'object') {
                    url = item.screenshot || item.url || item.src || item.image || '';
                    if (item.time != null && Number.isFinite(Number(item.time))) time = Number(item.time);
                }
                url = String(url || '').trim();
                if (!isSafeHttpUrl(url) || seen.has(url)) continue;
                seen.add(url);
                screenshots.push(time != null ? { screenshot: url, time } : { screenshot: url });
            }
            const sizeN = Number(preview.size);
            const countN = Number(preview.count);
            return {
                name: typeof preview.name === 'string' ? preview.name : (typeof preview.title === 'string' ? preview.title : ''),
                type: typeof preview.type === 'string' ? preview.type : '',
                fileType: typeof preview.fileType === 'string' ? preview.fileType : (typeof preview.file_type === 'string' ? preview.file_type : ''),
                size: Number.isFinite(sizeN) ? sizeN : null,
                count: Number.isFinite(countN) ? countN : null,
                screenshots,
                fetchedAt: typeof preview.fetchedAt === 'string' ? preview.fetchedAt : (preview.fetchedAt ? String(preview.fetchedAt) : ''),
                error: typeof preview.error === 'string' ? preview.error : ''
            };
        },

        // 在 data / trash 中定位番号条目
        findItemRecord: function(code) {
            if (!this.initialized) this.init();
            if (!code) return null;
            const normalizedCode = String(code).toUpperCase();
            const mainIdx = this.data.items.findIndex(item => item.code.toUpperCase() === normalizedCode);
            if (mainIdx >= 0) return { list: this.data.items, index: mainIdx, item: this.data.items[mainIdx], inTrash: false };
            const trashIdx = this.trash.items.findIndex(item => item.code.toUpperCase() === normalizedCode);
            if (trashIdx >= 0) return { list: this.trash.items, index: trashIdx, item: this.trash.items[trashIdx], inTrash: true };
            return null;
        },

        // 写入某条磁力的 whatslink 预览缓存并持久化
        setMagnetPreview: function(code, magnetIdOrIdx, preview) {
            const rec = this.findItemRecord(code);
            if (!rec) return false;
            const magnets = this.normMagnets(rec.item.magnet);
            if (!magnets.length) return false;
            const t = String(magnetIdOrIdx);
            let idx = magnets.findIndex((e, i) => {
                const eid = this.magnetId(e);
                return (eid && String(eid) === t) || (!eid && String(i) === t);
            });
            if (idx < 0 && /^\d+$/.test(t)) idx = Number(t);
            if (idx < 0 || idx >= magnets.length) return false;
            const cleaned = this.sanitizeMagnetPreview(preview);
            if (cleaned) magnets[idx].preview = cleaned;
            else delete magnets[idx].preview;
            rec.item.magnet = magnets;
            rec.item.modifiedDate = new Date().toISOString();
            return this.save();
        },

        getMagnetPreview: function(code, magnetIdOrIdx) {
            const rec = this.findItemRecord(code);
            if (!rec) return null;
            const magnets = this.normMagnets(rec.item.magnet);
            const t = String(magnetIdOrIdx);
            let idx = magnets.findIndex((e, i) => {
                const eid = this.magnetId(e);
                return (eid && String(eid) === t) || (!eid && String(i) === t);
            });
            if (idx < 0 && /^\d+$/.test(t)) idx = Number(t);
            if (idx < 0 || idx >= magnets.length) return null;
            return this.sanitizeMagnetPreview(magnets[idx].preview);
        },

        // 取磁力对象的值(兼容对象/字符串)
        magnetValue: function(m) {
            if (m == null) return '';
            if (typeof m === 'object') return m.value || '';
            return String(m).trim();
        },

        // 取磁力对象的 id(兼容对象/字符串;字符串返回 null)
        magnetId: function(m) {
            if (m && typeof m === 'object') return m.id || null;
            return null;
        },

        // 清理磁力链接用于显示:去掉 &dn=xxx 参数(dn 可能带乱码/番号后缀)
        cleanMagnet: function(m) {
            const s = this.magnetValue(m);
            if (!s) return '';
            // 去掉以 &dn= 或 ?dn= 开头的参数(保留 magnet:?xt=urn:... 主体及其余 tr 参数)
            return s.replace(/([&?])dn=[^&]*/gi, '');
        },

        // 提取磁力链接的 dn 参数值作为展示名;无 dn 时返回 cleanMagnet 后的精简磁力
        magnetName: function(m) {
            const s = this.magnetValue(m);
            if (!s) return '';
            const dm = s.match(/(?:[&?])dn=([^&]*)/i);
            if (dm && dm[1]) {
                try { return decodeURIComponent(dm[1]); }
                catch (e) { return dm[1]; }
            }
            return this.cleanMagnet(s);
        },

        markItem: function(code, status, title = '', remark, magnet) {
            if (!this.initialized) this.init();
            if (!code) return false;
            // remark 规范化:仅接受字符串;对象/数组转为空(防止 [object Object] 写入)
            if (remark != null && typeof remark !== 'string') {
                remark = '';
            }
            const normalizedCode = code.toUpperCase();
            if (!['unmarked', 'favorite', 'watched'].includes(status)) {
                status = 'unmarked';
            }
            const existingIndex = this.data.items.findIndex(item => item.code.toUpperCase() === normalizedCode);
            if (existingIndex >= 0) {
                const cur = this.data.items[existingIndex];
                // 不可变更新:换新对象引用,保证 memo(ItemRow) 能感知状态/备注/磁力变化
                this.data.items[existingIndex] = {
                    ...cur,
                    status: status,
                    ...(title ? { title: title } : {}),
                    ...(remark !== undefined ? { remarks: remark } : {}),
                    ...(magnet !== undefined ? { magnet: this.normMagnets(magnet) } : {}),
                    modifiedDate: new Date().toISOString()
                };
            } else {
                this.data.items.unshift({
                    code: normalizedCode,
                    title: title || normalizedCode,
                    status: status,
                    remarks: remark || '',
                    magnet: this.normMagnets(magnet),
                    tags: [],
                    createdDate: new Date().toISOString(),
                    modifiedDate: new Date().toISOString()
                });
            }
            return this.save();
        },

        setTags: function(code, tags) {
            if (!this.initialized) this.init();
            if (!code) return false;
            const rec = this.findItemRecord(code);
            if (!rec || rec.inTrash) return false;
            const arr = Array.isArray(tags) ? tags : (tags == null ? [] : [tags]);
            const cleaned = [];
            const seen = new Set();
            for (const t of arr) {
                const s = String(t == null ? '' : t).trim();
                if (s && !seen.has(s.toLowerCase())) {
                    seen.add(s.toLowerCase());
                    cleaned.push(s);
                }
            }
            rec.item.tags = cleaned;
            rec.item.modifiedDate = new Date().toISOString();
            return this.save();
        },

        clearAllPreviewCaches: function() {
            if (!this.initialized) this.init();
            let cleared = 0;
            const scan = (list) => {
                for (const it of list) {
                    const ms = this.normMagnets(it && it.magnet);
                    let changed = false;
                    for (const m of ms) {
                        if (m.preview) { delete m.preview; changed = true; cleared++; }
                    }
                    if (changed) { it.magnet = ms; }
                }
            };
            scan(this.data.items);
            scan(this.trash.items);
            if (cleared) this.save();
            try { if (typeof AVWIKI_PREVIEW !== 'undefined') AVWIKI_PREVIEW.clearCache(); } catch (e) {}
            return cleared;
        },

        previewCacheStats: function() {
            if (!this.initialized) this.init();
            let magnets = 0, screenshots = 0, bytes = 0;
            const scan = (list) => {
                for (const it of list) {
                    const ms = this.normMagnets(it && it.magnet);
                    for (const m of ms) {
                        if (m.preview) {
                            magnets++;
                            screenshots += (Array.isArray(m.preview.screenshots) ? m.preview.screenshots.length : 0);
                            try { bytes += JSON.stringify(m.preview).length; } catch (e) {}
                        }
                    }
                }
            };
            scan(this.data.items);
            scan(this.trash.items);
            return { magnets, screenshots, bytes };
        },

        exportData: function(filter = 'all') {
            if (!this.initialized) this.init();
            let exportData = {
                version: "1.0",
                exportDate: new Date().toISOString(),
                filter: filter,
                items: []
            };
            if (filter === 'trash') {
                exportData.items = [...this.trash.items];
            } else if (filter === 'all') {
                exportData.items = [...this.data.items];
            } else {
                exportData.items = this.data.items.filter(item => item.status === filter);
            }
            return exportData;
        },

        importData: function(data, mode = 'merge') {
            if (!this.initialized) this.init();
            try {
                if (!data.items || !Array.isArray(data.items)) {
                    throw new Error('导入的数据格式不正确');
                }
                if (mode === 'replace') {
                    this.data.items = data.items;
                } else if (mode === 'merge') {
                    for (const importedItem of data.items) {
                        if (!importedItem.code) continue;
                        const normalizedCode = importedItem.code.toUpperCase();
                        const existingIndex = this.data.items.findIndex(item =>
                            item.code.toUpperCase() === normalizedCode
                        );
                        if (existingIndex >= 0) {
                            this.data.items[existingIndex] = {
                                ...this.data.items[existingIndex],
                                ...importedItem,
                                code: normalizedCode,
                                modifiedDate: new Date().toISOString()
                            };
                        } else {
                            this.data.items.unshift({
                                ...importedItem,
                                code: normalizedCode,
                                createdDate: importedItem.createdDate || new Date().toISOString(),
                                modifiedDate: new Date().toISOString()
                            });
                        }
                    }
                }
                this.save();
                return { success: true, message: `成功导入 ${data.items.length} 个番号条目` };
            } catch (e) {
                console.error('导入番号数据失败:', e);
                return { success: false, message: '导入失败: ' + e.message };
            }
        }
    };

    const UTILS = {
        showToast: (message, type = 'info') => {
            const evt = new CustomEvent('emh_toast', { detail: { message, type } });
            window.dispatchEvent(evt);
        },
        formatBytes: (bytes) => {
            const n = Number(bytes);
            if (!Number.isFinite(n) || n < 0) return '';
            if (n < 1024) return n + ' B';
            const units = ['KB', 'MB', 'GB', 'TB'];
            let v = n;
            let i = -1;
            do { v /= 1024; i += 1; } while (v >= 1024 && i < units.length - 1);
            return v.toFixed(v >= 100 || i === 0 ? 0 : 1) + ' ' + units[i];
        },
        isSafeHttpUrl: (url) => {
            if (typeof url !== 'string') return false;
            try {
                const u = new URL(url.trim());
                return u.protocol === 'https:' || u.protocol === 'http:';
            } catch (_) { return false; }
        },
        formatTime: (sec) => {
            const s = Number(sec);
            if (!Number.isFinite(s) || s < 0) return '';
            const h = Math.floor(s / 3600);
            const m = Math.floor((s % 3600) / 60);
            const ss = Math.floor(s % 60);
            return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(ss).padStart(2, '0')}` : `${m}:${String(ss).padStart(2, '0')}`;
        }
    };

    // 磁力宫格缩略图懒加载(IntersectionObserver)
    const LAZY = {
        _io: null,
        get io() {
            if (!this._io && typeof IntersectionObserver !== 'undefined') {
                this._io = new IntersectionObserver((entries) => {
                    for (const en of entries) {
                        if (!en.isIntersecting) continue;
                        const el = en.target;
                        const src = el.getAttribute('data-src');
                        if (src) el.style.backgroundImage = 'url("' + src + '")';
                        this._io.unobserve(el);
                    }
                }, { rootMargin: '240px' });
            }
            return this._io;
        },
        observe(el) {
            if (el.dataset.lazy) return;
            el.dataset.lazy = '1';
            if (!this.io) {
                const src = el.getAttribute('data-src');
                if (src) el.style.backgroundImage = 'url("' + src + '")';
                return;
            }
            this.io.observe(el);
        }
    };

    // 通用内联 SVG 图标(线性描边,跟随 currentColor)
    const ICONS = {
        grid: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>'
    };

    // ===== 主题管理(深色优先默认 · data-emh-theme 属性驱动) =====
    const THEME = {
        KEY: 'emh_ui_theme',
        ORDER: ['dark', 'light', 'system'],
        get: function() {
            let t = 'dark';
            try {
                const saved = typeof GM_getValue === 'function' ? GM_getValue(this.KEY) : null;
                if (saved === 'dark' || saved === 'light' || saved === 'system') t = saved;
            } catch (e) {}
            return t;
        },
        apply: function(theme) {
            document.documentElement.setAttribute('data-emh-theme', theme);
        },
        set: function(theme) {
            if (!['dark', 'light', 'system'].includes(theme)) theme = 'dark';
            this.apply(theme);
            try { if (typeof GM_setValue === 'function') GM_setValue(this.KEY, theme); } catch (e) {}
        },
        next: function() {
            const cur = this.get();
            return this.ORDER[(this.ORDER.indexOf(cur) + 1) % this.ORDER.length];
        }
    };

    // ===== standalone 独立页:后台加载同源真实网址(原生 GM 能力,不依赖父页) =====
    const STANDALONE = {
        // 同源真实网址 + #emh-standalone 标记;该页脚本原生运行 → GM 数据/网络齐全,父页可关闭
        standaloneUrl: function() {
            return location.href.split('#')[0] + '#emh-standalone';
        },

        open: function() {
            const url = this.standaloneUrl();
            // 后台加载(不抢焦点);GM_openInTab 不可用时回退 window.open
            try {
                if (typeof GM_openInTab === 'function') {
                    GM_openInTab(url, { active: false, insert: true });
                    UTILS.showToast('已在后台加载番号库独立页', 'success');
                    return;
                }
            } catch (e) {}
            const w = window.open(url, '_blank');
            if (w) UTILS.showToast('已在新标签页打开番号库', 'success');
            else UTILS.showToast('浏览器拦截了新窗口,请允许弹窗后重试', 'warning');
        }
    };

    // whatslink 磁力截图预览(灯箱 + API 解析)
    const MAGNET_PREVIEW = {
        API: 'https://whatslink.info/api/v1/link',
        TIMEOUT: 20000,
        _ready: false,
        _urls: [],
        _index: 0,
        _open: false,
        _touchX: null,
        _els: null,
        _requestSeq: 0,
        _inflight: {},
        _lastErrorIdx: -1,

        _isCacheFresh: function(preview) {
            if (!preview || !preview.fetchedAt) return false;
            const t = new Date(preview.fetchedAt).getTime();
            if (!Number.isFinite(t)) return false;
            const ttl = preview.error ? CONFIG.magnetPreview.errorTtlMs : CONFIG.magnetPreview.cacheTtlMs;
            return (Date.now() - t) < ttl;
        },

        _errPreview: function(err) {
            return CODE_LIBRARY.sanitizeMagnetPreview({
                error: err || '预览失败',
                screenshots: [],
                fetchedAt: new Date().toISOString()
            });
        },

        ensureUi: function() {
            if (this._ready) return;
            if (!document.getElementById('emh-magnet-lb-style')) {
                const st = document.createElement('style');
                st.id = 'emh-magnet-lb-style';
                st.textContent = `
                    #emh-magnet-lightbox {
                        position: fixed; inset: 0; z-index: 10050; display: none;
                        align-items: center; justify-content: center;
                        background: rgba(0,0,0,0.88);
                        backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
                        user-select: none;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    }
                    #emh-magnet-lightbox.open { display: flex; }
                    #emh-magnet-lb-stage {
                        position: relative; width: 100%; height: 100%;
                        display: flex; align-items: center; justify-content: center;
                        padding: 48px 64px 56px; box-sizing: border-box;
                    }
                    #emh-magnet-lb-img {
                        max-width: 100%; max-height: 100%; object-fit: contain;
                        border-radius: 4px; box-shadow: 0 8px 32px rgba(0,0,0,0.45); background: #111;
                        transition: opacity 0.25s ease;
                    }
                    #emh-magnet-lb-img.is-loading { opacity: 0.35; }
                    .emh-lb-btn {
                        position: absolute; top: 50%; transform: translateY(-50%);
                        width: 44px; height: 44px; border: none; border-radius: 50%;
                        background: rgba(255,255,255,0.12); color: #fff; font-size: 28px;
                        line-height: 1; cursor: pointer; display: flex; align-items: center;
                        justify-content: center; z-index: 2; transition: background 0.15s, opacity 0.15s;
                    }
                    .emh-lb-btn:hover { background: rgba(255,255,255,0.22); }
                    .emh-lb-btn:disabled { opacity: 0.25; cursor: default; }
                    #emh-magnet-lb-close:focus-visible,
                    .emh-lb-btn:focus-visible { outline: 2px solid rgba(255,255,255,0.7); outline-offset: 2px; }
                    #emh-magnet-lb-prev { left: 12px; }
                    #emh-magnet-lb-next { right: 12px; }
                    #emh-magnet-lb-close {
                        position: absolute; top: 12px; right: 12px; width: 40px; height: 40px;
                        border: none; border-radius: 50%; background: rgba(255,255,255,0.12);
                        color: #fff; font-size: 24px; cursor: pointer; z-index: 3; line-height: 1;
                        transition: background 0.15s;
                    }
                    #emh-magnet-lb-close:hover { background: rgba(255,255,255,0.22); }
                    #emh-magnet-lb-counter {
                        position: absolute; bottom: 14px; left: 50%; transform: translateX(-50%);
                        color: rgba(255,255,255,0.9); font-size: 13px; font-weight: 600;
                        padding: 6px 12px; border-radius: 999px; background: rgba(0,0,0,0.45);
                        z-index: 2; pointer-events: none;
                    }
                    #emh-magnet-lb-meta {
                        position: absolute; top: 14px; left: 16px; right: 64px;
                        color: rgba(255,255,255,0.85); font-size: 12px; line-height: 1.4;
                        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                        z-index: 2; pointer-events: none;
                    }
                    #emh-magnet-lb-hint {
                        position: absolute; bottom: 14px; right: 16px;
                        color: rgba(255,255,255,0.45); font-size: 11px; z-index: 2; pointer-events: none;
                    }
                    #emh-magnet-lb-thumbs {
                        position: absolute; bottom: 46px; left: 50%; transform: translateX(-50%);
                        display: flex; gap: 6px; max-width: 72%; overflow-x: auto;
                        padding: 6px 8px; border-radius: 10px;
                        background: rgba(0,0,0,0.45); z-index: 2;
                        scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.3) transparent;
                    }
                    #emh-magnet-lb-thumbs::-webkit-scrollbar { height: 4px; }
                    #emh-magnet-lb-thumbs::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.3); border-radius: 2px; }
                    .emh-lb-thumb {
                        flex-shrink: 0; width: 56px; height: 36px; padding: 0; border: none; cursor: pointer;
                        border-radius: 4px; overflow: hidden; opacity: 0.55; transition: opacity 0.15s, box-shadow 0.15s;
                        background: #111;
                    }
                    .emh-lb-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
                    .emh-lb-thumb:hover { opacity: 0.85; }
                    .emh-lb-thumb.active { opacity: 1; box-shadow: 0 0 0 2px var(--emh-primary); }
                    .emh-lb-thumb:focus-visible { outline: 2px solid rgba(255,255,255,0.7); outline-offset: 1px; }
                    @media (max-width: 640px) {
                        #emh-magnet-lb-stage { padding: 44px 52px 52px; }
                        .emh-lb-btn { width: 38px; height: 38px; font-size: 24px; }
                        #emh-magnet-lb-hint { display: none; }
                        #emh-magnet-lb-thumbs { display: none; }
                    }
                `;
                document.head.appendChild(st);
            }
            const root = document.createElement('div');
            root.id = 'emh-magnet-lightbox';
            root.setAttribute('role', 'dialog');
            root.setAttribute('aria-modal', 'true');
            root.setAttribute('aria-label', '磁力截图预览');
            root.innerHTML = `
                <div id="emh-magnet-lb-stage">
                    <div id="emh-magnet-lb-meta"></div>
                    <button type="button" id="emh-magnet-lb-close" aria-label="关闭">×</button>
                    <button type="button" class="emh-lb-btn" id="emh-magnet-lb-prev" aria-label="上一张">‹</button>
                    <img id="emh-magnet-lb-img" alt="Screenshot" referrerpolicy="no-referrer" />
                    <button type="button" class="emh-lb-btn" id="emh-magnet-lb-next" aria-label="下一张">›</button>
                    <div id="emh-magnet-lb-counter"></div>
                    <div id="emh-magnet-lb-thumbs"></div>
                    <div id="emh-magnet-lb-hint">← → 切换 · Esc 关闭</div>
                </div>
            `;
            document.body.appendChild(root);
            this._els = {
                root,
                img: root.querySelector('#emh-magnet-lb-img'),
                prev: root.querySelector('#emh-magnet-lb-prev'),
                next: root.querySelector('#emh-magnet-lb-next'),
                close: root.querySelector('#emh-magnet-lb-close'),
                counter: root.querySelector('#emh-magnet-lb-counter'),
                meta: root.querySelector('#emh-magnet-lb-meta'),
                thumbs: root.querySelector('#emh-magnet-lb-thumbs')
            };
            this._els.prev.addEventListener('click', (e) => { e.stopPropagation(); this.step(-1); });
            this._els.next.addEventListener('click', (e) => { e.stopPropagation(); this.step(1); });
            this._els.close.addEventListener('click', (e) => { e.stopPropagation(); this.close(); });
            root.addEventListener('click', (e) => {
                if (e.target === root || e.target.id === 'emh-magnet-lb-stage') this.close();
            });
            root.addEventListener('touchstart', (e) => {
                if (e.touches && e.touches[0]) this._touchX = e.touches[0].clientX;
            }, { passive: true });
            root.addEventListener('touchend', (e) => {
                if (this._touchX == null || !e.changedTouches || !e.changedTouches[0]) {
                    this._touchX = null;
                    return;
                }
                const dx = e.changedTouches[0].clientX - this._touchX;
                this._touchX = null;
                if (Math.abs(dx) < 40) return;
                this.step(dx < 0 ? 1 : -1);
            }, { passive: true });
            document.addEventListener('keydown', (e) => {
                // 灯箱打开时接管方向键:必须 preventDefault 阻止浏览器前进/后退
                const visible = this._open || (this._els && this._els.root && this._els.root.classList.contains('open'));
                if (!visible) return;
                const k = e.key;
                if (k === 'Escape') { e.preventDefault(); e.stopPropagation(); this.close(); }
                else if (k === 'ArrowLeft') { e.preventDefault(); e.stopPropagation(); this.step(-1); }
                else if (k === 'ArrowRight') { e.preventDefault(); e.stopPropagation(); this.step(1); }
            }, { passive: false });
            this._ready = true;
        },

        _update: function() {
            const { img, prev, next, counter } = this._els;
            const total = this._urls.length;
            if (!total) {
                counter.textContent = '';
                prev.disabled = true;
                next.disabled = true;
                img.classList.remove('is-loading');
                img.removeAttribute('src');
                img.alt = 'Screenshot';
                if (this._els.thumbs) this._els.thumbs.innerHTML = '';
                return;
            }
            const idx = this._index;
            const url = this._urls[idx];
            counter.textContent = (idx + 1) + ' / ' + total;
            prev.disabled = total <= 1;
            next.disabled = total <= 1;
            if (img.getAttribute('src') === url) {
                img.classList.remove('is-loading');
                return;
            }
            img.classList.add('is-loading');
            img.onload = () => img.classList.remove('is-loading');
            img.onerror = () => {
                img.classList.remove('is-loading');
                if (total > 1 && idx !== this._lastErrorIdx) {
                    // 跳过失效截图自动切到下一张;同一张不重复跳过,避免坏图死循环
                    this._lastErrorIdx = idx;
                    this.step(1);
                    return;
                }
                img.removeAttribute('src');
                img.alt = '图片加载失败';
            };
            img.alt = 'Screenshot ' + (idx + 1);
            img.src = url;
            [idx - 1, idx + 1].forEach((n) => {
                if (n >= 0 && n < total) {
                    const pre = new Image();
                    pre.referrerPolicy = 'no-referrer';
                    pre.src = this._urls[n];
                }
            });
            if (this._els.thumbs) {
                const thumbs = this._els.thumbs;
                thumbs.innerHTML = '';
                this._urls.forEach((u, i) => {
                    const b = document.createElement('button');
                    b.type = 'button';
                    b.className = 'emh-lb-thumb' + (i === idx ? ' active' : '');
                    b.title = '跳转到截图 ' + (i + 1);
                    const im = new Image();
                    im.referrerPolicy = 'no-referrer';
                    im.alt = '';
                    im.src = u;
                    b.appendChild(im);
                    b.addEventListener('click', (e) => { e.stopPropagation(); this._index = i; this._update(); });
                    thumbs.appendChild(b);
                });
                thumbs.scrollLeft = Math.max(0, idx * 62 - thumbs.clientWidth / 2);
            }
        },

        open: function(urls, startIndex, metaText) {
            this.ensureUi();
            const list = (Array.isArray(urls) ? urls : []).filter(UTILS.isSafeHttpUrl);
            if (!list.length) {
                UTILS.showToast('暂无可用截图', 'warning');
                return;
            }
            let idx = Number(startIndex);
            if (!Number.isFinite(idx) || idx < 0) idx = 0;
            if (idx >= list.length) idx = list.length - 1;
            this._urls = list;
            this._index = idx;
            this._open = true;
            this._lastErrorIdx = -1;
            this._els.meta.textContent = metaText || '';
            this._els.root.classList.add('open');
            this._update();
        },

        close: function() {
            if (!this._open || !this._els) return;
            this._open = false;
            this._els.root.classList.remove('open');
            this._urls = [];
            this._index = 0;
            this._lastErrorIdx = -1;
            this._els.img.removeAttribute('src');
            this._els.img.alt = 'Screenshot';
            this._els.img.classList.remove('is-loading');
            this._els.counter.textContent = '';
            this._els.meta.textContent = '';
            if (this._els.thumbs) this._els.thumbs.innerHTML = '';
        },

        step: function(delta) {
            if (!this._open || !this._urls.length) return;
            const total = this._urls.length;
            this._index = (this._index + delta + total) % total;
            this._update();
        },

        parseApiResponse: function(rawText) {
            let data;
            try { data = JSON.parse(rawText); }
            catch (_) { return { ok: false, error: '接口返回非 JSON' }; }
            if (data == null || typeof data !== 'object' || Array.isArray(data)) {
                return { ok: false, error: '接口数据结构异常' };
            }
            const apiError = typeof data.error === 'string' ? data.error.trim() : '';
            if (apiError) return { ok: false, error: apiError };
            const preview = CODE_LIBRARY.sanitizeMagnetPreview({
                name: data.name || data.title || '',
                type: data.type || '',
                file_type: data.file_type || data.type || '',
                size: data.size,
                count: data.count,
                screenshots: data.screenshots,
                fetchedAt: new Date().toISOString(),
                error: ''
            });
            if (!preview) return { ok: false, error: '预览数据无效' };
            return { ok: true, preview };
        },

        fetchAndCache: function(code, magnetId, magnetValue, opts) {
            const options = opts || {};
            const force = !!options.force;
            const onStart = typeof options.onStart === 'function' ? options.onStart : null;
            const onDone = typeof options.onDone === 'function' ? options.onDone : null;

            const magnet = String(magnetValue || '').trim();
            if (!magnet || magnet.indexOf('magnet:') !== 0) {
                UTILS.showToast('无效的磁力链接', 'error');
                if (onDone) onDone(new Error('invalid magnet'));
                return;
            }
            if (typeof GM_xmlhttpRequest !== 'function') {
                UTILS.showToast('当前环境不支持跨域请求', 'error');
                if (onDone) onDone(new Error('no gm'));
                return;
            }

            // 全局递增序号:并发竞态下仅“最新一次用户请求”能打开灯箱/弹提示
            const mySeq = ++this._requestSeq;
            const key = String(code) + '::' + String(magnetId);

            // 命中缓存:优先展示旧截图;若过期则在展示的同时后台刷新
            const cached = !force ? CODE_LIBRARY.getMagnetPreview(code, magnetId) : null;
            if (cached && Array.isArray(cached.screenshots) && cached.screenshots.length) {
                const urls = cached.screenshots.map(s => s.screenshot || s.url || '').filter(Boolean);
                const metaParts = [cached.name, cached.fileType || cached.type, UTILS.formatBytes(cached.size)].filter(Boolean);
                this.open(urls, 0, metaParts.join(' · '));
                if (onDone) onDone(null, cached);
                if (this._isCacheFresh(cached)) return;
            } else if (cached && cached.error && !force && this._isCacheFresh(cached)) {
                // 错误缓存仍在有效期内:直接提示并跳过重复请求
                UTILS.showToast(cached.error || '上次预览失败', 'warning');
                if (onDone) onDone(new Error(cached.error || 'cached error'));
                return;
            }

            // 并发去重:同一磁力已有在途请求时复用结果,不重复发起 HTTP
            const existing = this._inflight[key];
            if (existing && !force) {
                existing.latestSeq = Math.max(existing.latestSeq, mySeq);
                existing.p.then((r) => {
                    if (existing.superseded) {
                        if (onDone) onDone(new Error('superseded'));
                        return;
                    }
                    if (onDone) onDone((r && r.error) ? new Error(r.error) : null, (r && r.preview) || null);
                });
                return;
            }
            if (existing && force) existing.superseded = true;

            if (onStart) onStart();
            UTILS.showToast('正在获取截图预览…', 'info');

            // 预先登记在途条目:请求回调据此判断自己是否仍是“最新且未被作废”
            const entry = this._inflight[key] = { p: null, latestSeq: mySeq, superseded: false };
            const p = new Promise((resolve) => {
                const finish = (errText, preview, noShotToast) => {
                    const isLatest = !entry.superseded && this._inflight[key] === entry && entry.latestSeq === this._requestSeq;
                    const urls = preview ? preview.screenshots.map(s => s.screenshot || s.url || '').filter(Boolean) : [];
                    if (isLatest) {
                        if (urls.length) {
                            const metaParts = [preview.name, preview.fileType || preview.type, UTILS.formatBytes(preview.size)].filter(Boolean);
                            this.open(urls, 0, metaParts.join(' · '));
                            if (!errText) UTILS.showToast('截图已缓存', 'success');
                        } else if (noShotToast) {
                            UTILS.showToast('接口未返回截图', 'warning');
                        } else if (errText) {
                            UTILS.showToast(errText, 'error');
                        }
                    }
                    // 仅在最新请求时持久化错误,避免旧请求的失败污染新结果
                    if (isLatest && errText && !preview) {
                        CODE_LIBRARY.setMagnetPreview(code, magnetId, this._errPreview(errText));
                    }
                    if (onDone) onDone(errText ? new Error(errText) : null, preview || null);
                    resolve({
                        error: errText,
                        urls,
                        meta: [preview && preview.name, preview && (preview.fileType || preview.type), UTILS.formatBytes(preview && preview.size)].filter(Boolean).join(' · '),
                        preview: preview || null
                    });
                };
                const url = this.API + '?url=' + encodeURIComponent(magnet);
                GM_xmlhttpRequest({
                    method: 'GET',
                    url,
                    timeout: this.TIMEOUT,
                    headers: { 'Accept': 'application/json, text/plain, */*' },
                    onload: (res) => {
                        const status = res && typeof res.status === 'number' ? res.status : 0;
                        if (status < 200 || status >= 300) { finish('预览失败:HTTP ' + status, null); return; }
                        const parsed = this.parseApiResponse(res.responseText || '');
                        if (!parsed.ok) { finish(parsed.error || '预览失败', null); return; }
                        CODE_LIBRARY.setMagnetPreview(code, magnetId, parsed.preview);
                        if (!parsed.preview || !parsed.preview.screenshots || !parsed.preview.screenshots.length) {
                            finish('', parsed.preview, true);
                            return;
                        }
                        finish('', parsed.preview, false);
                    },
                    onerror: () => finish('网络请求失败', null),
                    ontimeout: () => finish('请求超时', null)
                });
            });
            entry.p = p;
            p.then(() => { if (this._inflight[key] === entry) delete this._inflight[key]; });
        }
    };

    // avwikidb 作品页宫格截图预览(按番号抓取,独立于 whatslink 磁力预览)
    const AVWIKI_PREVIEW = {
        BASE: 'https://avwikidb.com/work/',
        TIMEOUT: 20000,
        _requestSeq: 0,
        _inflight: {},

        _cacheKey: 'emh_avwiki_cache',

        _loadCache: function() {
            try {
                const raw = GM_getValue(this._cacheKey);
                return (raw && typeof raw === 'object') ? raw : {};
            } catch (e) {}
            return {};
        },

        _saveCache: function(cache) {
            try { GM_setValue(this._cacheKey, cache); } catch (e) {}
        },

        _isFresh: function(rec, force) {
            if (force || !rec || !rec.fetchedAt) return false;
            const t = new Date(rec.fetchedAt).getTime();
            if (!Number.isFinite(t)) return false;
            const ttl = rec.error ? CONFIG.magnetPreview.errorTtlMs : CONFIG.magnetPreview.cacheTtlMs;
            return (Date.now() - t) < ttl;
        },

        codeUrl: function(code) {
            return this.BASE + encodeURIComponent(String(code || '').trim().toUpperCase()) + '/';
        },

        // 从 avwikidb work 页 HTML 提取宫格截图:优先 dmm 的 {cid}jp-N 场景图(#gallery),
        // 找不到时回退页内其它 jp/ps/pl 作品图;排除 logo/avatar/icon 等静态资源
        parseHtml: function(html) {
            const all = [];
            const seen = new Set();
            const RE = /<img\b[^>]*\bsrc=["']([^"']+)["'][^>]*>/gi;
            let m;
            while ((m = RE.exec(html)) !== null) {
                let raw = m[1] ? m[1].trim() : '';
                if (!raw) continue;
                // 反转义 HTML 实体(src 里的 &amp; 等)
                raw = raw.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
                let u = raw;
                // 相对路径拼成绝对
                if (/^\//.test(u)) u = 'https://avwikidb.com' + u;
                if (!UTILS.isSafeHttpUrl(u)) continue;
                if (/\.(?:svg)($|\?)/i.test(u)) continue;
                if (/logo|\.x\.svg|\/x\.svg|avatar|icon/i.test(u)) continue;
                const clean = u.split('?')[0];
                if (!/\.(?:jpg|png|jpeg|webp)$/i.test(clean)) continue;
                if (seen.has(clean)) continue;
                seen.add(clean);
                all.push(u);
                if (all.length >= 48) break;
            }
            // 优先 jp-N 场景截图(宫格),保持出现顺序
            const gallery = all.filter(u => /\/[^/]*?jp-\d+\.(?:jpg|png|jpeg|webp)(?:\?|$)/i.test(u));
            return (gallery.length ? gallery : all).slice(0, 48);
        },

        parseTitle: function(html) {
            const m = html.match(/<title[^>]*>([^<]*)<\/title>/i);
            if (m && m[1]) {
                const t = m[1].replace(/\s+/g, ' ').trim();
                return t ? t.slice(0, 120) : '';
            }
            return '';
        },

        fetchAndShow: function(code, opts) {
            const options = opts || {};
            const force = !!options.force;
            const onDone = typeof options.onDone === 'function' ? options.onDone : null;
            const itemCode = String(code || '').trim().toUpperCase();
            if (!itemCode) { UTILS.showToast('无效番号', 'error'); if (onDone) onDone(new Error('invalid code')); return; }
            if (typeof GM_xmlhttpRequest !== 'function') {
                UTILS.showToast('当前环境不支持跨域请求', 'error');
                if (onDone) onDone(new Error('no gm'));
                return;
            }

            const mySeq = ++this._requestSeq;
            const cache = this._loadCache();
            const rec = cache[itemCode];

            if (this._isFresh(rec, force)) {
                if (rec && rec.error) {
                    // 错误缓存仍在有效期:直接提示并跳过重复请求
                    UTILS.showToast(rec.error || '上次预览失败', 'warning');
                    if (onDone) onDone(new Error(rec.error || 'cached error'));
                    return;
                }
                if (Array.isArray(rec && rec.screenshots) && rec.screenshots.length) {
                    const urls = rec.screenshots.map(s => s.screenshot || s.url || '').filter(Boolean);
                    MAGNET_PREVIEW.open(urls, 0, [rec.name, 'AVWikiDB'].filter(Boolean).join(' · '));
                    if (onDone) onDone(null, rec);
                    return;
                }
            }

            const existing = this._inflight[itemCode];
            if (existing && !force) {
                existing.latestSeq = Math.max(existing.latestSeq, mySeq);
                existing.p.then((r) => {
                    if (existing.superseded) { if (onDone) onDone(new Error('superseded')); return; }
                    if (onDone) onDone(r.error ? new Error(r.error) : null, r.preview || null);
                });
                return;
            }
            if (existing && force) existing.superseded = true;

            const entry = this._inflight[itemCode] = { p: null, latestSeq: mySeq, superseded: false };

            UTILS.showToast('正在获取 AVWikiDB 截图…', 'info');

            const failCache = (err) => {
                cache[itemCode] = { error: err, screenshots: [], fetchedAt: new Date().toISOString() };
                this._saveCache(cache);
            };

            const p = new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: this.codeUrl(itemCode),
                    timeout: this.TIMEOUT,
                    headers: { 'Accept': 'text/html,application/xhtml+xml' },
                    onload: (res) => {
                        const status = res && typeof res.status === 'number' ? res.status : 0;
                        const html = (res && res.responseText) || '';
                        if (status < 200 || status >= 300) {
                            const err = 'AVWikiDB 请求失败:HTTP ' + status;
                            failCache(err);
                            UTILS.showToast(err, 'error');
                            resolve({ error: err, preview: null });
                            return;
                        }
                        const urls = this.parseHtml(html);
                        if (!urls.length) {
                            const err = 'AVWikiDB 未收录该番号或页面无宫格截图';
                            failCache(err);
                            UTILS.showToast(err, 'warning');
                            resolve({ error: err, preview: null });
                            return;
                        }
                        const name = this.parseTitle(html);
                        const preview = CODE_LIBRARY.sanitizeMagnetPreview({
                            name: name || itemCode,
                            type: 'AVWikiDB',
                            screenshots: urls,
                            fetchedAt: new Date().toISOString(),
                            error: ''
                        });
                        cache[itemCode] = preview;
                        this._saveCache(cache);
                        const isLatest = !entry.superseded && this._inflight[itemCode] === entry && entry.latestSeq === this._requestSeq;
                        if (isLatest) MAGNET_PREVIEW.open(urls, 0, [preview.name, 'AVWikiDB'].filter(Boolean).join(' · '));
                        UTILS.showToast('已获取 AVWikiDB 截图 ' + urls.length + ' 张', 'success');
                        resolve({ error: '', preview });
                    },
                    onerror: () => {
                        const err = 'AVWikiDB 网络请求失败';
                        failCache(err);
                        UTILS.showToast(err, 'error');
                        resolve({ error: err, preview: null });
                    },
                    ontimeout: () => {
                        const err = 'AVWikiDB 请求超时';
                        failCache(err);
                        UTILS.showToast(err, 'error');
                        resolve({ error: err, preview: null });
                    }
                });
            });
            entry.p = p;
            p.then((r) => {
                if (onDone) onDone(r.error ? new Error(r.error) : null, r.preview || null);
                if (this._inflight[itemCode] === entry) delete this._inflight[itemCode];
            });
        },

        // 清除全部 avwikidb 缓存
        clearCache: function() {
            try { GM_setValue(this._cacheKey, {}); } catch (e) {}
        }
    };

    function waitForElement(selector, callback, timeout = 7000) {
        const startTime = Date.now();
        const intervalId = setInterval(() => {
            const elements = document.querySelectorAll(selector);
            if (elements.length > 0) {
                clearInterval(intervalId);
                callback(elements[0]);
            } else if (Date.now() - startTime > timeout) {
                clearInterval(intervalId);
                console.warn(`EMH: Element "${selector}" not found within ${timeout}ms.`);
                callback(null);
            }
        }, 200);
    }

    function createCodeStatusIndicator(container, code) {
        if (!code || !container) return null;
        if (!CODE_LIBRARY.initialized) CODE_LIBRARY.init();
        const currentStatus = CODE_LIBRARY.getStatus(code);
        const statusIndicator = document.createElement('div');
        statusIndicator.className = 'emh-code-status-indicator';
        statusIndicator.dataset.code = code;
        statusIndicator.dataset.status = currentStatus;
        const statusColors = CONFIG.codeManager.statusColors;
        statusIndicator.style.backgroundColor = statusColors[currentStatus] || statusColors.unmarked;
        let statusText = '未标记';
        if (currentStatus === 'favorite') statusText = '已关注';
        if (currentStatus === 'watched') statusText = '已看过';
        if (currentStatus === 'watched') {
            statusIndicator.title = `状态: ${statusText} (请在番号库中修改状态)`;
            statusIndicator.style.cursor = 'default';
        } else {
            statusIndicator.title = `状态: ${statusText} (点击${currentStatus === 'favorite' ? '取消' : ''}关注)`;
            statusIndicator.style.cursor = 'pointer';
        }
        statusIndicator.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            const curStatus = CODE_LIBRARY.getStatus(code);
            if (curStatus === 'watched') {
                UTILS.showToast('已看状态请在番号库中修改', 'warning');
                return;
            }
            const newStatus = curStatus === 'favorite' ? 'unmarked' : 'favorite';
            CODE_LIBRARY.markItem(code, newStatus);
            updateCodeStatusIndicators();
            UTILS.showToast(`番号 ${code} ${newStatus === 'favorite' ? '已关注' : '已取消关注'}`, 'success');
        });
        container.appendChild(statusIndicator);
        return statusIndicator;
    }

    function updateCodeStatusIndicators() {
        document.querySelectorAll('.emh-code-status-indicator').forEach(indicator => {
            const code = indicator.dataset.code;
            if (!code) return;
            const currentStatus = CODE_LIBRARY.getStatus(code);
            indicator.dataset.status = currentStatus;
            const statusColors = CONFIG.codeManager.statusColors;
            indicator.style.backgroundColor = statusColors[currentStatus] || statusColors.unmarked;
            let statusText = '未标记';
            if (currentStatus === 'favorite') statusText = '已关注';
            if (currentStatus === 'watched') statusText = '已看过';
            if (currentStatus === 'watched') {
                indicator.title = `状态: ${statusText} (请在番号库中修改状态)`;
                indicator.style.cursor = 'default';
            } else {
                indicator.title = `状态: ${statusText} (点击${currentStatus === 'favorite' ? '取消' : ''}关注)`;
                indicator.style.cursor = 'pointer';
            }
        });
    }

    const SITE_HANDLERS = {
        javgg: {
            isMatch: () => document.domain.includes('javgg'),
            targetSelector: 'article.item.movies .data, h1.post-title, .videoinfo .meta',
            process: (targetElement) => {
                if (document.querySelector("article.item.movies")) {
                    const sidebar = document.querySelector("#contenedor > div > div.sidebar.right.scrolling");
                    if (sidebar) sidebar.remove();

                    const linkProviders = [
                        { code: "njav", url: CONFIG.alternateUrl.av123 + "$p", target: "_blank" },
                        { code: "jable", url: CONFIG.alternateUrl.jable + "$p/", target: "_blank" },
                        { code: "1cili", url: CONFIG.alternateUrl.cili1 + "$p", target: "_blank" }
                    ];

                    document.querySelectorAll("article.item.movies").forEach(entry => {
                        const dataElement = entry.querySelector(".data");
                        const anchorTag = dataElement ? dataElement.querySelector("h3 a") : null;
                        if (anchorTag) {
                            const videoCode = anchorTag.textContent.trim();
                            if (!videoCode) return;
                            if (dataElement.querySelector('.emh-javgg-controls')) return;

                            const statusContainer = document.createElement('div');
                            statusContainer.className = 'emh-code-status-container';
                            statusContainer.style.display = 'inline-block';
                            statusContainer.style.marginLeft = '10px';
                            createCodeStatusIndicator(statusContainer, videoCode);
                            anchorTag.parentNode.appendChild(statusContainer);

                            const controlsDiv = document.createElement('div');
                            controlsDiv.className = 'emh-javgg-controls';
                            linkProviders.forEach(provider => {
                                const newAnchorTag = document.createElement("a");
                                newAnchorTag.href = provider.url.replace("$p", videoCode);
                                newAnchorTag.target = provider.target;
                                newAnchorTag.textContent = provider.code;
                                controlsDiv.appendChild(newAnchorTag);
                            });
                            dataElement.appendChild(controlsDiv);
                        }
                    });
                }
            }
        }
    };

    // ===== Preact 依赖加载(多 CDN fallback) =====
    const DEPS = {
        preact: [
            'https://cdn.jsdelivr.net/npm/[email protected]/dist/preact.umd.js',
            'https://cdnjs.cloudflare.com/ajax/libs/preact/10.19.6/preact.umd.js'
        ],
        hooks: [
            'https://cdn.jsdelivr.net/npm/[email protected]/hooks/dist/hooks.umd.js',
            'https://cdnjs.cloudflare.com/ajax/libs/preact/10.19.6/hooks.umd.js'
        ],
        htm: [
            'https://cdn.jsdelivr.net/npm/[email protected]/dist/htm.umd.js',
            'https://cdnjs.cloudflare.com/ajax/libs/htm/3.1.1/htm.umd.js'
        ]
    };

    function loadScriptOnce(src) {
        return new Promise((resolve, reject) => {
            const existing = document.querySelector(`script[data-emh-src="${src}"]`);
            if (existing) { resolve(); return; }
            const script = document.createElement('script');
            script.src = src;
            script.dataset.emhSrc = src;
            script.onload = () => resolve();
            script.onerror = () => reject(new Error('加载失败: ' + src));
            document.head.appendChild(script);
        });
    }

    function getGlobal(name) {
        try {
            if (typeof unsafeWindow !== 'undefined' && unsafeWindow[name]) return unsafeWindow[name];
        } catch (e) {}
        try {
            if (window[name]) return window[name];
        } catch (e) {}
        return null;
    }

    async function ensurePreact() {
        if (getGlobal('preact') && getGlobal('preactHooks') && getGlobal('htm')) return true;
        const targets = [
            { name: 'preact', check: () => !!getGlobal('preact'), urls: DEPS.preact },
            { name: 'hooks', check: () => !!getGlobal('preactHooks'), urls: DEPS.hooks },
            { name: 'htm', check: () => !!getGlobal('htm'), urls: DEPS.htm }
        ];
        for (const dep of targets) {
            if (dep.check()) continue;
            let loaded = false;
            for (const url of dep.urls) {
                try { await loadScriptOnce(url); loaded = true; break; }
                catch (e) { console.warn(`EMH: ${dep.name} ${url}`, e.message); }
            }
            if (!loaded) { console.error(`EMH: 无法加载 ${dep.name}`); return false; }
        }
        return true;
    }

    function injectCoreStyles() {
        const id = 'emh-core-styles';
        if (document.getElementById(id)) return;
        const style = document.createElement('style');
        style.id = id;
        style.textContent = `
            :root,
            :root[data-emh-theme="dark"] {
                /* Linear 系 — 深色默认(暖黑基底) */
                --emh-primary: #5e6ad2;
                --emh-primary-hover: #6673e0;
                --emh-primary-soft: rgba(94, 106, 210, 0.14);
                --emh-primary-softer: rgba(94, 106, 210, 0.08);
                --emh-on-primary: #ffffff;
                /* 中性灰阶 */
                --emh-text: #f7f8f8;
                --emh-text-secondary: #9ca3af;
                --emh-text-muted: #6b7280;
                --emh-border: rgba(255, 255, 255, 0.06);
                --emh-border-strong: rgba(255, 255, 255, 0.12);
                --emh-bg: #08090a;
                --emh-surface: #16171c;
                --emh-surface-raised: #26272e;
                /* 极淡阴影(无发光/无彩色) */
                --emh-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
                --emh-shadow-md: 0 2px 8px rgba(0, 0, 0, 0.35), 0 1px 2px rgba(0, 0, 0, 0.25);
                --emh-shadow-lg:
                    0 0 0 1px rgba(255, 255, 255, 0.06),
                    0 24px 48px -12px rgba(0, 0, 0, 0.5),
                    0 8px 16px -8px rgba(0, 0, 0, 0.4);
                /* 语义状态色 */
                --emh-danger: #f87171;
                --emh-danger-soft: rgba(248, 113, 113, 0.14);
                --emh-success: #34d399;
                --emh-success-soft: rgba(52, 211, 153, 0.14);
                --emh-warning: #fbbf24;
                --emh-warning-soft: rgba(251, 191, 36, 0.14);
                --emh-on-solid: #18181b;
                /* 按钮灰阶 */
                --emh-btn-bg: rgba(255, 255, 255, 0.06);
                --emh-btn-hover: rgba(255, 255, 255, 0.12);
                --emh-btn-active: rgba(255, 255, 255, 0.18);
                --emh-focus-ring: rgba(94, 106, 210, 0.35);
                --emh-overlay: rgba(0, 0, 0, 0.6);
                --emh-radius: 12px;
                --emh-radius-sm: 6px;
                --emh-glass: rgba(22, 23, 28, 0.8);
                --emh-ease: cubic-bezier(0.22, 1, 0.36, 1);
                --emh-font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', Menlo, ui-monospace, monospace;
            }
            :root[data-emh-theme="light"] {
                --emh-primary: #5e6ad2;
                --emh-primary-hover: #4f5cc8;
                --emh-primary-soft: rgba(94, 106, 210, 0.1);
                --emh-primary-softer: rgba(94, 106, 210, 0.06);
                --emh-on-primary: #ffffff;
                --emh-text: #18181b;
                --emh-text-secondary: #52525b;
                --emh-text-muted: #a1a1aa;
                --emh-border: rgba(0, 0, 0, 0.08);
                --emh-border-strong: rgba(0, 0, 0, 0.14);
                --emh-bg: #fafafa;
                --emh-surface: #ffffff;
                --emh-surface-raised: #f4f4f5;
                --emh-shadow-sm: 0 1px 2px rgba(24, 24, 27, 0.04);
                --emh-shadow-md: 0 2px 8px rgba(24, 24, 27, 0.06), 0 1px 2px rgba(24, 24, 27, 0.04);
                --emh-shadow-lg:
                    0 0 0 1px rgba(24, 24, 27, 0.04),
                    0 24px 48px -12px rgba(24, 24, 27, 0.18),
                    0 8px 16px -8px rgba(24, 24, 27, 0.12);
                --emh-danger: #dc2626;
                --emh-danger-soft: rgba(220, 38, 38, 0.1);
                --emh-success: #059669;
                --emh-success-soft: rgba(5, 150, 105, 0.12);
                --emh-warning: #d97706;
                --emh-warning-soft: rgba(217, 119, 6, 0.12);
                --emh-on-solid: #ffffff;
                --emh-btn-bg: rgba(24, 24, 27, 0.05);
                --emh-btn-hover: rgba(24, 24, 27, 0.09);
                --emh-btn-active: rgba(24, 24, 27, 0.13);
                --emh-focus-ring: rgba(94, 106, 210, 0.3);
                --emh-overlay: rgba(24, 24, 27, 0.35);
                --emh-radius: 12px;
                --emh-radius-sm: 6px;
                --emh-glass: rgba(255, 255, 255, 0.72);
                --emh-ease: cubic-bezier(0.22, 1, 0.36, 1);
                --emh-font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', Menlo, ui-monospace, monospace;
            }
            /* 主题跟随系统(浅色系统)— 仅当用户选择 system 时生效,默认深色优先 */
            @media (prefers-color-scheme: light) {
                :root[data-emh-theme="system"] {
                    --emh-primary: #5e6ad2;
                    --emh-primary-hover: #4f5cc8;
                    --emh-primary-soft: rgba(94, 106, 210, 0.1);
                    --emh-primary-softer: rgba(94, 106, 210, 0.06);
                    --emh-on-primary: #ffffff;
                    --emh-text: #18181b;
                    --emh-text-secondary: #52525b;
                    --emh-text-muted: #a1a1aa;
                    --emh-border: rgba(0, 0, 0, 0.08);
                    --emh-border-strong: rgba(0, 0, 0, 0.14);
                    --emh-bg: #fafafa;
                    --emh-surface: #ffffff;
                    --emh-surface-raised: #f4f4f5;
                    --emh-shadow-sm: 0 1px 2px rgba(24, 24, 27, 0.04);
                    --emh-shadow-md: 0 2px 8px rgba(24, 24, 27, 0.06), 0 1px 2px rgba(24, 24, 27, 0.04);
                    --emh-shadow-lg:
                        0 0 0 1px rgba(24, 24, 27, 0.04),
                        0 24px 48px -12px rgba(24, 24, 27, 0.18),
                        0 8px 16px -8px rgba(24, 24, 27, 0.12);
                    --emh-danger: #dc2626;
                    --emh-danger-soft: rgba(220, 38, 38, 0.1);
                    --emh-success: #059669;
                    --emh-success-soft: rgba(5, 150, 105, 0.12);
                    --emh-warning: #d97706;
                    --emh-warning-soft: rgba(217, 119, 6, 0.12);
                    --emh-on-solid: #ffffff;
                    --emh-btn-bg: rgba(24, 24, 27, 0.05);
                    --emh-btn-hover: rgba(24, 24, 27, 0.09);
                    --emh-btn-active: rgba(24, 24, 27, 0.13);
                    --emh-focus-ring: rgba(94, 106, 210, 0.3);
                    --emh-overlay: rgba(24, 24, 27, 0.35);
                    --emh-radius: 12px;
                    --emh-radius-sm: 6px;
                    --emh-glass: rgba(255, 255, 255, 0.72);
                    --emh-ease: cubic-bezier(0.22, 1, 0.36, 1);
                    --emh-font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', Menlo, ui-monospace, monospace;
                }
            }
            .emh-code-manager-toggle {
                position: fixed; bottom: 24px; right: 24px; z-index: 10000;
                display: inline-flex; align-items: center; gap: 8px;
                padding: 10px 20px; border-radius: 999px;
                background: var(--emh-primary);
                color: var(--emh-on-primary); border: 1px solid transparent;
                cursor: pointer; font-size: 14px; font-weight: 600;
                letter-spacing: 0.1px;
                box-shadow: 0 4px 16px var(--emh-primary-soft), var(--emh-shadow-md);
                transition: background-color 0.15s ease, transform 0.1s ease, box-shadow 0.15s ease;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            }
            .emh-code-manager-toggle:hover {
                background-color: var(--emh-primary-hover);
                transform: translateY(-1px);
                box-shadow: 0 6px 20px var(--emh-primary-soft), var(--emh-shadow-md);
            }
            .emh-code-manager-toggle:active { transform: scale(0.96); }
            .emh-code-manager-toggle:focus-visible { outline: 2px solid var(--emh-on-primary); outline-offset: 2px; }
            .emh-javgg-controls {
                margin-top: 6px; display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: center;
                margin-left: 10px; vertical-align: middle; padding: 4px 6px;
                background-color: var(--emh-btn-bg); border-radius: var(--emh-radius-sm);
            }
            .emh-javgg-controls a {
                padding: 3px 10px; border-radius: 6px; font-size: 12px; font-weight: 500;
                color: var(--emh-text-secondary); text-decoration: none;
                transition: background 0.15s, color 0.15s;
            }
            .emh-javgg-controls a:hover { background: var(--emh-primary-soft); color: var(--emh-primary); }
            .btn {
                appearance: none; -webkit-appearance: none;
                display: inline-flex; align-items: center; justify-content: center; gap: 6px;
                padding: 8px 14px; border-radius: 9px;
                text-decoration: none; font-size: 13px; font-weight: 600;
                transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out, transform 0.1s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
                cursor: pointer; border: 1px solid transparent; white-space: nowrap;
                line-height: 1.2; font-family: inherit; box-sizing: border-box;
                color: var(--emh-text);
            }
            .btn:active { transform: scale(0.92); }
            .btn:focus-visible { outline: 2px solid var(--emh-primary); outline-offset: 2px; }
            /* 图标按钮:默认仅图标,hover/焦点展开文字标签 */
            .btn.emh-expand { gap: 0; padding: 8px 10px; }
            .btn .emh-expand-label {
                display: inline-block; max-width: 0; overflow: hidden; white-space: nowrap;
                opacity: 0; vertical-align: middle;
                transition: max-width 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.15s ease, margin-left 0.22s cubic-bezier(0.22, 1, 0.36, 1);
            }
            .btn.emh-expand:hover .emh-expand-label,
            .btn.emh-expand:focus-visible .emh-expand-label {
                max-width: 12em; opacity: 1; margin-left: 6px;
            }
            @media (prefers-reduced-motion: reduce) {
                .btn .emh-expand-label { transition: none !important; }
                .btn.emh-expand:hover .emh-expand-label,
                .btn.emh-expand:focus-visible .emh-expand-label { max-width: 12em; opacity: 1; margin-left: 6px; }
            }
            .my-btn-primary { background: var(--emh-primary-soft); color: var(--emh-primary); }
            .my-btn-primary:hover { background: var(--emh-primary); color: var(--emh-on-primary); box-shadow: 0 2px 8px var(--emh-primary-soft); }
            .my-btn-success { background: var(--emh-success-soft); color: var(--emh-success); }
            .my-btn-success:hover { background: var(--emh-success); color: var(--emh-on-solid); }
            .my-btn-danger { background: var(--emh-danger-soft); color: var(--emh-danger); }
            .my-btn-danger:hover { background: var(--emh-danger); color: var(--emh-on-solid); }
            .btn-outline { background: var(--emh-surface); color: var(--emh-text-secondary); border-color: var(--emh-border); }
            .btn-outline:hover { background: var(--emh-btn-hover); color: var(--emh-text); border-color: var(--emh-border-strong); }
            .emh-code-status-indicator {
                width: 16px; height: 16px; border-radius: 50%; cursor: pointer; margin-right: 8px;
                transition: transform 0.2s ease, box-shadow 0.2s ease; position: relative;
                border: 1px solid var(--emh-border);
                display: inline-block; vertical-align: middle;
            }
            .emh-code-status-indicator:hover { transform: scale(1.2); box-shadow: var(--emh-shadow-sm); }
            .emh-code-status-indicator[data-status="favorite"] { background-color: var(--emh-danger); }
            .emh-code-status-indicator[data-status="watched"] { background-color: var(--emh-success); }
            .emh-code-status-indicator[data-status="unmarked"] { background-color: var(--emh-text-muted); }
            #custom-toast-container { position: fixed; top: 70px; right: 20px; z-index: 10060; display: flex; flex-direction: column; gap: 8px; align-items: flex-end; pointer-events: none; }
            .custom-toast {
                padding: 10px 16px; border-radius: 12px; color: var(--emh-on-solid);
                box-shadow: var(--emh-shadow-md); max-width: min(320px, calc(100vw - 40px));
                text-align: left; pointer-events: auto;
                transition: opacity 0.3s ease, transform 0.3s ease;
                opacity: 0; transform: translateX(120%);
                font-size: 13px; font-weight: 500; display: flex; align-items: center;
                line-height: 1.4; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            }
            .custom-toast.show { opacity: 1; transform: translateX(0); }
            .custom-toast.leaving { opacity: 0; transform: translateX(120%); }
            .custom-toast svg { margin-right: 8px; flex-shrink: 0; }
            .custom-toast-success { background: var(--emh-success); }
            .custom-toast-error { background: var(--emh-danger); }
            .custom-toast-info { background: var(--emh-primary); color: var(--emh-on-primary); }
            .custom-toast-warning { background: var(--emh-warning); color: var(--emh-on-solid); }
            @media (prefers-reduced-motion: reduce) {
                .emh-code-manager-toggle,
                .emh-code-manager-toggle:hover,
                .emh-code-manager-toggle:active,
                .btn,
                .btn:active,
                .emh-javgg-controls a,
                .emh-code-status-indicator,
                .custom-toast {
                    transition: none !important;
                    animation: none !important;
                    transform: none !important;
                }
                .custom-toast { opacity: 1; transform: none; }
                .custom-toast.show { opacity: 1; transform: none; }
                .custom-toast.leaving { opacity: 0; transform: none; }
            }
        `;
        (document.head || document.documentElement).appendChild(style);
    }

    // standalone 加载封面:document-start 起隐藏站点内容,面板就绪前显示"加载中"
    function injectStandaloneCover() {
        if (document.getElementById('emh-standalone-cover-style')) return;
        const st = document.createElement('style');
        st.id = 'emh-standalone-cover-style';
        st.textContent = `
            html { background: #08090a !important; }
            body { visibility: hidden !important; }
            #emh-standalone-loading {
                position: fixed; inset: 0; z-index: 2147483646;
                display: flex; align-items: center; justify-content: center; gap: 12px;
                background: #08090a; color: #9ca3af;
                font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            }
            .emh-loading-spinner {
                width: 16px; height: 16px; border-radius: 50%;
                border: 2px solid rgba(255,255,255,0.12); border-top-color: #5e6ad2;
                animation: emh-loading-spin 0.8s linear infinite;
            }
            @keyframes emh-loading-spin { to { transform: rotate(360deg); } }
        `;
        (document.head || document.documentElement).appendChild(st);
        const ov = document.createElement('div');
        ov.id = 'emh-standalone-loading';
        ov.innerHTML = '<span class="emh-loading-spinner"></span>正在加载番号库…';
        document.documentElement.appendChild(ov);
    }

    function removeStandaloneCover() {
        const st = document.getElementById('emh-standalone-cover-style');
        if (st) st.remove();
        const ov = document.getElementById('emh-standalone-loading');
        if (ov) ov.remove();
    }

    // standalone 独立页布局:全屏面板 + 左列表右详情双栏
    function injectStandaloneStyles() {
        if (document.getElementById('emh-standalone-style')) return;
        const style = document.createElement('style');
        style.id = 'emh-standalone-style';
        style.textContent = `
            html, body { margin: 0; height: 100%; background: var(--emh-bg); }
            #emh-code-manager-toggle, .emh-panel-backdrop { display: none !important; }
            #emh-code-manager-panel .emh-panel-resize { display: none !important; }
            #emh-code-manager-panel {
                position: fixed; inset: 0; width: auto; height: 100vh;
                border-radius: 0; border-left: none;
                padding-right: min(580px, 47vw);
                background: var(--emh-bg);
            }
            /* 顶栏协调:左右栏等高等同背景,消除玻璃毛玻璃割裂 */
            /* 注:以下 padding/font-size 与 createStyles 同优先级且晚注入,
               必须 !important 才能覆盖(与 background/backdrop 同理) */
            .emh-panel-header, .emh-detail-header {
                height: 52px; padding: 0 16px !important; box-sizing: border-box;
                background: var(--emh-bg) !important;
                border-bottom: 1px solid var(--emh-border) !important;
                -webkit-backdrop-filter: none !important;
                backdrop-filter: none !important;
            }
            .emh-panel-header h2 { font-size: 14px !important; }
            .emh-detail-code { font-size: 15px !important; }
            .emh-panel-header .emh-panel-close { display: none !important; }
            .emh-detail-backdrop { display: none !important; }
            /* 组件样式(CodeManagerPanel.createStyles)在 bootPanel 中晚于本样式注入,
               同优先级时后注入者胜出,故对冲突属性加 !important 保证双栏覆盖生效 */
            /* 右栏:与左栏同基底,hairline 边框 + 阴影作分隔 */
            .emh-detail-drawer {
                position: fixed !important; top: 0; right: 0; bottom: 0;
                width: min(580px, 47vw) !important;
                background: var(--emh-bg) !important;
                border-radius: 0 !important;
                border-left: 1px solid var(--emh-border) !important;
                box-shadow: -16px 0 40px rgba(0, 0, 0, 0.35) !important;
                animation: none !important;
            }
            /* 详情 Hero/卡片层次已迁移至 createStyles(.emh-detail-body.unified),
               standalone 仅保留双栏面板覆盖,不再重复定义 */
        `;
        (document.head || document.documentElement).appendChild(style);
    }
    function createFallbackToggle(clickHandler) {
        const existing = document.getElementById('emh-code-manager-toggle');
        if (existing) existing.remove();
        const btn = document.createElement('button');
        btn.id = 'emh-code-manager-toggle';
        btn.className = 'emh-code-manager-toggle';
        btn.innerHTML = ICONS.grid + '<span>番号库</span>';
        btn.title = '管理番号库';
        btn.addEventListener('click', clickHandler);
        document.body.appendChild(btn);
        return btn;
    }

    // ===== 面板工厂:依赖就绪后构建 Preact 组件 =====
    function buildPanel() {
        const { h, render, Fragment } = getGlobal('preact');
        const { useEffect, useRef, useReducer, useState, useCallback } = getGlobal('preactHooks');
        const html = getGlobal('htm').bind(h);

        // 注意:preact.umd.js 不导出 memo(核心包才有),用 Component.shouldComponentUpdate 实现浅比较 memo
        const Memorize = (Comp) => class extends getGlobal('preact').Component {
            shouldComponentUpdate(nextProps) {
                const cur = this.props;
                for (const k in nextProps) {
                    if (Object.prototype.hasOwnProperty.call(nextProps, k) && nextProps[k] !== cur[k]) return true;
                }
                return false;
            }
            render() { return h(Comp, this.props); }
        };

        // 通用按钮图标(VNode,可直接插值到 htm JSX)
        const ICON = {
            heart: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`,
            check: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
            checkCircle: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`,
            trash: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
            restore: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>`,
            edit: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>`,
            search: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`,
            copy: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
            unfav: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>`,
            plus: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>`,
            list: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>`,
            import: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
            export: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>`,
            checkSquare: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 11 12 14 22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>`,
            refresh: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>`,
            x: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`
        };

        // 状态存储
        const PanelStore = {
            state: {
                visible: false,
                currentFilter: 'all',
                searchQuery: '',
                timeFilter: '',
                magnetFilter: '',
                selectedItems: [],
                multiSelectMode: false,
                confirm: null,
                prompt: null,
                detail: null,
                magnetSearch: null,
                batchProgress: null,
                toasts: [],
                selectedIndex: -1,
                helpOpen: false,
                menuOpen: false,
                lastSyncTimestamp: null,
                revision: 0
            },
            listeners: new Set(),
            set(patch) {
                this.state = { ...this.state, ...patch, revision: this.state.revision + 1 };
                this.notify();
            },
            get() { return this.state; },
            refresh() { this.set({}); },
            notify() { this.listeners.forEach(fn => fn()); },
            subscribe(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
        };

        const STATUS_TEXT = { favorite: '关注', watched: '已看', unmarked: '未标记' };

        const ICON_TOAST = {
            success: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
            error: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
            warning: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`,
            info: html`<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`
        };

        const EMPTY_ICONS = {
            library: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="21 8 21 21 3 21 3 8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/></svg>`,
            favorite: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>`,
            watched: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>`,
            trash: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>`,
            time: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>`,
            has: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>`,
            none: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/></svg>`,
            search: html`<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>`
        };

        // 监听外部 toast 事件,桥接到组件状态
        window.addEventListener('emh_toast', function handler(e) {
            const { message, type } = e.detail || {};
            if (!message) return;
            const id = Date.now() + Math.random().toString(36).slice(2, 7);
            const toasts = [...PanelStore.state.toasts, { id, message, type: type || 'info' }].slice(-5);
            PanelStore.set({ toasts });
            // 两阶段退场:先进入 leaving 过渡,再移除
            setTimeout(() => {
                PanelStore.set({ toasts: PanelStore.state.toasts.map(t => t.id === id ? { ...t, leaving: true } : t) });
            }, 3000);
            setTimeout(() => {
                PanelStore.set({ toasts: PanelStore.state.toasts.filter(t => t.id !== id) });
            }, 3300);
        });

        function ToastContainer() {
            const st = PanelStore.get();
            if (!st.toasts.length) return null;
            return html`
                <div id="custom-toast-container">
                    ${st.toasts.map(t => html`
                        <div key=${t.id} class="custom-toast custom-toast-${t.type} show ${t.leaving ? 'leaving' : ''}" role="status">
                            ${ICON_TOAST[t.type] || ICON_TOAST.info}
                            <span>${t.message}</span>
                        </div>
                    `)}
                </div>
            `;
        }

        function PromptModal({ promptState, onSubmit, onCancel }) {
            if (!promptState) return null;
            const inputRef = useRef(null);
            useEffect(() => { if (inputRef.current) inputRef.current.focus(); }, []);
            const submit = (e) => {
                e.preventDefault();
                onSubmit(inputRef.current ? inputRef.current.value : '');
            };
            return html`
                <div class="emh-panel-modal" style="display:flex;">
                    <div class="emh-panel-modal-content">
                        <h3>${promptState.title}</h3>
                        <form onSubmit=${submit}>
                            <input ref=${inputRef} class="emh-prompt-input" type="text"
                                   defaultValue=${promptState.initial || ''} placeholder=${promptState.placeholder || ''} />
                            <div class="emh-panel-modal-buttons">
                                <button type="submit" class="btn my-btn-primary emh-panel-modal-confirm">确定</button>
                                <button type="button" class="btn btn-outline emh-panel-modal-cancel" onClick=${onCancel}>取消</button>
                            </div>
                        </form>
                    </div>
                </div>
            `;
        }

        function StatusTag({ status }) {
            return html`<span class="emh-status-tag ${status}">${STATUS_TEXT[status] || '未标记'}</span>`;
        }

        function MagnetListModal({ magnetSearch, onPick, onClose }) {
            if (!magnetSearch) return null;
            return html`
                <div class="emh-panel-modal" style="display:flex;">
                    <div class="emh-panel-modal-content emh-magnet-modal-content">
                        <h3><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> 磁力搜索结果:${magnetSearch.code}</h3>
                        ${magnetSearch.loading ? html`
                            <div class="emh-magnet-loading">加载中...</div>
                        ` : magnetSearch.error ? html`
                            <div class="emh-magnet-error">${magnetSearch.error}</div>
                        ` : magnetSearch.results.length ? html`
                            <ul class="emh-magnet-list">
                                ${magnetSearch.results.map(r => html`
                                    <li key=${r.href} class="emh-magnet-item" onClick=${() => onPick(magnetSearch.code, r.href)}>
                                        <span class="emh-magnet-item-title" title="${r.title}">${r.title}</span>
                                        ${r.size ? html`<span class="emh-magnet-item-size">${r.size}</span>` : ''}
                                    </li>
                                `)}
                            </ul>
                            <div class="emh-magnet-hint">点击条目获取磁力链接并保存</div>
                        ` : html`
                            <div class="emh-magnet-error">暂无结果</div>
                        `}
                        <div class="emh-panel-modal-buttons">
                            <button class="btn btn-outline emh-panel-modal-cancel" onClick=${onClose}>关闭</button>
                        </div>
                    </div>
                </div>
            `;
        }

        function BatchProgressModal({ progress }) {
            if (!progress || !progress.running) return null;
            const total = progress.total || 0;
            const done = progress.done || 0;
            const pct = total ? Math.round((done / total) * 100) : 0;
            return html`
                <div class="emh-panel-modal" style="display:flex;">
                    <div class="emh-panel-modal-content emh-batch-modal-content">
                        <h3><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg> 批量获取磁力</h3>
                        <div class="emh-batch-progress">
                            <div class="emh-batch-progress-track">
                                <div class="emh-batch-progress-bar" style="width:${pct}%"></div>
                            </div>
                            <div class="emh-batch-progress-info">
                                <span class="emh-batch-progress-pct">${pct}%</span>
                                <span class="emh-batch-progress-text">${done}/${total}</span>
                            </div>
                        </div>
                        ${progress.current ? html`
                            <div class="emh-batch-current">正在处理:${progress.current}</div>
                        ` : ''}
                        ${progress.skipped || progress.failed ? html`
                            <div class="emh-batch-stats">
                                ${progress.skipped ? html`<span class="emh-batch-stat">跳过 ${progress.skipped}</span>` : ''}
                                ${progress.failed ? html`<span class="emh-batch-stat emh-batch-stat-fail">失败 ${progress.failed}</span>` : ''}
                            </div>
                        ` : ''}
                        <div class="emh-panel-modal-buttons">
                            <button class="btn btn-outline emh-panel-modal-cancel" disabled>处理中…</button>
                        </div>
                    </div>
                </div>
            `;
        }

        function HeaderMenu({ onClose, onClear, onOpenStandalone, stats }) {
            return html`
                <div class="emh-header-menu-backdrop" onClick=${onClose}></div>
                <div class="emh-header-menu">
                    <div class="emh-header-menu-title">预览缓存</div>
                    ${stats ? html`
                        <div class="emh-header-menu-stat">已缓存 ${stats.magnets} 条磁力 · ${stats.screenshots} 张截图 · ${UTILS.formatBytes(stats.bytes)}</div>
                    ` : ''}
                    ${onOpenStandalone ? html`
                        <button type="button" class="emh-header-menu-item emh-header-menu-item-plain" onClick=${onOpenStandalone}>在新标签页打开</button>
                    ` : ''}
                    <button type="button" class="emh-header-menu-item" onClick=${onClear}>清除全部预览缓存</button>
                </div>
            `;
        }

        function HelpModal({ onClose }) {
            const shortcuts = [
                ['↑ ↓ · Home · End', '在列表中选择番号'],
                ['Enter', '打开选中番号详情'],
                ['Esc', '关闭 / 逐层返回'],
                ['/', '聚焦搜索'],
                ['n', '新建番号'],
                ['Del', '移入回收站'],
                ['⌘C / Ctrl+C', '复制磁力'],
                ['r', '刷新预览(详情内)'],
                ['?', '本帮助']
            ];
            return html`
                <div class="emh-panel-modal" style="display:flex;" onClick=${onClose}>
                    <div class="emh-panel-modal-content emh-help-modal-content" onClick=${e => e.stopPropagation()}>
                        <h3><svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><line x1="6" y1="8" x2="6.01" y2="8"/><line x1="10" y1="8" x2="10.01" y2="8"/><line x1="14" y1="8" x2="14.01" y2="8"/><line x1="18" y1="8" x2="18.01" y2="8"/><line x1="6" y1="12" x2="6.01" y2="12"/><line x1="10" y1="12" x2="10.01" y2="12"/><line x1="14" y1="12" x2="14.01" y2="12"/><line x1="18" y1="12" x2="18.01" y2="12"/><line x1="6" y1="16" x2="18" y2="16"/></svg> 键盘快捷键</h3>
                        <ul class="emh-help-list">
                            ${shortcuts.map(s => html`
                                <li key=${s[0]}><span class="emh-kbd-chip">${s[0]}</span><span class="emh-help-desc">${s[1]}</span></li>
                            `)}
                        </ul>
                        <div class="emh-panel-modal-buttons">
                            <button class="btn btn-outline emh-panel-modal-cancel" onClick=${onClose}>关闭</button>
                        </div>
                    </div>
                </div>
            `;
        }

        function ItemRow({ item, view, multi, selected, kbdSel, onToggle, onOpenDetail, onFav, onWatch, onUnfav, onDelete, onRestore, onPurge }) {
            return html`
                <div class="emh-item ${item.status} ${selected ? 'selected' : ''} ${kbdSel ? 'kbd-sel' : ''}" data-code="${item.code}"
                     aria-selected=${kbdSel || undefined}
                     onClick=${multi ? () => onToggle(item.code) : null}>
                    <label class="emh-checkbox-wrap" style=${multi ? '' : 'visibility:hidden;'}>
                        <input type="checkbox" checked=${selected}
                               onClick=${e => e.stopPropagation()}
                               onChange=${() => onToggle(item.code)} />
                    </label>
                    <div class="emh-col-code" onClick=${multi ? null : e => { e.stopPropagation(); onOpenDetail(item.code); }}
                         style=${multi ? '' : 'cursor:pointer;'} title="查看详情">
                        <div class="emh-item-code" title="${item.code}">${item.code}</div>
                        ${typeof item.remarks === 'string' && item.remarks ? html`<div class="emh-item-remarks" title="${item.remarks.length > 80 ? item.remarks.slice(0, 80) + '…' : item.remarks}">${item.remarks}</div>` : null}
                    </div>
                    <span class="emh-col-status"><${StatusTag} status=${item.status} /></span>
                    <span class="emh-col-actions">
                        ${!multi ? html`
                            <span class="emh-item-actions" role="group" aria-label="操作">
                                ${view === 'trash' ? html`
                                    <button class="emh-act-restore" title="恢复" onClick=${e => { e.stopPropagation(); onRestore(item.code); }} aria-label="恢复">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
                                    </button>
                                    <button class="emh-act-purge" title="彻底删除" onClick=${e => { e.stopPropagation(); onPurge(item.code); }} aria-label="彻底删除">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
                                    </button>
                                ` : item.status === 'watched' ? html`
                                    <button class="emh-act-delete" title="删除到回收站" onClick=${e => { e.stopPropagation(); onDelete(item.code); }} aria-label="删除到回收站">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
                                    </button>
                                ` : item.status === 'favorite' ? html`
                                    <button class="emh-act-watched" title="标记已看" onClick=${e => { e.stopPropagation(); onWatch(item.code); }} aria-label="标记已看">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
                                    </button>
                                    <button class="emh-act-unfav" title="取消关注" onClick=${e => { e.stopPropagation(); onUnfav(item.code); }} aria-label="取消关注">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>
                                    </button>
                                ` : html`
                                    <button class="emh-act-favorite" title="标记关注" onClick=${e => { e.stopPropagation(); onFav(item.code); }} aria-label="标记关注">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
                                    </button>
                                    <button class="emh-act-watched" title="标记已看" onClick=${e => { e.stopPropagation(); onWatch(item.code); }} aria-label="标记已看">
                                        <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
                                    </button>
                                `}
                            </span>
                        ` : null}
                    </span>
                </div>
            `;
        }

        // memo 化:仅当 item/selected/kbdSel/view/multi 或稳定回调变化时重建行
        const ItemRowMemo = Memorize(ItemRow);

        function ConfirmModal({ confirm, onConfirm, onCancel }) {
            if (!confirm) return null;
            const confirmBtnClass = confirm.danger === 'soft'
                ? 'btn btn-outline emh-panel-modal-confirm'
                : 'btn my-btn-danger emh-panel-modal-confirm';
            return html`
                <div class="emh-panel-modal" style="display:flex;">
                    <div class="emh-panel-modal-content">
                        <h3>${confirm.message}</h3>
                        <div class="emh-panel-modal-buttons">
                            <button class="${confirmBtnClass}" onClick=${onConfirm}>确定</button>
                            <button class="btn btn-outline emh-panel-modal-cancel" onClick=${onCancel}>取消</button>
                        </div>
                    </div>
                </div>
            `;
        }

        function DetailDrawer({ item, inTrash, onClose, onEdit, onEditTags, onEditMagnet, onEditMagnetItem, onRemoveMagnet, onCopyMagnet, onCopyMagnetItem, onPreviewMagnet, onPreviewMagnetAt, onSearchMagnet, onFav, onWatch, onUnfav, onDelete, onRestore, onPurge, onPreviewAvwiki }) {
            const drawerBodyRef = useRef(null);
            // 宫格缩略图懒加载
            useEffect(() => {
                if (!drawerBodyRef.current) return;
                drawerBodyRef.current.querySelectorAll('.emh-preview-thumb[data-src]').forEach(el => LAZY.observe(el));
            });
            if (!item) return null;
            const created = item.createdDate ? new Date(item.createdDate).toLocaleString() : '';
            const modified = item.modifiedDate ? new Date(item.modifiedDate).toLocaleString() : '';
            const deleted = item.deleteDate ? new Date(item.deleteDate).toLocaleString() : '';
            const magnets = CODE_LIBRARY.normMagnets(item.magnet);
            const metaParts = [created ? `创建 ${created}` : '', modified ? `更新 ${modified}` : ''].filter(Boolean);
            const tagList = [];
            (function extract(input) {
                if (input == null) return;
                if (Array.isArray(input)) return input.forEach(extract);
                if (typeof input === 'string') { if (input.trim()) tagList.push(input.trim()); return; }
                if (typeof input === 'number') { tagList.push(String(input)); return; }
                if (typeof input === 'object' && input.value != null) extract(input.value);
            })(item.tags);

            // 详情层次统一(侧栏/新页签一致):Hero 标题 → meta 行 → 信息/磁力卡片
            const metaLine = html`<div class="emh-detail-meta-line">${metaParts.join(' · ')}</div>`;
            const titleBlock = (item.title && typeof item.title === 'string' && item.title !== item.code) ? html`
                <div class="emh-detail-field emh-detail-hero">
                    <span class="emh-detail-label">标题</span>
                    <span class="emh-detail-value">${item.title}</span>
                </div>
            ` : null;
            const remarksBlock = html`
                <div class="emh-detail-field">
                    <span class="emh-detail-label">备注</span>
                    <span class="emh-detail-value ${item.remarks ? '' : 'emh-detail-empty'}">${typeof item.remarks === 'string' && item.remarks ? item.remarks : '暂无备注'}</span>
                </div>
            `;
            const tagsBlock = html`
                <div class="emh-detail-field">
                    <span class="emh-detail-label">标签</span>
                    <div class="emh-detail-tags">
                        ${tagList.length ? tagList.map(t => html`<span class="emh-detail-tag">#${t}</span>`) : html`<span class="emh-detail-value emh-detail-empty">暂无标签</span>`}
                        ${!inTrash ? html`
                            <button class="emh-magnet-op emh-tag-edit" title="编辑标签" aria-label="编辑标签" onClick=${() => onEditTags(item.code)}>
                                <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
                            </button>
                        ` : ''}
                    </div>
                </div>
            `;
            const infoBlock = html`${remarksBlock}${tagsBlock}`;
            const magnetBlock = html`
                <div class="emh-detail-field">
                    <span class="emh-detail-label">磁力链接 ${magnets.length ? `(${magnets.length})` : ''}</span>
                    ${magnets.length ? html`
                        <ul class="emh-magnet-list">
                            ${magnets.map((m, idx) => {
                                const mid = CODE_LIBRARY.magnetId(m) || ('idx-' + idx);
                                const mvalue = CODE_LIBRARY.magnetValue(m);
                                const displayM = CODE_LIBRARY.magnetName(m);
                                const pv = CODE_LIBRARY.sanitizeMagnetPreview(m.preview);
                                const hasCache = !!(pv && Array.isArray(pv.screenshots) && pv.screenshots.length);
                                const shotCount = hasCache ? pv.screenshots.length : 0;
                                return html`
                                    <li key=${mid} class="emh-magnet-item-static" title="${mvalue}">
                                        <span class="emh-magnet-item-idx">#${idx + 1}</span>
                                        <span class="emh-magnet-item-text">${displayM.length > 60 ? displayM.slice(0, 60) + '…' : displayM}</span>
                                        <span class="emh-magnet-item-ops">
                                            <button class="emh-magnet-op emh-magnet-op-preview ${hasCache ? 'has-cache' : ''}"
                                                title=${hasCache ? `预览截图(已缓存 ${shotCount} 张,右键强制刷新)` : '预览截图'}
                                                onClick=${(e) => onPreviewMagnet(item.code, mid, { force: !!(e && e.shiftKey) })}
                                                onContextMenu=${(e) => { e.preventDefault(); onPreviewMagnet(item.code, mid, { force: true }); }}>
                                                <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>${hasCache ? html`<span class="emh-magnet-preview-badge">${shotCount}</span>` : ''}
                                            </button>
                                            <button class="emh-magnet-op" title="复制该磁力" onClick=${() => onCopyMagnetItem(item.code, mid)}>
                                                <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
                                            </button>
                                            ${!inTrash ? html`
                                                <button class="emh-magnet-op" title="修改该磁力" onClick=${() => onEditMagnetItem(item.code, mid)}>
                                                    <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>
                                                </button>
                                                <button class="emh-magnet-op emh-magnet-op-del" title="删除该磁力" onClick=${() => onRemoveMagnet(item.code, mid)}>
                                                    <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
                                                </button>
                                            ` : null}
                                        </span>
                                        ${hasCache ? html`
                                            <div class="emh-preview-grid">
                                                ${pv.screenshots.slice(0, 24).map((s, si) => html`
                                                    <button type="button" class="emh-preview-cell" data-idx=${si}
                                                        title=${(s.time != null ? '跳转到 ' + UTILS.formatTime(s.time) + ' 的截图' : '查看截图')}
                                                        onClick=${() => onPreviewMagnetAt(item.code, mid, si)}>
                                                        <span class="emh-preview-thumb" data-src="${s.screenshot}"></span>
                                                        ${s.time != null ? html`<span class="emh-preview-time">${UTILS.formatTime(s.time)}</span>` : ''}
                                                    </button>
                                                `)}
                                            </div>
                                        ` : ''}
                                    </li>
                                `;
                            })}
                        </ul>
                        <span class="emh-magnet-actions">
                            <button class="btn btn-outline emh-magnet-btn emh-expand" aria-label="复制全部" onClick=${() => onCopyMagnet(item.code)}>${ICON.copy}<span class="emh-expand-label">复制全部</span></button>
                            ${!inTrash ? html`
                                <button class="btn btn-outline emh-magnet-btn emh-expand" aria-label="搜索" onClick=${() => onSearchMagnet(item.code)}>${ICON.search}<span class="emh-expand-label">搜索</span></button>
                            ` : null}
                        </span>
                    ` : html`
                        <span class="emh-detail-value emh-detail-empty">暂无磁力链接</span>
                        ${!inTrash ? html`
                            <span class="emh-magnet-actions">
                                <button class="btn btn-outline emh-magnet-btn emh-expand" aria-label="搜索磁力" onClick=${() => onSearchMagnet(item.code)}>${ICON.search}<span class="emh-expand-label">搜索磁力</span></button>
                                <button class="btn btn-outline emh-magnet-btn emh-expand" aria-label="手动添加" onClick=${() => onEditMagnet(item.code)}>${ICON.edit}<span class="emh-expand-label">手动添加</span></button>
                            </span>
                        ` : null}
                    `}
                </div>
            `;
            const deletedBlock = deleted ? html`
                <div class="emh-detail-field">
                    <span class="emh-detail-label">删除时间</span>
                    <span class="emh-detail-value">${deleted}</span>
                </div>
            ` : null;
            return html`
                <div class="emh-detail-backdrop" onClick=${onClose}></div>
                <div class="emh-detail-drawer">
                    <div class="emh-detail-header">
                        <div class="emh-detail-title">
                            <span class="emh-detail-code" title="${item.code}">${item.code}</span>
                            <${StatusTag} status=${item.status} />
                        </div>
                        <button class="emh-panel-close" title="关闭 (Esc)" onClick=${onClose}>×</button>
                    </div>
                    <div class="emh-detail-body unified" ref=${drawerBodyRef}>
                        ${titleBlock}
                        ${metaParts.length ? metaLine : ''}
                        <div class="emh-detail-section">${infoBlock}</div>
                        <div class="emh-detail-section">${magnetBlock}</div>
                        ${deletedBlock}
                    </div>
                    <div class="emh-detail-actions">
                        ${!inTrash ? html`
                            <button class="btn btn-outline emh-expand" aria-label="编辑备注" onClick=${() => onEdit(item.code)}>${ICON.edit}<span class="emh-expand-label">编辑备注</span></button>
                            <button class="btn btn-outline emh-expand" aria-label="AVWikiDB 截图预览" title="从 AVWikiDB 抓取该番号作品页宫格截图(Shift 强制刷新)" onClick=${(e) => onPreviewAvwiki(item.code, { force: !!(e && e.shiftKey) })}>${ICON.refresh}<span class="emh-expand-label">AVWikiDB 截图</span></button>
                        ` : null}
                        ${inTrash ? html`
                            <button class="btn btn-outline emh-expand" aria-label="恢复" onClick=${() => onRestore(item.code)}>${ICON.restore}<span class="emh-expand-label">恢复</span></button>
                            <button class="btn my-btn-danger emh-expand" aria-label="彻底删除" onClick=${() => onPurge(item.code)}>${ICON.trash}<span class="emh-expand-label">彻底删除</span></button>
                        ` : item.status === 'unmarked' ? html`
                            <button class="btn btn-outline emh-expand" aria-label="关注" onClick=${() => onFav(item.code)}>${ICON.heart}<span class="emh-expand-label">关注</span></button>
                            <button class="btn btn-outline emh-expand" aria-label="标记已看" onClick=${() => onWatch(item.code)}>${ICON.checkCircle}<span class="emh-expand-label">标记已看</span></button>
                        ` : item.status === 'favorite' ? html`
                            <button class="btn btn-outline emh-expand" aria-label="标记已看" onClick=${() => onWatch(item.code)}>${ICON.checkCircle}<span class="emh-expand-label">标记已看</span></button>
                            <button class="btn btn-outline emh-expand" aria-label="取消关注" onClick=${() => onUnfav(item.code)}>${ICON.unfav}<span class="emh-expand-label">取消关注</span></button>
                        ` : html`
                            <button class="btn my-btn-danger emh-expand" aria-label="删除到回收站" onClick=${() => onDelete(item.code)}>${ICON.trash}<span class="emh-expand-label">删除到回收站</span></button>
                        `}
                    </div>
                    ${!inTrash && item.status !== 'watched' ? html`
                        <div class="emh-detail-danger">
                            <button class="btn btn-outline" onClick=${() => onDelete(item.code)}>${ICON.trash} 删除到回收站</button>
                        </div>
                    ` : null}
                </div>
            `;
        }

        function CodeManagerApp() {
            const [, forceUpdate] = useReducer(x => x + 1, 0);
            const headRef = useRef(null);
            const searchRef = useRef(null);
            const contentRef = useRef(null);
            const resizeRef = useRef(null);
            const actionsRef = useRef(null);
            const debounceRef = useRef(null);
            const [searchDraft, setSearchDraft] = useState('');

            // 搜索防抖:200ms 后才写 store,避免逐字符全量重渲染
            const onSearchInput = (e) => {
                const v = e.target.value;
                setSearchDraft(v);
                if (debounceRef.current) clearTimeout(debounceRef.current);
                debounceRef.current = setTimeout(() => {
                    debounceRef.current = null;
                    PanelStore.set({ searchQuery: v });
                }, 200);
            };
            const clearSearch = () => {
                if (debounceRef.current) { clearTimeout(debounceRef.current); debounceRef.current = null; }
                setSearchDraft('');
                PanelStore.set({ searchQuery: '' });
                if (searchRef.current) searchRef.current.focus();
            };
            useEffect(() => {
                return () => { if (debounceRef.current) clearTimeout(debounceRef.current); };
            }, []);

            useEffect(() => {
                const unsub = PanelStore.subscribe(() => forceUpdate());
                return unsub;
            }, []);

            const st = PanelStore.get();

            // store 的 searchQuery 外部变化(清空/同步)时回写草稿
            useEffect(() => {
                setSearchDraft(st.searchQuery);
            }, [st.searchQuery]);

            const actions = {
                hidePanel: () => PanelStore.set({ visible: false, multiSelectMode: false, selectedItems: [], helpOpen: false, menuOpen: false }),
                setFilter: (f) => PanelStore.set({ currentFilter: f, timeFilter: f === 'all' ? st.timeFilter : '', multiSelectMode: false, selectedItems: [], selectedIndex: -1 }),
                toggleMulti: () => PanelStore.set({ multiSelectMode: !st.multiSelectMode, selectedItems: [], selectedIndex: -1 }),
                toggleItem: (code) => {
                    const sel = st.selectedItems.includes(code)
                        ? st.selectedItems.filter(c => c !== code)
                        : [...st.selectedItems, code];
                    PanelStore.set({ selectedItems: sel });
                },
                selectAll: () => {
                    if (st.selectedItems.length === items.length) PanelStore.set({ selectedItems: [] });
                    else PanelStore.set({ selectedItems: items.map(i => i.code) });
                },
                markFav: (code) => { CODE_LIBRARY.markItem(code, 'favorite'); UTILS.showToast(`番号 ${code} 已标记为关注`, 'success'); },
                markWatched: (code) => { CODE_LIBRARY.markItem(code, 'watched'); UTILS.showToast(`番号 ${code} 已标记为已看`, 'success'); },
                unfavorite: (code) => { CODE_LIBRARY.markItem(code, 'unmarked'); UTILS.showToast(`番号 ${code} 已取消关注`, 'success'); },
                editRemark: (code) => {
                    const cur = CODE_LIBRARY.getItem(code);
                    if (!cur) { UTILS.showToast('回收站条目请先恢复再编辑', 'warning'); return; }
                    PanelStore.set({ prompt: {
                        title: `编辑备注 (${code})`,
                        initial: (cur.remarks || cur.remark) || '',
                        placeholder: '输入备注内容',
                        onSubmit: (remark) => {
                            if (remark !== null) {
                                CODE_LIBRARY.markItem(code, cur.status || 'unmarked', undefined, remark);
                                UTILS.showToast('备注已更新', 'success');
                            }
                        }
                    } });
                },
                editTags: (code) => {
                    const cur = CODE_LIBRARY.getItem(code);
                    if (!cur) { UTILS.showToast('回收站条目请先恢复再编辑标签', 'warning'); return; }
                    const list = [];
                    (function extract(input) {
                        if (input == null) return;
                        if (Array.isArray(input)) return input.forEach(extract);
                        if (typeof input === 'string') { if (input.trim()) list.push(input.trim()); return; }
                        if (typeof input === 'number') { list.push(String(input)); return; }
                        if (typeof input === 'object' && input.value != null) extract(input.value);
                    })(cur.tags);
                    PanelStore.set({ prompt: {
                        title: `编辑标签 (${code})`,
                        initial: list.join(', '),
                        placeholder: '多个标签用逗号分隔',
                        onSubmit: (val) => {
                            if (val !== null) {
                                const arr = String(val).split(/[,,、]/).map(s => s.trim()).filter(Boolean);
                                CODE_LIBRARY.setTags(code, arr);
                                UTILS.showToast('标签已更新', 'success');
                            }
                        }
                    } });
                },
                editMagnet: (code) => {
                    const cur = CODE_LIBRARY.getItem(code);
                    if (!cur) { UTILS.showToast('回收站条目请先恢复再编辑', 'warning'); return; }
                    const curMag = CODE_LIBRARY.normMagnets(cur.magnet);
                    PanelStore.set({ prompt: {
                        title: `编辑磁力链接 (${code})`,
                        initial: curMag.map(e => CODE_LIBRARY.magnetValue(e)).join('\n'),
                        placeholder: '每行一个磁力链接,可多行',
                        onSubmit: (magnet) => {
                            if (magnet !== null) {
                                const arr = CODE_LIBRARY.normMagnets(String(magnet).split('\n'));
                                CODE_LIBRARY.markItem(code, cur.status || 'unmarked', undefined, undefined, arr);
                                UTILS.showToast('磁力链接已保存', 'success');
                            }
                        }
                    } });
                },
                editMagnetItem: (code, idOrIdx) => {
                    const cur = CODE_LIBRARY.getItem(code);
                    if (!cur) { UTILS.showToast('回收站条目请先恢复再编辑', 'warning'); return; }
                    const arr = CODE_LIBRARY.normMagnets(cur.magnet);
                    // 按 id 定位;若无 id(旧数据)回退按索引
                    const findIdx = (target) => {
                        const t = String(target);
                        return arr.findIndex(e => {
                            const eid = CODE_LIBRARY.magnetId(e);
                            return (eid && String(eid) === t) || (!eid && (typeof target === 'number' ? true : false));
                        });
                    };
                    const idx = findIdx(idOrIdx);
                    if (idx === -1 || !arr[idx]) return;
                    const target = arr[idx];
                    PanelStore.set({ prompt: {
                        title: `修改磁力 #${idx + 1} (${code})`,
                        initial: CODE_LIBRARY.magnetValue(target),
                        placeholder: '粘贴新磁力链接',
                        onSubmit: (magnet) => {
                            if (magnet !== null && magnet.trim()) {
                                const nextVal = magnet.trim();
                                const newArr = arr.map((e, i) => {
                                    if (i !== idx) return e;
                                    const next = { id: e.id, value: nextVal };
                                    // 磁力变更后清除旧预览缓存
                                    return next;
                                });
                                CODE_LIBRARY.markItem(code, cur.status || 'unmarked', undefined, undefined, newArr);
                                UTILS.showToast('磁力已修改', 'success');
                            }
                        }
                    } });
                },
                removeMagnet: (code, idOrIdx) => {
                    const cur = CODE_LIBRARY.getItem(code);
                    if (!cur) { UTILS.showToast('回收站条目请先恢复再编辑', 'warning'); return; }
                    const arr = CODE_LIBRARY.normMagnets(cur.magnet);
                    const t = String(idOrIdx);
                    const idx = arr.findIndex(e => {
                        const eid = CODE_LIBRARY.magnetId(e);
                        return (eid && String(eid) === t) || (!eid && typeof idOrIdx === 'number' && arr.indexOf(e) === idOrIdx);
                    });
                    if (idx === -1 || !arr[idx]) return;
                    PanelStore.set({ confirm: { message: `确定删除磁力 #${idx + 1} 吗?`, danger: 'soft', onConfirm: () => {
                        const newArr = arr.filter((_, i) => i !== idx);
                        CODE_LIBRARY.markItem(code, cur.status || 'unmarked', undefined, undefined, newArr);
                        UTILS.showToast('磁力已删除', 'success');
                    } } });
                },
                searchMagnet: (code) => {
                    // 从 1cili 拉取磁力搜索结果列表;回收站条目禁止写入
                    if (!CODE_LIBRARY.getItem(code)) { UTILS.showToast('回收站条目请先恢复再搜索磁力', 'warning'); return; }
                    PanelStore.set({ magnetSearch: { code, loading: true, results: [], error: '' } });
                    const url = `https://1cili.com/search?q=${encodeURIComponent(code)}`;
                    const done = (results, error) => {
                        if (error) PanelStore.set({ magnetSearch: { code, loading: false, results: [], error } });
                        else PanelStore.set({ magnetSearch: { code, loading: false, results, error: '' } });
                    };
                    const onload = (resp) => {
                        try {
                            const htmlText = resp.responseText || '';
                            const parser = new DOMParser();
                            const doc = parser.parseFromString(htmlText, 'text/html');
                            const rows = [];
                            doc.querySelectorAll('table.file-list tbody tr').forEach(tr => {
                                const a = tr.querySelector('td a[href^="/!"]');
                                if (!a) return;
                                const title = (a.textContent || '').trim().replace(/\s+/g, ' ');
                                const sizeEl = tr.querySelector('.td-size');
                                const size = sizeEl ? sizeEl.textContent.trim() : '';
                                rows.push({ title, size, href: a.getAttribute('href') });
                            });
                            done(rows, rows.length ? '' : '未找到相关磁力资源');
                        } catch (e) { done([], '解析搜索结果失败'); }
                    };
                    if (typeof GM_xmlhttpRequest !== 'undefined') {
                        GM_xmlhttpRequest({ method: 'GET', url, onload, onerror: (e) => done([], '网络请求失败'), timeout: 15000, ontimeout: () => done([], '请求超时') });
                    } else {
                        fetch(url).then(r => r.text()).then(t => onload({ responseText: t })).catch(() => done([], '网络请求失败'));
                    }
                },
                fetchMagnetDetail: (code, href) => {
                    const base = 'https://1cili.com';
                    const url = base + href;
                    const onload = (resp) => {
                        try {
                            const htmlText = resp.responseText || '';
                            const doc = new DOMParser().parseFromString(htmlText, 'text/html');
                            const input = doc.querySelector('#input-magnet');
                            const magnet = input ? (input.value || '').trim() : '';
                            if (magnet) {
                                const cur = CODE_LIBRARY.getItem(code);
                                // 回收站条目禁止 markItem,避免静默重建主库条目
                                if (!cur) { UTILS.showToast('回收站条目请先恢复再添加磁力', 'warning'); return; }
                                // 提取影片信息(标题)填入备注
                                let remark = (cur.remarks || cur.remark) || '';
                                const infoDd = Array.prototype.slice.call(doc.querySelectorAll('.torrent-info dt') || [])
                                    .find(dt => (dt.textContent || '').includes('影片信息'));
                                const titleText = infoDd && infoDd.nextElementSibling
                                    ? (infoDd.nextElementSibling.textContent || '').replace(/\s+/g, ' ').trim()
                                    : '';
                                if (titleText && !remark) {
                                    remark = titleText;
                                } else if (titleText && !remark.includes(titleText)) {
                                    remark = remark ? `${remark} | ${titleText}` : titleText;
                                }
                                // 追加到现有磁力数组(去重),不覆盖已有磁力
                                const existingMagnets = CODE_LIBRARY.normMagnets(cur && cur.magnet);
                                const existingVals = existingMagnets.map(e => CODE_LIBRARY.magnetValue(e));
                                if (existingVals.includes(magnet)) {
                                    UTILS.showToast('该磁力已存在', 'info');
                                } else {
                                    const newArr = [...existingMagnets, magnet]; // normMagnets 在 markItem 内统一转对象数组
                                    CODE_LIBRARY.markItem(code, cur.status || 'unmarked', undefined, remark, newArr);
                                    UTILS.showToast('磁力链接已添加' + (titleText ? ',并填入影片信息' : ''), 'success');
                                }
                                PanelStore.set({ magnetSearch: null });
                            } else {
                                UTILS.showToast('详情页未找到磁力链接', 'error');
                            }
                        } catch (e) { UTILS.showToast('解析详情失败', 'error'); }
                    };
                    if (typeof GM_xmlhttpRequest !== 'undefined') {
                        GM_xmlhttpRequest({ method: 'GET', url, onload, onerror: () => UTILS.showToast('请求失败', 'error'), timeout: 15000, ontimeout: () => UTILS.showToast('请求超时', 'error') });
                    } else {
                        fetch(url).then(r => r.text()).then(t => onload({ responseText: t })).catch(() => UTILS.showToast('请求失败', 'error'));
                    }
                },
                closeMagnetSearch: () => PanelStore.set({ magnetSearch: null }),
                batchFetchMagnets: () => {
                    // 深拷贝选中番号,避免处理过程中 selectedItems 变化导致循环无限追加
                    const codes = (st.selectedItems || []).slice();
                    const totalCount = codes.length;
                    if (!totalCount) { UTILS.showToast('请先勾选番号', 'warning'); return; }
                    // 立即清空选中项(副本已捕获),防止处理中任何追加/重渲染干扰循环
                    PanelStore.set({ multiSelectMode: false, selectedItems: [] });
                    // 初始化进度
                    PanelStore.set({ batchProgress: { running: true, total: totalCount, done: 0, skipped: 0, failed: 0, current: '' } });
                    // 串行处理,避免并发请求过多
                    (async () => {
                        const doGet = (url) => new Promise((resolve) => {
                            const cb = (resp) => resolve(resp.responseText || '');
                            if (typeof GM_xmlhttpRequest !== 'undefined') {
                                GM_xmlhttpRequest({ method: 'GET', url, onload: cb, onerror: () => resolve(''), timeout: 15000, ontimeout: () => resolve('') });
                            } else {
                                fetch(url).then(r => r.text()).then(cb).catch(() => resolve(''));
                            }
                        });
                        const extractMagnetFromDetail = (htmlText) => {
                            const doc = new DOMParser().parseFromString(htmlText, 'text/html');
                            const input = doc.querySelector('#input-magnet');
                            // 用 .value 而非 getAttribute('value'),HTML 实体(&amp;)会正确解码为 &
                            return input ? (input.value || '').trim() : '';
                        };
                        const extractTitleFromDetail = (htmlText) => {
                            const doc = new DOMParser().parseFromString(htmlText, 'text/html');
                            const infoDd = Array.prototype.slice.call(doc.querySelectorAll('.torrent-info dt') || [])
                                .find(dt => (dt.textContent || '').includes('影片信息'));
                            return infoDd && infoDd.nextElementSibling
                                ? (infoDd.nextElementSibling.textContent || '').replace(/\s+/g, ' ').trim()
                                : '';
                        };
                        const isVariantMatch = (title, code) => {
                            // 匹配 番号 的 C/UC/CH 变体(如 番号-C、番号_C、764ch、_HD_CH、-UC 等)
                            if (!title) return false;
                            const t = String(title).toUpperCase().replace(/\s+/g, '');
                            const c = String(code).toUpperCase().replace(/\s+/g, '');
                            if (!t.startsWith(c)) return false;
                            const rest = t.slice(c.length);
                            if (!rest) return false;
                            // 变体 C/UC/CH 前允许 - _ . 分隔,后跟非字母数字或结束
                            return /[-_.]?(C|UC|CH)([^A-Z0-9]|$)/.test(rest);
                        };
                        let okCount = 0;
                        let skipCount = 0;
                        let failCount = 0;
                        const upd = (partial) => PanelStore.set({ batchProgress: { ...PanelStore.state.batchProgress, ...partial } });
                        for (let i = 0; i < codes.length; i++) {
                            const code = codes[i];
                            upd({ current: code });
                            // 已有磁力的番号直接跳过,避免重复获取/覆盖
                            const existing = CODE_LIBRARY.getItem(code);
                            if (existing && CODE_LIBRARY.normMagnets(existing.magnet).length) {
                                skipCount++;
                                upd({ done: i + 1, skipped: skipCount });
                                continue;
                            }
                            const searchHtml = await doGet(`https://1cili.com/search?q=${encodeURIComponent(code)}`);
                            if (!searchHtml) { failCount++; upd({ done: i + 1, failed: failCount }); continue; }
                            const doc = new DOMParser().parseFromString(searchHtml, 'text/html');
                            const rows = [];
                            doc.querySelectorAll('table.file-list tbody tr').forEach(tr => {
                                const a = tr.querySelector('td a[href^="/!"]');
                                if (a && a.getAttribute('href')) {
                                    rows.push({ href: a.getAttribute('href'), title: (a.textContent || '').trim().replace(/\s+/g, ' ') });
                                }
                            });
                            const filteredRows = rows.filter(r => isVariantMatch(r.title, code));
                            if (!filteredRows.length) { failCount++; upd({ done: i + 1, failed: failCount }); continue; }
                            const magnets = [];
                            let firstTitle = '';
                            for (const row of filteredRows) {
                                const detailHtml = await doGet('https://1cili.com' + row.href);
                                if (!detailHtml) continue;
                                const m = extractMagnetFromDetail(detailHtml);
                                if (m && !magnets.includes(m)) {
                                    magnets.push(m);
                                    if (!firstTitle) firstTitle = extractTitleFromDetail(detailHtml);
                                }
                            }
                            if (!magnets.length) { failCount++; upd({ done: i + 1, failed: failCount }); continue; }
                            const cur = CODE_LIBRARY.getItem(code);
                            const remark = firstTitle || (cur && (cur.remarks || cur.remark)) || '';
                            CODE_LIBRARY.markItem(code, (cur && cur.status) || 'unmarked', undefined, remark, magnets);
                            okCount++;
                            upd({ done: i + 1 });
                        }
                        PanelStore.set({ multiSelectMode: false, selectedItems: [] });
                        const processed = codes.length - skipCount;
                        PanelStore.set({ batchProgress: null });
                        UTILS.showToast(`批量完成:${okCount}/${processed} 个番号${skipCount ? `,${skipCount} 个已有磁力已跳过` : ''}${failCount ? `,${failCount} 个失败` : ''}`, okCount === processed ? 'success' : 'warning');
                    })();
                },
                resolveItem: (code) => CODE_LIBRARY.getItem(code) || CODE_LIBRARY.trash.items.find(i => i.code.toUpperCase() === String(code).toUpperCase()) || null,
                previewAvwiki: (code, opts) => {
                    const item = actions.resolveItem(code);
                    if (!item) { UTILS.showToast('未找到该番号', 'warning'); return; }
                    AVWIKI_PREVIEW.fetchAndShow(item.code, opts || {});
                },
                previewMagnet: (code, idOrIdx, opts) => {
                    const item = actions.resolveItem(code);
                    if (!item) { UTILS.showToast('未找到该番号', 'warning'); return; }
                    const magnets = CODE_LIBRARY.normMagnets(item.magnet);
                    const t = String(idOrIdx);
                    let idx = magnets.findIndex((e, i) => {
                        const eid = CODE_LIBRARY.magnetId(e);
                        return (eid && String(eid) === t) || String(i) === t;
                    });
                    if (idx < 0 && /^\d+$/.test(t)) idx = Number(t);
                    if (idx < 0 || idx >= magnets.length) { UTILS.showToast('磁力不存在', 'warning'); return; }
                    const entry = magnets[idx];
                    const mid = CODE_LIBRARY.magnetId(entry) || String(idx);
                    const mval = CODE_LIBRARY.magnetValue(entry);
                    MAGNET_PREVIEW.fetchAndCache(code, mid, mval, opts || {});
                },
                previewMagnetAt: (code, idOrIdx, shotIdx) => {
                    // 宫格点击:直接打开已缓存截图的灯箱(指定起始索引),不触发新请求
                    const item = actions.resolveItem(code);
                    if (!item) return;
                    const magnets = CODE_LIBRARY.normMagnets(item.magnet);
                    const t = String(idOrIdx);
                    let idx = magnets.findIndex((e, i) => {
                        const eid = CODE_LIBRARY.magnetId(e);
                        return (eid && String(eid) === t) || String(i) === t;
                    });
                    if (idx < 0 && /^\d+$/.test(t)) idx = Number(t);
                    if (idx < 0 || idx >= magnets.length) return;
                    const pv = CODE_LIBRARY.sanitizeMagnetPreview(magnets[idx].preview);
                    if (!pv || !Array.isArray(pv.screenshots) || !pv.screenshots.length) return;
                    const urls = pv.screenshots.map(s => s.screenshot || s.url || '').filter(Boolean);
                    const meta = [pv.name, pv.fileType || pv.type, UTILS.formatBytes(pv.size)].filter(Boolean).join(' · ');
                    MAGNET_PREVIEW.open(urls, Number.isFinite(Number(shotIdx)) ? Number(shotIdx) : 0, meta);
                },
                copyMagnetItem: (code, idOrIdx) => {
                    const item = actions.resolveItem(code);
                    const magnets = CODE_LIBRARY.normMagnets(item && item.magnet);
                    const t = String(idOrIdx);
                    const entry = magnets.find(e => {
                        const eid = CODE_LIBRARY.magnetId(e);
                        return (eid && String(eid) === t) || (!eid && typeof idOrIdx === 'number' && magnets.indexOf(e) === idOrIdx);
                    }) || magnets[Number(idOrIdx)];
                    if (!entry) { UTILS.showToast('磁力不存在', 'warning'); return; }
                    const mval = CODE_LIBRARY.magnetValue(entry);
                    const text = CODE_LIBRARY.cleanMagnet(entry) || mval;
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        navigator.clipboard.writeText(text).then(() => UTILS.showToast('磁力已复制', 'success')).catch(() => UTILS.showToast('复制失败', 'error'));
                    } else {
                        const ta = document.createElement('textarea');
                        ta.value = text; document.body.appendChild(ta); ta.select();
                        try { document.execCommand('copy'); UTILS.showToast('磁力已复制', 'success'); }
                        catch (e) { UTILS.showToast('复制失败', 'error'); }
                        document.body.removeChild(ta);
                    }
                },
                copyMagnet: (code) => {
                    const item = actions.resolveItem(code);
                    const magnets = CODE_LIBRARY.normMagnets(item && item.magnet);
                    if (!magnets.length) { UTILS.showToast('该番号暂无磁力链接', 'warning'); return; }
                    const text = magnets.map(e => CODE_LIBRARY.magnetValue(e)).join('\n');
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        navigator.clipboard.writeText(text).then(() => UTILS.showToast(`已复制 ${magnets.length} 条磁力`, 'success')).catch(() => UTILS.showToast('复制失败', 'error'));
                    } else {
                        const ta = document.createElement('textarea');
                        ta.value = text; document.body.appendChild(ta); ta.select();
                        try { document.execCommand('copy'); UTILS.showToast(`已复制 ${magnets.length} 条磁力`, 'success'); }
                        catch (e) { UTILS.showToast('复制失败', 'error'); }
                        document.body.removeChild(ta);
                    }
                },
                deleteToTrash: (code) => PanelStore.set({ confirm: { message: '将从列表移除,可在回收站恢复', danger: 'soft', onConfirm: () => { CODE_LIBRARY.delete(code); if (st.detail === code) PanelStore.set({ detail: null }); UTILS.showToast(`番号 ${code} 已移至回收站`, 'success'); } } }),
                restoreFromTrash: (code) => {
                    const trashItems = CODE_LIBRARY.trash.items;
                    const idx = trashItems.findIndex(i => i.code.toUpperCase() === code.toUpperCase());
                    if (idx === -1) { UTILS.showToast('未找到该记录', 'error'); return; }
                    if (CODE_LIBRARY.getItem(code)) { UTILS.showToast('番号库中已存在该番号', 'warning'); return; }
                    const item = trashItems.splice(idx, 1)[0];
                    delete item.deleteDate;
                    CODE_LIBRARY.data.items.unshift(item);
                    CODE_LIBRARY.save();
                    if (st.detail === code) PanelStore.set({ detail: null });
                    UTILS.showToast(`番号 ${code} 已恢复`, 'success');
                },
                permanentDelete: (code) => PanelStore.set({ confirm: { message: `将永久删除 ${code},无法恢复`, danger: 'hard', onConfirm: () => {
                    CODE_LIBRARY.trash.items = CODE_LIBRARY.trash.items.filter(i => i.code.toUpperCase() !== code.toUpperCase());
                    CODE_LIBRARY.save();
                    if (st.detail === code) PanelStore.set({ detail: null });
                    UTILS.showToast(`番号 ${code} 已永久删除`, 'success');
                } } }),
                addCode: () => {
                    PanelStore.set({ prompt: {
                        title: '添加番号',
                        initial: '',
                        placeholder: '输入番号,如 ABC-123',
                        onSubmit: (code) => {
                            if (code && code.trim()) { CODE_LIBRARY.add(code.trim()); UTILS.showToast(`番号 ${code.trim()} 已添加`, 'success'); }
                        }
                    } });
                },
                exportData: () => {
                    const data = CODE_LIBRARY.exportData(st.currentFilter);
                    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url; a.download = 'emh_code_library.json'; a.click();
                    URL.revokeObjectURL(url);
                    UTILS.showToast('数据导出成功', 'success');
                },
                importData: () => {
                    const input = document.createElement('input');
                    input.type = 'file'; input.accept = '.json';
                    input.onchange = (e) => {
                        const file = e.target.files[0];
                        if (file) {
                            const reader = new FileReader();
                            reader.onload = (e) => {
                                try {
                                    CODE_LIBRARY.importData(JSON.parse(e.target.result));
                                    UTILS.showToast('数据导入成功', 'success');
                                } catch (err) { UTILS.showToast('数据导入失败', 'error'); }
                            };
                            reader.readAsText(file);
                        }
                    };
                    input.click();
                },
                batchMark: (status) => {
                    const count = st.selectedItems.length;
                    st.selectedItems.forEach(code => CODE_LIBRARY.markItem(code, status));
                    PanelStore.set({ multiSelectMode: false, selectedItems: [] });
                    UTILS.showToast(`已批量标记 ${count} 个番号`, 'success');
                },
                batchDelete: () => PanelStore.set({ confirm: { message: `将从列表移除选中的 ${st.selectedItems.length} 项,可在回收站恢复`, danger: 'soft', onConfirm: () => { const count = st.selectedItems.length; st.selectedItems.forEach(code => CODE_LIBRARY.delete(code)); PanelStore.set({ multiSelectMode: false, selectedItems: [] }); UTILS.showToast(`已将 ${count} 个番号移至回收站`, 'success'); } } }),
                clearTrash: () => {
                    if (!CODE_LIBRARY.trash.items.length) { UTILS.showToast('回收站已经是空的', 'info'); return; }
                    PanelStore.set({ confirm: { message: '确定要清空回收站吗?此操作不可撤销!', danger: 'hard', onConfirm: () => { CODE_LIBRARY.trash.items = []; CODE_LIBRARY.save(); UTILS.showToast('回收站已清空', 'success'); } } });
                },
                doConfirm: () => {
                    if (st.confirm) {
                        const cb = st.confirm.onConfirm;
                        PanelStore.set({ confirm: null });
                        if (cb) cb();
                    }
                },
                cancelConfirm: () => PanelStore.set({ confirm: null }),
                doPromptSubmit: (value) => {
                    if (st.prompt) {
                        const cb = st.prompt.onSubmit;
                        PanelStore.set({ prompt: null });
                        if (cb) cb(value);
                    }
                },
                cancelPrompt: () => PanelStore.set({ prompt: null }),
                openDetail: (code) => {
                    const item = CODE_LIBRARY.getItem(code) || CODE_LIBRARY.trash.items.find(i => i.code.toUpperCase() === code.toUpperCase());
                    if (item) PanelStore.set({ detail: code });
                },
                closeDetail: () => PanelStore.set({ detail: null }),
                toggleTheme: () => { THEME.set(THEME.next()); PanelStore.set({}); },
                toggleHelp: () => PanelStore.set({ helpOpen: !PanelStore.state.helpOpen }),
                toggleMenu: () => PanelStore.set({ menuOpen: !PanelStore.state.menuOpen }),
                openStandalone: () => { PanelStore.set({ menuOpen: false }); STANDALONE.open(); },
                clearPreviewCaches: () => {
                    const s = CODE_LIBRARY.previewCacheStats();
                    PanelStore.set({ menuOpen: false, confirm: {
                        message: `确定清除全部预览缓存(${s.magnets} 条磁力 / ${s.screenshots} 张截图)?之后可重新拉取。`,
                        danger: 'soft',
                        onConfirm: () => {
                            const n = CODE_LIBRARY.clearAllPreviewCaches();
                            UTILS.showToast(`已清除 ${n} 条磁力 + AVWikiDB 的预览缓存`, 'success');
                        }
                    } });
                },
                kbdStep: (delta) => {
                    const len = items.length;
                    if (!len) return;
                    const cur = st.selectedIndex < 0 ? (delta > 0 ? -1 : 0) : st.selectedIndex;
                    PanelStore.set({ selectedIndex: Math.max(0, Math.min(len - 1, cur + delta)) });
                },
                kbdJump: (i) => {
                    if (!items.length) return;
                    PanelStore.set({ selectedIndex: Math.max(0, Math.min(items.length - 1, i)) });
                },
                kbdOpen: () => {
                    const it = items[st.selectedIndex];
                    if (it) actions.openDetail(it.code);
                },
                kbdDelete: () => {
                    if (st.currentFilter === 'trash') return;
                    const it = items[st.selectedIndex];
                    if (it) actions.deleteToTrash(it.code);
                },
                kbdCopy: () => {
                    const it = items[st.selectedIndex];
                    if (it) actions.copyMagnet(it.code);
                },
                kbdRefreshPreview: () => {
                    const code = st.detail;
                    if (!code) return;
                    const item = actions.resolveItem(code);
                    if (!item) return;
                    const magnets = CODE_LIBRARY.normMagnets(item.magnet);
                    if (!magnets.length) return;
                    const entry = magnets[0];
                    const mid = CODE_LIBRARY.magnetId(entry) || '0';
                    actions.previewMagnet(code, mid, { force: true });
                }
            };

            // actions 每次渲染重建;经 ref 转发给稳定回调,保证 memo(ItemRow) 生效且无过期闭包
            actionsRef.current = actions;
            const stableOnToggle = useCallback((code) => actionsRef.current.toggleItem(code), []);
            const stableOnOpenDetail = useCallback((code) => actionsRef.current.openDetail(code), []);
            const stableOnFav = useCallback((code) => actionsRef.current.markFav(code), []);
            const stableOnWatch = useCallback((code) => actionsRef.current.markWatched(code), []);
            const stableOnUnfav = useCallback((code) => actionsRef.current.unfavorite(code), []);
            const stableOnDelete = useCallback((code) => actionsRef.current.deleteToTrash(code), []);
            const stableOnRestore = useCallback((code) => actionsRef.current.restoreFromTrash(code), []);
            const stableOnPurge = useCallback((code) => actionsRef.current.permanentDelete(code), []);

            const kbdRef = useRef(null);
            kbdRef.current = (e) => {
                if (!st.visible) return;
                // 灯箱打开时由 MAGNET_PREVIEW 处理键盘;方向键兜底 preventDefault 防止浏览器历史导航
                if (MAGNET_PREVIEW._open) {
                    if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') e.preventDefault();
                    return;
                }
                const key = e.key;
                if (key === 'Escape') {
                    if (st.menuOpen) { e.preventDefault(); actions.toggleMenu(); return; }
                    if (st.helpOpen) { e.preventDefault(); actions.toggleHelp(); return; }
                    if (st.prompt) { e.preventDefault(); actions.cancelPrompt(); return; }
                    if (st.confirm) { e.preventDefault(); actions.cancelConfirm(); return; }
                    if (st.magnetSearch) { e.preventDefault(); actions.closeMagnetSearch(); return; }
                    if (st.detail) { e.preventDefault(); actions.closeDetail(); return; }
                    e.preventDefault(); actions.hidePanel(); return;
                }
                // 文本输入焦点时不劫持快捷键
                const ae = document.activeElement;
                if (ae && (ae.tagName === 'INPUT' || ae.tagName === 'TEXTAREA' || ae.isContentEditable)) return;
                if (key === '?') { e.preventDefault(); actions.toggleHelp(); return; }
                if (st.helpOpen || st.menuOpen || st.prompt || st.confirm || st.magnetSearch || st.batchProgress) return;
                if (st.detail) {
                    if (key === 'r' || key === 'R') { e.preventDefault(); actions.kbdRefreshPreview(); }
                    return;
                }
                if (key === '/') { e.preventDefault(); if (searchRef.current) searchRef.current.focus(); return; }
                if (key === 'n' || key === 'N') { e.preventDefault(); actions.addCode(); return; }
                if (key === 'ArrowDown') { e.preventDefault(); actions.kbdStep(1); return; }
                if (key === 'ArrowUp') { e.preventDefault(); actions.kbdStep(-1); return; }
                if (key === 'Home') { e.preventDefault(); actions.kbdJump(0); return; }
                if (key === 'End') { e.preventDefault(); actions.kbdJump(items.length - 1); return; }
                if (key === 'Enter') {
                    // 面板内交互控件(按钮/链接/下拉)聚焦时,Enter 交由原生激活,避免劫持导致按钮失效
                    if (ae && (ae.tagName === 'BUTTON' || ae.tagName === 'A' || ae.tagName === 'SELECT') && ae.closest && ae.closest('.emh-code-manager-panel')) return;
                    e.preventDefault(); actions.kbdOpen(); return;
                }
                if (key === 'Delete' || key === 'Backspace') {
                    // 仅在确有选中行(且非回收站视图)时劫持,避免无谓吞掉浏览器默认行为
                    if (st.selectedIndex >= 0 && st.currentFilter !== 'trash' && items[st.selectedIndex]) { e.preventDefault(); actions.kbdDelete(); }
                    return;
                }
                if ((e.metaKey || e.ctrlKey) && (key === 'c' || key === 'C')) {
                    // 仅在确有选中行时劫持复制
                    if (st.selectedIndex >= 0 && items[st.selectedIndex]) { e.preventDefault(); actions.kbdCopy(); }
                    return;
                }
            };
            useEffect(() => {
                const onKey = (e) => { if (kbdRef.current) kbdRef.current(e); };
                document.addEventListener('keydown', onKey);
                return () => document.removeEventListener('keydown', onKey);
            }, []);

            useEffect(() => {
                document.body.style.overflow = st.visible ? 'hidden' : '';
                return () => { document.body.style.overflow = ''; };
            }, [st.visible]);

            // 面板宽度可调:左缘拖拽 320-900px,持久化 GM
            useEffect(() => {
                const panel = document.getElementById('emh-code-manager-panel');
                const handle = resizeRef.current;
                if (!panel || !handle) return;
                let w = 520;
                try {
                    const saved = typeof GM_getValue === 'function' ? GM_getValue('emh_panel_size') : null;
                    if (saved) { const n = Number(saved); if (Number.isFinite(n) && n >= 320 && n <= 900) w = n; }
                } catch (e) {}
                panel.style.setProperty('--emh-panel-w', w + 'px');
                let startX = 0, startW = w, active = false;
                const onMove = (e) => {
                    if (!active) return;
                    const nw = Math.max(320, Math.min(900, startW + (startX - e.clientX)));
                    panel.style.setProperty('--emh-panel-w', nw + 'px');
                };
                const onUp = () => {
                    if (!active) return;
                    active = false;
                    panel.style.transition = '';
                    document.removeEventListener('pointermove', onMove);
                    document.removeEventListener('pointerup', onUp);
                    const cur = parseFloat(panel.style.getPropertyValue('--emh-panel-w')) || 520;
                    try { if (typeof GM_setValue === 'function') GM_setValue('emh_panel_size', String(Math.round(cur))); } catch (err) {}
                };
                const onDown = (e) => {
                    e.preventDefault();
                    active = true;
                    startX = e.clientX;
                    startW = parseFloat(panel.style.getPropertyValue('--emh-panel-w')) || 520;
                    panel.style.transition = 'none';
                    document.addEventListener('pointermove', onMove);
                    document.addEventListener('pointerup', onUp);
                };
                handle.addEventListener('pointerdown', onDown);
                return () => {
                    handle.removeEventListener('pointerdown', onDown);
                    document.removeEventListener('pointermove', onMove);
                    document.removeEventListener('pointerup', onUp);
                };
            }, [st.visible]);

            const all = CODE_LIBRARY.getAll();
            const favList = CODE_LIBRARY.getFavorites();
            const watchedList = CODE_LIBRARY.getWatched();
            const trashList = CODE_LIBRARY.getTrash();

            let items = st.currentFilter === 'favorite' ? favList
                : st.currentFilter === 'watched' ? watchedList
                : st.currentFilter === 'trash' ? trashList : all;

            if (st.timeFilter) {
                const now = Date.now(); const day = 86400000;
                let cutoff = now;
                if (st.timeFilter === 'today') cutoff = new Date().setHours(0, 0, 0, 0);
                else if (st.timeFilter === '7d') cutoff = now - 7 * day;
                else if (st.timeFilter === '30d') cutoff = now - 30 * day;
                items = items.filter(i => i.createdDate && new Date(i.createdDate).getTime() >= cutoff);
            }
            if (st.magnetFilter === 'has') {
                items = items.filter(i => CODE_LIBRARY.normMagnets(i.magnet).length > 0);
            } else if (st.magnetFilter === 'none') {
                items = items.filter(i => CODE_LIBRARY.normMagnets(i.magnet).length === 0);
            }
            const q = st.searchQuery.toLowerCase();
            if (q) items = items.filter(i => i.code.toLowerCase().includes(q) || (typeof i.remarks === 'string' && i.remarks.toLowerCase().includes(q)));

            // 键盘选中索引随列表变化收敛到有效范围(须在 items 计算之后)
            useEffect(() => {
                if (items.length === 0) {
                    if (st.selectedIndex !== -1) PanelStore.set({ selectedIndex: -1 });
                } else if (st.selectedIndex >= items.length) {
                    PanelStore.set({ selectedIndex: items.length - 1 });
                }
            }, [items.length, st.selectedIndex]);

            // 键盘选中项滚动到可视区(用 scrollTop,不用 scrollIntoView)
            useEffect(() => {
                if (st.selectedIndex < 0 || !contentRef.current) return;
                const el = contentRef.current.querySelectorAll('.emh-item[data-code]')[st.selectedIndex];
                if (!el) return;
                const cont = contentRef.current;
                cont.scrollTop = Math.max(0, el.offsetTop - cont.clientHeight / 2);
            }, [st.selectedIndex]);

            // standalone 双栏:选中行 → 右侧详情联动
            useEffect(() => {
                if (window.__EMH_STANDALONE && st.selectedIndex >= 0) {
                    const it = items[st.selectedIndex];
                    if (it && st.detail !== it.code) actions.openDetail(it.code);
                }
            }, [st.selectedIndex]);

            let emptyMsg = '番号库为空,点击"添加"开始';
            let emptyIcon = 'library';
            if (st.currentFilter === 'favorite') { emptyMsg = '暂无关注番号'; emptyIcon = 'favorite'; }
            else if (st.currentFilter === 'watched') { emptyMsg = '暂无已看记录'; emptyIcon = 'watched'; }
            else if (st.currentFilter === 'trash') { emptyMsg = '回收站为空'; emptyIcon = 'trash'; }
            if (st.timeFilter) { emptyMsg = st.timeFilter === 'today' ? '今天暂无新增' : '该时间段内无记录'; emptyIcon = 'time'; }
            if (st.magnetFilter === 'has') { emptyMsg = '没有有磁力的番号'; emptyIcon = 'has'; }
            else if (st.magnetFilter === 'none') { emptyMsg = '没有无磁力的番号'; emptyIcon = 'none'; }
            if (q) { emptyMsg = `未找到匹配 "${st.searchQuery}" 的番号`; emptyIcon = 'search'; }

            useEffect(() => {
                if (!headRef.current) return;
                const total = items.length;
                const selCount = st.selectedItems.length;
                headRef.current.checked = total > 0 && selCount === total;
                headRef.current.indeterminate = selCount > 0 && selCount < total;
            }, [items, st.selectedItems]);

            const isTrash = st.currentFilter === 'trash';
            const rowView = st.currentFilter === 'favorite' ? 'favorite'
                : st.currentFilter === 'watched' ? 'watched'
                : st.currentFilter === 'trash' ? 'trash' : 'unmarked';
            const tabDefs = [
                { f: 'all', label: '全部', n: all.length },
                { f: 'favorite', label: '关注', n: favList.length },
                { f: 'watched', label: '已看', n: watchedList.length },
                { f: 'trash', label: '回收站', n: trashList.length }
            ];

            const headChange = (e) => {
                if (isTrash) return;
                if (e.target.checked) PanelStore.set({ multiSelectMode: true, selectedItems: items.map(i => i.code) });
                else PanelStore.set({ multiSelectMode: false, selectedItems: [] });
            };

            return html`
                <${Fragment}>
                    ${st.visible ? html`
                        <div class="emh-panel-backdrop" onClick=${actions.hidePanel}></div>
                        <div id="emh-code-manager-panel" class="emh-code-manager-panel visible">
                        <div ref=${resizeRef} class="emh-panel-resize" title="拖动调整宽度"></div>
                        <div class="emh-panel-header">
                            <h2><span class="emh-panel-logo"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></span> ${window.__EMH_STANDALONE ? '番号库' : '管理中心'} <span class="emh-header-count">${items.length > 0 ? `(${items.length})` : ''}</span></h2>
                            <div class="emh-panel-controls">
                                <button class="emh-theme-toggle" title="更多选项" onClick=${actions.toggleMenu}>
                                    <svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor"><circle cx="12" cy="5" r="1.6"/><circle cx="12" cy="12" r="1.6"/><circle cx="12" cy="19" r="1.6"/></svg>
                                </button>
                                <button class="emh-theme-toggle" title="快捷键帮助 (? )" onClick=${actions.toggleHelp}>
                                    <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
                                </button>
                                <button class="emh-theme-toggle" title="切换主题(当前:${THEME.get() === 'dark' ? '深色' : THEME.get() === 'light' ? '浅色' : '跟随系统'})" onClick=${actions.toggleTheme}>
                                    ${THEME.get() === 'dark' ? html`<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>` : THEME.get() === 'light' ? html`<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>` : html`<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>`}
                                </button>
                                <button class="emh-panel-close" title="关闭 (Esc)" onClick=${actions.hidePanel}>×</button>
                            </div>
                        </div>
                            <div class="emh-panel-tabs">
                                ${tabDefs.map(t => html`
                                    <button data-filter="${t.f}" class="${st.currentFilter === t.f ? 'active' : ''}"
                                            onClick=${() => actions.setFilter(t.f)}>${t.label} ${t.n > 0 ? html`<span class="emh-tab-count">${t.n}</span>` : ''}</button>
                                `)}
                            </div>
                            <div class="emh-panel-search">
                                <div class="emh-search-wrapper">
                                    <input ref=${searchRef} type="text" placeholder="搜索番号或备注…"
                                           value=${searchDraft}
                                           onInput=${onSearchInput} />
                                    <button class="emh-search-clear ${searchDraft ? 'visible' : ''}" title="清除"
                                            onClick=${clearSearch}>×</button>
                                </div>
                                <div class="emh-filter-row">
                                    <span class="emh-filter-tag ${st.timeFilter === '' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ timeFilter: '' })}>全部</span>
                                    <span class="emh-filter-tag ${st.timeFilter === 'today' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ timeFilter: 'today' })}>今天</span>
                                    <span class="emh-filter-tag ${st.timeFilter === '7d' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ timeFilter: '7d' })}>7天</span>
                                    <span class="emh-filter-tag ${st.timeFilter === '30d' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ timeFilter: '30d' })}>30天</span>
                                    <span class="emh-filter-divider"></span>
                                    <span class="emh-filter-tag ${st.magnetFilter === '' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ magnetFilter: '' })}>磁力不限</span>
                                    <span class="emh-filter-tag ${st.magnetFilter === 'has' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ magnetFilter: 'has' })}>有磁力</span>
                                    <span class="emh-filter-tag ${st.magnetFilter === 'none' ? 'active' : ''}"
                                          onClick=${() => PanelStore.set({ magnetFilter: 'none' })}>无磁力</span>
                                </div>
                            </div>
                            <div class="emh-list-header">
                                <label class="emh-checkbox-wrap" style=${isTrash ? 'visibility:hidden;' : ''}><input type="checkbox" ref=${headRef}
                                       onClick=${e => e.stopPropagation()} onChange=${headChange} /></label>
                                <span class="emh-col-code">番号</span>
                                <span class="emh-col-status">状态</span>
                                <span class="emh-col-actions">操作</span>
                            </div>
                            <div class="emh-panel-content ${st.multiSelectMode ? 'multi-select' : ''}" ref=${contentRef}>
                                ${items.length > 0 ? items.map((item, idx) => html`
                                    <${ItemRowMemo} key=${item.code} item=${item} view=${rowView} multi=${st.multiSelectMode}
                                        selected=${st.selectedItems.includes(item.code)}
                                        kbdSel=${!st.multiSelectMode && st.selectedIndex === idx}
                                        onToggle=${stableOnToggle} onOpenDetail=${stableOnOpenDetail}
                                        onFav=${stableOnFav} onWatch=${stableOnWatch}
                                        onUnfav=${stableOnUnfav}
                                        onDelete=${stableOnDelete}
                                        onRestore=${stableOnRestore} onPurge=${stableOnPurge} />
                                `) : html`
                                    <div class="emh-empty-state"><div class="emh-empty-state-icon">${EMPTY_ICONS[emptyIcon] || EMPTY_ICONS.library}</div><div>${emptyMsg}</div></div>
                                `}
                            </div>
                            ${st.multiSelectMode ? html`
                                <div class="emh-panel-multi-actions" style="display:flex;">
                                    <span class="emh-selected-count">已选 ${st.selectedItems.length} 项</span>
                                    <button class="btn btn-outline emh-expand" aria-label="全选" onClick=${actions.selectAll}>${ICON.checkSquare}<span class="emh-expand-label">全选</span></button>
                                    <button class="btn my-btn-primary emh-expand" aria-label="磁力" onClick=${actions.batchFetchMagnets}>${ICON.refresh}<span class="emh-expand-label">磁力</span></button>
                                    <button class="btn my-btn-success emh-expand" aria-label="关注" onClick=${() => actions.batchMark('favorite')}>${ICON.heart}<span class="emh-expand-label">关注</span></button>
                                    <button class="btn my-btn-success emh-expand" aria-label="已看" onClick=${() => actions.batchMark('watched')}>${ICON.checkCircle}<span class="emh-expand-label">已看</span></button>
                                    <button class="btn my-btn-danger emh-expand" aria-label="删除" onClick=${actions.batchDelete}>${ICON.trash}<span class="emh-expand-label">删除</span></button>
                                    <button class="btn btn-outline emh-expand" aria-label="取消" onClick=${actions.toggleMulti}>${ICON.x}<span class="emh-expand-label">取消</span></button>
                                </div>
                            ` : html`
                                <div class="emh-panel-actions">
                                    <button class="btn my-btn-primary emh-btn-add emh-expand" aria-label="添加" onClick=${actions.addCode} style=${isTrash ? 'display:none;' : ''}>${ICON.plus}<span class="emh-expand-label">添加</span></button>
                                    <button class="btn btn-outline emh-expand" aria-label="批量" onClick=${actions.toggleMulti} style=${isTrash ? 'display:none;' : ''}>${ICON.list}<span class="emh-expand-label">批量</span></button>
                                    <button class="btn btn-outline emh-expand" aria-label="导入" onClick=${actions.importData}>${ICON.import}<span class="emh-expand-label">导入</span></button>
                                    <button class="btn btn-outline emh-expand" aria-label="导出" onClick=${actions.exportData}>${ICON.export}<span class="emh-expand-label">导出</span></button>
                                    <button class="btn my-btn-danger emh-expand" aria-label="清空" onClick=${actions.clearTrash} style=${isTrash && trashList.length > 0 ? '' : 'display:none;'}>${ICON.trash}<span class="emh-expand-label">清空</span></button>
                                </div>
                            `}
                            <${ConfirmModal} confirm=${st.confirm} onConfirm=${actions.doConfirm} onCancel=${actions.cancelConfirm} />
                            <${PromptModal} promptState=${st.prompt} onSubmit=${actions.doPromptSubmit} onCancel=${actions.cancelPrompt} />
                            <${MagnetListModal} magnetSearch=${st.magnetSearch} onPick=${actions.fetchMagnetDetail} onClose=${actions.closeMagnetSearch} />
                            <${BatchProgressModal} progress=${st.batchProgress} />
                            ${st.helpOpen ? html`<${HelpModal} onClose=${actions.toggleHelp} />` : ''}
                            ${st.menuOpen ? html`<${HeaderMenu} onClose=${actions.toggleMenu} onClear=${actions.clearPreviewCaches} onOpenStandalone=${window.__EMH_STANDALONE ? null : actions.openStandalone} stats=${CODE_LIBRARY.previewCacheStats()} />` : ''}
                            ${st.detail ? (() => {
                                const detailItem = CODE_LIBRARY.getItem(st.detail) || (trashList.find(i => i.code.toUpperCase() === st.detail.toUpperCase())) || null;
                                const detailInTrash = detailItem ? trashList.some(i => i.code.toUpperCase() === detailItem.code.toUpperCase()) : false;
                                return detailItem ? html`
                                    <${DetailDrawer} item=${detailItem} inTrash=${detailInTrash} onClose=${actions.closeDetail}
                                        onEdit=${actions.editRemark} onEditTags=${actions.editTags}
                                        onEditMagnet=${actions.editMagnet}
                                        onEditMagnetItem=${actions.editMagnetItem} onRemoveMagnet=${actions.removeMagnet}
                                        onCopyMagnetItem=${actions.copyMagnetItem}
                                        onCopyMagnet=${actions.copyMagnet} onPreviewMagnet=${actions.previewMagnet}
                                        onPreviewMagnetAt=${actions.previewMagnetAt}
                                        onPreviewAvwiki=${actions.previewAvwiki}
                                        onSearchMagnet=${actions.searchMagnet}
                                        onFav=${actions.markFav}
                                        onWatch=${actions.markWatched} onUnfav=${actions.unfavorite}
                                        onDelete=${actions.deleteToTrash}
                                        onRestore=${actions.restoreFromTrash} onPurge=${actions.permanentDelete} />
                                ` : null;
                            })() : null}
                        </div>
                    ` : null}
                    <${ToastContainer} />
                </${Fragment}>
            `;
        }

        const CodeManagerPanel = {
            get isVisible() { return PanelStore.state.visible; },
            get lastSyncTimestamp() { return PanelStore.state.lastSyncTimestamp; },
            set lastSyncTimestamp(v) { PanelStore.set({ lastSyncTimestamp: v }); },
            initialized: false,
            mountEl: null,

            init: function() {
                if (this.initialized) return;
                this.initialized = true;
                // standalone:首帧即展开(render 前设 visible),避免 showPanel 在
                // useEffect 订阅注册前丢失更新(preact useEffect 延迟执行)
                if (window.__EMH_STANDALONE) PanelStore.set({ visible: true });
                this.createStyles();
                this.createToggleButton();
                this.mountEl = document.createElement('div');
                this.mountEl.id = 'emh-code-manager-mount';
                document.body.appendChild(this.mountEl);
                render(h(CodeManagerApp), this.mountEl);
            },

            createToggleButton: function() {
                const existing = document.getElementById('emh-code-manager-toggle');
                if (existing) existing.remove();
                const btn = document.createElement('button');
                btn.id = 'emh-code-manager-toggle';
                btn.className = 'emh-code-manager-toggle';
                btn.innerHTML = ICONS.grid + '<span>番号库</span>';
                btn.title = '管理番号库';
                btn.addEventListener('click', () => this.togglePanel());
                document.body.appendChild(btn);
            },

            togglePanel: function() {
                PanelStore.state.visible ? PanelStore.set({ visible: false, multiSelectMode: false, selectedItems: [], helpOpen: false }) : PanelStore.set({ visible: true });
            },

            showPanel: function() { PanelStore.set({ visible: true }); },

            hidePanel: function() { PanelStore.set({ visible: false, multiSelectMode: false, selectedItems: [], helpOpen: false }); },

            refreshPanelContent: function() { PanelStore.refresh(); },

            createStyles: function() {
                const styleElement = document.createElement('style');
                styleElement.textContent = `
                    .emh-code-manager-panel {
                        position: fixed; top: 0;
                        right: calc(-1 * (min(var(--emh-panel-w, 520px), calc(100vw - 24px)) + 40px));
                        width: min(var(--emh-panel-w, 520px), calc(100vw - 24px)); height: 100vh;
                        background: var(--emh-bg);
                        box-shadow: var(--emh-shadow-lg); z-index: 10010;
                        border-left: 1px solid var(--emh-border);
                        transition: right 0.3s cubic-bezier(0.25,0.8,0.25,1);
                        display: flex; flex-direction: column;
                        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                        font-size: 13px; color: var(--emh-text);
                        border-radius: 16px 0 0 16px;
                        overflow: hidden;
                    }
                    .emh-code-manager-panel.visible { right: 0; }
                    .emh-panel-resize {
                        position: absolute; left: 0; top: 0; bottom: 0; width: 12px;
                        cursor: ew-resize; z-index: 5; touch-action: none;
                    }
                    .emh-panel-resize::after {
                        content: ''; position: absolute; left: 4px; top: 50%; transform: translateY(-50%);
                        width: 4px; height: 44px; border-radius: 999px;
                        background: var(--emh-border-strong); opacity: 0; transition: opacity 0.15s;
                    }
                    .emh-panel-resize:hover::after, .emh-panel-resize:active::after { opacity: 1; }
                    .emh-panel-backdrop {
                        position: fixed; top: 0; left: 0; right: 0; bottom: 0;
                        background: var(--emh-overlay); z-index: 10009;
                        animation: emh-fade-in 0.2s ease;
                        -webkit-backdrop-filter: blur(4px);
                        backdrop-filter: blur(4px);
                    }
                    @keyframes emh-fade-in { from { opacity: 0; } to { opacity: 1; } }
                    .emh-panel-header {
                        display: flex; justify-content: space-between; align-items: center;
                        height: 52px; padding: 0 16px; box-sizing: border-box;
                        background: var(--emh-bg);
                        border-bottom: 1px solid var(--emh-border);
                        position: relative; z-index: 1; flex-shrink: 0;
                    }
                    .emh-panel-header h2 { margin: 0; font-size: 14px; font-weight: 700; color: var(--emh-text); letter-spacing: 0.1px; display: flex; align-items: center; gap: 8px; }
                    .emh-panel-logo { font-size: 18px; line-height: 1; color: var(--emh-primary); display: inline-flex; align-items: center; }
                    .emh-panel-close {
                        background: var(--emh-btn-bg); border: none; font-size: 18px; cursor: pointer; color: var(--emh-text-secondary);
                        width: 30px; height: 30px; border-radius: var(--emh-radius-sm); transition: background 0.15s, color 0.15s; line-height: 1;
                        display: inline-flex; align-items: center; justify-content: center;
                    }
                    .emh-panel-close:hover { background: var(--emh-danger-soft); color: var(--emh-danger); }
                    .emh-panel-controls { display: inline-flex; align-items: center; gap: 6px; }
                    .emh-theme-toggle {
                        background: var(--emh-btn-bg); border: none; cursor: pointer; color: var(--emh-text-secondary);
                        width: 30px; height: 30px; border-radius: var(--emh-radius-sm); transition: background 0.15s, color 0.15s; line-height: 1;
                        display: inline-flex; align-items: center; justify-content: center;
                    }
                    .emh-theme-toggle:hover { background: var(--emh-primary-soft); color: var(--emh-primary); }
                    .emh-theme-toggle:focus-visible { outline: 2px solid var(--emh-primary); outline-offset: 2px; }
                    .emh-header-menu-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 10011; background: transparent; }
                    .emh-header-menu {
                        position: absolute; top: 48px; right: 12px; z-index: 10012;
                        min-width: 224px; padding: 8px; border-radius: var(--emh-radius);
                        background: var(--emh-surface-raised); border: 1px solid var(--emh-border);
                        box-shadow: var(--emh-shadow-lg);
                        animation: emh-modal-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
                    }
                    .emh-header-menu-title { font-size: 11px; font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase; color: var(--emh-text-muted); padding: 4px 8px 6px; }
                    .emh-header-menu-stat { font-size: 12px; color: var(--emh-text-secondary); padding: 2px 8px 8px; line-height: 1.5; }
                    .emh-header-menu-item {
                        display: block; width: 100%; text-align: left; padding: 8px 10px; border-radius: 8px;
                        border: none; background: none; cursor: pointer; font-size: 13px; color: var(--emh-text);
                        transition: background 0.15s, color 0.15s;
                    }
                    .emh-header-menu-item:hover { background: var(--emh-danger-soft); color: var(--emh-danger); }
                    .emh-header-menu-item:focus-visible { outline: 2px solid var(--emh-primary); outline-offset: -2px; }
                    .emh-header-menu-item-plain:hover { background: var(--emh-primary-soft); color: var(--emh-primary); }
                    .emh-panel-close:focus-visible,
                    .emh-search-clear:focus-visible,
                    .emh-magnet-op:focus-visible { outline: 2px solid var(--emh-primary); outline-offset: 2px; }
                    .emh-header-count { font-size: 12px; font-weight: 500; color: var(--emh-text-muted); margin-left: 6px; }
                    .emh-tab-count { font-size: 11px; background: var(--emh-btn-bg); color: var(--emh-text-secondary); padding: 1px 7px; border-radius: 999px; margin-left: 5px; font-weight: 600; }
                    .emh-panel-tabs button.active .emh-tab-count { background: var(--emh-primary-soft); color: var(--emh-primary); }
                    .emh-panel-tabs { display: flex; background: var(--emh-surface); border-bottom: 1px solid var(--emh-border); padding: 0 12px; flex-shrink: 0; }
                    .emh-panel-tabs button {
                        background: none; border: none; padding: 11px 14px; font-size: 13px; font-weight: 500;
                        cursor: pointer; color: var(--emh-text-secondary); position: relative; transition: color 0.15s, background-color 0.15s;
                        letter-spacing: 0.1px; border-radius: 8px;
                    }
                    .emh-panel-tabs button:hover { color: var(--emh-primary); background: var(--emh-primary-softer); }
                    .emh-panel-tabs button:focus-visible { outline: 2px solid var(--emh-primary); outline-offset: -2px; }
                    .emh-panel-tabs button.active { color: var(--emh-primary); font-weight: 600; background: none; }
                    .emh-panel-tabs button.active::after {
                        content: ''; position: absolute; bottom: 0; left: 12px; right: 12px;
                        height: 2.5px; background: var(--emh-primary); border-radius: 2px;
                    }
                    .emh-panel-search { padding: 14px 20px 12px; background: var(--emh-surface); border-bottom: 1px solid var(--emh-border); flex-shrink: 0; }
                    .emh-search-wrapper { position: relative; display: flex; }
                    .emh-search-wrapper input {
                        flex: 1; width: 100%; box-sizing: border-box; padding: 9px 36px 9px 14px; border: 1px solid var(--emh-border);
                        border-radius: 10px; outline: none; font-size: 13px; letter-spacing: 0.1px;
                        transition: border-color 0.15s, box-shadow 0.15s, background 0.15s; background: var(--emh-bg);
                        font-family: inherit; box-shadow: inset 0 1px 2px rgba(0,0,0,0.03); color: var(--emh-text);
                    }
                    .emh-search-wrapper input:hover { border-color: var(--emh-border-strong); }
                    .emh-search-wrapper input:focus { border-color: var(--emh-primary); box-shadow: 0 0 0 3px var(--emh-focus-ring); background: var(--emh-surface); }
                    .emh-search-wrapper input::placeholder { color: var(--emh-text-muted); }
                    .emh-search-clear {
                        position: absolute; right: 8px; top: 50%; transform: translateY(-50%);
                        background: var(--emh-btn-bg); border: none; color: var(--emh-text-muted); font-size: 14px;
                        cursor: pointer; width: 22px; height: 22px; border-radius: 50%; line-height: 1;
                        display: none; transition: background 0.15s, color 0.15s; align-items: center; justify-content: center;
                    }
                    .emh-search-clear:hover { background: var(--emh-btn-hover); color: var(--emh-text); }
                    .emh-search-clear.visible { display: inline-flex; }
                    .emh-filter-row { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; align-items: center; }
                    .emh-filter-divider { width: 1px; height: 16px; background: var(--emh-border-strong); margin: 0 2px; flex-shrink: 0; }
                    .emh-filter-tag {
                        display: inline-flex; align-items: center; justify-content: center;
                        padding: 4px 12px; border-radius: 999px; cursor: pointer;
                        font-size: 12px; font-weight: 500; color: var(--emh-text-secondary);
                        background: var(--emh-surface); border: 1px solid var(--emh-border);
                        transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
                        user-select: none;
                    }
                    .emh-filter-tag:hover { border-color: var(--emh-primary); color: var(--emh-primary); background: var(--emh-primary-softer); }
                    .emh-filter-tag.active {
                        background: var(--emh-primary); color: var(--emh-on-primary);
                        border-color: var(--emh-primary); box-shadow: 0 2px 8px var(--emh-primary-soft);
                    }
                    .emh-list-header {
                        display: flex; align-items: center; padding: 8px 20px;
                        background: var(--emh-bg); border-bottom: 1px solid var(--emh-border);
                        font-size: 11px; font-weight: 600; color: var(--emh-text-muted);
                        text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0;
                    }
                    .emh-col-code { flex: 1; padding-left: 8px; }
                    .emh-col-status { width: 64px; text-align: center; }
                    .emh-col-actions { width: 80px; text-align: right; }
                    .emh-checkbox-wrap { display: inline-flex; align-items: center; }
                    .emh-checkbox-wrap input[type="checkbox"] { width: 15px; height: 15px; cursor: pointer; accent-color: var(--emh-primary); }
                    .emh-panel-content {
                        flex: 1; overflow-y: auto; padding: 10px 14px;
                        background: var(--emh-bg); scrollbar-width: thin;
                        scrollbar-color: var(--emh-border-strong) transparent;
                        position: relative;
                    }
                    .emh-panel-content::-webkit-scrollbar { width: 6px; }
                    .emh-panel-content::-webkit-scrollbar-thumb { background: var(--emh-border-strong); border-radius: 3px; }
                    .emh-panel-content::-webkit-scrollbar-track { background: transparent; }
                    .emh-panel-actions, .emh-panel-multi-actions {
                        padding: 12px 20px; display: flex; flex-wrap: wrap; gap: 10px; align-items: stretch;
                        border-top: 1px solid var(--emh-border); flex-shrink: 0;
                        background: var(--emh-bg);
                    }
                    .emh-panel-actions .btn, .emh-panel-multi-actions .btn {
                        flex: 0 0 auto; margin-left: 0; justify-content: center;
                        padding: 9px 12px; min-height: 36px;
                    }
                    .emh-panel-actions .emh-btn-add { font-size: 13px; box-shadow: 0 2px 8px var(--emh-primary-soft); }
                    .emh-panel-multi-actions { align-items: center; flex-wrap: wrap; gap: 8px; }
                    .emh-panel-multi-actions .btn { flex: 0 0 auto; padding: 8px 10px; min-height: 34px; }
                    .emh-panel-multi-actions .emh-selected-count { flex: 1 1 100%; text-align: center; margin-bottom: 2px; }
                    .emh-item {
                        display: flex; align-items: center; gap: 6px; padding: 10px 14px;
                        border: 1px solid var(--emh-border); border-radius: 10px; margin-bottom: 6px;
                        background: var(--emh-surface); box-shadow: var(--emh-shadow-sm);
                        transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease, background 0.15s ease;
                    }
                    .emh-item:hover { border-color: var(--emh-border-strong); box-shadow: var(--emh-shadow-md); transform: translateY(-1px); }
                    .emh-item.favorite { border-left: 3px solid var(--emh-danger); }
                    .emh-item.watched { border-left: 3px solid var(--emh-success); }
                    .emh-item.selected { background: var(--emh-primary-soft); border-color: var(--emh-primary); box-shadow: 0 0 0 1px var(--emh-focus-ring); }
                    .emh-item.kbd-sel { border-color: var(--emh-primary); box-shadow: 0 0 0 2px var(--emh-focus-ring); }
                    .emh-item-code {
                        font-weight: 600; color: var(--emh-text); font-size: 13px;
                        font-family: var(--emh-font-mono); letter-spacing: 0.1px;
                        font-variant-numeric: tabular-nums;
                        overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%;
                    }
                    .emh-item-remarks {
                        font-size: 12px; color: var(--emh-text-muted);
                        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                        margin-top: 2px; letter-spacing: 0.1px; max-width: 100%;
                    }
                    .emh-item-actions {
                        display: inline-flex; align-items: center; gap: 2px;
                        padding: 2px; border-radius: var(--emh-radius-sm);
                        background: var(--emh-surface-raised); border: 1px solid var(--emh-border);
                        opacity: 0; transform: translateX(6px);
                        transition: opacity 0.18s ease, transform 0.18s ease;
                        box-shadow: var(--emh-shadow-sm);
                    }
                    .emh-item:hover .emh-item-actions { opacity: 1; transform: translateX(0); }
                    .emh-item-actions button {
                        background: none; border: none; cursor: pointer; padding: 0;
                        width: 28px; height: 28px; min-width: 28px; min-height: 28px; border-radius: 6px;
                        font-size: 13px; opacity: 0.75; transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
                        line-height: 1;
                        display: inline-flex; align-items: center; justify-content: center;
                        color: var(--emh-text-secondary);
                    }
                    .emh-item-actions button:hover { opacity: 1; background: var(--emh-btn-hover); transform: scale(1.06); box-shadow: var(--emh-shadow-sm); }
                    .emh-item-actions button.emh-act-favorite:hover { background: var(--emh-danger-soft); color: var(--emh-danger); }
                    .emh-item-actions button.emh-act-watched:hover { background: var(--emh-success-soft); color: var(--emh-success); }
                    .emh-item-actions button.emh-act-delete:hover { background: var(--emh-danger-soft); color: var(--emh-danger); }
                    .emh-item-actions button.emh-act-unfav:hover { background: var(--emh-warning-soft); color: var(--emh-warning); }
                    .emh-item-actions button.emh-act-restore:hover { background: var(--emh-primary-soft); color: var(--emh-primary); }
                    .emh-item-actions button.emh-act-purge:hover { background: var(--emh-danger-soft); color: var(--emh-danger); }
                    .emh-item > .emh-checkbox-wrap { width: 22px; flex-shrink: 0; }
                    .emh-item > .emh-col-code { flex: 1; min-width: 0; padding-left: 2px; overflow: hidden; }
                    .emh-item > .emh-col-status { width: 64px; text-align: center; flex-shrink: 0; }
                    .emh-item > .emh-col-actions { width: 80px; text-align: right; flex-shrink: 0; display: flex; justify-content: flex-end; }
                    .emh-panel-content.multi-select .emh-item > .emh-col-actions { width: 0; overflow: hidden; }
                    .emh-panel-content.multi-select .emh-list-header .emh-col-actions { display: none; }
                    .emh-status-tag { display: inline-block; font-size: 11px; font-weight: 600; padding: 1px 9px; border-radius: 999px; line-height: 1.7; white-space: nowrap; letter-spacing: 0.1px; border: 1px solid transparent; }
                    .emh-status-tag.unmarked { background: var(--emh-btn-bg); color: var(--emh-text-secondary); border-color: var(--emh-border); }
                    .emh-status-tag.favorite { background: var(--emh-danger-soft); color: var(--emh-danger); border-color: var(--emh-danger-soft); }
                    .emh-status-tag.watched { background: var(--emh-success-soft); color: var(--emh-success); border-color: var(--emh-success-soft); }
                    .emh-empty-state { text-align: center; color: var(--emh-text-muted); padding: 48px 0; font-size: 13px; letter-spacing: 0.2px; }
                    .emh-empty-state-icon { font-size: 36px; margin-bottom: 10px; opacity: 0.45; }
                    .emh-panel-content .emh-item { animation: emh-item-in 0.18s ease both; }
                    .emh-panel-content .emh-item:nth-child(1) { animation-delay: 0s; }
                    .emh-panel-content .emh-item:nth-child(2) { animation-delay: 0.02s; }
                    .emh-panel-content .emh-item:nth-child(3) { animation-delay: 0.04s; }
                    .emh-panel-content .emh-item:nth-child(4) { animation-delay: 0.06s; }
                    .emh-panel-content .emh-item:nth-child(5) { animation-delay: 0.08s; }
                    @keyframes emh-item-in { from { opacity: 0; } to { opacity: 1; } }
                    .emh-panel-modal {
                        position: absolute; top: 0; left: 0; right: 0; bottom: 0;
                        background: var(--emh-overlay); display: none; justify-content: center; align-items: center;
                        z-index: 10014; backdrop-filter: blur(2px);
                    }
                    .emh-panel-modal-content {
                        background: var(--emh-surface-raised); padding: 24px; border-radius: var(--emh-radius);
                        width: 80%; max-width: 320px; text-align: center;
                        box-shadow: var(--emh-shadow-lg); border: 1px solid var(--emh-border);
                        animation: emh-modal-in 0.18s cubic-bezier(0.16, 1, 0.3, 1);
                    }
                    @keyframes emh-modal-in { from { opacity: 0; transform: translateY(6px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
                    .emh-panel-modal-content h3 { margin: 0 0 18px 0; color: var(--emh-text); font-size: 15px; font-weight: 600; line-height: 1.5; }
                    .emh-prompt-input {
                        width: 100%; box-sizing: border-box; padding: 9px 12px;
                        border: 1px solid var(--emh-border); border-radius: var(--emh-radius-sm); font-size: 13px;
                        outline: none; transition: border-color 0.15s, box-shadow 0.15s;
                        font-family: inherit; color: var(--emh-text); background: var(--emh-bg);
                    }
                    .emh-prompt-input:focus { border-color: var(--emh-primary); box-shadow: 0 0 0 3px var(--emh-focus-ring); background: var(--emh-surface); }
                    .emh-panel-modal-buttons { display: flex; justify-content: center; gap: 12px; margin-top: 18px; }
                    .emh-panel-modal-buttons .btn { margin-left: 0; min-height: 36px; }
                    .emh-help-modal-content { max-width: 360px; text-align: left; }
                    .emh-help-list { list-style: none; margin: 0 0 4px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
                    .emh-help-list li { display: flex; align-items: center; gap: 12px; font-size: 13px; color: var(--emh-text); }
                    .emh-kbd-chip {
                        display: inline-flex; align-items: center; justify-content: center;
                        min-width: 36px; padding: 2px 8px; border-radius: 6px;
                        font-family: var(--emh-font-mono); font-size: 11px; font-weight: 600;
                        color: var(--emh-primary); background: var(--emh-btn-bg); border: 1px solid var(--emh-border);
                        white-space: nowrap; flex-shrink: 0;
                    }
                    .emh-help-desc { color: var(--emh-text-secondary); }
                    .emh-selected-count { align-self: center; font-size: 12px; font-weight: 600; color: var(--emh-primary); letter-spacing: 0.2px; white-space: nowrap; }
                    .emh-detail-backdrop {
                        position: absolute; top: 0; left: 0; right: 0; bottom: 0;
                        background: var(--emh-overlay); z-index: 10012;
                        animation: emh-fade-in 0.2s ease; backdrop-filter: blur(2px);
                    }
                    .emh-detail-drawer {
                        position: absolute; top: 0; right: 0; width: clamp(320px, 42%, 460px); height: 100%;
                        background: var(--emh-bg); z-index: 10013;
                        box-shadow: var(--emh-shadow-lg);
                        animation: emh-detail-in 0.3s cubic-bezier(0.25,0.8,0.25,1);
                        display: flex; flex-direction: column;
                        border-radius: 16px 0 0 16px;
                        border-left: 1px solid var(--emh-border);
                        overflow: hidden;
                        font-family: inherit;
                    }
                    @keyframes emh-detail-in { from { transform: translateX(100%); } to { transform: translateX(0); } }
                    .emh-detail-header {
                        display: flex; justify-content: space-between; align-items: center;
                        height: 52px; padding: 0 16px; box-sizing: border-box;
                        border-bottom: 1px solid var(--emh-border);
                        background: var(--emh-bg); flex-shrink: 0;
                    }
                    .emh-detail-title { display: flex; align-items: center; gap: 10px; min-width: 0; }
                    .emh-detail-code {
                        font-weight: 700; color: var(--emh-text); font-size: 15px;
                        font-family: var(--emh-font-mono); letter-spacing: 0.1px;
                        font-variant-numeric: tabular-nums;
                        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                    }
                    .emh-detail-body { flex: 1; overflow-y: auto; padding: 16px 20px; background: var(--emh-bg); scrollbar-width: thin; scrollbar-color: var(--emh-border-strong) transparent; }
                    .emh-detail-body::-webkit-scrollbar { width: 6px; }
                    .emh-detail-body::-webkit-scrollbar-thumb { background: var(--emh-border-strong); border-radius: 3px; }
                    .emh-detail-body::-webkit-scrollbar-track { background: transparent; }
                    /* 详情层次(侧栏/新页签统一):Hero 标题 → meta 行 → 信息/磁力卡片 */
                    .emh-detail-body.unified { padding: 14px 16px 20px; }
                    .emh-detail-body.unified .emh-detail-hero {
                        padding: 0 0 12px; margin: 2px 0 10px;
                        border-bottom: 1px solid var(--emh-border);
                    }
                    .emh-detail-body.unified .emh-detail-hero .emh-detail-label { display: none; }
                    .emh-detail-body.unified .emh-detail-hero .emh-detail-value {
                        font-size: 18px; font-weight: 700; color: var(--emh-text); line-height: 1.35;
                    }
                    .emh-detail-body.unified .emh-detail-section {
                        background: var(--emh-surface); border: 1px solid var(--emh-border);
                        border-radius: 12px; padding: 10px 14px; margin-bottom: 12px;
                    }
                    .emh-detail-body.unified .emh-detail-section .emh-detail-field {
                        padding: 9px 0; border-bottom: 1px solid var(--emh-border);
                    }
                    .emh-detail-body.unified .emh-detail-section .emh-detail-field:last-child { border-bottom: none; }
                    .emh-detail-body.unified .emh-detail-meta-line {
                        display: flex; flex-wrap: wrap; gap: 14px;
                        font-size: 11px; color: var(--emh-text-muted);
                        padding: 0 2px 12px; margin-bottom: 12px;
                        border-bottom: 1px dashed var(--emh-border);
                    }
                    .emh-detail-field {
                        display: flex; flex-direction: column; gap: 4px; padding: 10px 0;
                        border-bottom: 1px solid var(--emh-border);
                    }
                    .emh-detail-field:last-child { border-bottom: none; }
                    .emh-detail-label {
                        font-size: 11px; font-weight: 600; color: var(--emh-text-muted);
                        text-transform: uppercase; letter-spacing: 0.5px;
                    }
                    .emh-detail-value { font-size: 13px; color: var(--emh-text); line-height: 1.5; word-break: break-all; }
                    .emh-detail-empty { color: var(--emh-text-muted); }
                    .emh-magnet-actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
                    .emh-magnet-btn { padding: 4px 10px; font-size: 12px; margin-left: 0; }
                    .emh-magnet-btn.emh-expand { padding: 4px 10px; }
                    .emh-magnet-modal-content { max-width: 420px; max-height: 70vh; display: flex; flex-direction: column; text-align: left; }
                    .emh-batch-modal-content { max-width: 380px; }
                    .emh-batch-progress { margin: 4px 0 14px; }
                    .emh-batch-progress-track {
                        height: 8px; border-radius: 4px; background: var(--emh-btn-bg);
                        overflow: hidden;
                    }
                    .emh-batch-progress-bar {
                        height: 100%; border-radius: 4px;
                        background: linear-gradient(90deg, var(--emh-primary), var(--emh-primary-hover));
                        transition: width 0.3s ease;
                    }
                    .emh-batch-progress-info { display: flex; justify-content: space-between; margin-top: 6px; font-size: 12px; color: var(--emh-text-muted); }
                    .emh-batch-progress-pct { font-weight: 700; color: var(--emh-primary); }
                    .emh-batch-current { margin: 8px 0; padding: 8px 10px; border-radius: var(--emh-radius-sm); background: var(--emh-bg); border: 1px solid var(--emh-border); font-size: 13px; color: var(--emh-text); word-break: break-all; }
                    .emh-batch-stats { display: flex; gap: 12px; justify-content: center; margin-top: 4px; font-size: 12px; color: var(--emh-text-muted); }
                    .emh-batch-stat-fail { color: var(--emh-danger); }
                    .emh-magnet-loading { padding: 24px 0; text-align: center; color: var(--emh-text-muted); font-size: 13px; }
                    .emh-magnet-error { padding: 24px 0; text-align: center; color: var(--emh-danger); font-size: 13px; }
                    .emh-magnet-list { list-style: none; margin: 0; padding: 0; overflow-y: auto; max-height: 45vh; scrollbar-width: thin; scrollbar-color: var(--emh-border-strong) transparent; }
                    .emh-magnet-list::-webkit-scrollbar { width: 6px; }
                    .emh-magnet-list::-webkit-scrollbar-thumb { background: var(--emh-border-strong); border-radius: 3px; }
                    .emh-magnet-list::-webkit-scrollbar-track { background: transparent; }
                    .emh-magnet-item {
                        display: flex; align-items: center; justify-content: space-between; gap: 10px;
                        padding: 9px 12px; margin-bottom: 4px; border-radius: var(--emh-radius-sm);
                        background: var(--emh-bg); border: 1px solid var(--emh-border);
                        cursor: pointer; transition: border-color 0.15s ease, background 0.15s ease;
                    }
                    .emh-magnet-item:hover { border-color: var(--emh-primary); background: var(--emh-primary-soft); }
                    .emh-magnet-item-title {
                        flex: 1; font-size: 13px; color: var(--emh-text); line-height: 1.4;
                        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
                    }
                    .emh-magnet-item-size { font-size: 12px; color: var(--emh-text-muted); white-space: nowrap; }
                    .emh-magnet-hint { margin-top: 10px; text-align: center; font-size: 12px; color: var(--emh-text-muted); }
                    .emh-magnet-item-static {
                        display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap;
                        padding: 6px 8px; margin-bottom: 3px; border-radius: 6px;
                        background: var(--emh-bg); border: 1px solid var(--emh-border);
                    }
                    .emh-magnet-item-idx {
                        flex-shrink: 0; font-size: 11px; color: var(--emh-text-muted);
                        background: var(--emh-btn-bg); border-radius: 4px; padding: 1px 5px; margin-top: 1px;
                    }
                    .emh-magnet-item-text {
                        flex: 1; min-width: 0; font-size: 11px; color: var(--emh-text);
                        word-break: break-all; line-height: 1.5;
                        font-family: var(--emh-font-mono);
                    }
                    .emh-magnet-item-ops { flex-shrink: 0; display: inline-flex; gap: 2px; align-items: center; }
                    .emh-magnet-op {
                        background: none; border: none; cursor: pointer; padding: 0;
                        width: 24px; height: 24px; min-width: 24px; min-height: 24px;
                        border-radius: 6px; font-size: 12px; line-height: 1;
                        display: inline-flex; align-items: center; justify-content: center;
                        color: var(--emh-text-muted); transition: background 0.15s ease, color 0.15s ease, opacity 0.15s ease; opacity: 0.7;
                    }
                    .emh-magnet-op:hover { opacity: 1; background: var(--emh-btn-hover); color: var(--emh-primary); }
                    .emh-magnet-op-del:hover { background: var(--emh-danger-soft); color: var(--emh-danger); }
                    .emh-magnet-op-preview {
                        position: relative;
                    }
                    .emh-magnet-op-preview.has-cache { color: var(--emh-primary); opacity: 0.95; }
                    .emh-magnet-op-preview:hover { color: var(--emh-primary); }
                    .emh-magnet-preview-badge {
                        position: absolute; top: -4px; right: -5px;
                        min-width: 12px; height: 12px; padding: 0 3px;
                        border-radius: 999px; font-size: 9px; font-weight: 700; line-height: 12px;
                        background: var(--emh-primary); color: var(--emh-on-primary);
                        text-align: center; pointer-events: none;
                    }
                    .emh-preview-grid {
                        flex: 1 1 100%; display: grid;
                        grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
                        gap: 6px; margin-top: 8px; padding-top: 8px;
                        border-top: 1px dashed var(--emh-border);
                    }
                    .emh-preview-cell {
                        position: relative; padding: 0; border: none; cursor: pointer;
                        border-radius: 6px; overflow: hidden; background: var(--emh-surface-raised);
                        aspect-ratio: 16 / 9; display: block; width: 100%;
                        transition: opacity 0.15s ease, transform 0.15s ease;
                    }
                    .emh-preview-cell:hover { transform: translateY(-1px); }
                    .emh-preview-cell:focus-visible { outline: 2px solid var(--emh-primary); outline-offset: 2px; }
                    .emh-preview-thumb {
                        position: absolute; inset: 0;
                        background-position: center; background-size: cover; background-repeat: no-repeat;
                        background-color: var(--emh-surface-raised);
                    }
                    .emh-preview-time {
                        position: absolute; right: 3px; top: 3px;
                        font-family: var(--emh-font-mono); font-size: 9px; font-weight: 600; line-height: 1;
                        padding: 2px 4px; border-radius: 4px;
                        background: rgba(0, 0, 0, 0.6); color: #fff;
                        pointer-events: none;
                    }
                    .emh-detail-tags { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
                    .emh-tag-edit { opacity: 0.7; }
                    .emh-detail-tag {
                        font-size: 11px; padding: 1px 7px; border-radius: 999px;
                        background: var(--emh-btn-bg); color: var(--emh-text-secondary);
                    }
                    .emh-detail-actions {
                        padding: 12px 16px; border-top: 1px solid var(--emh-border);
                        display: flex; flex-wrap: wrap; gap: 6px; flex-shrink: 0;
                        background: var(--emh-bg);
                    }
                    .emh-detail-actions .btn { margin-left: 0; justify-content: center; min-height: 34px; }
                    .emh-detail-danger {
                        padding: 12px 20px 16px; border-top: 1px solid var(--emh-danger-soft);
                        display: flex; gap: 8px; background: var(--emh-danger-soft); flex-shrink: 0;
                    }
                    .emh-detail-danger .btn { margin-left: 0; justify-content: center; width: 100%; color: var(--emh-danger); min-height: 36px; }
                    @media (max-width: 576px) { .emh-code-manager-panel { width: 100%; right: -100%; border-radius: 0; } .emh-panel-resize { display: none; } }
                    @media (prefers-reduced-motion: reduce) {
                        .emh-code-manager-panel,
                        .emh-panel-backdrop,
                        .emh-item,
                        .emh-item:hover,
                        .emh-item-actions,
                        .emh-item:hover .emh-item-actions,
                        .emh-item-actions button,
                        .emh-item-actions button:hover,
                        .emh-filter-tag,
                        .emh-panel-tabs button,
                        .emh-search-wrapper input,
                        .emh-detail-drawer,
                        .emh-detail-backdrop,
                        .emh-magnet-item,
                        .emh-batch-progress-bar,
                        .emh-panel-modal-content,
                        .emh-header-menu {
                            transition: none !important;
                            animation: none !important;
                        }
                        .emh-item:hover { transform: none; }
                        .emh-item-actions { opacity: 1; transform: none; }
                        .emh-item-actions button:hover { transform: none; }
                        .emh-panel-content .emh-item { animation: none; }
                    }
                `;
                document.head.appendChild(styleElement);
            }
        };

        return CodeManagerPanel;
    }

    function main() {
        for (const [name, handler] of Object.entries(SITE_HANDLERS)) {
            if (handler.isMatch()) {
                if (handler.targetSelector) {
                    waitForElement(handler.targetSelector, (targetElement) => {
                        if (targetElement || name === 'javgg') {
                            try {
                                setTimeout(() => handler.process(targetElement), 50);
                            } catch (e) { console.error(`EMH: Error processing handler ${name}:`, e); }
                        }
                    });
                } else {
                    try { setTimeout(() => handler.process(null), 150); }
                    catch (e) { console.error(`EMH: Error processing handler ${name}:`, e); }
                }
                break;
            }
        }
    }

    function setupSyncListeners() {
        window.addEventListener('emh_library_updated', function(e) {
            if (e.detail.type === 'library_update') {
                updateCodeStatusIndicators();
                if (window.CodeManagerPanel && window.CodeManagerPanel.isVisible) window.CodeManagerPanel.refreshPanelContent();
            }
        });

        if (typeof GM_addValueChangeListener !== 'undefined') {
            GM_addValueChangeListener('emh_sync_timestamp', function(name, old_value, new_value, remote) {
                if (remote) {
                    CODE_LIBRARY.init(true);
                    updateCodeStatusIndicators();
                    if (window.CodeManagerPanel && window.CodeManagerPanel.isVisible) window.CodeManagerPanel.refreshPanelContent();
                }
            });
        }

        setInterval(function() {
            if (typeof GM_getValue !== 'undefined' && window.CodeManagerPanel) {
                const lastUpdate = GM_getValue('emh_sync_timestamp');
                if (lastUpdate && lastUpdate !== window.CodeManagerPanel.lastSyncTimestamp) {
                    window.CodeManagerPanel.lastSyncTimestamp = lastUpdate;
                    CODE_LIBRARY.init(true);
                    updateCodeStatusIndicators();
                    if (window.CodeManagerPanel.isVisible) window.CodeManagerPanel.refreshPanelContent();
                }
            }
        }, 2000);
    }

    async function bootPanel() {
        injectCoreStyles();
        if (window.__EMH_STANDALONE) injectStandaloneStyles();
        // @run-at document-start:body 可能尚未解析,等待其出现再挂载面板
        if (!document.body) {
            await new Promise((resolve) => {
                const iv = setInterval(() => { if (document.body) { clearInterval(iv); resolve(); } }, 15);
            });
        }
        try {
            if (await ensurePreact()) {
                window.CodeManagerPanel = buildPanel();
                window.CodeManagerPanel.init(); // init 内部 createToggleButton 创建按钮
                if (window.__EMH_STANDALONE) {
                    window.CodeManagerPanel.showPanel();
                    removeStandaloneCover();
                }
            } else {
                console.error('EMH: Preact 依赖加载失败,使用降级按钮');
                if (window.__EMH_STANDALONE) removeStandaloneCover();
                createFallbackToggle(() => UTILS.showToast('面板组件依赖加载失败,请检查网络后刷新页面', 'error'));
            }
        } catch (e) {
            console.error('EMH: 面板初始化失败', e);
            // 初始化异常时也保证按钮可见
            if (window.__EMH_STANDALONE) removeStandaloneCover();
            createFallbackToggle(() => UTILS.showToast('面板初始化失败,请刷新页面重试', 'error'));
        }
    }

    function initialize() {
        // standalone 独立页:同源真实网址 + #emh-standalone 标记 → 原生 GM 能力,无需父页桥
        if (location.hash.indexOf('emh-standalone') >= 0 || location.search.indexOf('emh-standalone') >= 0) {
            window.__EMH_STANDALONE = true;
            try { document.title = '番号库 · 独立页'; } catch (e) {}
            // document-start 起隐藏站点、显示加载中
            injectStandaloneCover();
        }
        THEME.apply(THEME.get());
        CODE_LIBRARY.init();
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => main());
        } else {
            main();
        }
        setupSyncListeners();
        bootPanel();
    }

    initialize();
})();