// ==UserScript==
// @name Javdb 增强脚本
// @name:zh Javdb 增强脚本
// @name:en Javdb Enhanced Script
// @namespace http://tampermonkey.net/
// @version 2.5.1
// @icon https://javdb.com/favicon-32x32.png
// @description 增强 Javdb 浏览体验:热力图高亮、热度排序、隐藏低分、列表页管理“已看/想看”、点击抓取并预览大图。兼容自动翻页脚本。
// @description:zh 增强 Javdb 浏览体验:热力图高亮、热度排序、隐藏低分、列表页管理“已看/想看”、点击抓取并预览大图。兼容自动翻页脚本。
// @description:en Enhances Javdb: heatmap highlighting, sort by heat, hide low score, list-page status (Watched/Want), click-to-fetch preview image. Compatible with auto-paging scripts.
// @author 黄页大嫖客 (Modified by Gemini), Refined by Assistant
// @match https://javdb*.com/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @connect javdb*.com
// @connect javstore.net
// @license GPLv3
// @homeURL https://sleazyfork.org/zh-CN/scripts/539525
// @supportURL https://sleazyfork.org/zh-CN/scripts/539525/feedback
// ==/UserScript==
(function() {
    'use strict';
    const getLang = () => {
        const pageLang = document.body?.dataset?.lang;
        if (pageLang === 'zh' || pageLang === 'en') return pageLang;
        return (navigator.language || navigator.userLanguage || 'zh').toLowerCase().startsWith('zh') ? 'zh' : 'en';
    };
    const lang = getLang();
    const I18N = {
        zh: {
            settings: '设置',
            highlightFeature: '启用高亮功能',
            sortFeature: '启用排序功能',
            hideFeature: '启用隐藏低分影片',
            hideFeatureDesc: '(隐藏排序分低于 C_MIN 的影片)',
            statusFeature: '启用状态标记功能',
            previewFeature: '启用预览图显示功能',
            previewPrivacyNote: '(将向第三方 javstore.net 发送番号以抓取预览图)',
            previewConsentTitle: '第三方请求提示',
            previewConsentBody: '启用“预览图”后,脚本会将影片番号发送至第三方网站(javstore.net)以抓取预览。是否同意?',
            previewConsentAgree: '同意并继续',
            previewConsentCancel: '取消',
            delegationFeature: '启用事件委托(性能模式)',
            hideOnMouseLeave: '移开鼠标隐藏状态图标',
            heatmapMin: '热力图最低分 (C)',
            heatmapMax: '热力图最高分 (C_MAX)',
            heatmapCurveFactor: '热力图曲线因子 (<1 拉伸低分, >1 拉伸高分)',
            ratedByThreshold: '基准人数 (m)',
            reset: '重置热力图',
            resetDone: '已重置为默认值',
            sort: '排序',
            unmarked: '暂未标记',
            want: '想看',
            watched: '已看',
            modify: '修改',
            delete: '删除',
            confirmDelete: '确认删除标记?',
            confirm: '确认',
            cancel: '取消',
            preview: '查看预览图',
            noPreview: '无可用预览图',
            loadingPreview: '正在获取预览图...',
            errorNeedLogin: '需要登录后才能标记状态',
            errorNetworkBusy: '网络繁忙,请稍后重试',
            errorCsrf: '无法获取安全令牌,请刷新页面后重试',
            previewConsentRequired: '需同意第三方请求后才能抓取预览',
            previewClickToFetch: '点击以抓取预览',
            errorPreview: '获取失败 (可重试)',
            tooltipHeatmapMin: '热力图的起始分 (0%)。\n低分和少评分的影片会趋向此值。\n也是隐藏低分的阈值。',
            tooltipHeatmapMax: '热力图的最高分 (100%)。\n达到此值的影片会显示为最热的红色。\n仅影响颜色,不影响排序。',
            tooltipRatedBy: '系统的“自信度”,代表“虚拟评分人数”。\n值越高,系统越“保守”,需要更多真实评分才能摆脱基准分。',
            tooltipHeatmapCurve: '调整颜色渐变曲线。\n<1: 拉开中低分(蓝/绿)的差异。\n>1: 拉开高分(黄/红)的差异。\n=1: 线性过渡。',
        },
        en: {
            settings: 'Settings',
            highlightFeature: 'Enable Highlight Feature',
            sortFeature: 'Enable Sort Feature',
            hideFeature: 'Enable Hide Low Score Videos',
            hideFeatureDesc: '(Hide items with sort score below C_MIN)',
            statusFeature: 'Enable Status Tag Feature',
            previewFeature: 'Enable Preview Image Feature',
            previewPrivacyNote: '(Sends the code to third-party javstore.net to fetch previews)',
            previewConsentTitle: 'Third-party Request Notice',
            previewConsentBody: 'When enabled, the script sends the video code to a third-party site (javstore.net) to fetch previews. Do you consent?',
            previewConsentAgree: 'Agree and continue',
            previewConsentCancel: 'Cancel',
            delegationFeature: 'Enable event delegation (Performance mode)',
            hideOnMouseLeave: 'Hide status icons on mouse leave',
            heatmapMin: 'Heatmap Min Score (C)',
            heatmapMax: 'Heatmap Max Score (C_MAX)',
            heatmapCurveFactor: 'Heatmap Curve (<1 stretches low, >1 stretches high)',
            ratedByThreshold: 'Rated-By Threshold (m)',
            reset: 'Reset Heatmap',
            resetDone: 'Reset to defaults',
            sort: 'Sort',
            unmarked: 'Unmarked',
            want: 'Want',
            watched: 'Watched',
            modify: 'Modify',
            delete: 'Delete',
            confirmDelete: 'Confirm Delete?',
            confirm: 'Confirm',
            cancel: 'Cancel',
            preview: 'View Preview Image',
            noPreview: 'No Preview Available',
            loadingPreview: 'Loading preview...',
            errorNeedLogin: 'Login required to set status',
            errorNetworkBusy: 'Network busy, please try again later',
            errorCsrf: 'Failed to get security token, please refresh the page',
            previewConsentRequired: 'Consent required before fetching preview',
            previewClickToFetch: 'Click to fetch preview',
            errorPreview: 'Fetch failed (Retryable)',
            tooltipHeatmapMin: "The starting score (0%) for the heatmap.\nLow-score and low-rated items are pulled towards this value.\nAlso the threshold for hiding items.",
            tooltipHeatmapMax: "The 'perfect score' (100%) for the heatmap.\nItems at this score appear bright red.\nOnly affects color, not sorting.",
            tooltipRatedBy: "System 'confidence', a 'virtual rating count'.\nHigher value = more 'conservative' (needs more real ratings to trust the score).",
            tooltipHeatmapCurve: "Adjusts the color curve.\n<1: Emphasizes differences in the low-mid range (blue/green).\n>1: Emphasizes differences in the high range (yellow/red).\n=1: Linear.",
        }
    };
    const T = (key) => I18N[lang]?.[key] ?? I18N['zh'][key];
    // --- Config & Globals ---
    const SETTINGS_KEY = 'JavdbEnhanced_Settings';
    const DEFAULT_NAVBAR_HEIGHT = 58;
    const PREVIEW_MODAL_MIN_SCALE = 0.2;
    const PREVIEW_MODAL_MAX_SCALE = 8;
    const PREVIEW_ICON_INACTIVE_OPACITY = 0.55;
    const MAX_CACHE_SIZE = 500;
    const ITEM_SELECTOR = '.movie-list .item, .is-user-page .column.is-one-quarter';
    const scoreRegexes = [
        /([\d.]+)\s*\/\s*5[^\d]*?(\d+)/,
        /([\d.]+)[^\d]+(\d+)/,
        /评分[::]\s*([\d.]+)[^\d]+(\d+)/
    ];
    const DEFAULT_SETTINGS = {
        highlight: true,
        sort: true,
        hideLowScore: false,
        status: true,
        enablePreview: true,
        hideOnMouseLeave: true,
        heatmapMin: 3.75,
        heatmapMax: 4.75,
        heatmapCurveFactor: 0.5,
        ratedByThreshold: 200,
        fetchDelay: 300,
        useEventDelegation: false,
        previewThirdPartyConsent: false
    };
    let settings = { ...DEFAULT_SETTINGS };
    let authenticityToken = null;
    const statusCache = new Map();
    const previewCache = new Map();
    const STATUS_CONCURRENCY = 3;
    const PREVIEW_CONCURRENCY = 2;
    const statusLimiter = createLimiter(STATUS_CONCURRENCY);
    const previewLimiter = createLimiter(PREVIEW_CONCURRENCY);
    let delegationInitialized = false;
    const hoverTimers = new WeakMap();
    // --- ICONS ---
    const ICONS = {
        plus: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>`,
        eye: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5C21.27 7.61 17 4.5 12 4.5zm0 10c-2.48 0-4.5-2.02-4.5-4.5S9.52 5.5 12 5.5s4.5 2.02 4.5 4.5-2.02 4.5-4.5 4.5zm0-7C10.62 7.5 9.5 8.62 9.5 10s1.12 2.5 2.5 2.5 2.5-1.12 2.5-2.5S13.38 7.5 12 7.5z"/></svg>`,
        heart: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>`,
        preview: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>`,
        brokenImage: `<svg viewBox="0 0 24 24"><path fill="currentColor" d="M21 5v6.59l-2.29-2.3c-.39-.39-1.03-.39-1.42 0L14 12.59L10.71 9.3a.996.996 0 0 0-1.41 0L6 12.59L3 9.58V5c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2m-3 6.42l3 3.01V19c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2v-6.58l2.29 2.29c.39.39 1.02.39 1.41 0l3.3-3.3l3.29 3.29c.39.39 1.02.39 1.41 0z"/></svg>`,
        loading: `<svg viewBox="0 0 24 24"><path fill="currentColor" d="M18 15v4c0 .55-.45 1-1 1H5c-.55 0-1-.45-1-1V7c0-.55.45-1 1-1h3.02c.55 0 1-.45 1-1s-.45-1-1-1H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2v-5c0-.55-.45-1-1-1s-1 .45-1 1m-2.5 3H6.52c-.42 0-.65-.48-.39-.81l1.74-2.23a.5.5 0 0 1 .78-.01l1.56 1.88l2.35-3.02c.2-.26.6-.26.79.01l2.55 3.39c.25.32.01.79-.4.79m3.8-9.11c.48-.77.75-1.67.69-2.66c-.13-2.15-1.84-3.97-3.97-4.2A4.5 4.5 0 0 0 11 6.5c0 2.49 2.01 4.5 4.49 4.5c.88 0 1.7-.26 2.39-.7l2.41 2.41c.39.39 1.03.39 1.42 0s.39-1.03 0-1.42zM15.5 9a2.5 2.5 0 0 1 0-5a2.5 2.5 0 0 1 0 5"/></svg>`
    };
    // --- UI Templates ---
    const STATUS_BUTTONS_TEMPLATE = `
        <div class="cover-status-buttons">
            <div class="cover-ui-wrapper">
                <div class="csb-outer-container">
                    <div class="csb-status-container">
                        <div class="state state-unmarked">
                            <div class="status-main-icon" title="${T('unmarked')}">${ICONS.plus}</div>
                            <div class="action-buttons-wrapper">
                                <div class="action-buttons">
                                    <button class="button btn-set-wanted">${T('want')}</button>
                                    <button class="button btn-set-watched js-set-watched">${T('watched')}</button>
                                </div>
                            </div>
                        </div>
                        <div class="state state-watched">
                            <div class="status-main-icon" title="${T('watched')}">
                                ${ICONS.eye}
                                <div class="star-arc-container">${'<span>★</span>'.repeat(5)}</div>
                            </div>
                            <div class="action-buttons-wrapper">
                                <div class="action-buttons">
                                    <button class="button btn-modify">${T('modify')}</button>
                                    <button class="button btn-delete js-delete">${T('delete')}</button>
                                </div>
                            </div>
                        </div>
                        <div class="state state-wanted">
                            <div class="status-main-icon" title="${T('want')}">${ICONS.heart}</div>
                            <div class="action-buttons-wrapper">
                                <div class="action-buttons">
                                    <button class="button btn-set-watched js-set-watched">${T('watched')}</button>
                                    <button class="button btn-delete js-delete">${T('delete')}</button>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    `;
    const PREVIEW_ICON_TEMPLATE = `
        <button type="button" class="csb-preview-icon-container" title="${T('previewClickToFetch')}" aria-label="${T('preview')}">
            ${ICONS.preview}
        </button>
    `;
    // --- Styles ---
    GM_addStyle(`
        @keyframes jdbe-flip {
            from { transform: rotateY(0deg); }
            to { transform: rotateY(360deg); }
        }
        @keyframes jdbe-shake {
            10%, 90% { transform: translate3d(-1px, 0, 0); }
            20%, 80% { transform: translate3d(2px, 0, 0); }
            30%, 50%, 70% { transform: translate3d(-3px, 0, 0); }
            40%, 60% { transform: translate3d(3px, 0, 0); }
        }
        :root {
            --color-unmarked: #f5f5f5;
            --color-unmarked-icon: #333;
            --color-watched: #3273dc;
            --color-watched-hover: #4a89e8;
            --color-wanted: #e83e8c;
            --color-wanted-hover: #f06fab;
            --color-delete: #ff3860;
            --color-delete-hover: #ff5c7c;
            --color-modify: #ffc107;
            --color-modify-hover: #ffce3a;
            --color-star: #ccc;
            --color-star-filled: #ffdd44;
            --color-preview-has: #48c78e;
            --color-preview-no: #f5f5f5;
            --color-preview-no-icon: #666;
            --color-preview-failed: #dbdbdb;
            --color-preview-failed-icon: #7a7a7a;
            --preview-icon-inactive-opacity: ${PREVIEW_ICON_INACTIVE_OPACITY};
        }
        .item .cover {
            position: relative;
            overflow: visible;
        }
        .item .cover img.cover-img-blurred {
            filter: blur(0.25rem) brightness(0.8);
            transition: filter 0.3s ease;
        }
        .cover-status-buttons {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            z-index: 6;
            pointer-events: none;
        }
        .cover-ui-wrapper {
            position: relative;
            width: 100%;
            height: 100%;
        }
        .csb-outer-container {
            position: absolute;
            top: 0.5rem;
            left: 0.5rem;
            opacity: 0;
            visibility: hidden;
            transition: opacity 0.25s ease, visibility 0.25s ease;
        }
        .item:hover .csb-outer-container,
        .item.status-ui-persist .csb-outer-container {
            opacity: 1;
            visibility: visible;
        }
        .csb-status-container {
            position: relative;
        }
        .item.api-error .csb-status-container {
            animation: jdbe-shake 0.5s ease-in-out;
        }
        .item[data-api-busy="true"] .csb-status-container {
            pointer-events: none;
            opacity: 0.5;
            cursor: wait;
        }
        .status-main-icon {
            position: relative;
            width: 2.25rem;
            height: 2.25rem;
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            cursor: pointer;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            transition: all 0.3s ease-in-out;
            flex-shrink: 0;
            z-index: 10;
        }
        .status-main-icon svg {
            width: 1.25rem;
            height: 1.25rem;
        }
        .csb-status-container.show-unmarked .status-main-icon {
            background-color: var(--color-unmarked);
            color: var(--color-unmarked-icon);
        }
        .csb-status-container.show-watched .status-main-icon {
            background-color: var(--color-watched);
            color: white;
        }
        .csb-status-container.show-wanted .status-main-icon {
            background-color: var(--color-wanted);
            color: white;
        }
        .action-buttons-wrapper {
            position: absolute;
            left: 0.5rem;
            top: 50%;
            transform: translateY(-50%);
            display: flex;
            align-items: center;
            max-width: 0;
            opacity: 0;
            visibility: hidden;
            transition: max-width 0.4s ease, opacity 0.3s ease 0.1s;
            overflow: hidden;
            pointer-events: none;
        }
        .action-buttons {
            display: flex;
            align-items: center;
            background-color: rgba(30,30,30,0.8);
            backdrop-filter: blur(2px);
            padding: 0.4rem 0.6rem 0.4rem 2rem;
            border-radius: 2rem;
            white-space: nowrap;
        }
        .csb-status-container:hover .action-buttons-wrapper {
            opacity: 1;
            visibility: visible;
            max-width: 300px;
            pointer-events: auto;
        }
        .csb-status-container:hover .status-main-icon {
            box-shadow: 0 4px 12px rgba(0,0,0,0.4);
        }
        .action-buttons .button {
            padding: 0.25rem 0.625rem;
            font-size: 0.8rem;
            border: none;
            border-radius: 0.25rem;
            color: white;
            cursor: pointer;
            margin: 0 0.25rem;
            transition: all 0.2s ease;
        }
        .action-buttons .button:hover {
            transform: translateY(-1px);
        }
        .action-buttons .button:active {
            transform: translateY(0);
            filter: brightness(0.9);
        }
        .btn-set-wanted { background-color: var(--color-wanted); }
        .btn-set-watched { background-color: var(--color-watched); }
        .btn-modify { background-color: var(--color-modify); }
        .btn-delete { background-color: var(--color-delete); }
        .btn-set-wanted:hover { background-color: var(--color-wanted-hover); }
        .btn-set-watched:hover { background-color: var(--color-watched-hover); }
        .btn-modify:hover { background-color: var(--color-modify-hover); }
        .btn-delete:hover { background-color: var(--color-delete-hover); }
        .star-arc-container {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            pointer-events: none;
            z-index: 11;
        }
        .star-arc-container span {
            position: absolute;
            top: 50%;
            left: 50%;
            font-size: 0.7rem;
            color: var(--color-star);
            text-shadow: 0 0 2px black;
            transform-origin: center;
        }
        .star-arc-container span.is-filled {
            color: var(--color-star-filled);
        }
        .star-arc-container span:nth-child(1) { transform: translate(-50%, -50%) rotate(-60deg) translateY(-12px) rotate(60deg); }
        .star-arc-container span:nth-child(2) { transform: translate(-50%, -50%) rotate(-30deg) translateY(-12px) rotate(30deg); }
        .star-arc-container span:nth-child(3) { transform: translate(-50%, -50%) rotate(0deg) translateY(-12px) rotate(0deg); }
        .star-arc-container span:nth-child(4) { transform: translate(-50%, -50%) rotate(30deg) translateY(-12px) rotate(-30deg); }
        .star-arc-container span:nth-child(5) { transform: translate(-50%, -50%) rotate(60deg) translateY(-12px) rotate(-60deg); }
        .csb-preview-icon-container {
            position: absolute;
            top: 0.5rem;
            right: 0.5rem;
            z-index: 7;
            flex-shrink: 0;
            width: 2.25rem;
            height: 2.25rem;
            border-radius: 50%;
            display: flex;
            justify-content: center;
            align-items: center;
            color: var(--color-preview-no-icon);
            background-color: var(--color-preview-no);
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            transition: opacity 0.15s ease-in-out;
            opacity: var(--preview-icon-inactive-opacity);
            outline: none;
            cursor: pointer;
            border: none;
            padding: 0;
            margin: 0;
            font-family: inherit;
            font-size: inherit;
        }
        .csb-preview-icon-container svg {
            width: 1.25rem;
            height: 1.25rem;
        }
        .csb-preview-icon-container:hover,
        .csb-preview-icon-container:focus-visible,
        .csb-preview-icon-container:active {
            opacity: 1;
        }
        .csb-preview-icon-container.is-loading svg {
            animation: jdbe-flip 1.5s ease-in-out infinite;
        }
        .csb-preview-icon-container.has-preview {
            background-color: var(--color-preview-has);
            color: white;
        }
        .csb-preview-icon-container.fetch-failed {
            background-color: var(--color-preview-failed);
            color: var(--color-preview-failed-icon);
            cursor: not-allowed;
        }
        /* [NEW] Style for temporary, retryable errors */
        .csb-preview-icon-container.fetch-error {
            background-color: var(--color-delete); /* Use 'delete' red color */
            color: white;
            cursor: pointer; /* Allow retry */
        }
        .state {
            display: none;
        }
        .csb-status-container.show-unmarked .state-unmarked,
        .csb-status-container.show-watched .state-watched,
        .csb-status-container.show-wanted .state-wanted {
            display: contents;
        }
        .cover-modal-base {
            display: flex;
            justify-content: center;
            align-items: center;
            position: absolute;
            z-index: 10;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: transparent;
            flex-direction: column;
            gap: 0.625rem;
        }
        #preview-modal {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.8);
            display: flex;
            justify-content: center;
            align-items: center;
            z-index: 9999;
            cursor: zoom-out;
            overflow: auto;
        }
        #preview-modal-content {
            cursor: grab;
            text-align: center;
        }
        #preview-modal-content.is-panning {
            cursor: grabbing;
        }
        #preview-modal-content img {
            max-width: 95vw;
            max-height: 95vh;
            object-fit: contain;
            box-shadow: 0 8px 30px rgba(0,0,0,0.5);
            border-radius: 4px;
            transition: transform 0.2s ease-out;
            transform-origin: center center;
        }
        #sort-by-heat-btn-container {
            position: fixed;
            bottom: 1.7rem;
            right: 0;
            z-index: 9998;
        }
        #sort-by-heat-btn-container .button {
            width: 2.45rem;
            height: 1.7rem;
            font-size: 0.8rem;
            background-color: #fa6699;
            color: white;
            border: none;
            padding: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
        }
        .jdbe-modal {
            position: fixed;
            z-index: 10000;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .jdbe-modal-content {
            background: #fff;
            padding: 1.25rem;
            border-radius: 0.5rem;
            box-shadow: 0 0.3125rem 1rem rgba(0,0,0,0.3);
            position: relative;
            max-width: 480px;
        }
        .jdbe-modal-close {
            position: absolute;
            top: 0.5rem;
            right: 0.5rem;
            font-size: 1.5rem;
            border: none;
            background: transparent;
            cursor: pointer;
        }
        .jdbe-modal-body .jdbe-setting-row {
            display: flex;
            align-items: center;
            margin: 8px 0;
            gap: 8px;
        }
        .jdbe-modal-body label {
            flex: 1;
            display: flex;
            align-items: center;
            user-select: none;
        }
        .jdbe-modal-body input[type="checkbox"] {
            margin-right: 8px;
        }
        .jdbe-modal-body input[type="number"] {
            width: 70px;
            padding: 4px;
            border: 1px solid #ccc;
            border-radius: 4px;
        }
        .jdbe-modal-body .jdbe-setting-row.is-indented {
            padding-left: 20px;
        }
        .jdbe-modal-body .jdbe-setting-desc {
            font-size: 11px;
            color: #666;
            margin-left: 26px;
            margin-top: -5px;
        }
        .jdbe-help-icon {
            display: inline-flex;
            justify-content: center;
            align-items: center;
            width: 14px;
            height: 14px;
            border-radius: 50%;
            background-color: #aaa;
            color: white;
            font-size: 10px;
            font-weight: bold;
            margin-left: 6px;
            cursor: help;
            user-select: none;
            font-style: normal;
        }
        .rating-modal-container {
            z-index: 15;
        }
        .rating-stars {
            display: flex;
            flex-direction: row-reverse;
        }
        .rating-stars input[type="radio"] {
            display: none;
        }
        .rating-stars label {
            font-size: 1.5rem;
            color: var(--color-star);
            cursor: pointer;
            text-shadow: 0 0 3px rgba(0,0,0,0.5);
        }
        .rating-stars label:hover, .rating-stars label:hover ~ label, .rating-stars input[type="radio"]:checked ~ label {
            color: var(--color-star-filled);
        }
        .rating-modal-container .btn-cancel-rating {
            background-color: #f5f5f5;
            padding: 0.25rem 0.5rem;
            font-size: 0.7rem;
            border-radius: 0.25rem;
            border: none;
            cursor: pointer;
            margin-top: 0.3125rem;
        }
        .confirm-delete-modal p {
            color: white;
            font-weight: bold;
            text-shadow: 0 0 3px rgba(0,0,0,0.5);
        }
        .confirm-delete-modal .buttons {
            display: flex;
            gap: 0.625rem;
        }
        .confirm-delete-modal .is-danger {
            background-color: #ff3860;
            color: white;
        }
        /* [MODIFIED] Added 'div.box' for highlight disabling */
        body.jdbe-highlight-disabled .item a.box,
        body.jdbe-highlight-disabled .item div.box {
            background-color: transparent !important;
        }
        body.jdbe-status-disabled .cover-status-buttons {
            display: none !important;
        }
        body.jdbe-preview-disabled .csb-preview-icon-container {
            display: none !important;
        }
        body.jdbe-hide-low-score-enabled .item[data-jdbe-hide="true"] {
            display: none !important;
        }
        .jdbe-toast {
            position: fixed;
            left: 50%;
            transform: translateX(-50%);
            bottom: 2rem;
            background: rgba(0,0,0,0.85);
            color: #fff;
            padding: 0.5rem 0.75rem;
            border-radius: 0.5rem;
            z-index: 10001;
            font-size: 0.85rem;
            box-shadow: 0 4px 12px rgba(0,0,0,0.35);
        }
        .jdbe-privacy-note {
            display:inline-block;
            margin-left: 8px;
            color:#999;
            font-size: 12px;
        }
    `);
    // --- Helpers ---
    function setPreviewCache(key, value) {
        previewCache.set(key, value);
        limitCacheSize(previewCache, MAX_CACHE_SIZE);
    }
    function limitCacheSize(cache, maxSize) {
        if (cache.size > maxSize) {
            const oldestKey = cache.keys().next().value;
            cache.delete(oldestKey);
        }
    }
    function toast(message, timeout = 2000) {
        const div = document.createElement('div');
        div.className = 'jdbe-toast';
        div.textContent = message;
        document.body.appendChild(div);
        setTimeout(() => { div.remove(); }, timeout);
    }
    function createLimiter(max) {
        let active = 0;
        const queue = [];
        const runNext = () => {
            if (active >= max || queue.length === 0) return;
            active++;
            const job = queue.shift();
            Promise.resolve().then(job.fn).then(job.resolve).catch(job.reject).finally(() => {
                active--; runNext();
            });
        };
        return {
            run(fn) {
                return new Promise((resolve, reject) => {
                    queue.push({ fn, resolve, reject });
                    runNext();
                });
            }
        };
    }
    function normalizeCode(raw) {
        if (!raw) return '';
        let code = raw.trim().toUpperCase();
        code = code.replace(/\s+/g, '-');
        code = code.replace(/^FC2[-\s]PPV[-\s_](\d+)$/i, (_, num) => `FC2-PPV-${num}`);
        return code;
    }
    function setModalLock(item, locked) {
        const coverImg = item.querySelector('.cover img');
        if (locked) {
            item.dataset.modalOpen = 'true';
            if (coverImg) coverImg.classList.add('cover-img-blurred');
        } else {
            delete item.dataset.modalOpen;
            if (coverImg) coverImg.classList.remove('cover-img-blurred');
        }
    }
    function showStatusUI(item) {
        if (item.dataset.modalOpen === 'true') return;
        const overlay = item.querySelector('.cover-status-buttons');
        if (overlay) overlay.style.pointerEvents = 'auto';
        if (!settings.hideOnMouseLeave) item.classList.add('status-ui-persist');
    }
    function hideStatusUI(item) {
        if (item.dataset.modalOpen === 'true') return;
        const overlay = item.querySelector('.cover-status-buttons');
        if (overlay) overlay.style.pointerEvents = 'none';
        item.classList.remove('status-ui-persist');
    }
    function createCoverModal(item, className, innerHTML) {
        const cover = item.querySelector('.cover');
        if (!cover || cover.querySelector(`.${className}`)) return null;
        setModalLock(item, true);
        const modal = document.createElement('div');
        modal.className = `${className} cover-modal-base`;
        modal.innerHTML = innerHTML;
        cover.appendChild(modal);
        const closeModal = () => {
            setModalLock(item, false);
            if (!item.matches(':hover') && settings.hideOnMouseLeave) {
                hideStatusUI(item);
            } else {
                showStatusUI(item);
            }
            if (cover.contains(modal)) cover.removeChild(modal);
        };
        return { modal, closeModal };
    }
    function presentPreviewConsentModal(triggerElement) {
        return new Promise((resolve) => {
            const m = document.createElement('div');
            m.className = 'jdbe-modal';
            m.setAttribute('aria-modal', 'true');
            m.innerHTML = `
                <div class="jdbe-modal-content">
                    <button class="jdbe-modal-close" aria-label="close">×</button>
                    <h3 style="margin-top:0;">${T('previewConsentTitle')}</h3>
                    <p>${T('previewConsentBody')}</p>
                    <p style="color:#666;font-size:12px;margin:8px 0 16px;">Domains: javstore.net</p>
                    <div style="display:flex;gap:8px;justify-content:flex-end;">
                        <button class="button btn-cancel">${T('previewConsentCancel')}</button>
                        <button class="button btn-agree" style="background:#3273dc;color:#fff;border:none;padding:6px 10px;border-radius:4px;">${T('previewConsentAgree')}</button>
                    </div>
                </div>
            `;
            document.body.appendChild(m);
            const agreeButton = m.querySelector('.btn-agree');
            agreeButton.focus();
            function close(val) {
                try { m.remove(); } catch (e) {}
                triggerElement?.focus();
                resolve(!!val);
            }
            m.addEventListener('click', (e) => { if (e.target === m) close(false); });
            m.querySelector('.jdbe-modal-close').addEventListener('click', () => close(false));
            m.querySelector('.btn-cancel').addEventListener('click', () => close(false));
            agreeButton.addEventListener('click', () => close(true));
        });
    }
    function appendUIOnce(parent, childSelector, template) {
        if (!parent || parent.querySelector(childSelector)) return parent ? parent.querySelector(childSelector) : null;
        const wrapper = document.createElement('div');
        wrapper.innerHTML = template;
        const element = wrapper.firstElementChild;
        if (element) {
            parent.appendChild(element);
        }
        return element;
    }
    // --- Core Logic ---
    function loadSettings() {
        const saved = GM_getValue(SETTINGS_KEY, {});
        settings = { ...DEFAULT_SETTINGS, ...saved };
    }
    function saveSettings() {
        GM_setValue(SETTINGS_KEY, settings);
    }
    function openSettingsModal() {
        const existing = document.getElementById('jdbe-settings-modal');
        if (existing) {
            existing.style.display = 'flex';
            existing.querySelector('.jdbe-modal-close').focus();
            return;
        }
        const modal = document.createElement('div');
        modal.id = 'jdbe-settings-modal';
        modal.className = 'jdbe-modal';
        modal.setAttribute('aria-modal', 'true');
        modal.innerHTML = `
            <div class="jdbe-modal-content">
                <button class="jdbe-modal-close">×</button>
                <h2>${T('settings')}</h2>
                <div class="jdbe-modal-body"></div>
            </div>
        `;
        const body = modal.querySelector('.jdbe-modal-body');
        document.body.appendChild(modal);
        const closeButton = modal.querySelector('.jdbe-modal-close');
        closeButton.focus();
        const closeModal = () => modal.style.display = 'none';
        closeButton.addEventListener('click', closeModal);
        modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });
        const controls = [
            { id: 'setting-highlight', key: 'highlight', label: T('highlightFeature'), type: 'checkbox' },
            { id: 'setting-sort', key: 'sort', label: T('sortFeature'), dependsOn: 'highlight', type: 'checkbox' },
            { id: 'setting-hide-low-score', key: 'hideLowScore', label: T('hideFeature'), dependsOn: 'highlight', type: 'checkbox', desc: T('hideFeatureDesc') },
            { id: 'setting-heatmap-min', key: 'heatmapMin', label: T('heatmapMin'), dependsOn: 'highlight', type: 'number', step: 0.05, indented: true, tooltipKey: 'tooltipHeatmapMin' },
            { id: 'setting-heatmap-max', key: 'heatmapMax', label: T('heatmapMax'), dependsOn: 'highlight', type: 'number', step: 0.05, indented: true, tooltipKey: 'tooltipHeatmapMax' },
            { id: 'setting-heatmap-curve', key: 'heatmapCurveFactor', label: T('heatmapCurveFactor'), dependsOn: 'highlight', type: 'number', step: 0.1, indented: true, tooltipKey: 'tooltipHeatmapCurve' },
            { id: 'setting-ratedby', key: 'ratedByThreshold', label: T('ratedByThreshold'), dependsOn: 'highlight', type: 'number', step: 10, indented: true, tooltipKey: 'tooltipRatedBy' },
            { id: 'setting-status', key: 'status', label: T('statusFeature'), type: 'checkbox' },
            { id: 'setting-hover-status', key: 'hideOnMouseLeave', label: T('hideOnMouseLeave'), dependsOn: 'status', type: 'checkbox', indented: true },
            { id: 'setting-preview', key: 'enablePreview', label: T('previewFeature'), type: 'checkbox' },
            { id: 'setting-delegation', key: 'useEventDelegation', label: T('delegationFeature'), type: 'checkbox' }
        ];
        controls.forEach(c => {
            const wrapper = document.createElement('div');
            wrapper.className = 'jdbe-setting-row';
            if (c.indented) wrapper.classList.add('is-indented');
            const label = document.createElement('label');
            label.htmlFor = c.id;
            const input = document.createElement('input');
            input.type = c.type;
            input.id = c.id;
            if (c.readonly) input.disabled = true;
            if (c.type === 'checkbox') {
                label.appendChild(input);
                label.appendChild(document.createTextNode(` ${c.label}`));
            } else if (c.type === 'number') {
                label.appendChild(document.createTextNode(`${c.label}`));
                input.step = c.step || 'any';
                input.className = 'jdbe-input-number';
            }
            if (c.tooltipKey) {
                const helpIcon = document.createElement('i');
                helpIcon.className = 'jdbe-help-icon';
                helpIcon.textContent = '?';
                helpIcon.title = T(c.tooltipKey);
                label.appendChild(helpIcon);
            }
            wrapper.appendChild(label);
            if (c.type === 'number') wrapper.appendChild(input);
            if (c.id === 'setting-preview') {
                const note = document.createElement('span');
                note.className = 'jdbe-privacy-note';
                note.textContent = T('previewPrivacyNote');
                wrapper.appendChild(note);
            }
            body.appendChild(wrapper);
            if (c.desc) {
                const desc = document.createElement('div');
                desc.className = 'jdbe-setting-desc';
                desc.textContent = c.desc;
                wrapper.appendChild(desc);
            }
        });
        const resetWrapper = document.createElement('div');
        resetWrapper.style.margin = '10px 0 5px 0';
        resetWrapper.style.paddingLeft = '188px';
        const resetButton = document.createElement('button');
        resetButton.id = 'jdbe-reset-heatmap';
        resetButton.type = 'button';
        resetButton.className = 'button is-small';
        resetButton.textContent = T('reset');
        resetWrapper.appendChild(resetButton);
        body.appendChild(resetWrapper);
        resetButton.addEventListener('click', () => {
            settings.heatmapMin = DEFAULT_SETTINGS.heatmapMin;
            settings.heatmapMax = DEFAULT_SETTINGS.heatmapMax;
            settings.heatmapCurveFactor = DEFAULT_SETTINGS.heatmapCurveFactor;
            settings.ratedByThreshold = DEFAULT_SETTINGS.ratedByThreshold;
            saveSettings();
            updateUI();
            document.querySelectorAll(ITEM_SELECTOR).forEach(item => {
                item.dataset.highlightProcessed = 'false';
                processItem(item);
            });
            toast(T('resetDone'), 1500);
        });
        const updateUI = () => {
            controls.forEach(c => {
                const input = document.getElementById(c.id);
                if (c.type === 'checkbox') {
                    input.checked = !!settings[c.key];
                } else if (c.type === 'number') {
                    input.value = settings[c.key];
                }
                const row = input.closest('.jdbe-setting-row');
                if (c.dependsOn) {
                    row.style.display = settings[c.dependsOn] ? 'flex' : 'none';
                    input.disabled = c.readonly ? true : !settings[c.dependsOn];
                }
            });
        };
        modal.addEventListener('change', async (e) => {
            const control = controls.find(x => x.id === e.target.id);
            if (!control) return;
            if (control.id === 'setting-preview' && e.target.checked && !settings.previewThirdPartyConsent) {
                const agreed = await presentPreviewConsentModal();
                if (agreed) {
                    settings.previewThirdPartyConsent = true;
                } else {
                    e.target.checked = false;
                    settings.enablePreview = false;
                    saveSettings();
                    updateUIVisibility();
                    updateUI();
                    return;
                }
            }
            if (control.type === 'checkbox') {
                settings[control.key] = e.target.checked;
            } else if (control.type === 'number') {
                settings[control.key] = parseFloat(e.target.value) || 0;
            }
            if (control.id === 'setting-highlight' && !settings.highlight) {
                settings.sort = false;
                settings.hideLowScore = false;
            }
            if (control.id === 'setting-delegation') {
                if (settings.useEventDelegation && !delegationInitialized) {
                    initDelegatedEvents();
                }
            }
            saveSettings();
            updateUIVisibility();
            updateUI();
            if (control.key.startsWith('heatmap') || control.key.startsWith('ratedBy') || control.key === 'hideLowScore') {
                document.querySelectorAll(ITEM_SELECTOR).forEach(item => {
                    item.dataset.highlightProcessed = 'false';
                    processItem(item);
                });
            }
        });
        updateUI();
    }
    function updateUIVisibility() {
        document.body.classList.toggle('jdbe-highlight-disabled', !settings.highlight);
        document.body.classList.toggle('jdbe-status-disabled', !settings.status);
        document.body.classList.toggle('jdbe-preview-disabled', !settings.enablePreview);
        document.body.classList.toggle('jdbe-hide-low-score-enabled', settings.hideLowScore);
        const sortButton = document.getElementById('sort-by-heat-btn-container');
        if (sortButton) {
            const shouldShow = settings.sort && settings.highlight && document.querySelector(ITEM_SELECTOR);
            sortButton.style.display = shouldShow ? 'block' : 'none';
        }
    }
    function getCsrfToken() {
        const meta = document.querySelector('meta[name="csrf-token"]');
        authenticityToken = meta ? meta.content : null;
    }
    function isLoggedIn() {
        return !!document.querySelector('a[href="/logout"]');
    }
    function gmRequest(options) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                onload: (res) => {
                    if (res.status >= 200 && res.status < 300) {
                        resolve(res);
                    } else {
                        reject(res);
                    }
                },
                onerror: reject,
                onabort: reject,
                ontimeout: reject,
                ...options
            });
        });
    }
    async function ensureCsrfToken(item) {
        if (authenticityToken) return authenticityToken;
        getCsrfToken();
        if (authenticityToken) return authenticityToken;
        try {
            const anchor = item.querySelector('a.box') || item.querySelector('.box a');
            if (!anchor) return null;
            const res = await statusLimiter.run(() => gmRequest({ method: "GET", url: anchor.href }));
            if (res.status >= 200 && res.status < 300) {
                const doc = new DOMParser().parseFromString(res.responseText, "text/html");
                const meta = doc.querySelector('meta[name="csrf-token"]');
                authenticityToken = meta ? meta.content : null;
            }
        } catch (e) {}
        return authenticityToken;
    }
    async function updateReviewStatus(action, videoId, item, rating) {
        if (item.dataset.apiBusy === 'true') return;
        if (!isLoggedIn()) {
            toast(T('errorNeedLogin'));
            return;
        }
        const token = await ensureCsrfToken(item);
        if (!token) {
            item.classList.add('api-error');
            toast(T('errorCsrf'));
            setTimeout(() => item.classList.remove('api-error'), 500);
            return;
        }
        item.dataset.apiBusy = 'true';
        let urlPath = '';
        let body = '';
        const reviewId = item.dataset.reviewId;
        switch (action) {
            case 'watched': {
                if (rating == null) {
                    delete item.dataset.apiBusy;
                    return;
                }
                urlPath = reviewId ? `/v/${videoId}/reviews/${reviewId}` : `/v/${videoId}/reviews`;
                const baseBody = `authenticity_token=${encodeURIComponent(token)}&video_review[status]=watched&video_review[score]=${rating}&video_review[content]=`;
                body = reviewId ? `_method=patch&${baseBody}` : baseBody;
                break;
            }
            case 'wanted':
                urlPath = `/v/${videoId}/reviews/want_to_watch`;
                body = `authenticity_token=${encodeURIComponent(token)}`;
                break;
            case 'delete':
                if (!reviewId) {
                    delete item.dataset.apiBusy;
                    return;
                }
                urlPath = `/v/${videoId}/reviews/${reviewId}`;
                body = `_method=delete&authenticity_token=${encodeURIComponent(token)}`;
                break;
            default:
                delete item.dataset.apiBusy;
                return;
        }
        try {
            await statusLimiter.run(() => gmRequest({
                method: 'POST',
                url: window.location.origin + urlPath,
                headers: { "Content-Type": "application/x-www-form-urlencoded" },
                data: body
            }));
            statusCache.delete(videoId);
            await fetchItemData(item, true);
            if (settings.status) showStatusUI(item);
        } catch (error) {
            console.error('JavDB Enhanced: Status update failed.', error);
            if (error.status === 422) {
                toast(T('errorCsrf'));
                authenticityToken = null;
            } else {
                toast(T('errorNetworkBusy'));
            }
            item.classList.add('api-error');
            setTimeout(() => item.classList.remove('api-error'), 500);
        } finally {
            delete item.dataset.apiBusy;
        }
    }
    async function fetchPreviewImage(code) {
        if (!settings.previewThirdPartyConsent) return { status: 'consent-required' };
        const normalized = normalizeCode(code);
        if (!normalized) return { status: 'no-data' };
        if (previewCache.has(normalized)) return previewCache.get(normalized);
        let resDoc;
        try {
            const res = await previewLimiter.run(() => gmRequest({ method: "GET", url: `https://javstore.net/search/${encodeURIComponent(normalized)}.html` }));
            resDoc = new DOMParser().parseFromString(res.responseText, "text/html");
        } catch (e) {
            console.error(`JavDB Enhanced: search failed for ${normalized}`, e);
            return { status: 'error' };
        }
        let searchResults = resDoc.querySelectorAll("#content_news li > a, div.item > a");
        if (!searchResults.length) {
            const result = { status: 'no-data' };
            setPreviewCache(normalized, result);
            return result;
        }
        const regex = new RegExp(normalized.replace(/[-_ ]/g, '[-_ ]?'), 'i');
        const foundLink = Array.from(searchResults).find(a => {
            const str = (a.title || a.textContent || '').toUpperCase();
            const imgEl = a.querySelector("img");
            return regex.test(str) && imgEl && /^https?:\/\//i.test(imgEl.src);
        });
        if (!foundLink) {
            const result = { status: 'no-data' };
            setPreviewCache(normalized, result);
            return result;
        }
        try {
            const res = await previewLimiter.run(() => gmRequest({ method: "GET", url: foundLink.href }));
            resDoc = new DOMParser().parseFromString(res.responseText, "text/html");
        } catch (e) {
            console.error(`JavDB Enhanced: detail fetch failed ${foundLink.href}`, e);
            return { status: 'error' };
        }
        const imgLink = resDoc.querySelector(".news > a, .screencap > a");
        const potentialUrl = imgLink ? imgLink.href : '';
        const safeUrlRegex = /^https?:\/\/[^\s/$.?#].[^\s]*\.(jpg|jpeg|png|webp)$/i;
        if (potentialUrl && safeUrlRegex.test(potentialUrl)) {
            const result = { status: 'success', img: potentialUrl };
            setPreviewCache(normalized, result);
            return result;
        }
        const result = { status: 'no-data' };
        setPreviewCache(normalized, result);
        return result;
    }
    function showPreviewModal(imageUrl, triggerElement) {
        if (document.getElementById('preview-modal')) return;
        const modal = document.createElement('div');
        modal.id = 'preview-modal';
        modal.setAttribute('aria-modal', 'true');
        const content = document.createElement('div');
        content.id = 'preview-modal-content';
        const img = document.createElement('img');
        img.src = imageUrl;
        content.appendChild(img);
        modal.appendChild(content);
        document.body.appendChild(modal);
        document.body.style.overflow = 'hidden';
        let scale = 1, panX = 0, panY = 0;
        let isPanning = false, startX = 0, startY = 0;
        const updateTransform = () => {
            content.style.transform = `translate(${panX}px, ${panY}px) scale(${scale})`;
        };
        const onWheel = (e) => {
            e.preventDefault();
            const delta = e.deltaY > 0 ? -0.1 : 0.1;
            const newScale = scale + delta * scale;
            scale = Math.min(Math.max(PREVIEW_MODAL_MIN_SCALE, newScale), PREVIEW_MODAL_MAX_SCALE);
            updateTransform();
        };
        const onMouseDown = (e) => {
            e.preventDefault();
            isPanning = true;
            startX = e.clientX - panX;
            startY = e.clientY - panY;
            content.classList.add('is-panning');
        };
        const onMouseMove = (e) => {
            if (!isPanning) return;
            e.preventDefault();
            panX = e.clientX - startX;
            panY = e.clientY - startY;
            updateTransform();
        };
        const onMouseUp = () => {
            if (isPanning) {
                isPanning = false;
                content.classList.remove('is-panning');
            }
        };
        const closeModal = (e) => {
            if (e && e.target !== modal) return;
            try { document.body.removeChild(modal); } catch (err) {}
            document.body.style.overflow = '';
            window.removeEventListener('keydown', closeOnEsc);
            window.removeEventListener('mousemove', onMouseMove);
            window.removeEventListener('mouseup', onMouseUp);
            triggerElement?.focus();
        };
        const closeOnEsc = (e) => {
            if (e.key === 'Escape') closeModal({ target: modal });
        };
        modal.addEventListener('wheel', onWheel, { passive: false });
        content.addEventListener('mousedown', onMouseDown);
        window.addEventListener('mousemove', onMouseMove);
        window.addEventListener('mouseup', onMouseUp);
        modal.addEventListener('click', closeModal);
        window.addEventListener('keydown', closeOnEsc);
    }
    function parseStatusFromDoc(doc) {
        let status = 'unmarked';
        let reviewId = '';
        let rating = 0;
        const watchedTag = doc.querySelector('.review-title .tag.is-success.is-light');
        const wantedTag = doc.querySelector('.review-title .tag.is-info.is-light');
        const deleteLink = doc.querySelector('a[data-method="delete"][href*="/reviews/"]');
        if (watchedTag) {
            status = 'watched';
            const checkedInput = doc.querySelector('.rating-star .control input:checked');
            if (checkedInput) rating = parseInt(checkedInput.value, 10) || 0;
        } else if (wantedTag) {
            status = 'wanted';
        }
        if (deleteLink) {
            const m = deleteLink.href.match(/\/reviews\/(\d+)/);
            if (m) reviewId = m[1];
        }
        return { status, reviewId, rating };
    }
    function applyStatusToItem(item, parsed) {
        const statusContainer = item.querySelector('.csb-status-container');
        if (!statusContainer || !parsed) return;
        const { status, reviewId, rating } = parsed;
        statusContainer.className = 'csb-status-container';
        item.dataset.reviewId = reviewId || '';
        const ratingDisplay = statusContainer.querySelector('.star-arc-container');
        if (ratingDisplay) {
            ratingDisplay.querySelectorAll('span').forEach((star, index) => {
                star.classList.toggle('is-filled', status === 'watched' && index < (rating || 0));
            });
        }
        statusContainer.classList.add(`show-${status || 'unmarked'}`);
    }
    async function fetchItemData(item, forceUpdate = false) {
        if ((item.dataset.statusChecked && !forceUpdate) || item.dataset.statusFetching) return;
        item.dataset.statusFetching = 'true';
        const anchor = item.querySelector('a.box') || item.querySelector('.box a');
        if (!anchor) {
            delete item.dataset.statusFetching;
            return;
        }
        let videoId = anchor.href.split('/').pop();
        if (settings.status && isLoggedIn()) {
            if (!statusCache.has(videoId) || forceUpdate) {
                try {
                    const res = await statusLimiter.run(() => gmRequest({ method: "GET", url: anchor.href }));
                    if (res.status >= 200 && res.status < 300) {
                        const doc = new DOMParser().parseFromString(res.responseText, "text/html");
                        const parsed = parseStatusFromDoc(doc);
                        statusCache.set(videoId, parsed);
                        limitCacheSize(statusCache, MAX_CACHE_SIZE);
                        applyStatusToItem(item, parsed);
                    }
                } catch (error) {
                    console.error('JavDB Enhanced: fetch item failed', error);
                }
            } else {
                applyStatusToItem(item, statusCache.get(videoId));
            }
        }
        item.dataset.statusChecked = 'true';
        delete item.dataset.statusFetching;
    }
    function showRatingModal(item, videoId) {
        const html = `
            <div class="rating-stars">
                ${[5, 4, 3, 2, 1].map(i => `<input type="radio" id="star${i}-${videoId}" name="rating-${videoId}" value="${i}"><label for="star${i}-${videoId}">★</label>`).join('')}
            </div>
            <button class="btn-cancel-rating">${T('cancel')}</button>
        `;
        const result = createCoverModal(item, 'rating-modal-container', html);
        if (!result) return;
        const { modal, closeModal } = result;
        modal.addEventListener('click', (e) => {
            e.stopPropagation();
        });
        modal.querySelectorAll('input[type="radio"]').forEach(radio => {
            radio.addEventListener('change', (e) => {
                updateReviewStatus('watched', videoId, item, e.target.value);
                closeModal();
            });
        });
        modal.querySelector('.btn-cancel-rating').addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            closeModal();
        });
    }
    function showDeleteConfirmation(item, videoId) {
        const html = `
            <p>${T('confirmDelete')}</p>
            <div class="buttons">
                <button class="button is-danger btn-confirm-delete">${T('confirm')}</button>
                <button class="button is-light btn-cancel-delete">${T('cancel')}</button>
            </div>
        `;
        const result = createCoverModal(item, 'confirm-delete-modal', html);
        if (!result) return;
        const { modal, closeModal } = result;
        modal.querySelector('.btn-confirm-delete').addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            updateReviewStatus('delete', videoId, item);
            closeModal();
        });
        modal.querySelector('.btn-cancel-delete').addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            closeModal();
        });
    }
    function initHoverBehavior(item) {
        if (item.dataset.hoverInit) return;
        item.dataset.hoverInit = 'true';
        let hoverTimer;
        const startFetch = async () => {
            if (hoverTimer) clearTimeout(hoverTimer);
            hoverTimer = setTimeout(async () => {
                showStatusUI(item);
                if (!item.dataset.statusChecked) {
                    await fetchItemData(item, false);
                }
            }, settings.fetchDelay);
        };
        item.addEventListener('mouseenter', startFetch);
        item.addEventListener('mouseleave', () => {
            if (hoverTimer) clearTimeout(hoverTimer);
            if (settings.hideOnMouseLeave) hideStatusUI(item);
        });
    }
    async function handlePreviewTrigger(item, previewIcon) {
        if (!settings.enablePreview || !previewIcon || previewIcon.classList.contains('fetch-failed')) return;
        previewIcon.classList.remove('fetch-error');
        if (item.dataset.previewUrl && previewIcon.classList.contains('has-preview')) {
            showPreviewModal(item.dataset.previewUrl, previewIcon);
            return;
        }
        if (!settings.previewThirdPartyConsent) {
            const agreed = await presentPreviewConsentModal(previewIcon);
            if (agreed) {
                settings.previewThirdPartyConsent = true;
                saveSettings();
            } else {
                return;
            }
        }
        const titleElement = item.querySelector('.video-title strong');
        if (!titleElement) return;
        previewIcon.classList.remove('has-preview');
        previewIcon.classList.add('is-loading');
        previewIcon.innerHTML = ICONS.loading;
        previewIcon.title = T('loadingPreview');
        const rawCode = titleElement.textContent.trim();
        const previewData = await fetchPreviewImage(rawCode);
        previewIcon.classList.remove('is-loading');
        if (previewData.status === 'success') {
            item.dataset.previewUrl = previewData.img;
            previewIcon.classList.add('has-preview');
            previewIcon.innerHTML = ICONS.preview;
            previewIcon.title = T('preview');
            showPreviewModal(previewData.img, previewIcon);
        } else if (previewData.status === 'no-data') {
            previewIcon.classList.add('fetch-failed');
            previewIcon.innerHTML = ICONS.brokenImage;
            previewIcon.title = T('noPreview');
        } else {
            previewIcon.classList.add('fetch-error');
            previewIcon.innerHTML = ICONS.brokenImage;
            previewIcon.title = T('errorPreview');
        }
    }
    function initDelegatedEvents() {
        if (delegationInitialized) return;
        delegationInitialized = true;
        const body = document.body;
        body.addEventListener('mouseover', e => {
            const item = e.target.closest?.(ITEM_SELECTOR);
            if (!item || (e.relatedTarget && item.contains(e.relatedTarget))) return;
            const t = setTimeout(async () => {
                showStatusUI(item);
                if (!item.dataset.statusChecked) {
                    await fetchItemData(item, false);
                }
            }, settings.fetchDelay);
            clearTimeout(hoverTimers.get(item));
            hoverTimers.set(item, t);
        });
        body.addEventListener('mouseout', e => {
            const item = e.target.closest?.(ITEM_SELECTOR);
            if (!item || (e.relatedTarget && item.contains(e.relatedTarget))) return;
            clearTimeout(hoverTimers.get(item));
            hoverTimers.delete(item);
            if (settings.hideOnMouseLeave) hideStatusUI(item);
        });
        body.addEventListener('click', e => {
            const target = e.target;
            const btnWanted = target.closest('.btn-set-wanted');
            const btnWatched = target.closest('.js-set-watched, .btn-modify');
            const btnDelete = target.closest('.js-delete');
            const previewIcon = target.closest('.csb-preview-icon-container');
            if (!btnWanted && !btnWatched && !btnDelete && !previewIcon) return;
            const item = target.closest(ITEM_SELECTOR);
            if (!item) return;
            e.preventDefault();
            e.stopPropagation();
            const videoId = (item.querySelector('a.box') || item.querySelector('.box a'))?.href.split('/').pop();
            if (previewIcon) {
                handlePreviewTrigger(item, previewIcon);
            } else if (videoId) {
                if (btnWanted) updateReviewStatus('wanted', videoId, item);
                else if (btnWatched) showRatingModal(item, videoId);
                else if (btnDelete) showDeleteConfirmation(item, videoId);
            }
        }, true);
        body.addEventListener('keydown', e => {
            const previewIcon = e.target.closest?.('.csb-preview-icon-container');
            if (!previewIcon || (e.key !== 'Enter' && e.key !== ' ')) return;
            e.preventDefault();
            e.stopPropagation();
            const item = previewIcon.closest(ITEM_SELECTOR);
            if (!item) return;
            handlePreviewTrigger(item, previewIcon);
        }, true);
    }
    function processItem(item) {
        const anchorElement = item.querySelector('a.box') || item.querySelector('.box a');
        const elementToColor = item.querySelector('a.box') || item.querySelector('div.box');
        const scoreElement = item.querySelector('.score .value');
        let weightedRating = 0;
        if (settings.highlight && item.dataset.highlightProcessed !== 'true' && scoreElement) {
            item.dataset.highlightProcessed = 'true';
            const C_MIN = settings.heatmapMin;
            const m = settings.ratedByThreshold;
            const C_MAX = settings.heatmapMax;
            const curveFactor = settings.heatmapCurveFactor || 1.0;
            delete item.dataset.heatColor;
            for (const re of scoreRegexes) {
                const m_text = (scoreElement.textContent || '').trim().match(re);
                if (m_text) {
                    const parsedScore = { score: parseFloat(m_text[1]), ratedBy: parseInt(m_text[2], 10) };
                    weightedRating = (parsedScore.ratedBy * parsedScore.score + m * C_MIN) / (parsedScore.ratedBy + m);
                    if (parsedScore.score >= C_MIN) {
                        const valueToColor = weightedRating;
                        let normalizedHeat = (valueToColor - C_MIN) / (C_MAX - C_MIN);
                        normalizedHeat = Math.max(0, Math.min(1, normalizedHeat));
                        if (curveFactor !== 1.0) {
                            normalizedHeat = Math.pow(normalizedHeat, curveFactor);
                        }
                        item.dataset.heatColor = getHeatmapColor(normalizedHeat);
                    }
                    break;
                }
            }
            item.dataset.heat = String(weightedRating);
            if (elementToColor) {
                elementToColor.style.backgroundColor = (settings.highlight && item.dataset.heatColor) ? item.dataset.heatColor : '';
            }
        } else if (elementToColor && !item.dataset.highlightProcessed) {
            item.dataset.heat = '0';
        }
        if (settings.highlight) {
            const score = parseFloat(item.dataset.heat || '0');
            const c_min = settings.heatmapMin;
            if (score > 0 && score < c_min) {
                item.dataset.jdbeHide = 'true';
            } else {
                item.dataset.jdbeHide = 'false';
            }
        }
        const coverElement = item.querySelector('.cover');
        if (coverElement && anchorElement && !item.dataset.statusProcessed) {
            item.dataset.statusProcessed = 'true';
            const overlayRoot = appendUIOnce(coverElement, '.cover-status-buttons', STATUS_BUTTONS_TEMPLATE);
            if (overlayRoot && !settings.useEventDelegation) {
                const videoId = anchorElement.href.split('/').pop();
                overlayRoot.querySelector('.btn-set-wanted')?.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); updateReviewStatus('wanted', videoId, item); });
                overlayRoot.querySelectorAll('.js-set-watched, .btn-modify').forEach(btn => btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); showRatingModal(item, videoId); }));
                overlayRoot.querySelectorAll('.js-delete').forEach(btn => btn.addEventListener('click', e => { e.preventDefault(); e.stopPropagation(); showDeleteConfirmation(item, videoId); }));
            }
            const previewIcon = appendUIOnce(coverElement, '.csb-preview-icon-container', PREVIEW_ICON_TEMPLATE);
            if (previewIcon && !settings.useEventDelegation) {
                const trigger = (e) => {
                    e.preventDefault(); e.stopPropagation();
                    handlePreviewTrigger(item, previewIcon);
                };
                previewIcon.addEventListener('click', trigger);
                previewIcon.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') trigger(e); });
            }
            if (!settings.useEventDelegation) {
                 initHoverBehavior(item);
            }
        }
    }
    function getHeatmapColor(heat) {
        const h = Math.min(Math.max(heat, 0), 1);
        const r = h > 0.5 ? Math.round(255 * ((h - 0.5) * 2)) : 0;
        const g = h < 0.5 ? Math.round(255 * (h * 2)) : Math.round(255 * (1 - (h - 0.5) * 2));
        const b = h < 0.5 ? Math.round(255 * (1 - (h * 2))) : 0;
        return `rgba(${r},${g},${b},0.5)`;
    }
    function createIndependentSortButton() {
        if (document.getElementById('sort-by-heat-btn-container')) return;
        const hasItems = document.querySelector(ITEM_SELECTOR);
        if (!hasItems) return;
        const cont = document.createElement('div');
        cont.id = 'sort-by-heat-btn-container';
        cont.innerHTML = `<a class="button">${T('sort')}</a>`;
        cont.addEventListener('click', e => { e.preventDefault(); sortItemsByHeat(); });
        document.body.appendChild(cont);
        updateUIVisibility();
    }
    function sortItemsByHeat() {
        const containers = Array.from(document.querySelectorAll('.movie-list, .is-user-page .columns.is-multiline'));
        if (containers.length === 0) return;
        const allItems = Array.from(document.querySelectorAll(ITEM_SELECTOR));
        allItems.sort((a, b) => (parseFloat(b.dataset.heat || '0') - parseFloat(a.dataset.heat || '0')));
        const primaryContainer = containers[0];
        allItems.forEach(item => primaryContainer.appendChild(item));
        const navBar = document.querySelector('.navbar.is-fixed-top');
        const offset = navBar ? navBar.offsetHeight : DEFAULT_NAVBAR_HEIGHT;
        window.scrollTo({ top: primaryContainer.getBoundingClientRect().top + window.scrollY - offset, behavior: 'smooth' });
    }
    function main() {
        loadSettings();
        GM_registerMenuCommand(T('settings'), openSettingsModal);
        getCsrfToken();
        updateUIVisibility();
        const obs = new MutationObserver(muts => {
            const itemsToProcess = new Set();
            for (const mut of muts) {
                if (mut.addedNodes && mut.addedNodes.length) {
                    for (const node of mut.addedNodes) {
                        if (node.nodeType !== 1) continue;
                        if (node.matches?.(ITEM_SELECTOR)) {
                            itemsToProcess.add(node);
                        }
                        if (node.querySelectorAll) {
                            node.querySelectorAll(ITEM_SELECTOR).forEach(it => itemsToProcess.add(it));
                        }
                    }
                }
                if (mut.type === 'childList' && mut.target && mut.target.closest) {
                    const potentialItem = mut.target.closest(ITEM_SELECTOR);
                    if (potentialItem && !itemsToProcess.has(potentialItem)) {
                        itemsToProcess.add(potentialItem);
                    }
                }
            }
            if (itemsToProcess.size > 0) {
                itemsToProcess.forEach(item => {
                    if (item.querySelector('.score .value') || item.querySelector('.meta')) {
                        item.dataset.highlightProcessed = 'false';
                    }
                    processItem(item);
                });
                createIndependentSortButton();
            }
        });
        obs.observe(document.body, { childList: true, subtree: true });
        document.querySelectorAll(ITEM_SELECTOR).forEach(processItem);
        createIndependentSortButton();
        if (settings.useEventDelegation) {
            initDelegatedEvents();
        }
    }
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }
})();