☰

NHentai Flow

Enhances the site with instant previews, visual markers for favorites, a reading queue, tag tools, smart navigation, and more.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴、Greasemonkey 油猴子 或 Violentmonkey 暴力猴,才能安装此脚本。

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴 或 Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴 或 Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         NHentai Flow
// @namespace    NEnhanced
// @version      1.6.2
// @description  Enhances the site with instant previews, visual markers for favorites, a reading queue, tag tools, smart navigation, and more.
// @author       Testador
// @match        https://nhentai.net/*
// @grant        GM_addStyle
// @grant        GM_openInTab
// @run-at       document-end
// @icon         data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjIiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIj4KCTxkZWZzPgoJCTxpbWFnZSAgd2lkdGg9IjUxMiIgaGVpZ2h0PSI1MTIiIGlkPSJpbWcxIiBocmVmPSJkYXRhOmltYWdlL3BuZztiYXNlNjQsaVZCT1J3MEtHZ29BQUFBTlNVaEVVZ0FBQWdBQUFBSUFBUU1BQUFET3RrYTVBQUFBQVhOU1IwSUIyY2tzZndBQUFBTlFURlJGSWpSRklTL3B2QUFBQUl4SlJFRlVlSnp0ekRFTkFBQU1BNkRWdittSjZOVUVCSkFyUlNBUUNBUUNnVUFnRUFnRUFvRkFJQkFJQkFLQlFDQVFDQVFDZ1VBZ0VBZ0VBb0ZBSUJBSUJBS0JRQ0FRQ0FRQ2dVQWdFQWdFQW9GQUlCQUlCQUtCUUNBUUNBUUNnVUFnRUFnRUFvRkFJQkFJQkFLQlFDQVFDQVFDZ1VBZ0VBZ0VBb0ZBSUJBSUJBS0JRQ0FRQ0FRQ2dXQTdlTXFlQWdFaVJEM0hBQUFBQUVsRlRrU3VRbUNDIi8+Cgk8L2RlZnM+Cgk8c3R5bGU+CgkJLnMwIHsgZmlsbDogI2VkMjc1NCB9IAoJCS5zMSB7IGZpbGw6ICNlY2Q5ZmYgfSAKCTwvc3R5bGU+Cgk8dXNlIGlkPSJCYWNrZ3JvdW5kIiBocmVmPSIjaW1nMSIgeD0iMCIgeT0iMCIvPgoJPHBhdGggY2xhc3M9InMwIiBkPSJtMTM2LjYgMzI0LjA3YzkuNjUtMi40MSAxOS4wNy0zLjM4IDI4LTMuMzggMzUuMjUgMCA2Ni42MyAxNy4xNCA5MC4yOSA0OS45NyAxNC0xMi4wNyAyNC4zOC0xNC43MiAzMi44My0xNC43MiA1Ljc5IDAgMTAuODYgMS4yIDE1LjQ1IDEuMiA5LjQxIDAgMTcuMzgtNC44MiAyOC4yNC0zNC43Ni02Ljc2LTMuMTQtMTQuNzMtMy42Mi0yMi45My0zLjYyLTMuMTQgMC02LjI4LTAuMTEtOS40MiAwLTQwLjg0IDEuNDUtMTExLjQtNzIuODYtMTIuNzktMTI4LjE4LTYuNTYtNi43Mi0xMDAuMzgtOTkuOTEtMjA3Ljc3LTcxLjA2LTI5LjgxIDguMDEtNTIuODggMjMuMzMtNjkuNSAzNy44OCA0NS41My0xNC45NCA5NS43MSAwLjkxIDEyNC4xMSAzOS4wNyAyNy43NiAzNy4yOSAyOS4xOSA4OC40OCAzLjQ5IDEyNy42eiIvPgoJPHBhdGggaWQ9Ii5GIiBjbGFzcz0iczEiIGQ9Im0xNTIuMTEgNTEyYzMzLjYgMCA2MS41My0yNCA2MS41My02MC42NSAwLTMwLjk5LTIxLjM5LTUxLjkzLTUzLjY4LTUxLjkzLTM0LjQ3IDAtNjEuOTYgMjQtNjEuOTYgNjAuNjUgMCAzMC4xMSAyMi4yNSA1MS45MyA1NC4xMSA1MS45M3ptMjg4Ljg3LTIzMy40NWMxNy40NiAwIDM4LjQgMy45MiA1My4yNCAxMy4wOWw0NC4wNy03NS45M2MtMjIuNjktMTMuMDktNjAuNjUtMjAuNTEtOTIuNTEtMjAuNTEtMTAxLjIzIDAtMTU4LjQgNDUuODItMTc1Ljg1IDEzMy41M2wtMzUuNzggMTc4LjkxaDEwMi45OGwxOS42My05Ny43NWgxMDcuNzlsMTUuNy03Ny42N2gtMTA4LjIxbDEuMzEtNS42N2M2LjU0LTMyLjczIDMwLjU0LTQ4IDY3LjYzLTQ4eiIvPgo8L3N2Zz4=
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================================================
    // 0. SETTINGS & CONFIGURATION MANAGER
    // ==========================================================================

    const DEFAULT_SETTINGS = {
        previewNav: true,
        highlightVisited: true,
        showTagOverlay: true,
        enableQueue: true,
        enableTagSelect: true,
        enableSavedSearch: true,
        smartNav: true,
        paginationRight: false,
        enableContextMenu: true,
        smartNavScroll: 1500,
        smartNavSwipe: 400
    };

    const NUM_LIMITS = { smartNavScroll: [300, 6000, 100], smartNavSwipe: [100, 900, 25] };

    function numSetting(key) {
        const lim = NUM_LIMITS[key];
        const v = Number(settings[key]);
        if (!lim || !Number.isFinite(v)) return DEFAULT_SETTINGS[key];
        return Math.min(lim[1], Math.max(lim[0], v));
    }

    function readJSON(key, fallback) {
        try {
            const raw = localStorage.getItem(key);
            return raw === null ? fallback : JSON.parse(raw);
        } catch (e) {
            return fallback;
        }
    }

    function readList(key) {
        const v = readJSON(key, []);
        return Array.isArray(v) ? v : [];
    }

    function loadSettings(stored) {
        const out = { ...DEFAULT_SETTINGS };
        if (stored && typeof stored === 'object') {
            Object.keys(DEFAULT_SETTINGS).forEach(k => { if (k in stored) out[k] = stored[k]; });
        }
        return out;
    }

    let settings = loadSettings(readJSON('nhentai_flow_settings', {}));

    function saveSettings() {
        localStorage.setItem('nhentai_flow_settings', JSON.stringify(settings));
        applySettings();
    }

    function applySettings() {
        const body = document.body;

        settings.highlightVisited ? body.classList.add('f-visited') : body.classList.remove('f-visited');
        settings.paginationRight ? body.classList.add('f-pag-right') : body.classList.remove('f-pag-right');
        settings.showTagOverlay ? body.classList.add('f-tags') : body.classList.remove('f-tags');
        settings.enableQueue ? body.classList.add('f-queue') : body.classList.remove('f-queue');
        settings.enableTagSelect ? body.classList.add('f-tag-select') : body.classList.remove('f-tag-select');
        settings.enableSavedSearch ? body.classList.add('f-saved-search') : body.classList.remove('f-saved-search');
        settings.previewNav ? body.classList.remove('disable-preview-nav') : body.classList.add('disable-preview-nav');
    }

    applySettings();

    const SEL = {
        // layout
        content: '#content',
        nav: 'nav',
        navList: 'nav[role="navigation"] ul',
        navListFallback: '.menu.left',
        favoritesLink: 'nav a[href*="favorites"]',
        searchForm: 'form.search',
        searchInput: 'input[name="q"]',

        // gallery cards (home, searches, favorites)
        gallery: '.gallery',
        cover: 'a.cover',
        coverImage: 'a.cover img',
        caption: '.caption',
        galleryLink: 'a[href*="/g/"]',

        // listings
        sortBar: '.sort',
        popularBlock: '.index-popular',
        pagination: '.pagination',
        paginationLast: '.pagination .last',
        paginationPage: '.pagination .page',
        paginationNext: '.pagination a.next, a[rel="next"]',
        removeButton: '.remove-button',

        // gallery page (/g/ID/)
        infoButtons: '#info-block .buttons',
        galleryTitle: 'h1.title',
        bigCover: '#cover img',
        tags: '#tags',
        tagLink: 'a.tagchip, a.tag',
        tagLinkSelected: '.tagchip.tag-selected, .tag.tag-selected',
        tagName: '.name',
        favoriteButton: '#favorite',
        favoriteLabel: '.text',
        commentList: '#comment-container',
        commentPost: '#comment-post-container',
        comment: '.comment',

        // reader (/g/ID/PAGE/)
        imageContainer: '#image-container',
        readerBar: '.reader-bar',
        readerButtonsRight: '.reader-buttons-right',
        readerNext: '.reader-pagination .next',
        readerPrev: '.reader-pagination .previous'
    };

    const SITE_CLASS = { grid: 'container index-container' };

    // ==========================================================================
    // 1. SHARED UTILS & CSS
    // ==========================================================================
    const stopEvent = (e) => { e.preventDefault(); e.stopPropagation(); };
    const stopPropOnly = (e) => { e.stopPropagation(); };
    const sleep = (ms) => new Promise(r => setTimeout(r, ms));
    const DOM_PARSER = new DOMParser();
    const metaCache = new Map();
    const states = new Map();
    let hoveredGallery = null;
    let hoverTimeout = null;
    let readingQueue = readList('nhentai_queue_v1');
    const pendingQueueDeletes = new Set();
    let favCache = new Set(readList('nhentai_flow_favs'));
    let saveQueueTimeout;
    let currentSortMode = localStorage.getItem('nhentai_queue_sort') || 'newest';
    let dockCurrentPage = 1;
    const DOCK_ITEMS_PER_PAGE = 12;
    let isReader = false;

    function checkPage() {
        const path = window.location.pathname;
        const currentlyReader = /^\/g\/\d+\/\d+/.test(path);

        if (currentlyReader !== isReader || !document.body.dataset.checked) {
            isReader = currentlyReader;
            document.body.dataset.checked = "true";

            if (isReader) {
                document.body.classList.remove('is-gallery-page');
            } else {
                document.body.classList.add('is-gallery-page');
            }

            initQueueWidget();
        }
    }
    checkPage();

    const css = `
        /* --- CORE PREVIEW STYLES  --- */
        .gallery, .gallery-favorite { position: relative; vertical-align: bottom; }
        .gallery:hover, .gallery.is-previewing { z-index: 150; }
        .gallery.is-previewing { display: inline-flex !important; flex-direction: column; justify-content: flex-end; }
        .gallery.is-previewing .cover { padding-bottom: 0 !important; height: auto !important; display: block; flex-grow: 0; }
        .gallery.is-previewing .cover img { position: relative !important; height: auto !important; width: 100% !important; max-height: none !important; object-fit: contain; }
        .inline-preview-ui { opacity: 0; pointer-events: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 10; }
        .gallery:hover .inline-preview-ui { opacity: 1; pointer-events: auto; }
        .card-overlay { right: 50px !important; z-index: 14 !important; }
        @media (min-width: 600px) { span.username { display: none } }

        /* --- HIGHLIGHT VISITED --- */
        body.f-visited .gallery a:visited .caption { background: #3d643f !important; }

        /* --- MARKERS SYSTEM --- */
        .reading-marker { position: absolute; top: -2px; font-size: large; z-index: 5; pointer-events: none; filter: drop-shadow(0 0 1px #000); }
        .marker-queue { left: 45px; }
        .marker-fav { left: 10px; }
        .marker-progress { right: 10px; }
        .marker-yellow i { color: #ed7c26; }
        .marker-blue i { color: #5b26ed; }
        .marker-pink i { color: #ed2553; }
        .gallery:hover .reading-marker { opacity: 0; }

        /* --- CONTEXT MENU --- */
        .nh-context-menu { position: fixed; z-index: 10000; max-width: calc(100vw - 16px); width: 160px; background: #1f1f1f; border-radius: 5px; box-shadow: 0 2px 6px rgba(0,0,0,0.5); display: none; flex-direction: column; padding: 5px 0 0 0 ; font-size: 12px; }
        .nh-context-menu.is-visible { display: flex; }
        .nh-cm-item { padding: 8px 15px; cursor: pointer; display: flex; color: #ddd; }
        .nh-cm-item:hover { background: #4d4d4d; }
        .nh-cm-footer { margin-top: 5px; padding: 8px 0; border-top: 1px solid #333; color: #666; font-size: 0.7em; cursor: default; background: #1a1a1a; border-radius: 0 0 5px 5px; }
        .nh-cm-separator { height: 1px; background: #333; margin: 5px 5px}
        .nh-cm-parent { justify-content: space-between; align-items: center; }
        .nh-cm-group button.nh-cm-item { width: 100%; box-sizing: border-box; border: 0; background: transparent; font: inherit; }
        .nh-cm-group button.nh-cm-item:hover:not(:disabled),
        .nh-cm-group button.nh-cm-item:focus-visible,
        .nh-cm-group.is-open > .nh-cm-parent { background: #4d4d4d; }
        .nh-cm-submenu { position: fixed; z-index: 10001; display: none; flex-direction: column; padding: 5px 0; background: #1f1f1f; border-radius: 5px; box-shadow: 0 2px 6px rgba(0,0,0,.5); }
        .nh-cm-group.is-open > .nh-cm-submenu { display: flex; }
        .nh-cm-group button.nh-cm-item:disabled { opacity: .45; cursor: default; }
        .nh-cm-explore-submenu { width: 130px; max-height: calc(33vh - 16px); overflow-y: auto; overscroll-behavior: contain; box-sizing: border-box; }
        .nh-cm-section { padding: 6px 15px 3px; color: #fff; font-size: 11px; font-weight: bold; text-transform: uppercase; text-align: left; }
        .nh-cm-status { padding: 8px 15px; color: #999; }
        .nh-cm-explore-link { white-space: normal; overflow-wrap: anywhere; text-align: left; }

        /* --- UI ELEMENTS (Hotzones/Seek) --- */
        .hotzone { position: absolute; top: 0; height: calc(100% - 15px); width: 40%; cursor: default; z-index: 20; }
        .hotzone-left { left: 0; } .hotzone-right { right: 0; }
        .seek-container { position: absolute; bottom: 0; left: 0; width: 100%; height: 30px; z-index: 40; cursor: pointer; display: flex; align-items: flex-end; }
        .seek-bg { width: 100%; height: 15px; background: rgba(31,31,31,0.5); transform: scaleY(0.2); transform-origin: bottom; transition: transform 0.1s; position: relative; }
        .seek-container:hover .seek-bg { transform: scaleY(1); }
        .seek-fill { height: 100%; background: #ed2553; width: 100%; transform: scaleX(0); transform-origin: left; transition: transform 0.1s; }
        .seek-tooltip { position: absolute; bottom: 17px; transform: translateX(-50%); background: #ed2553; color: #fff; font-size: 10px; padding: 2px 4px; border-radius: .3em; opacity: 0; pointer-events: none; white-space: nowrap; font-weight: bold; transition: opacity 0.1s; }
        .seek-container:hover .seek-tooltip { opacity: 1; }

        /* --- PREVIEW NAV SETTINGS --- */
        body.disable-preview-nav .hotzone,
        body.disable-preview-nav .seek-container { display: none !important; }

        /* --- TAGS & QUEUE TRIGGERS --- */
        .tag-trigger, .queue-trigger { width: 18%; position: absolute; background: rgba(31,31,31,0.5); color: #fff; font-size: 10px; font-weight: 700; padding: 4px 0px; }
        .tag-trigger { border-radius: .3em 0 .3em 0; z-index: 50; cursor: default; }
        .tag-trigger::after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
        .tag-trigger:hover::after { width: 275%; background: #404040; border-radius: .3em 0 .3em 0; z-index:-1; }
        .queue-trigger { right: 0; border-radius: 0 .3em 0 .3em; z-index: 50; cursor: pointer; }
        .queue-trigger:hover, .tag-trigger:hover { background: #404040; }
        .queue-trigger.in-queue { background: #5b26ed; }
        body:not(.f-tags) .tag-trigger { display: none !important; }
        body:not(.f-queue) .queue-trigger { display: none !important; }

        /* --- TAG POPUP --- */
        .tag-popup { opacity: 0; pointer-events: none; transition: opacity 0.2s ease; position: absolute; top: 22px; left: 0px; width: 100%; max-height: 80%; overflow-y: auto; overscroll-behavior: contain; background: #1f1f1f; border-radius: 0 0 .3em .3em; padding: 2px 2px 10px; z-index: 60; cursor: default; font-weight: bold; text-align: left; }
        .tag-trigger:hover + .tag-popup, .tag-popup:hover { opacity: 1; pointer-events: auto; }
        .tag-category { margin-bottom: 2px; margin-top: 6px; font-size: 10px; text-transform: uppercase; }
        .tag-pill { display: inline-block; background: #4d4d4d; padding: .13em .39em; margin: 1px; border-radius: .3em; font-size: 12px; }
        .tag-pill.tier-mythic { border: 1px solid #b655f7; color: #d6a0fb; text-shadow: 0 0 5px rgba(168, 85, 247, 0.8); }
        .tag-pill.tier-rare { border: 1px solid #eab308; color: #fef08a; }
        .tag-pill.tier-uncommon { border: 1px solid #0740EB; }
        .tag-pill.style-lgbt { border: none !important; background-image: linear-gradient(144deg, rgba(231, 0, 0, 1) 0%, rgba(255, 140, 0, 1) 20%, rgba(255, 239, 0, 1) 40%, rgba(0, 129, 31, 1) 60%, rgba(0, 68, 255, 1) 80%, rgba(118, 0, 137, 1) 100%); color: #000000 !important; font-weight: 900; text-shadow: 0 0 2px rgba(255,255,255,0.8); }
        @media (max-width: 600px) {
        .tag-pill { font-size: 11px; }
        .tag-trigger::after { display: none; }
        .tag-trigger, .queue-trigger { width: 21%; padding: 6px 0px; }}

        /* --- READER STYLES --- */
        #image-container { cursor: default; }
        .exit-fs-indicator { display: none; }
        :fullscreen .exit-fs-indicator { display: block; position: fixed; top: 0; left: 50%; transform: translateX(-50%); font-size: 40px; cursor: pointer; transition: transform 0.2s, opacity 0.2s, color 0.2s; padding: 20px 65px; opacity: 0; }
        :fullscreen .exit-fs-indicator:hover { color: #ed2553; transform: translateX(-50%) scale(1.4); opacity: 1; }
        .reader-bar:last-of-type .reader-buttons-right .zoom-buttons, .reader-bar:last-of-type .reader-buttons-right .reader-settings { display: none !important; }
        @media (max-width: 600px) {
            .reader-bar:last-of-type { flex-wrap: wrap !important; height: auto !important; padding-bottom: 8px !important; gap: 5px; }
            .reader-bar:last-of-type .reader-buttons-right { width: 100% !important; justify-content: center !important; padding-top: 8px; }
            .reader-bar:last-of-type .reader-buttons-right .btn { background: #4d4d4d; border-radius: 5px; }
        }

        /* --- SMART NAVIGATION & PAGINATION --- */
        body.is-gallery-page #content { padding-bottom: 200px !important; }
        @media (min-width: 1300px) {
            body.is-gallery-page .pagination.desktop-pagination { position: fixed !important; top: 50% !important; align-items: center !important; transform: translateY(-50%) !important; display: flex !important; flex-direction: column !important; z-index: 4; left: 8px !important; right: auto !important; }
            body.is-gallery-page a.first, body.is-gallery-page a.previous, body.is-gallery-page a.last, body.is-gallery-page a.next { transform: rotate(90deg); }
            body.f-pag-right.is-gallery-page .pagination { left: auto !important; right: 16px !important;  }
        }

        /* --- SETTINGS MODAL --- */
        .nf-modal-overlay { --nf-accent: #ed2553; --nf-accent-hover: #ff3b6a; --nf-bg: #1b1b1b; --nf-bg-2: #232323; --nf-line: rgba(255,255,255,.08); --nf-text: #eaeaea; --nf-muted: #8c8c8c;
            position: fixed; inset: 0; z-index: 999999; display: flex; justify-content: center; align-items: center; padding: 16px; box-sizing: border-box;
            background: rgba(0,0,0,.72); opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .15s ease, visibility 0s linear .15s; }
        .nf-modal-overlay.active { opacity: 1; visibility: visible; pointer-events: auto; transition: opacity .15s ease; }
        .nf-modal { display: flex; flex-direction: column; width: 100%; max-width: 530px; max-height: min(90vh, 760px); box-sizing: border-box; overflow: hidden;
            background: var(--nf-bg); color: var(--nf-text); border-radius: 5px; box-shadow: 0 24px 64px rgba(0,0,0,.6);
            font-size: 14px; line-height: 1.4; transform: translateY(8px) scale(.98); transition: transform .15s ease; outline: none; }
        .nf-modal-overlay.active .nf-modal { transform: none; }
        .nf-modal *, .nf-modal *::before, .nf-modal *::after { box-sizing: border-box; }
        .nf-modal-header { flex: none; display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; background: var(--nf-bg-2); }
        .nf-modal-header h2 { margin: 0; font-size: 17px; font-weight: 700; display: flex; align-items: center; gap: 10px; color: var(--nf-text); }
        .nf-modal-header h2 i { color: var(--nf-accent); }
        .nf-modal-close { flex: none; width: 32px; height: 32px; display: inline-flex; align-items: center; justify-content: center; padding: 0; border: 0; border-radius: 3px; background: transparent; color: var(--nf-muted); font-size: 16px; cursor: pointer; transition: background .12s, color .12s; }
        .nf-modal-close:hover { background: rgba(255,255,255,.08); color: #fff; }
        .nf-modal-body { flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 4px 12px 16px; }
        .nf-section-title { margin: 18px 8px 6px; font-size: 11px; text-align: left; font-weight: 700; letter-spacing: .09em; text-transform: uppercase; color: var(--nf-muted); }
        .nf-setting-row { display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 10px 8px; border-radius: 3px; cursor: pointer; transition: background .12s; }
        .nf-setting-row:hover { background: rgba(255,255,255,.04); }
        .nf-setting-text { display: flex; flex-direction: column; min-width: 0; }
        .nf-setting-name { font-size: 14px; font-weight: 600; color: var(--nf-text); }
        .nf-setting-desc { font-size: 12px; text-align: left; font-weight: normal; white-space: initial; color: var(--nf-muted); }
        .nf-setting-checkbox { -webkit-appearance: none; appearance: none; flex: none; position: relative; width: 38px; height: 22px; margin: 0; border: 0; border-radius: 11px; background: #3b3b3b; cursor: pointer; transition: background .15s; }
        .nf-setting-checkbox::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border-radius: 50%; background: #fff; transition: transform .15s; }
        .nf-setting-checkbox:checked { background: var(--nf-accent); }
        .nf-setting-checkbox:checked::after { transform: translateX(16px); }
        .nf-setting-checkbox:focus-visible, .nf-btn:focus-visible, .nf-modal-close:focus-visible { outline: 2px solid var(--nf-accent); outline-offset: 2px; }
        .nf-range-row { margin: 2px 0 4px 18px; padding: 8px 8px 10px 14px; border-left: 2px solid var(--nf-line); border-radius: 0 8px 8px 0; transition: opacity .15s; }
        .nf-range-row.is-disabled { opacity: .4; pointer-events: none; }
        .nf-range-head { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
        .nf-range-right { display: flex; align-items: center; gap: 6px; }
        .nf-range-value { font-size: 12px; font-weight: 700; color: var(--nf-accent); font-variant-numeric: tabular-nums; }
        .nf-range-reset { width: 24px; height: 24px; padding: 0; border: 0; border-radius: 3px; background: transparent; color: var(--nf-muted); font-size: 12px; cursor: pointer; transition: background .12s, color .12s; }
        .nf-range-reset:hover { background: rgba(255,255,255,.08); color: #fff; }
        .nf-setting-range { display: block; width: 100%; margin: 8px 0 0; accent-color: var(--nf-accent); cursor: pointer; }
        .nf-setting-range:focus-visible, .nf-range-reset:focus-visible { outline: 2px solid var(--nf-accent); outline-offset: 2px; }
        .nf-data-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; padding: 0 8px; }
        .nf-btn { -webkit-appearance: none; appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 8px; padding: 9px 14px; border: none; border-radius: 3px; background: #2a2a2a; color: var(--nf-text); font: inherit; font-size: 13px; font-weight: 600; cursor: pointer; transition: background .12s, border-color .12s; }
        .nf-btn:hover:not(:disabled) { background: #343434; border-color: rgba(255,255,255,.2); }
        .nf-btn:disabled { opacity: .6; cursor: default; }
        .nf-btn-primary { background: var(--nf-accent); border-color: var(--nf-accent); color: #fff; }
        .nf-btn-primary:hover:not(:disabled) { background: var(--nf-accent-hover); border-color: var(--nf-accent-hover); }
        .nf-modal-footer { flex: none; display: flex; justify-content: space-between; align-items: center; gap: 12px; padding: 14px 20px; background: var(--nf-bg-2); }
        .nf-footer-hint { font-size: 12px; color: var(--nf-muted); }
        .nf-footer-actions { display: flex; gap: 8px; }
        @media (max-width: 520px) {
            .nf-modal-overlay { padding: 10px; }
            .nf-data-grid { grid-template-columns: 1fr; }
            .nf-footer-hint { display: none; }
            .nf-modal-footer { justify-content: flex-end; }
        }
        @media (prefers-reduced-motion: reduce) {
            .nf-modal-overlay, .nf-modal, .nf-setting-checkbox, .nf-setting-checkbox::after { transition: none; }
        }

        /* --- GALLERY PAGE BTN --- */
        @media (min-width: 1300px) { .btn-group { width: 580px; } }
        .btn-queue-add { background: #5b26ed !important; }
        .btn-queue-add:hover { background: #7f55f1 !important; }
        .tag-container .tagchip.tag-selected .name { background: #3d643f !important; opacity: 1 !important; }
        .tags-selecting-mode .tagchip:not(.tag-selected) { opacity: .5; }
        body:not(.f-queue) .btn-queue-add, body:not(.f-queue) .btn-next-queue { display: none !important; }
        body:not(.f-tag-select) .btn-tag-selector { display: none !important; }
        body.tag-select-active #info > :not(#tags):not(.buttons), body.tag-select-active #cover { display: none !important; }
        body.tag-select-active #info-block, body.tag-select-active #info {width: 100% !important; max-width: 100% !important; margin: 0 !important; padding: 0 !important; }
        .comments-scroll-btn { color: #d9d9d9; position: fixed; bottom: 20px; left: 20px; display: flex; padding: .7em; border-radius: 5px; min-width: 40px; min-height: 40px; background: #1a1a1a; cursor: pointer; align-items: center; justify-content: center; pointer-events: auto;}
        .comments-count, .queue-count { position: absolute; top: -4px; right: -4px; background: #ed2553; font-size: 10px; font-weight: bold; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; padding: 0 4px; user-select: none; box-shadow: 0 2px 4px rgba(0,0,0,0.5); }

        /* --- QUEUE --- */
        .queue-dock { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; align-items: flex-end; z-index: 160; pointer-events: none; }
        body:not(.f-queue) .queue-dock { display: none !important; }
        .queue-toggle-btn { color: #d9d9d9; padding: .7em; border-radius: 5px; min-width: 40px; min-height: 40px; background: #1a1a1a; font-size: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; position: relative; pointer-events: auto; }
        .queue-toggle-btn:hover { background: #333; }
        .queue-toggle-btn.active { background: #2e2e2e; }
        .queue-count { background: #5b26ed; }
        .queue-panel { color: #d9d9d9; background: #1f1f1f; max-width: 95vw; width: 350px; border-radius: 5px; overflow: hidden; margin-bottom: 10px; box-shadow: 0 0 5px rgba(0,0,0,.5); display: block; visibility: hidden; opacity: 0; transform: translateX(100%); transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s; pointer-events: auto; }
        .queue-panel.is-visible { visibility: visible; opacity: 1; transform: translateX(0); }
        .queue-header { justify-content: end !important; }
        .queue-header, .queue-dock-pagination { padding: 10px; background: #383838; display: flex; user-select: none; justify-content: space-between; align-items: center; font-weight: bold; }
        .queue-header-actions .is-hidden { display: none !important; }
        .queue-btn-action, .queue-dock-btn { cursor: pointer; user-select: none; font-size: 12px; color: #fff !important; background: #4d4d4d; padding: 4px 10px; border-radius: 3px; }
        .queue-dock-btn { border: none; padding: 4px 18px; }
        .queue-btn-action:hover, .queue-dock-btn:hover:not(:disabled) { background: #595959; }
        .queue-done { background: #ed2553; }
        .queue-done:hover { background: #f15478; }
        .queue-dock-btn:disabled { opacity: 0.3; cursor: not-allowed; }
        .queue-list { display: grid; grid-template-columns: repeat(4, 1fr); scrollbar-width: none; touch-action: pan-y; touch-action: pan-x; gap: 6px; padding: 8px; max-height: 430px; overflow-y: auto; overscroll-behavior: contain; margin: 0; list-style: none; content-visibility: auto; contain-intrinsic-size: 430px; }
        .queue-item { border-radius: .3em; background: #404040; animation: queue-fade-in 0.25s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
        .queue-item:hover { background: #3d643f; }
        .queue-image-wrapper { position: relative; display: block; border-radius: .3em .3em 0 0; overflow: hidden; }
        .queue-item img { width: 100%; aspect-ratio: 100/141; object-fit: cover; display: block; content-visibility: auto; contain-intrinsic-size: 111.38px; }
        .queue-id { color: #d9d9d9; font-size: 11px; padding: 3px; }
        .queue-remove { position: absolute; top: 0; right: 0; background: rgba(31, 31, 31, .5); width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 10; font-size: 24px; }
        .queue-list:not(.is-editing) .queue-remove { display: none; }
        .queue-list.is-editing .queue-item { background: #404040 !important; }
        .queue-list.is-editing .queue-item:hover { background: rgba(89, 38, 38, .9) !important; }
        .queue-list.is-editing .queue-item img { opacity: 0.6; filter: grayscale(40%); }
        .queue-list.is-editing .cover-link { pointer-events: none; }
        .queue-list.is-editing .queue-item.to-delete, .queue-list.is-editing .queue-item.to-delete:hover { background: #592626 !important; }
        .queue-list.is-editing .queue-item.to-delete .queue-remove { background: rgba(37, 24, 24, .65); }
        .queue-list.is-editing .queue-item.to-delete .queue-id { text-decoration: line-through; }
        .queue-item:not(.to-delete) .queue-remove .fa-undo, .queue-item.to-delete .queue-remove .fa-times { display: none; }
        @keyframes queue-fade-in {
            0% { transform: translateY(20px); }
            100% { transform: translateY(0); }
        }
        @media (max-width: 1200px) {
        .btn-q-tool, .q-discover, .q-sort-label { font-size: 16px; }
        .q-discover { min-width: 121px !important }
        .queue-dock { bottom: 0; right: 0; }
        .queue-toggle-btn { font-size: 20px;; border-radius: 5px 0 0 0; min-width: 51px; }
        .comments-scroll-btn { bottom: 0; left: 0; font-size: 20px;; border-radius: 0 5px 0 0; min-width: 51px; }
        .queue-count { right: 34px; }
        .queue-toggle-btn:hover, .queue-toggle-btn.active { background: #1a1a1a; }
        .queue-panel { margin-right: 10px; }
        .queue-list.is-editing .queue-item:hover { background: #404040 !important; }
        @keyframes queue-fade-in {
            0% { transform: translateX(10px); }
            100% { transform: translateX(0); }
        }
        }

        /* --- QUEUE PAGE --- */
        .queue-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; margin-top: -15px; margin-bottom: -4px; }
        .queue-group { display: flex; margin: 10px; flex-wrap: wrap;}
        .queue-group.segmented { border-radius: 5px; overflow: hidden; }
        .queue-group.segmented .btn-q-tool { border-radius: 0; }
        .queue-group.segmented .btn-q-tool + .btn-q-tool { border-left: 1px solid #0d0d0d; }
        .queue-group.segmented .q-sort-label + .btn-q-tool { border-left: 1px solid #0d0d0d; }
        .btn-q-tool, .q-sort-label, .q-discover { background: #1a1a1a; color: #d9d9d9; border: none; border-radius: 5px; padding: .5em; font-size: 20px; line-height: 1.42857143; cursor: pointer; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; }
        .btn-q-tool::after { content: attr(data-text); font-weight: bold; height: 0; visibility: hidden; overflow: hidden; user-select: none; pointer-events: none; }
        .btn-q-tool.active { background: #2e2e2e; font-weight: bold; }
        .q-sort-label { border-radius: 0; cursor: default; }
        .q-discover {background: linear-gradient(45deg, #5b26ed, #ed2553); min-width: 151px; }
        .btn-q-tool:hover { background: #333; }

        /* --- SAVED SEARCHES STYLES  --- */
        .search-saved-trigger { position: absolute; left: -5px; top: 0; height: 97%; width: 32px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: #888; z-index: 5; }
        .search-saved-trigger:hover, .search-saved-trigger.is-active { color: #999; }
        .saved-search-extension { position: relative; width: 100%; background: #1f1f1f; box-sizing: border-box; z-index: 5; display: block; overflow: hidden; max-height: 0; contain: layout paint; }
        .saved-search-extension.is-visible { max-height: 500px; }
        .sse-inner { padding: 0 20px; opacity: 0; transform: translateY(-5%); transition: opacity 0.3s ease, transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
        .saved-search-extension.is-visible .sse-inner { opacity: 1; transform: translateY(0); }
        .sse-header { display: flex; align-items: center; margin-bottom: 12px; border-bottom: 1px solid #2e2e2e; padding: 8px 0; flex-wrap: wrap; }
        .sse-actions { margin-left: auto; }
        .btn-sse-save, .btn-sse-edit { background: #4d4d4d; color: #fff; border: none; padding: 0 12px; border-radius: 3px; line-height: 40px; cursor: pointer; font-weight: bold; }
        .btn-sse-save:hover, .btn-sse-edit:hover { background: #595959; }
        #btn-confirm-del, #btn-finish-reorder { background: #ed2553; }
        #btn-confirm-del:hover, #btn-finish-reorder:hover { background: #f15478; }
        .sse-empty { color: #999; font-style: italic; }
        .sse-list { display: flex; flex-wrap: wrap; gap: 8px; padding-bottom: 20px; padding-right: 5px; overflow-y: auto; overscroll-behavior: contain; max-height: 40vh; }
        .ss-pill { font-weight: 700; display: inline-flex; align-items: stretch; border-radius: .3em; overflow: hidden; user-select: none; border-left: 10px solid var(--ss-category-color, #888); }
        .ss-pill[data-category="tag"] { --ss-category-color: hsl(0, 0%, 22%); }
        .ss-pill[data-category="artist"] { --ss-category-color: #592673; }
        .ss-pill[data-category="parody"] { --ss-category-color: #20604a; }
        .ss-pill[data-category="character"] { --ss-category-color: #265973; }
        .ss-pill[data-category="group"] { --ss-category-color: #735926; }
        .ss-pill[data-category="language"] { --ss-category-color: #364563; }
        .ss-pill[data-category="category"] { --ss-category-color: #862d2d; }
        .ss-part { padding: .13em .39em; cursor: pointer; display: flex; align-items: center; background: #4d4d4d }
        .ss-add { padding: .13em .69em; justify-content: center; background: #333; color: grey; box-shadow: inset 0 0 .4em #2b2b2b; min-width: 30px; }
        .ss-add:hover { background: #404040; color: #fff; }
        .ss-text:hover { background: #595959; color: #fff; }
        .ss-add.ss-add-positive { background: #6c6; color: #fff; }
        .ss-add.ss-add-positive:hover { background: #8cd98c; }
        .ss-add.ss-add-negative { background: #643d3d; color: #fff;}
        .ss-add.ss-add-negative:hover { background: #855151;}
        .sse-list.delete-mode .ss-add, .sse-list.reorder-mode .ss-add { display: none }
        .ss-pill.to-delete .ss-text, .ss-separator.to-delete { background: #251818; opacity: .7; text-decoration: line-through; }
        .sse-list.reorder-mode .ss-pill { cursor: grab; }
        .sse-list.reorder-mode .ss-pill:active, .sse-list.reorder-mode .ss-separator:active { cursor: grabbing; }
        .sse-list.reorder-mode .ss-text { pointer-events: none; }
        .ss-pill.is-dragging, .ss-separator.is-dragging { opacity: 0.4; transform: scale(0.90); transition: transform 0.2s; }
        .ss-pill.is-pressing, .ss-separator.is-pressing { transform: scale(0.95); transition: transform 0.2s; opacity: 0.8; }
        .ss-pill.drag-over { box-shadow: 5px 5px #ed2553; }
        .ss-separator { flex-basis: 100%; height: 2px; background: #2e2e2e; margin: 4px 0; position: relative; }
        .sse-list.reorder-mode .ss-separator { cursor: grab; height: 12px; background: transparent; border: 1px dashed #555; }
        .ss-separator.drag-over { border: 2px solid #ed2553 !important; }
        .sse-list.delete-mode .ss-separator { cursor: pointer; height: 8px; }
        body:not(.f-saved-search) .search-saved-trigger,
        body:not(.f-saved-search) .saved-search-extension { display: none !important; }
        body:has(.saved-search-extension.is-visible) .ac-dropdown { display: none !important; }
        .search input { padding-left: 2em; }
        @media (max-width: 600px) { .sse-list { max-height: 410px; } }

        /* --- QUEUE VIEW (own container, Svelte keeps owning #content) --- */
        body.nf-queue-view #content { display: none !important; }
        #nf-queue-root { padding-bottom: 200px !important; }

        /* --- QUEUE PAGINATION FALLBACK (used only until the site's own pagination is captured) --- */
        .nf-pag-fallback { display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 4px; margin: 20px 0; }
        .nf-pag-fallback a { display: inline-block; min-width: 34px; padding: 8px 10px; text-align: center; border-radius: 3px; background: #1f1f1f; color: #d9d9d9; text-decoration: none; font-weight: bold; }
        .nf-pag-fallback a:hover { background: #383838; }
        .nf-pag-fallback a.current { background: #ed2553; color: #fff; }

        /* --- SCROLL PROGRESS BAR (smart navigation) --- */
        #nh-scroll-progress-wrap { position: fixed; left: 0; bottom: 0; width: 100%; height: 4px; background: transparent; z-index: 2147483647; pointer-events: none; opacity: 0; transition: opacity 0.2s ease; }
        #nh-scroll-progress-wrap.active { opacity: 1; }
        #nh-scroll-progress-bar { height: 100%; width: 0%; background: #ed2553; box-shadow: 0 0 8px #ed2553; border-radius: 0 2px 2px 0; }
        @media (pointer: coarse) { #nh-scroll-progress-wrap { height: 6px; bottom: env(safe-area-inset-bottom, 0px); } }

        /* --- SEARCH SHORTCUT HINT --- */
        .search-slash-hint { position: absolute; left: 30px; top: 50%; transform: translateY(-50%); color: #999; font-size: 12px; pointer-events: none; font-family: Consolas, monospace; }
        form.search input:focus ~ .search-slash-hint, form.search input:not(:placeholder-shown) ~ .search-slash-hint { opacity: 0; }
        form.search { position: relative; }
        @media (max-width: 1000px) { .search-slash-hint { opacity: 0 } }

    `;

    (typeof GM_addStyle !== "undefined") ? GM_addStyle(css) : document.head.appendChild(Object.assign(document.createElement('style'), { textContent: css }));

    // ==========================================================================
    // 2. QUEUE LOGIC
    // ==========================================================================

    function saveQueue() {
        updateQueueWidget();
        updateAllQueueButtons();

        clearTimeout(saveQueueTimeout);
        saveQueueTimeout = setTimeout(() => {
            localStorage.setItem('nhentai_queue_v1', JSON.stringify(readingQueue));
        }, 500);
    }

    function toggleQueueItem(id, title, coverUrl, galleryUrl) {
        const index = readingQueue.findIndex(i => i.id == id);

        if (index > -1) {
            readingQueue.splice(index, 1);
        } else {
            const newItem = { id, title, coverUrl, galleryUrl, addedAt: Date.now() };

            readingQueue.push(newItem);

            if (currentSortMode === 'newest') {
                readingQueue.sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0));
            } else if (currentSortMode === 'oldest') {
                readingQueue.sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
            }
        }

        saveQueue();
    }

    function isQueued(id) {
        return readingQueue.some(i => i.id == id);
    }

    function clearQueue() {
        if(confirm('Clear reading queue?')) {
            readingQueue = [];
            saveQueue();
            if (window.location.search.includes('view=queue')) renderQueuePage();
        }
    }

    function updateAllQueueButtons() {
        if (!settings.enableQueue) return;

        document.querySelectorAll(`${SEL.gallery}[data-gid]`).forEach(gallery => {
            const id = gallery.dataset.gid;
            const btn = gallery.querySelector('.queue-trigger');
            if (btn) {
                if (isQueued(id)) {
                    btn.classList.add('in-queue');
                    btn.innerHTML = '<i class="fa fa-check"></i>';
                } else {
                    btn.classList.remove('in-queue');
                    btn.innerHTML = '<i class="fa fa-plus"></i>';
                }
            }

            if (gallery.updateMarkersFn) {
                gallery.updateMarkersFn();
            }
        });

        const pageBtn = document.querySelector('.btn-queue-add');
        if (pageBtn) {
            const id = window.location.href.match(/\/g\/(\d+)/)?.[1];
            if (id && isQueued(id)) {
                pageBtn.innerHTML = '<i class="far fa-clock"></i> Remove';
                pageBtn.classList.add('in-queue');
            } else {
                pageBtn.innerHTML = '<i class="fa fa-clock"></i> Queue';
                pageBtn.classList.remove('in-queue');
            }
        }
    }

    function updateQueueWidget() {
        if (!settings.enableQueue) return;

        const list = document.querySelector('.queue-list');
        const count = document.querySelector('.queue-count');
        const paginationContainer = document.querySelector('.queue-dock-pagination');
        if (!list || !count) return;
        const queuedIds = new Set(readingQueue.map(item => String(item.id)));
        for (const id of pendingQueueDeletes) {
            if (!queuedIds.has(id)) pendingQueueDeletes.delete(id);
        }

        const doneBtn = document.querySelector('.queue-done');
        if (doneBtn) {
            doneBtn.classList.toggle(
                'is-hidden',
                !list.classList.contains('is-editing') || pendingQueueDeletes.size === 0
            );
        }

        const totalItems = readingQueue.length;
        count.textContent = totalItems;
        count.style.display = totalItems > 0 ? 'flex' : 'none';

        if (totalItems === 0) {
            list.innerHTML = '<li style="grid-column: 1 / -1; padding: 20px; color: #999; font-size: 12px; font-style: italic;">Queue is empty.<p>Looking for inspiration?</p></li>';
            if (paginationContainer) paginationContainer.style.display = 'none';
            return;
        }

        const totalPages = Math.ceil(totalItems / DOCK_ITEMS_PER_PAGE);

        if (dockCurrentPage > totalPages) dockCurrentPage = Math.max(1, totalPages);

        const startIdx = (dockCurrentPage - 1) * DOCK_ITEMS_PER_PAGE;
        const currentItems = readingQueue.slice(startIdx, startIdx + DOCK_ITEMS_PER_PAGE);

        list.innerHTML = currentItems.map(item => `
            <li class="queue-item${pendingQueueDeletes.has(String(item.id)) ? ' to-delete' : ''}" data-id="${item.id}">
              <div class="queue-image-wrapper">
                <div class="queue-remove" data-id="${item.id}">
                    <i class="fa fa-times"></i>
                    <i class="fa fa-undo" style="color: #facc15;"></i>
                </div>
                <a href="${item.galleryUrl}" class="cover-link">
                    <img src="${item.coverUrl}" loading="lazy">
                </a>
              </div>
                <a title="${item.title}">
                    <div class="queue-id">#${item.id}</div>
                </a>
            </li>
        `).join('');

        if (paginationContainer) {
            if (totalPages > 1) {
                paginationContainer.style.display = 'flex';
                paginationContainer.innerHTML = `
                    <button class="queue-dock-btn queue-dock-prev" ${dockCurrentPage === 1 ? 'disabled' : ''}><i class="fa fa-chevron-left"></i></button>
                    <span class="queue-page-info">${dockCurrentPage} / ${totalPages}</span>
                    <button class="queue-dock-btn queue-dock-next" ${dockCurrentPage === totalPages ? 'disabled' : ''}><i class="fa fa-chevron-right"></i></button>
                `;
            } else {
                paginationContainer.style.display = 'none';
            }
        }
    }

    function initQueueWidget() {
        if (!settings.enableQueue) return;
        const queueDock = document.querySelector('.queue-dock');
        if (queueDock) {
            queueDock.style.display = isReader ? 'none' : 'flex';}
        if (document.querySelector('.queue-dock')) return;

        const dock = document.createElement('div');
        dock.className = 'queue-dock';

        dock.innerHTML = `
            <div class="queue-panel">
                <div class="queue-header">
                    <div class="queue-header-actions">
                        <span class="queue-btn-action queue-edit-mode">Delete Mode</span>
                        <a href="/?view=queue" class="queue-btn-action queue-view-full">View Page</a>
                        <span class="queue-btn-action queue-done is-hidden">Save Changes</span>
                        <span class="queue-btn-action queue-cancel is-hidden">Cancel</span>
                        <span class="queue-btn-action queue-clear is-hidden">Select All</span>
                    </div>
                </div>
                <ul class="queue-list"></ul>
                <div class="queue-dock-pagination" style="display: none;"></div>
            </div>
            <div class="queue-toggle-btn" title="Toggle Queue">
                <i class="fa fa-clock"></i>
                <div class="queue-count">0</div>
            </div>
        `;

        document.body.appendChild(dock);

        const toggle = dock.querySelector('.queue-toggle-btn');
        const panel = dock.querySelector('.queue-panel');
        const list = dock.querySelector('.queue-list');
        const paginationContainer = dock.querySelector('.queue-dock-pagination');

        const viewBtn = dock.querySelector('.queue-view-full');
        const editBtn = dock.querySelector('.queue-edit-mode');
        const clearBtn = dock.querySelector('.queue-clear');
        const doneBtn = dock.querySelector('.queue-done');
        const cancelBtn = dock.querySelector('.queue-cancel');

        let isEditingQueue = false;
        const toggleEditMode = (forceState = null) => {
            isEditingQueue = forceState !== null ? forceState : !isEditingQueue;
            pendingQueueDeletes.clear();

            list.classList.toggle('is-editing', isEditingQueue);
            editBtn.classList.toggle('is-hidden', isEditingQueue);
            viewBtn.classList.toggle('is-hidden', isEditingQueue);
            clearBtn.classList.toggle('is-hidden', !isEditingQueue);
            cancelBtn.classList.toggle('is-hidden', !isEditingQueue);
            doneBtn.classList.add('is-hidden');

            list.querySelectorAll('.queue-item.to-delete').forEach(item => {
                item.classList.remove('to-delete');
            });
        };

        editBtn.onclick = () => toggleEditMode(true);
        cancelBtn.onclick = () => toggleEditMode(false);

        doneBtn.onclick = () => {
            if (!isEditingQueue || pendingQueueDeletes.size === 0) return;

            readingQueue = readingQueue.filter(item =>
                                               !pendingQueueDeletes.has(String(item.id))
                                              );

            toggleEditMode(false);
            saveQueue();
            if (isQueueView()) renderQueuePage();
        };

        clearBtn.onclick = () => {
            if (!isEditingQueue) return;

            readingQueue.forEach(item => pendingQueueDeletes.add(String(item.id)));
            updateQueueWidget();
        };

        list.addEventListener('click', (e) => {
            if (!isEditingQueue) return;

            const itemEl = e.target.closest('.queue-item');
            if (!itemEl) return;

            e.preventDefault();
            e.stopPropagation();

            const id = itemEl.dataset.id;
            if (pendingQueueDeletes.has(id)) pendingQueueDeletes.delete(id);
            else pendingQueueDeletes.add(id);

            itemEl.classList.toggle('to-delete', pendingQueueDeletes.has(id));
            doneBtn.classList.toggle('is-hidden', pendingQueueDeletes.size === 0);
        });

        paginationContainer.addEventListener('click', (e) => {
            if (e.target.closest('.queue-dock-prev')) {
                if (dockCurrentPage > 1) {
                    dockCurrentPage--;
                    updateQueueWidget();
                    list.scrollTop = 0;
                }
            } else if (e.target.closest('.queue-dock-next')) {
                const totalPages = Math.ceil(readingQueue.length / DOCK_ITEMS_PER_PAGE);
                if (dockCurrentPage < totalPages) {
                    dockCurrentPage++;
                    updateQueueWidget();
                    list.scrollTop = 0;
                }
            }
        });

        let isScrollOnCooldown = false;

        list.addEventListener('wheel', (e) => {
            e.preventDefault();

            if (isScrollOnCooldown) return;

            const totalPages = Math.ceil(readingQueue.length / DOCK_ITEMS_PER_PAGE);
            if (totalPages <= 1) return;

            isScrollOnCooldown = true;

            if (e.deltaY > 0) {
                if (dockCurrentPage < totalPages) {
                    dockCurrentPage++;
                    updateQueueWidget();
                }
            } else if (e.deltaY < 0) {
                if (dockCurrentPage > 1) {
                    dockCurrentPage--;
                    updateQueueWidget();
                }
            }
            setTimeout(() => { isScrollOnCooldown = false; }, 150);

        }, { passive: false });

        let touchStartX = 0;
        let touchStartY = 0;

        list.addEventListener('touchstart', (e) => {
            touchStartX = e.changedTouches[0].screenX;
            touchStartY = e.changedTouches[0].screenY;
        }, { passive: true });

        list.addEventListener('touchend', (e) => {
            const touchEndX = e.changedTouches[0].screenX;
            const touchEndY = e.changedTouches[0].screenY;

            const deltaX = touchEndX - touchStartX;
            const deltaY = touchEndY - touchStartY;

            if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 40) {
                const totalPages = Math.ceil(readingQueue.length / DOCK_ITEMS_PER_PAGE);

                if (deltaX < 0) {
                    if (dockCurrentPage < totalPages) {
                        dockCurrentPage++;
                        updateQueueWidget();
                    }
                } else {
                    if (dockCurrentPage > 1) {
                        dockCurrentPage--;
                        updateQueueWidget();
                    }
                }
            }
        }, { passive: true });

        window.addEventListener('storage', (e) => {
            if (e.key === 'nhentai_queue_v1' && e.newValue) {
                readingQueue = JSON.parse(e.newValue);
                updateQueueWidget();
                updateAllQueueButtons();
            }
        });

        toggle.onclick = (e) => {
            const isVisible = panel.classList.toggle('is-visible');
            toggle.classList.toggle('active', isVisible);

            if (!isVisible) toggleEditMode(false);

            if (isVisible) {
                const currentId = window.location.pathname.match(/\/g\/(\d+)/)?.[1];

                if (currentId) {
                    const itemIndex = readingQueue.findIndex(item => item.id === currentId);

                    if (itemIndex > -1) {
                        const targetPage = Math.floor(itemIndex / DOCK_ITEMS_PER_PAGE) + 1;

                        if (dockCurrentPage !== targetPage) {
                            dockCurrentPage = targetPage;
                            updateQueueWidget();
                        }
                    }
                }
            }
        };

        updateQueueWidget();
    }

    const PAGTPL_KEY = 'nhentai_flow_pagtpl';
    let pagTpl = null;
    pagTpl = readJSON(PAGTPL_KEY, null);

    const PAG_ICON = {
        first: '<i class="fa fa-chevron-left"></i><i class="fa fa-chevron-left"></i>',
        previous: '<i class="fa fa-chevron-left"></i>',
        next: '<i class="fa fa-chevron-right"></i>',
        last: '<i class="fa fa-chevron-right"></i><i class="fa fa-chevron-right"></i>'
    };

    function pagSig() {
        return pagTpl && pagTpl.sections && pagTpl.sections.length
            ? pagTpl.sections.map(x => x.tag + '.' + x.className).join('|')
        : 'fallback';
    }

    function captureNativePagination() {
        const secs = Array.from(document.querySelectorAll(SEL.pagination)).filter(el => !el.closest('#nf-queue-root'));
        if (!secs.length) return;

        const tpl = { sections: [], roles: Object.assign({}, pagTpl && pagTpl.roles) };
        secs.forEach(sec => {
            tpl.sections.push({ tag: sec.tagName.toLowerCase(), className: sec.className });
            sec.querySelectorAll('a, span, button').forEach(el => {
                const c = el.classList;
                let role = null;
                if (c.contains('first')) role = 'first';
                else if (c.contains('previous')) role = 'previous';
                else if (c.contains('next')) role = 'next';
                else if (c.contains('last')) role = 'last';
                else if (c.contains('page')) role = c.contains('current') ? 'current' : 'page';
                if (!role) return;
                let cls = String(el.className);
                if (role === 'page') cls = cls.split(/\s+/).filter(t => t && t !== 'current').join(' ');
                tpl.roles[role] = {
                    tag: el.tagName.toLowerCase(),
                    cls,
                    html: (role === 'page' || role === 'current') ? null : el.innerHTML
                };
            });
        });

        const before = JSON.stringify(pagTpl);
        const after = JSON.stringify(tpl);
        if (before !== after) {
            pagTpl = tpl;
            try { localStorage.setItem(PAGTPL_KEY, after); } catch (e) { /* ignore */ }
        }
    }

    function pagRole(role) {
        const R = (pagTpl && pagTpl.roles) || {};
        if (R[role]) return { tag: R[role].tag, cls: R[role].cls, html: R[role].html };

        const strip = (cls, tok) => cls.split(/\s+/).filter(t => t && t !== tok).join(' ');
        const swap = (cls, from, to) => cls.split(/\s+/).map(t => (t === from ? to : t)).join(' ');
        if (role === 'current' && R.page) return { tag: R.page.tag, cls: R.page.cls + ' current', html: null };
        if (role === 'page' && R.current) return { tag: R.current.tag, cls: strip(R.current.cls, 'current'), html: null };

        const sources = { first: ['previous', 'last', 'next'], previous: ['next', 'first', 'last'], next: ['previous', 'last', 'first'], last: ['next', 'first', 'previous'] };
        for (const src of (sources[role] || [])) {
            if (R[src]) return { tag: R[src].tag, cls: swap(R[src].cls, src, role), html: null };
        }
        return { tag: 'a', cls: role === 'current' ? 'page current' : role, html: null };
    }

    function createPagination(totalItems, currentPage, itemsPerPage) {
        const totalPages = Math.ceil(totalItems / itemsPerPage);
        if (totalPages <= 1) return null;

        const hasTpl = !!(pagTpl && pagTpl.sections && pagTpl.sections.length);
        const defs = hasTpl
        ? pagTpl.sections
        : [{ tag: 'section', className: 'pagination desktop-pagination nf-pag-fallback' }];
        const frag = document.createDocumentFragment();
        const href = (p) => `/?view=queue&page=${p}`;

        defs.forEach(def => {
            const sec = document.createElement(def.tag);
            sec.className = def.className;

            const add = (role, p, label) => {
                const d = pagRole(role);
                const el = document.createElement(d.tag);
                el.className = d.cls;
                if (d.tag === 'a') el.href = href(p);
                el.innerHTML = d.html != null ? d.html : (PAG_ICON[role] || label);
                sec.appendChild(el);
            };

            if (currentPage > 1) {
                add('first', 1);
                add('previous', currentPage - 1);
            }
            for (let i = 1; i <= totalPages; i++) {
                if (i === 1 || (i >= currentPage - 5 && i <= currentPage + 5)) {
                    add(i === currentPage ? 'current' : 'page', i, String(i));
                }
            }
            if (currentPage < totalPages) {
                add('next', currentPage + 1);
                add('last', totalPages);
            }
            frag.appendChild(sec);
        });

        return frag;
    }

    function applyQueueSort(mode) {
        currentSortMode = mode;
        localStorage.setItem('nhentai_queue_sort', mode);

        if (mode === 'shuffle') {
            for (let i = readingQueue.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [readingQueue[i], readingQueue[j]] = [readingQueue[j], readingQueue[i]];
            }
        } else if (mode === 'newest') {
            readingQueue.sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0));
        } else if (mode === 'oldest') {
            readingQueue.sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
        }

        saveQueue();
        renderQueuePage();
    }

    function renderQueuePage() {
        const params = new URLSearchParams(window.location.search);
        if (params.get('view') !== 'queue') return;

        document.title = "Reading Queue";
        const content = getQueueRoot(true);
        if (!content) return;
        content.innerHTML = '';
        content.dataset.rendered = window.location.search;
        content.dataset.pagSig = pagSig();

        const ITEMS_PER_PAGE = 25;
        const currentPage = parseInt(params.get('page')) || 1;
        const totalItems = readingQueue.length;

        const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
        const endIndex = startIndex + ITEMS_PER_PAGE;
        const currentItems = readingQueue.slice(startIndex, endIndex);

        const header = document.createElement('div');
        header.innerHTML = `
            <h1>
                <span>
                <i class="fa fa-clock color-icon"></i> Reading queue
                </span>
                <span class="count">(${totalItems})</span>
            </h1>
        `;
        content.appendChild(header);

        if (totalItems >= 0) {
            const toolbar = document.createElement('div');
            toolbar.className = 'queue-toolbar';

            const leftGroup = document.createElement('div');
            leftGroup.className = 'queue-group';
            leftGroup.className = 'queue-group segmented';

            const rightGroup = document.createElement('div');
            rightGroup.className = 'queue-group right';

            const buttons = [
                { id: 'newest', label: 'newest' },
                { id: 'oldest', label: 'oldest' },
                { id: 'shuffle', label: 'shuffle' }
            ];

            if (totalItems > 1) {
                const sortLabel = document.createElement('span');
                sortLabel.className = 'q-sort-label';
                sortLabel.textContent = 'Sort:';
                leftGroup.appendChild(sortLabel);

                buttons.forEach(btn => {
                    const buttonEl = document.createElement('button');
                    const isActive = currentSortMode === btn.id ? 'active' : '';
                    buttonEl.className = `btn-q-tool ${isActive}`;
                    buttonEl.setAttribute('data-text', btn.label);
                    buttonEl.innerHTML = `${btn.label}`;
                    buttonEl.onclick = () => applyQueueSort(btn.id);
                    leftGroup.appendChild(buttonEl);
                });
            }

            const discoverBtn = document.createElement('button');
            discoverBtn.className = 'q-discover';
            discoverBtn.innerHTML = 'Discover & Fill';
            discoverBtn.title = "Let the script pick new galleries for your queue based on your favorites.";

            discoverBtn.onclick = async (e) => {
                const originalText = 'Discover & Fill';
                const allButtons = toolbar.querySelectorAll('button');

                allButtons.forEach(btn => {
                    btn.disabled = true;
                    btn.style.opacity = '0.5';
                    btn.style.cursor = 'not-allowed';
                });

                discoverBtn.style.opacity = '1';

                try {
                    await runDiscoverFlow((status) => {
                        discoverBtn.innerHTML = `${status}`;
                    });

                    discoverBtn.innerHTML = 'Completed';
                    discoverBtn.style.background = '#3c763d';

                    setTimeout(() => {
                        renderQueuePage()
                    }, 1000);

                } catch (err) {
                    console.error(err);
                    discoverBtn.innerHTML = /Not logged in/.test(err && err.message) ? 'Login required' : 'Error: ' + String((err && err.message) || 'unknown').slice(0, 40);
                    discoverBtn.style.background = '#a94442';

                    setTimeout(() => {
                        discoverBtn.innerHTML = originalText;
                        discoverBtn.style.background = 'linear-gradient(45deg, #5b26ed, #ed2553)';

                        allButtons.forEach(btn => {
                            btn.disabled = false;
                            btn.style.opacity = '1';
                            btn.style.cursor = 'pointer';
                        });
                    }, 3000);
                }
            };

            rightGroup.appendChild(discoverBtn);
            toolbar.appendChild(leftGroup);
            toolbar.appendChild(rightGroup);
            content.appendChild(toolbar);
        }

        const grid = document.createElement('div');
        grid.className = SITE_CLASS.grid;

        if (totalItems === 0) {
            grid.innerHTML = '<div style="padding: 50px; text-align: center; font-size: 18px; color: #666;">Your queue is empty.<p>Time to find some inspiration for the task ahead.</p></div>';
        } else {
            const fragment = document.createDocumentFragment();

            currentItems.forEach(item => {
                const galleryDiv = document.createElement('div');
                galleryDiv.className = 'gallery';
                galleryDiv.setAttribute('data-gid', item.id);

                const defaultPadding = '145.6%';

                galleryDiv.innerHTML = `
                    <a href="${item.galleryUrl}" class="cover" style="padding:0 0 ${defaultPadding} 0; position: relative; display: block;">
                        <img class="lazyload"
                             src="${item.coverUrl}"
                             data-src="${item.coverUrl}"
                             loading="lazy"
                             style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: cover;" />
                        <div class="caption">${item.title}</div>
                    </a>
                `;

                const img = galleryDiv.querySelector('img');
                const setRatio = function() {
                    if (this.naturalWidth && this.naturalHeight) {
                        const aspectRatio = (this.naturalHeight / this.naturalWidth) * 100;
                        const coverLink = galleryDiv.querySelector('.cover');
                        coverLink.style.padding = `0 0 ${aspectRatio}% 0`;
                        this.onload = null;
                    }
                };

                img.onload = setRatio;
                if (img.complete) setRatio.call(img);

                fragment.appendChild(galleryDiv);
            });

            grid.appendChild(fragment);
        }

        content.appendChild(grid);

        if (totalItems > ITEMS_PER_PAGE) {
            const pagination = createPagination(totalItems, currentPage, ITEMS_PER_PAGE);
            if (pagination) content.appendChild(pagination);
        }

        grid.querySelectorAll(SEL.gallery).forEach(initPreviewUI);
    }

    function areTitlesSimilar(title1, title2) {
        const tokenize = (str) => {
            return new Set(
                str.toLowerCase()
                .replace(/[([{].*?[)\]}]/g, '')
                .replace(/[^\w\s]|_/g, "")
                .split(/\s+/)
                .filter(w => w.length > 2)
            );
        };

        const t1 = tokenize(title1);
        const t2 = tokenize(title2);

        if (t1.size < 2 || t2.size < 2) return false;

        let matches = 0;
        t1.forEach(word => {
            if (t2.has(word)) matches++;
        });

        const minSize = Math.min(t1.size, t2.size);
        const similarity = matches / minSize;

        return similarity >= 0.75;
    }

    async function runDiscoverFlow(updateStatus) {
        const IGNORE_TAGS = [
            'sole male', 'sole female', 'males only', 'big breasts', 'nakadashi', 'ahegao',
            'full color', 'full censorship', 'uncensored', 'mosaic censorship',
        ];

        updateStatus("Processing…");

        let first;
        try {
            first = await apiJson('/api/v2/favorites?page=1&q=');
        } catch (e) {
            if (e.status === 401 || e.status === 403) throw new Error("Not logged in");
            throw e;
        }

        const totalPages = pagesFromApi(first);
        let favList = listFromApi(first);
        if (totalPages > 1) {
            const randomPage = Math.floor(Math.random() * totalPages) + 1;
            if (randomPage > 1) {
                favList = listFromApi(await apiJson(`/api/v2/favorites?page=${randomPage}&q=`));
            }
        }

        const favGalleries = favList.map(g => String(g.id)).filter(Boolean);
        if (favGalleries.length === 0) throw new Error("No favorites found on this page");

        const sampleSize = Math.min(5, favGalleries.length);
        const shuffledFavs = favGalleries.sort(() => 0.5 - Math.random()).slice(0, sampleSize);

        const tagCounts = {};

        for (const id of shuffledFavs) {
            const meta = await getMeta(id);
            if (meta && meta.tags) {
                meta.tags.forEach(t => {
                    if (t.type === 'tag' && !IGNORE_TAGS.includes(t.name)) {
                        tagCounts[t.name] = (tagCounts[t.name] || 0) + 1;
                    }
                });
            }
            await new Promise(r => setTimeout(r, 350));
        }

        const sortedTags = Object.entries(tagCounts)
        .sort(([,a], [,b]) => b - a)
        .map(([tag]) => tag);

        if (sortedTags.length === 0) throw new Error("Not enough data");

        const topTags = sortedTags.slice(0, 2);
        const spiceTag = sortedTags.slice(2, 10)[Math.floor(Math.random() * Math.min(8, sortedTags.length - 2))];
        if (spiceTag) topTags.push(spiceTag);

        const queryStr = topTags.map(t => `tag:"${t}"`).join(' ');

        const [candidatesWeek, candidatesAll] = await Promise.all([
            searchGalleriesApi(`${queryStr} uploaded:>6d`, 'popular-week'),
            searchGalleriesApi(queryStr, 'popular')
        ]);

        const newItems = [];
        const processedIds = new Set();
        readingQueue.forEach(q => processedIds.add(String(q.id)));

        const tryAddCandidate = async (candidate) => {
            if (!candidate) return false;

            if (processedIds.has(String(candidate.id))) return false;

            const allTitles = [...readingQueue, ...newItems];

            const isTitleDup = allTitles.some(item => areTitlesSimilar(item.title, candidate.title));

            if (isTitleDup) {
                return false;
            }

            processedIds.add(String(candidate.id));

            const isFav = favCache.has(String(candidate.id));
            if (isFav) {
                return false;
            }

            newItems.push(candidate);
            return true;
        };

        let weekAdded = 0;
        let attempts = 0;

        while (newItems.length < 5 && attempts < 30) {
            attempts++;

            let success = false;

            if (weekAdded < 3 && candidatesWeek.length > 0) {
                success = await tryAddCandidate(candidatesWeek.shift());
                if (success) weekAdded++;
            } else if (candidatesAll.length > 0) {
                success = await tryAddCandidate(candidatesAll.shift());
            } else if (candidatesWeek.length > 0) {
                success = await tryAddCandidate(candidatesWeek.shift());
                if (success) weekAdded++;
            } else {
                break;
            }
        }

        if (newItems.length === 0) throw new Error("No new galleries found");

        newItems.forEach(item => {
            toggleQueueItem(item.id, item.title, item.coverUrl, item.galleryUrl);
        });

        await new Promise(r => setTimeout(r, 1000));
    }

    // ==========================================================================
    // 3. FAVORITES MANAGER
    // ==========================================================================

    function saveFavCache() {
        localStorage.setItem('nhentai_flow_favs', JSON.stringify([...favCache]));
    }

    function reloadFavCacheFromStorage() {
        try {
            favCache = new Set(readList('nhentai_flow_favs'));
            updateAllMarkers();
        } catch (e) { /* ignore */ }
    }

    window.addEventListener('storage', (e) => {
        if (e.key === 'nhentai_flow_favs') reloadFavCacheFromStorage();
    });
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) reloadFavCacheFromStorage();
    });

    function addFav(id) {
        if (!favCache.has(String(id))) {
            favCache.add(String(id));
            saveFavCache();
            updateAllMarkers();
        }
    }

    const recentlyRemoved = new Map();

    function removeFav(id) {
        recentlyRemoved.set(String(id), Date.now());
        if (favCache.has(String(id))) {
            favCache.delete(String(id));
            saveFavCache();
            updateAllMarkers();
        }
    }

    function updateAllMarkers() {
        document.querySelectorAll(SEL.gallery).forEach(g => {
            if (g.updateMarkersFn) g.updateMarkersFn();
        });
    }

    // ==========================================================================
    // CDN: servers come from GET /api/v2/cdn (docs: never hardcode subdomains)
    // ==========================================================================
    const CDN_KEY = 'nhentai_flow_cdn';
    const CDN_TTL = 6 * 60 * 60 * 1000;
    const CDN_FALLBACK = { images: ['https://i.nhentai.net'], thumbs: ['https://t.nhentai.net'] };
    let cdnState = null;
    let cdnPromise = null;

    try {
        const raw = JSON.parse(localStorage.getItem(CDN_KEY) || 'null');
        if (raw && Array.isArray(raw.images) && Array.isArray(raw.thumbs)) cdnState = raw;
    } catch (e) { /* ignore */ }

    function cdnHost(h) {
        h = String(h).trim();
        if (!h) return '';
        if (h.startsWith('//')) h = 'https:' + h;
        if (!/^https?:\/\//.test(h)) h = 'https://' + h;
        return h.replace(/\/+$/, '');
    }

    function parseCdn(json) {
        const images = [], thumbs = [];
        const push = (val, hint) => {
            if (typeof val === 'string') {
                const host = cdnHost(val);
                if (!host) return;
                const isThumb = hint === 'thumb' || (hint !== 'image' && /\/\/t\d*\./.test(host));
                (isThumb ? thumbs : images).push(host);
            } else if (Array.isArray(val)) {
                val.forEach(v => push(v, hint));
            } else if (val && typeof val === 'object') {
                const direct = val.url || val.host || val.base_url || val.domain || val.server;
                if (direct) { push(direct, hint); return; }
                Object.keys(val).forEach(k => {
                    const h = /thumb|cover/i.test(k) ? 'thumb' : (/image|media|page|gallery/i.test(k) ? 'image' : hint);
                    push(val[k], h);
                });
            }
        };
        push(json, null);
        return { images: [...new Set(images)], thumbs: [...new Set(thumbs)] };
    }

    let cdnFailUntil = 0;

    function ensureCdn() {
        if (cdnState && Date.now() - (cdnState.ts || 0) < CDN_TTL) return Promise.resolve(cdnState);
        if (cdnPromise) return cdnPromise;
        if (Date.now() < cdnFailUntil) return Promise.resolve(cdnState || CDN_FALLBACK);
        cdnPromise = (async () => {
            try {
                const json = await apiJson('/api/v2/cdn');
                const parsed = (json && (Array.isArray(json.image_servers) || Array.isArray(json.thumb_servers)))
                ? {
                    images: [...new Set((json.image_servers || []).map(cdnHost).filter(Boolean))],
                    thumbs: [...new Set((json.thumb_servers || []).map(cdnHost).filter(Boolean))]
                }
                : parseCdn(json);
                if (parsed.images.length || parsed.thumbs.length) {
                    cdnState = {
                        ts: Date.now(),
                        images: parsed.images.length ? parsed.images : CDN_FALLBACK.images,
                        thumbs: parsed.thumbs.length ? parsed.thumbs : (parsed.images.length ? parsed.images : CDN_FALLBACK.thumbs)
                    };
                    try { localStorage.setItem(CDN_KEY, JSON.stringify(cdnState)); } catch (e) { /* ignore */ }
                } else {
                    console.warn('[NHentai Flow] /api/v2/cdn: unrecognized response, using fallback', json);
                }
            } catch (e) {
                console.warn('[NHentai Flow] CDN list unavailable, using fallback:', e.message || e);
            }
            if (!(cdnState && Date.now() - (cdnState.ts || 0) < CDN_TTL)) cdnFailUntil = Date.now() + 60000;
            cdnPromise = null;
            return cdnState || CDN_FALLBACK;
        })();
        return cdnPromise;
    }

    function pickServer(list, path) {
        let h = 0;
        for (let i = 0; i < path.length; i++) h = (h * 31 + path.charCodeAt(i)) >>> 0;
        return list[h % list.length];
    }

    function cdnUrl(kind, path) {
        const p = String(path || '').replace(/^\//, '');
        if (!p) return '';
        const st = cdnState || CDN_FALLBACK;
        const list = (kind === 'thumb' ? st.thumbs : st.images) || [];
        return `${list.length ? pickServer(list, p) : CDN_FALLBACK[kind === 'thumb' ? 'thumbs' : 'images'][0]}/${p}`;
    }

    function getAccessToken() {
        const m = document.cookie.match(/(?:^|;\s*)access_token=([^;]+)/);
        if (m) return decodeURIComponent(m[1]);
        for (const store of [window.localStorage, window.sessionStorage]) {
            try {
                for (let i = 0; i < store.length; i++) {
                    const k = store.key(i);
                    if (/access[_-]?token/i.test(k)) {
                        const v = store.getItem(k);
                        if (v) return v.replace(/^"|"$/g, '');
                    }
                }
            } catch (e) { /* ignore */ }
        }
        return null;
    }

    function authHeaders() {
        const token = getAccessToken();
        return token ? { 'Authorization': 'User ' + token } : {};
    }

    const RL_WINDOW = 60000;
    const RL_MAX_WAIT = 90000;
    const RL_COOLDOWN_KEY = 'nhentai_flow_cooldown';
    const RL_LIMITS = {
        //  [anonymous, logged in]          documented limit (anonymous / logged in)
        meta:     [12, 28], //  GET /galleries/{id}                  20 / 45
        list:     [8, 18], //   GET /galleries, /tagged, /random     15 / 30
        search:   [6, 12], //   GET /search                          10 / 20
        favs:     [10, 10], //  GET /favorites, /favorites/random    15 per user
        comments: [7, 12], //   GET /galleries/{id}/comments/count   12 / 20
        cdn:      [4, 4] //     GET /cdn
    };

    function rlBucket(url) {
        const p = String(url).split('?')[0];
        if (/\/api\/v2\/galleries\/\d+\/comments\/count$/.test(p)) return 'comments';
        if (/\/api\/v2\/galleries\/\d+$/.test(p)) return 'meta';
        if (/\/api\/v2\/galleries(\/tagged|\/popular|\/random)?$/.test(p)) return 'list';
        if (/\/api\/v2\/search$/.test(p)) return 'search';
        if (/\/api\/v2\/favorites(\/random)?$/.test(p)) return 'favs';
        if (/\/api\/v2\/cdn$/.test(p)) return 'cdn';
        return null;
    }

    function rlMax(bucket) {
        const lim = RL_LIMITS[bucket];
        if (!lim) return Infinity;
        return getAccessToken() ? lim[1] : lim[0];
    }

    function rlCooldownLeft() {
        try { return Math.max(0, (Number(localStorage.getItem(RL_COOLDOWN_KEY)) || 0) - Date.now()); } catch (e) { return 0; }
    }

    function rlSetCooldown(ms) {
        try {
            const until = Date.now() + ms;
            if (until > (Number(localStorage.getItem(RL_COOLDOWN_KEY)) || 0)) localStorage.setItem(RL_COOLDOWN_KEY, String(until));
        } catch (e) { /* ignore */ }
    }

    async function rlAcquire(bucket, wait) {
        if (!bucket) return true;
        const key = 'nhentai_flow_rl_' + bucket;
        const max = rlMax(bucket);
        const deadline = Date.now() + RL_MAX_WAIT;
        for (;;) {
            const cool = rlCooldownLeft();
            if (cool > 0) {
                if (!wait || Date.now() + cool > deadline) return false;
                await sleep(cool + Math.random() * 300);
                continue;
            }
            const now = Date.now();
            const stamps = readList(key).filter(t => typeof t === 'number' && now - t < RL_WINDOW);
            if (stamps.length < max) {
                stamps.push(now);
                try { localStorage.setItem(key, JSON.stringify(stamps)); } catch (e) { /* ignore */ }
                return true;
            }
            if (!wait) return false;
            const delay = RL_WINDOW - (now - stamps[0]) + 100 + Math.random() * 300;
            if (now + delay > deadline) return false;
            await sleep(delay);
        }
    }

    async function fetchRetry(url, opts, retries, rl) {
        const max = retries === undefined ? 3 : retries;
        const wait = !(rl && rl.wait === false);
        const bucket = rlBucket(url);
        for (let attempt = 0; ; attempt++) {
            if (!(await rlAcquire(bucket, wait))) {
                const err = new Error('No free request slot');
                err.rateLimited = true;
                throw err;
            }
            const res = await fetch(url, opts);
            if (res.status !== 429) return res;
            const ra = parseFloat(res.headers.get('Retry-After'));
            const backoff = !isNaN(ra) && ra > 0 ? ra * 1000 : 10000 * Math.pow(2, attempt);
            rlSetCooldown(Math.min(backoff, 120000) + Math.random() * 1000);
            if (!wait || attempt >= max) return res;
        }
    }

    async function apiJson(url, opts, rl) {
        const o = opts || {};
        const res = await fetchRetry(url, Object.assign({}, o, {
            credentials: 'same-origin',
            headers: Object.assign({ 'Accept': 'application/json' }, authHeaders(), o.headers || {})
        }), undefined, rl);
        if (!res.ok) {
            const err = new Error('HTTP ' + res.status);
            err.status = res.status;
            throw err;
        }
        return res.json();
    }

    function listFromApi(json) {
        if (Array.isArray(json)) return json;
        if (!json) return [];
        return json.result || json.results || json.favorites || json.galleries || json.data || [];
    }

    function pagesFromApi(json) {
        if (!json) return 1;
        const n = json.num_pages || json.total_pages || json.pages;
        if (n) return parseInt(n, 10) || 1;
        if (json.total && json.per_page) return Math.max(1, Math.ceil(json.total / json.per_page));
        return 1;
    }

    function normalizeApiGallery(g) {
        const id = String(g.id);
        const t = g.title;
        const title = g.english_title || g.japanese_title ||
              (typeof t === 'string' ? t : (t && (t.english || t.pretty || t.japanese))) || ('#' + id);
        let thumb = g.thumbnail || g.thumb || (g.cover && (g.cover.path || g.cover)) ||
            (g.images && g.images.cover && g.images.cover.path) || '';
        if (thumb && typeof thumb === 'object') thumb = thumb.path || '';
        if (thumb && !/^https?:/.test(thumb)) thumb = cdnUrl('thumb', thumb);
        return { id, title, coverUrl: thumb, galleryUrl: `${location.origin}/g/${id}/` };
    }

    async function searchGalleriesApi(queryStr, sort) {
        const json = await apiJson(`/api/v2/search?query=${encodeURIComponent(queryStr)}&sort=${sort}&page=1`);
        return listFromApi(json).map(normalizeApiGallery);
    }

    function initFavoritesPage() {
        if (!/^\/(?:user\/)?favorites\/?$/.test(window.location.pathname)) return;
        let added = 0;
        document.querySelectorAll(SEL.removeButton).forEach(btn => {
            const id = cardIdFrom(btn);
            if (!id) return;
            const removedAt = recentlyRemoved.get(String(id));
            if (removedAt && Date.now() - removedAt < 30000) return;
            if (!favCache.has(String(id))) {
                favCache.add(String(id));
                added++;
            }
        });
        if (added > 0) {
            saveFavCache();
            updateAllMarkers();
        }
    }

    async function syncFavorites(updateStatus) {
        const report = typeof updateStatus === 'function' ? updateStatus : () => {};

        let first;
        try {
            first = await apiJson('/api/v2/favorites?page=1&q=');
        } catch (e) {
            if (e.status === 401 || e.status === 403) throw new Error('Login required');
            throw e;
        }

        const totalPages = pagesFromApi(first);
        const ids = new Set(listFromApi(first).map(g => String(g.id)));

        for (let i = 2; i <= totalPages; i++) {
            report(`${i}/${totalPages} (${ids.size} IDs)`);
            await sleep(6500);
            const json = await apiJson(`/api/v2/favorites?page=${i}&q=`);
            listFromApi(json).forEach(g => ids.add(String(g.id)));
        }

        favCache = ids;
        saveFavCache();
        updateAllMarkers();
        return ids.size;
    }


    // ==========================================================================
    // 4. PREVIEW LOGIC
    // ==========================================================================

    const metaInflight = new Map();

    const metaFail = new Map();

    const META_STORE_TTL = 30 * 60 * 1000;
    const META_STORE_MAX = 40;

    function metaStoreGet(id) {
        try {
            const raw = sessionStorage.getItem('nf_meta_' + id);
            if (!raw) return null;
            const o = JSON.parse(raw);
            if (!o || !o.meta || Date.now() - o.ts > META_STORE_TTL) { sessionStorage.removeItem('nf_meta_' + id); return null; }
            return o.meta;
        } catch (e) { return null; }
    }

    function metaStorePut(id, meta) {
        try {
            const idx = JSON.parse(sessionStorage.getItem('nf_meta_idx') || '[]').filter(x => x !== String(id));
            idx.push(String(id));
            while (idx.length > META_STORE_MAX) sessionStorage.removeItem('nf_meta_' + idx.shift());
            sessionStorage.setItem('nf_meta_' + id, JSON.stringify({ ts: Date.now(), meta }));
            sessionStorage.setItem('nf_meta_idx', JSON.stringify(idx));
        } catch (e) { /* quota or storage disabled: the in-memory cache still works */ }
    }

    function getMeta(id, force, rl) {
        if (!force && metaCache.has(id)) return Promise.resolve(metaCache.get(id));
        if (!force) {
            const stored = metaStoreGet(id);
            if (stored) { metaCache.set(id, stored); return Promise.resolve(stored); }
        }
        if (metaInflight.has(id)) return metaInflight.get(id);
        if ((metaFail.get(id) || 0) > Date.now()) return Promise.resolve(null);
        const p = fetchMeta(id, rl).finally(() => metaInflight.delete(id));
        metaInflight.set(id, p);
        return p;
    }

    async function fetchMeta(id, rl) {
        try {
            await ensureCdn();
            const response = await fetchRetry(`/api/v2/galleries/${id}`, { credentials: 'same-origin', headers: Object.assign({ 'Accept': 'application/json' }, authHeaders()) }, undefined, rl);
            if (!response.ok) throw new Error(`Erro na API: ${response.status}`);

            const data = await response.json();


            let coverPath = '';
            if (data.cover && data.cover.path) coverPath = data.cover.path;
            else if (data.images && data.images.cover && data.images.cover.path) coverPath = data.images.cover.path;

            const coverUrl = coverPath ? cdnUrl('thumb', coverPath) : '';

            let pagesArray = [];
            if (data.pages && Array.isArray(data.pages)) pagesArray = data.pages;
            else if (data.images && data.images.pages && Array.isArray(data.images.pages)) pagesArray = data.images.pages;

            const meta = {
                id: data.id,
                media_id: data.media_id,
                title: data.title.english || data.title.pretty || data.title.japanese,
                cover: coverUrl,
                tags: data.tags || [],
                total: data.num_pages || 1,
                pagesData: pagesArray
            };

            metaCache.set(id, meta);
            metaStorePut(id, meta);
            return meta;

        } catch (error) {
            if (!error.rateLimited) {
                metaFail.set(id, Date.now() + 60000);
                console.error('[NHentai Flow] Failure to load preview via API v2 to ID', id, error);
            }
            return null;
        }
    }

    function buildTagList(tags) {
        const groups = { artist: [], parody: [], character: [], tag: [] };
        const fmt = (n) => n >= 1000 ? (n/1000).toFixed(1) + 'k' : n;
        const getTier = (c) => {
            if (c < 1000) return 'tier-mythic';
            if (c < 5000) return 'tier-rare';
            if (c < 20000) return 'tier-uncommon';
            return '';
        };
        const getGenreStyle = (name) => {
            if (['yaoi', 'males only', 'bara', 'yuri', 'females only', 'lesbian', 'futanari', 'tomgirl', 'otokonoko', 'dickgirl', 'shemale', 'bisexual'].includes(name)) return 'style-lgbt';
            return '';
        };

        tags.forEach(t => {
            const count = t.count || 0;
            let className = '';
            if (t.type === 'tag') {
                className = `${getTier(count)} ${getGenreStyle(t.name)}`;
            }
            const html = `<span class="tag-pill ${className}" title="${t.name} (${fmt(count)})">${t.name}</span>`;
            if (groups[t.type]) groups[t.type].push(html);
            else if (t.type === 'group') groups.artist.push(`<span class="tag-pill">[${t.name}]</span>`);
        });

        let html = '';
        const addGroup = (title, list) => { if (list.length) html += `<div class="tag-category">${title}</div>` + list.join(''); };
        addGroup('Artists', groups.artist); addGroup('Parodies', groups.parody); addGroup('Characters', groups.character); addGroup('Tags', groups.tag);
        return html || '<div style="padding:5px">No tags</div>';
    }

    function update(gallery, val, isJump = false, fromHover = false) {
        if (isStaleCard(gallery)) { initPreviewUI(gallery); return Promise.resolve(); }
        const id = gallery.dataset.gid;
        const state = states.get(id) || { curr: 1, req: 0 };
        states.set(id, state);

        return getMeta(id, false, fromHover ? { wait: false } : undefined).then(meta => {
            if (!meta) return;

            let next = isJump ? val : state.curr + val;
            if (next < 1) next = 1; if (next > meta.total) next = meta.total;

            const popup = gallery.querySelector('.tag-popup');
            if (popup && !popup.innerHTML) {
                try {
                    popup.innerHTML = buildTagList(meta.tags);
                } catch (e) {
                    console.error('[NHentai Flow] Erro isolado ao construir tags:', e);
                }
            }

            if (next === state.curr && !isJump && val !== 0) return;
            state.curr = next;
            const reqId = ++state.req;

            if (state.curr !== 1) gallery.classList.add('is-previewing');

            const barFill = gallery.querySelector('.seek-fill');
            if (barFill) barFill.style.transform = `scaleX(${state.curr / meta.total})`;

            let src = '';
            let aspectRatio = '';

            if (meta.pagesData && meta.pagesData[state.curr - 1]) {
                const pageData = meta.pagesData[state.curr - 1];
                if (pageData.path) {
                    src = cdnUrl('image', pageData.path);
                    aspectRatio = `${pageData.width}/${pageData.height}`;
                }
            }

            if (!src) return;

            const img = gallery.querySelector(SEL.coverImage);
            if (!img) return;

            if (!img.dataset.originalSrc) {
                img.dataset.originalSrc = img.src;
            }

            if (state.loader) { state.loader.onload = null; state.loader.onerror = null; state.loader.src = ''; }
            const loader = new Image();
            state.loader = loader;

            loader.onload = () => {
                if (state.req === reqId && hoveredGallery === gallery && gallery.dataset.gid === id) {
                    if (aspectRatio) img.style.aspectRatio = aspectRatio;
                    img.src = src;
                }
                loader.onload = null;
                loader.onerror = null;
            };

            loader.onerror = () => {
                loader.onload = null;
                loader.onerror = null;
            };

            loader.src = src;
        });
    }

    function cardGalleryId(gallery) {
        const link = gallery.querySelector(SEL.cover);
        return { link, id: link ? link.href.match(/\/g\/(\d+)\//)?.[1] : null };
    }

    function isStaleCard(gallery) {
        if (gallery.dataset.init !== '1') return false;
        const cur = cardGalleryId(gallery);
        return cur.link !== gallery._nfLink || cur.id !== gallery.dataset.gid;
    }

    function hasUnsyncedCards() {
        const cards = document.querySelectorAll(SEL.gallery);
        for (let i = 0; i < cards.length; i++) {
            if (cards[i].dataset.init !== '1' || isStaleCard(cards[i])) return true;
        }
        return false;
    }

    function resetCard(gallery) {
        if (hoveredGallery === gallery) hoveredGallery = null;
        gallery.onmouseenter = null;
        gallery.onmouseleave = null;
        gallery.updateMarkersFn = null;
        gallery._nfLink = null;
        gallery.classList.remove('is-previewing');
        gallery.style.width = '';
        gallery.style.height = '';
        gallery.removeAttribute('data-fixed-dimensions');
        gallery.removeAttribute('data-init');
        gallery.querySelectorAll('.reading-marker, .inline-preview-ui').forEach(m => m.remove());
        const img = gallery.querySelector(SEL.coverImage);
        if (img) { img.removeAttribute('data-original-src'); img.style.aspectRatio = ''; }
    }

    function initPreviewUI(gallery) {
        if (isStaleCard(gallery)) resetCard(gallery);
        const { link, id } = cardGalleryId(gallery);
        if (!link || gallery.dataset.init === '1') return;
        if (!id) return;

        gallery.dataset.gid = id;
        gallery._nfLink = link;
        gallery.dataset.init = '1';

        // 1. Visual Markers
        const progressMarker = document.createElement('div');
        progressMarker.className = 'reading-marker marker-progress marker-yellow';
        progressMarker.style.display = 'none';
        progressMarker.innerHTML = `<span class="fa-stack"><i class="fas fa-bookmark fa-stack-2x"></i><strong class="fa-stack-1x pg-num" style="color: #fff; font-size: 12px !important; margin-top: -3px;"></strong></span>`;
        gallery.appendChild(progressMarker);

        const queueMarker = document.createElement('div');
        queueMarker.className = 'reading-marker marker-queue marker-blue';
        queueMarker.style.display = 'none';
        queueMarker.innerHTML = `<span class="fa-stack"><i class="fas fa-bookmark fa-stack-2x"></i><i class="fas fa-clock fa-stack-1x" style="color:#fff; font-size:15px; margin-top:-3px;"></i></span>`;
        gallery.appendChild(queueMarker);

        const favMarker = document.createElement('div');
        favMarker.className = 'reading-marker marker-fav marker-pink';
        favMarker.style.display = 'none';
        favMarker.innerHTML = `<span class="fa-stack"><i class="fas fa-bookmark fa-stack-2x"></i><i class="fas fa-heart fa-stack-1x" style="color:#fff; font-size:15px; margin:-3px 0 0 0;"></i></span>`;
        gallery.appendChild(favMarker);

        const ctx = {
            isQueue: new URLSearchParams(window.location.search).get('view') === 'queue',
            isFavorites: window.location.pathname.includes('/favorites')
        };

        const updateMarkers = () => {
            queueMarker.style.display = (!ctx.isQueue && settings.enableQueue && isQueued(id)) ? 'block' : 'none';
            favMarker.style.display = (!ctx.isFavorites && favCache.has(String(id))) ? 'block' : 'none';
            const state = states.get(id);
            if (state?.curr > 1) {
                progressMarker.style.display = 'block';
                progressMarker.querySelector('.pg-num').textContent = state.curr;
            } else {
                progressMarker.style.display = 'none';
            }
        };
        gallery.updateMarkersFn = updateMarkers;
        updateMarkers();

        let isUiInjected = false;

        // 2. Hover Events
        gallery.onmouseenter = () => {
            if (isStaleCard(gallery)) {
                initPreviewUI(gallery);
                if (gallery.onmouseenter) gallery.onmouseenter();
                return;
            }
            hoveredGallery = gallery;

            if (!gallery.dataset.fixedDimensions) {
                const rect = gallery.getBoundingClientRect();
                gallery.style.width = `${rect.width}px`;
                gallery.style.height = `${rect.height}px`;
                gallery.dataset.fixedDimensions = '1';
            }

            if (!isUiInjected) {
                const ui = document.createElement('div');
                ui.className = 'inline-preview-ui';
                ui.innerHTML = `
                    <div class="tag-trigger">TAGS</div>
                    <div class="tag-popup"></div>
                    <div class="queue-trigger" title="Add/Remove from Queue (Q)"><i class="fa fa-plus"></i></div>
                    <div class="hotzone hotzone-left"></div>
                    <div class="hotzone hotzone-right"></div>
                    <div class="seek-container"><div class="seek-bg"><div class="seek-fill"></div></div><div class="seek-tooltip">Pg 1</div></div>
                `;

                const qBtn = ui.querySelector('.queue-trigger');
                if (isQueued(id)) { qBtn.classList.add('in-queue'); qBtn.innerHTML = '<i class="fa fa-check"></i>'; }

                qBtn.onclick = (e) => {
                    e.preventDefault(); e.stopPropagation();
                    const imgEl = gallery.querySelector(SEL.coverImage);
                    const coverUrl = imgEl.dataset.src || imgEl.dataset.originalSrc || imgEl.src;
                    const capEl = gallery.querySelector(SEL.caption);
                    const cardTitle = capEl ? capEl.textContent.trim() : '';

                    if (metaCache.has(id)) {
                        toggleQueueItem(id, metaCache.get(id).title, coverUrl, link.href);
                    } else if (cardTitle) {
                        toggleQueueItem(id, cardTitle, coverUrl, link.href);
                    } else {
                        qBtn.innerHTML = '<i class="fas fa-ellipsis-h"></i>';
                        getMeta(id).then(meta => {
                            if (!meta) { qBtn.innerHTML = '<i class="fa fa-exclamation"></i>'; return; }
                            toggleQueueItem(id, meta.title, coverUrl, link.href);
                        });
                    }
                    setTimeout(updateMarkers, 50);
                };

                const tagTrigger = ui.querySelector('.tag-trigger');
                if (tagTrigger) {
                    tagTrigger.addEventListener('click', stopEvent);
                    tagTrigger.addEventListener('touchstart', stopPropOnly, { passive: true });
                    const loadTags = () => {
                        const pop = ui.querySelector('.tag-popup');
                        if (pop && !pop.innerHTML) update(gallery, 0);
                    };
                    tagTrigger.addEventListener('mouseenter', loadTags);
                    tagTrigger.addEventListener('touchstart', loadTags, { passive: true });
                }

                const tagPopup = ui.querySelector('.tag-popup');
                if (tagPopup) {
                    tagPopup.addEventListener('click', stopEvent);
                    tagPopup.addEventListener('touchstart', stopPropOnly, { passive: true });
                }

                ui.querySelector('.hotzone-left').onclick = (e) => { stopEvent(e); update(gallery, -1); };
                ui.querySelector('.hotzone-right').onclick = (e) => { stopEvent(e); update(gallery, 1); };

                const seek = ui.querySelector('.seek-container');
                const tip = ui.querySelector('.seek-tooltip');
                let seekRect = null;

                seek.onmouseenter = () => { seekRect = seek.getBoundingClientRect(); };
                seek.onclick = (e) => {
                    stopEvent(e);
                    if (!seekRect) seekRect = seek.getBoundingClientRect();
                    const pct = Math.max(0, Math.min(1, (e.clientX - seekRect.left) / seekRect.width));
                    if (!metaCache.has(id)) {
                        update(gallery, 0).then(() => { update(gallery, Math.ceil(pct * metaCache.get(id).total) || 1, true); });
                    } else {
                        update(gallery, Math.ceil(pct * metaCache.get(id).total) || 1, true);
                    }
                };
                seek.onmousemove = (e) => {
                    if (!metaCache.has(id)) return;
                    if (!seekRect) seekRect = seek.getBoundingClientRect();
                    const pct = Math.max(0, Math.min(1, (e.clientX - seekRect.left) / seekRect.width));
                    tip.style.left = `${e.clientX - seekRect.left}px`;
                    tip.textContent = Math.ceil(pct * metaCache.get(id).total) || 1;
                };

                link.style.position = 'relative';
                link.appendChild(ui);
                isUiInjected = true;
            }

            if (!metaCache.has(id)) {
                hoverTimeout = setTimeout(() => { update(gallery, 0, false, true); }, 300);
            } else { update(gallery, 0); }
        };

        gallery.onmouseleave = () => {
            hoveredGallery = null;
            if (hoverTimeout) { clearTimeout(hoverTimeout); hoverTimeout = null; }
            gallery.classList.remove('is-previewing');

            const img = link.querySelector('img');
            if (img && img.dataset.originalSrc) {
                img.src = img.dataset.originalSrc;
                img.style.aspectRatio = '';
            }

            updateMarkers();
            gallery.style.width = '';
            gallery.style.height = '';
            gallery.dataset.fixedDimensions = '';
        };
    }

    // ==========================================================================
    // 5. READER LOGIC
    // ==========================================================================

    let readerDirty = true;
    let readerGlobalsInit = false;

    function toggleReaderFullscreen() {
        const c = document.querySelector(SEL.imageContainer);
        if (!c) return;
        if (!document.fullscreenElement) {
            c.requestFullscreen().catch(() => {});
        } else {
            document.exitFullscreen();
        }
    }

    function extractGalleryId(json) {
        if (json === null || json === undefined) return null;
        if (typeof json === 'number' || (typeof json === 'string' && /^\d+$/.test(json))) return String(json);
        if (typeof json !== 'object') return null;
        for (const k of ['id', 'gallery_id', 'galleryId']) {
            if (json[k] !== undefined && /^\d+$/.test(String(json[k]))) return String(json[k]);
        }
        for (const k of ['gallery', 'result', 'data']) {
            const nested = extractGalleryId(json[k]);
            if (nested) return nested;
        }
        return null;
    }

    async function getRandomFavoriteId() {
        const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];
        let noList = false;
        try {
            const rid = extractGalleryId(await apiJson('/api/v2/favorites/random'));
            if (rid) return rid;
        } catch (e) {
            if (e.status === 401 || e.status === 403 || e.status === 404) noList = true;
            else console.warn('[NHentai Flow] Random favorite endpoint failed:', e);
        }
        try {
            if (noList) throw new Error('favorites list not available');
            const first = await apiJson('/api/v2/favorites?page=1&q=');
            const totalPages = pagesFromApi(first);
            let list = listFromApi(first);
            if (totalPages > 1) {
                const p = Math.floor(Math.random() * totalPages) + 1;
                if (p > 1) list = listFromApi(await apiJson(`/api/v2/favorites?page=${p}&q=`));
            }
            if (list.length) return String(pick(list).id);
        } catch (e) {
            console.warn('[NHentai Flow] Random favorite via API failed:', e);
        }
        if (favCache.size) return pick([...favCache]);
        try {
            const r = await fetch('/favorites/random');
            const m = r.url.match(/\/g\/(\d+)/);
            if (m) return m[1];
        } catch (e) { /* ignore */ }
        return null;
    }

    function initReaderMode() {
        const imageContainer = document.querySelector(SEL.imageContainer);
        if (!imageContainer) return;

        if (!imageContainer.dataset.readerInit) {
            imageContainer.dataset.readerInit = '1';

            const exitIcon = document.createElement('div');
            exitIcon.className = 'exit-fs-indicator';
            exitIcon.innerHTML = '<i class="fa fa-times"></i>';
            exitIcon.onclick = (e) => {
                e.stopPropagation();
                if (document.fullscreenElement) document.exitFullscreen();
            };
            imageContainer.appendChild(exitIcon);

            imageContainer.addEventListener('click', (e) => {
                if (e.target.tagName === 'IMG' || e.target.tagName === 'A') return;
                const rect = imageContainer.getBoundingClientRect();
                if (e.clientX - rect.left > rect.width / 2) {
                    const nextBtn = document.querySelector(SEL.readerNext);
                    if (nextBtn) nextBtn.click();
                } else {
                    const prevBtn = document.querySelector(SEL.readerPrev);
                    if (prevBtn) prevBtn.click();
                }
            });
        }

        if (!readerGlobalsInit) {
            readerGlobalsInit = true;

            document.addEventListener('keydown', (e) => {
                if (!isReader) return;
                if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
                if (e.key === 't' || e.key === 'T') { e.preventDefault(); toggleReaderFullscreen(); }
            });

            document.addEventListener('fullscreenchange', () => {
                if (!document.fullscreenElement) {
                    const fsBtn = document.querySelector('.btn-fullscreen-custom');
                    if (fsBtn) fsBtn.innerHTML = '<i class="fa fa-expand"></i> Fullscreen';
                    const c = document.querySelector(SEL.imageContainer);
                    if (c) setTimeout(() => { c.scrollIntoView({ behavior: 'auto' }); });
                }
            });

            let isWheeling = false;
            document.addEventListener('wheel', (e) => {
                if (!document.fullscreenElement) return;
                e.preventDefault();

                if (isWheeling) return;

                isWheeling = true;
                setTimeout(() => { isWheeling = false; }, 150);

                if (e.deltaY > 0) {
                    const nextBtn = document.querySelector(SEL.readerNext);
                    if (nextBtn) nextBtn.click();
                } else {
                    const prevBtn = document.querySelector(SEL.readerPrev);
                    if (prevBtn) prevBtn.click();
                }
            }, { passive: false });
        }

        const toolbars = document.querySelectorAll(SEL.readerBar);
        if (toolbars.length === 0) return;

        const bottomToolbar = toolbars[toolbars.length - 1];
        const containerRight = bottomToolbar.querySelector(SEL.readerButtonsRight);
        if (!containerRight) return;

        // 1. Fullscreen
        if (!containerRight.querySelector('.btn-fullscreen-custom')) {
            const fsBtn = document.createElement('button');
            fsBtn.className = 'btn btn-unstyled btn-fullscreen-custom';
            fsBtn.innerHTML = '<i class="fa fa-expand"></i> Fullscreen';
            fsBtn.title = "Toggle Fullscreen (T)";
            fsBtn.style.marginRight = '5px';
            fsBtn.onclick = toggleReaderFullscreen;
            containerRight.insertBefore(fsBtn, containerRight.firstChild);
        }

        // 2. Next in queue (rebuilt every time, it depends on the current gallery)
        document.querySelectorAll('.btn-next-queue').forEach(b => b.remove());
        if (settings.enableQueue) {
            const fsBtn = containerRight.querySelector('.btn-fullscreen-custom');
            const currentId = window.location.pathname.match(/\/g\/(\d+)/)?.[1];

            if (currentId && readingQueue.length > 0) {
                const currentIndex = readingQueue.findIndex(i => i.id == currentId);
                if (currentIndex > -1 && currentIndex < readingQueue.length - 1) {
                    const nextItem = readingQueue[currentIndex + 1];

                    const nextQBtn = document.createElement('a');
                    nextQBtn.className = 'btn btn-unstyled btn-next-queue';
                    nextQBtn.innerHTML = `<i class="fa fa-step-forward"></i> Next in queue`;
                    nextQBtn.href = `/g/${nextItem.id}/1/`;
                    nextQBtn.title = `Read Next in Queue: ${nextItem.title}`;

                    nextQBtn.setAttribute('data-sveltekit-reload', '');
                    nextQBtn.setAttribute('rel', 'external');
                    nextQBtn.addEventListener('click', (e) => {
                        if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
                        e.preventDefault();
                        e.stopPropagation();
                        window.location.assign(nextQBtn.href);
                    });

                    if (fsBtn) {
                        fsBtn.parentNode.insertBefore(nextQBtn, fsBtn.nextSibling);
                    } else {
                        containerRight.appendChild(nextQBtn);
                    }
                }
            }
        }

        // 3. Random Favorite (picked on click, through the API)
        const hasFavorites = !!document.querySelector(SEL.favoritesLink);
        if (hasFavorites && !containerRight.querySelector('.btn-random-fav')) {
            const randFavBtn = document.createElement('button');
            randFavBtn.type = 'button';
            randFavBtn.className = 'btn btn-unstyled btn-random-fav';
            randFavBtn.innerHTML = '<i class="fa fa-random"></i> Fav';
            randFavBtn.title = "Read Random Favorite";
            randFavBtn.style.marginRight = '5px';

            randFavBtn.onclick = async () => {
                if (randFavBtn.dataset.busy) return;
                randFavBtn.dataset.busy = '1';
                randFavBtn.innerHTML = '<i class="fa fa-circle-notch fa-spin"></i>';
                const id = await getRandomFavoriteId();
                if (id) {
                    window.location.href = `/g/${id}/1/`;
                } else {
                    randFavBtn.innerHTML = '<i class="fa fa-exclamation-triangle"></i>';
                    setTimeout(() => {
                        randFavBtn.innerHTML = '<i class="fa fa-random"></i> Fav';
                        delete randFavBtn.dataset.busy;
                    }, 2000);
                }
            };

            containerRight.insertBefore(randFavBtn, containerRight.firstChild);
        }

        readerDirty = false;
    }

    // ==========================================================================
    // 6. RANDOM CONTEXTUAL
    // ==========================================================================

    function currentListingQuery() {
        const q = new URLSearchParams(window.location.search);
        let query = q.get('q');
        let sort = q.get('sort') || '';
        if (query === null) {
            const m = window.location.pathname.match(/^\/(tag|artist|character|parody|group|category|language)\/([^/]+)(?:\/(popular(?:-week|-today)?))?\/?$/);
            if (m) {
                query = `${m[1]}:"${decodeURIComponent(m[2]).replace(/-/g, ' ')}"`;
                if (m[3]) sort = m[3];
            }
        }
        return { query, sort };
    }

    async function randomListingGallery(page) {
        const { query, sort } = currentListingQuery();
        if (query === null) {
            if (window.location.pathname !== '/') throw new Error('No query to search with');
            const all = listFromApi(await apiJson(`/api/v2/galleries?page=${page}`));
            if (!all.length) throw new Error('No galleries found');
            return `${window.location.origin}/g/${all[Math.floor(Math.random() * all.length)].id}/`;
        }
        const url = `/api/v2/search?query=${encodeURIComponent(query)}${sort ? '&sort=' + encodeURIComponent(sort) : ''}&page=${page}`;
        const list = listFromApi(await apiJson(url));
        if (!list.length) throw new Error('No galleries found');
        return `${window.location.origin}/g/${list[Math.floor(Math.random() * list.length)].id}/`;
    }

    function initRandomContextual() {
        const sortContainer = document.querySelector(SEL.sortBar);
        if (!sortContainer || sortContainer.querySelector('.btn-random-ctx')) return;

        const btnContainer = document.createElement('div');
        btnContainer.className = 'sort-type';

        const btn = document.createElement('a');
        btn.className = 'btn-random-ctx';
        const ORIGINAL_HTML = '<i class="fa fa-random"></i>';
        btn.innerHTML = ORIGINAL_HTML;
        btn.style.cursor = 'pointer';
        btn.title = "Roll a random gallery from these search results";

        const resetBtn = () => {
            btn.innerHTML = ORIGINAL_HTML;
            btn.style.pointerEvents = 'auto';
            btn.style.opacity = '1';
        };

        window.addEventListener('pageshow', (event) => {
            if (event.persisted) { resetBtn(); }
        });
        resetBtn();

        btn.onclick = async (e) => {
            e.preventDefault();
            btn.innerHTML = '<i class="fa fa-circle-notch fa-spin"></i>';
            btn.style.pointerEvents = 'none';
            btn.style.opacity = '0.5';

            try {
                const lastPageBtn = document.querySelector(SEL.paginationLast);
                let totalPages = 1;
                if (lastPageBtn) {
                    const match = lastPageBtn.href.match(/page=(\d+)/);
                    if (match) totalPages = parseInt(match[1], 10);
                } else {
                    const pages = document.querySelectorAll(SEL.paginationPage);
                    if (pages.length > 0) {
                        const lastNum = pages[pages.length - 1].textContent;
                        if (!isNaN(lastNum)) totalPages = parseInt(lastNum, 10);
                    }
                }
                if (totalPages === 1) {
                    const galleries = document.querySelectorAll(`${SEL.gallery} ${SEL.cover}`);
                    if (galleries.length === 0) throw new Error("No galleries found");
                    const randomGallery = galleries[Math.floor(Math.random() * galleries.length)];
                    window.location.href = randomGallery.href;
                    return;
                }

                const randomPage = Math.floor(Math.random() * totalPages) + 1;
                window.location.href = await randomListingGallery(randomPage);

            } catch (err) {
                btn.innerHTML = '<i class="fa fa-exclamation-triangle"></i>';
                setTimeout(() => { resetBtn(); }, 2000);
            }
        };

        btnContainer.appendChild(btn);
        sortContainer.appendChild(btnContainer);
    }

    // ==========================================================================
    // 7. POPULAR SHORTCUT BUTTON
    // ==========================================================================

    function initPopularShortcut() {
        const popularContainer = document.querySelector(SEL.popularBlock);
        if (!popularContainer || popularContainer.querySelector('.btn-view-all-popular')) return;

        const link = document.createElement('a');
        link.href = '/search/?q=pages%3A%3E0&sort=popular-today';
        link.className = 'btn btn-secondary btn-view-all-popular';
        link.style.display = 'block';
        link.style.marginTop = '5px';
        link.innerHTML = '<i class="fa fa-compass"></i> Explore Today’s Trending';
        popularContainer.appendChild(link);
    }

    // ==========================================================================
    // 8. SMART NAVIGATION
    // ==========================================================================

    const SN_CFG = {
        decayDelay: 2500,
        decayFraction: 0.2,
        bottomTolerance: 4,
        scrollToTopOnNav: true,
        cooldownAfterNav: 600,
        touchBottomTolerance: 24
    };
    const SN = { installed: false, touching: false, touchX: 0, touchY: 0, progress: 0, lastWheel: 0, triggered: false, raf: null, lastFrame: 0, cooldownUntil: 0, bar: null, wrap: null };

    function snThreshold() { return numSetting('smartNavScroll'); }

    function snWheelPixels(e) {
        return e.deltaY * (e.deltaMode === 1 ? 33 : (e.deltaMode === 2 ? window.innerHeight : 1));
    }

    function snEnabled() {
        return !!settings.smartNav && !/^\/g\//.test(window.location.pathname);
    }

    function snRender() {
        if (!SN.bar) return;
        const pct = Math.min(100, (SN.progress / snThreshold()) * 100);
        SN.bar.style.width = pct + '%';
        SN.wrap.classList.toggle('active', SN.progress > 0);
    }

    function snReset() {
        SN.progress = 0;
        snRender();
    }

    function snAtBottom(tolerance) {
        const tol = tolerance === undefined ? SN_CFG.bottomTolerance : tolerance;
        const doc = document.documentElement;
        const scrollTop = window.pageYOffset || doc.scrollTop || 0;
        const fullHeight = Math.max(doc.scrollHeight, document.body.scrollHeight);
        return scrollTop + window.innerHeight >= fullHeight - tol;
    }

    function snAccumulate(amount) {
        if (!snFindNext()) return;
        SN.lastWheel = performance.now();
        SN.progress = Math.min(snThreshold(), SN.progress + amount);
        snRender();
        if (SN.raf === null) { SN.lastFrame = 0; SN.raf = requestAnimationFrame(snTick); }
        if (SN.progress >= snThreshold()) snGo();
    }

    function snFindNext() {
        const els = Array.from(document.querySelectorAll(SEL.paginationNext))
        .filter(el => el.href && el.getClientRects().length > 0);
        return els.length ? els[els.length - 1] : null;
    }

    function snTick(now) {
        if (SN.triggered) {
            SN.raf = null;
            SN.lastFrame = 0;
            return;
        }
        const dt = SN.lastFrame ? (now - SN.lastFrame) / 1000 : 0;
        SN.lastFrame = now;

        if (SN.progress > 0 && !SN.touching && performance.now() - SN.lastWheel > SN_CFG.decayDelay) {
            SN.progress = Math.max(0, SN.progress - snThreshold() * SN_CFG.decayFraction * dt);
            snRender();
        }

        if (SN.progress > 0) {
            SN.raf = requestAnimationFrame(snTick);
        } else {
            SN.raf = null;
            SN.lastFrame = 0;
        }
    }

    function snGo() {
        const next = snFindNext();
        if (!next) { snReset(); return; }
        SN.triggered = true;
        SN.bar.style.width = '100%';
        const urlBefore = window.location.href;
        next.click();

        setTimeout(() => {
            if (SN.triggered && window.location.href === urlBefore) {
                SN.triggered = false;
                snReset();
            }
        }, 3000);
    }

    function smartNavOnRoute() {
        if (!SN.installed) return;
        const wasOurNavigation = SN.triggered;
        SN.triggered = false;
        SN.progress = 0;
        if (SN.raf !== null) { cancelAnimationFrame(SN.raf); SN.raf = null; }
        SN.lastFrame = 0;
        SN.cooldownUntil = performance.now() + SN_CFG.cooldownAfterNav;
        snRender();
        if (wasOurNavigation && SN_CFG.scrollToTopOnNav && snEnabled()) window.scrollTo(0, 0);
    }

    function initSmartNavigation() {
        if (SN.installed) return;
        SN.installed = true;

        SN.wrap = document.createElement('div');
        SN.wrap.id = 'nh-scroll-progress-wrap';
        SN.bar = document.createElement('div');
        SN.bar.id = 'nh-scroll-progress-bar';
        SN.wrap.appendChild(SN.bar);
        document.body.appendChild(SN.wrap);

        window.addEventListener('wheel', (e) => {
            if (!snEnabled()) {
                if (SN.progress > 0) snReset();
                return;
            }
            if (SN.triggered || performance.now() < SN.cooldownUntil) return;
            if (e.target && e.target.closest && e.target.closest('.queue-panel, .nf-modal-overlay, .saved-search-extension')) return;

            if (e.deltaY > 0 && snAtBottom()) {
                snAccumulate(snWheelPixels(e));
            } else if (e.deltaY < 0) {
                snReset();
            }
        }, { passive: true });

        // ---------- Touch (mobile): swipe up at the bottom of the page ----------
        const skipTouchTarget = (t) => t && t.closest && t.closest('.queue-panel, .nf-modal-overlay, .saved-search-extension');

        window.addEventListener('touchstart', (e) => {
            if (e.touches.length !== 1) { SN.touching = false; return; }
            SN.touching = true;
            SN.touchX = e.touches[0].clientX;
            SN.touchY = e.touches[0].clientY;
        }, { passive: true });

        window.addEventListener('touchmove', (e) => {
            if (!SN.touching || e.touches.length !== 1) return;
            const t = e.touches[0];
            const dx = SN.touchX - t.clientX;
            const dy = SN.touchY - t.clientY;
            SN.touchX = t.clientX;
            SN.touchY = t.clientY;

            if (!snEnabled()) {
                if (SN.progress > 0) snReset();
                return;
            }
            if (SN.triggered || performance.now() < SN.cooldownUntil) return;
            if (skipTouchTarget(e.target)) return;
            if (Math.abs(dx) > Math.abs(dy)) return;

            const perPx = snThreshold() / numSetting('smartNavSwipe');
            if (dy > 0 && snAtBottom(SN_CFG.touchBottomTolerance)) {
                snAccumulate(dy * perPx);
            } else if (dy < 0 && SN.progress > 0) {
                SN.progress = Math.max(0, SN.progress + dy * perPx * 3);
                snRender();
            }
        }, { passive: true });

        const endTouch = () => {
            SN.touching = false;
            SN.lastWheel = performance.now();
        };
        window.addEventListener('touchend', endTouch, { passive: true });
        window.addEventListener('touchcancel', endTouch, { passive: true });

        window.addEventListener('scroll', () => {
            if (!SN.triggered && SN.progress > 0 && !snAtBottom(SN.touching ? SN_CFG.touchBottomTolerance : undefined)) snReset();
        }, { passive: true });

        window.addEventListener('pageshow', () => {
            SN.triggered = false;
            snReset();
        });
    }

    // ==========================================================================
    // 9. GALLERY PAGE FEATURES
    // ==========================================================================

    const CB = { btn: null, badge: null, icon: null, list: null, post: null, listObs: null, atComments: false, shown: null, count: -1, gid: null, apiCount: null, scrollRaf: 0, scrollBound: false };
    const cbTotals = new Map();
    const cbPending = new Set();

    const cbFailUntil = new Map();

    function cbFetchTotal(gid) {
        if (cbTotals.has(gid) || cbPending.has(gid)) return;
        if ((cbFailUntil.get(gid) || 0) > Date.now()) return;
        cbPending.add(gid);
        fetchRetry(`/api/v2/galleries/${gid}/comments/count`, { credentials: 'same-origin', headers: Object.assign({ accept: 'application/json' }, authHeaders()) }, 0, { wait: false })
            .then(r => (r.ok ? r.text() : Promise.reject(new Error(String(r.status)))))
            .then(t => {
            const n = parseInt(t, 10);
            if (Number.isFinite(n) && n >= 0) { cbTotals.set(gid, n); cbUpdateCount(); }
        })
            .catch(() => { cbFailUntil.set(gid, Date.now() + 60000); /* keep the DOM count */ })
            .finally(() => cbPending.delete(gid));
    }

    function cbFormat(n) {
        return n >= 10000 ? Math.round(n / 1000) + 'k' : String(n);
    }

    function cbUpdateIcon() {
        CB.scrollRaf = 0;
        if (!CB.post || !CB.post.isConnected) return;
        const at = CB.post.getBoundingClientRect().top < window.innerHeight * 0.6;
        if (at !== CB.atComments) { CB.atComments = at; cbSetIcon(); }
    }
    function cbOnScroll() {
        if (!CB.scrollRaf) CB.scrollRaf = requestAnimationFrame(cbUpdateIcon);
    }

    function cbSetIcon() {
        if (CB.icon) CB.icon.className = CB.atComments ? 'fa fa-arrow-up btn-icon' : 'fa fa-comments btn-icon';
    }

    function cbUpdateCount() {
        if (!CB.badge) return;
        const list = document.querySelector(SEL.commentList);
        const dom = list ? list.querySelectorAll(SEL.comment).length : 0;
        const api = CB.gid && cbTotals.has(CB.gid) ? cbTotals.get(CB.gid) : 0;
        const count = Math.max(dom, api);
        if (count === CB.count) return;
        CB.count = count;
        CB.badge.style.display = count > 0 ? 'flex' : 'none';
        CB.badge.textContent = cbFormat(count);
    }

    function cbCreate() {
        const btn = document.createElement('div');
        btn.className = 'comments-scroll-btn';
        btn.innerHTML = `
            <i class="fa fa-comments btn-icon"></i>
            <div class="comments-count" style="display: none;">0</div>
        `;
        btn.onclick = (e) => {
            e.preventDefault();
            const freshPost = document.querySelector(SEL.commentPost);
            if (!CB.atComments && freshPost) {
                freshPost.scrollIntoView({ behavior: 'smooth', block: 'start' });
            } else {
                window.scrollTo({ top: 0, behavior: 'smooth' });
            }
        };
        document.body.appendChild(btn);
        CB.btn = btn;
        CB.badge = btn.querySelector('.comments-count');
        CB.icon = btn.querySelector('.btn-icon');
        CB.count = -1;
        CB.shown = null;
    }

    function syncCommentsButton() {
        const onGallery = /^\/g\/\d+/.test(window.location.pathname);
        const post = onGallery ? document.querySelector(SEL.commentPost) : null;
        const list = onGallery ? document.querySelector(SEL.commentList) : null;
        const visible = !!(post && list);

        if (!visible) {
            if (CB.btn && CB.shown !== false) { CB.btn.style.display = 'none'; CB.shown = false; }
            return;
        }

        if (!CB.btn || !CB.btn.isConnected) cbCreate();
        if (CB.shown !== true) { CB.btn.style.display = ''; CB.shown = true; }

        if (CB.list !== list) {
            if (CB.listObs) CB.listObs.disconnect();
            CB.list = list;
            CB.listObs = new MutationObserver(cbUpdateCount);
            CB.listObs.observe(list, { childList: true, subtree: true });
        }

        if (!CB.scrollBound) {
            CB.scrollBound = true;
            window.addEventListener('scroll', cbOnScroll, { passive: true });
            window.addEventListener('resize', cbOnScroll, { passive: true });
        }
        if (CB.post !== post) {
            CB.post = post;
            CB.atComments = false;
            cbSetIcon();
            cbUpdateIcon();
        }

        const gid = window.location.pathname.match(/^\/g\/(\d+)/)[1];
        if (CB.gid !== gid) { CB.gid = gid; CB.count = -1; }
        cbFetchTotal(gid);

        cbUpdateCount();
    }

    function initGalleryPageFeatures() {
        const _gm = window.location.pathname.match(/\/g\/(\d+)/);
        document.querySelectorAll('.btn-queue-add').forEach(btn => {
            if (!_gm || btn.dataset.gid !== _gm[1] || !btn.isConnected) btn.remove();
        });

        const match = window.location.pathname.match(/\/g\/(\d+)/);
        if (!match) return;
        const btnContainer = document.querySelector(SEL.infoButtons);
        const searchInput = document.querySelector(`${SEL.searchForm} ${SEL.searchInput}`);
        if (!btnContainer) return;

        // 1. Queue Button
        if (settings.enableQueue && !document.querySelector('.btn-queue-add')) {
            const qBtn = document.createElement('button');
            qBtn.className = 'btn btn-secondary btn-queue-add';
            qBtn.innerHTML = '<i class="fa fa-clock"></i> Queue';

            const galleryId = window.location.href.match(/\/g\/(\d+)/)?.[1];

            if (galleryId) {
                if (isQueued(galleryId)) {
                    qBtn.innerHTML = '<i class="far fa-clock"></i> Remove';
                    qBtn.classList.add('in-queue');
                }

                qBtn.onclick = () => {
                    const title = document.querySelector(SEL.galleryTitle).textContent;
                    const coverImg = document.querySelector(SEL.bigCover);
                    const coverUrl = coverImg ? (coverImg.dataset.src || coverImg.src) : '';
                    toggleQueueItem(galleryId, title, coverUrl, window.location.href);
                };
                qBtn.dataset.gid = galleryId;
                btnContainer.appendChild(qBtn);
            }
        }

        // 2. Tag Selector
        if (!searchInput || document.querySelector('.btn-tag-selector') || !settings.enableTagSelect) return;
        if (!document.querySelector(SEL.tags)) return;

        let isSelectionMode = false;
        const toggleBtn = document.createElement('button');
        toggleBtn.className = 'btn btn-secondary btn-tag-selector';
        toggleBtn.innerHTML = '<i class="fa fa-tags"></i> Tag Select';
        toggleBtn.type = 'button';
        const queueBtn = btnContainer.querySelector('.btn-queue-add');
        if (queueBtn) {
            btnContainer.insertBefore(toggleBtn, queueBtn);
        } else {
            btnContainer.appendChild(toggleBtn);
        }

        const tagsContainer = document.querySelector(SEL.tags);
        tagsContainer.addEventListener('click', (e) => {
            if (!isSelectionMode) return;
            const tagLink = e.target.closest(SEL.tagLink);
            if (tagLink) {
                e.preventDefault(); e.stopPropagation();
                if (!tagLink.href.includes('q=pages')) {
                    tagLink.classList.toggle('tag-selected');
                    updateSearchBar();
                }
            }
        }, true);

        toggleBtn.onclick = () => {
            isSelectionMode = !isSelectionMode;
            if (isSelectionMode) {
                toggleBtn.classList.add('is-active');
                tagsContainer.classList.add('tags-selecting-mode');
                document.body.classList.add('tag-select-active');
                toggleBtn.innerHTML = '<i class="fa fa-check"></i> Done';
                window.scrollTo({ top: 0 });
            } else {
                toggleBtn.classList.remove('is-active');
                tagsContainer.classList.remove('tags-selecting-mode');
                document.body.classList.remove('tag-select-active');
                toggleBtn.innerHTML = '<i class="fa fa-tags"></i> Tag Select';
            }
        };

        function updateSearchBar() {
            const selectedTags = tagsContainer.querySelectorAll(SEL.tagLinkSelected);
            const queryTerms = Array.from(selectedTags).map(tag => {
                const nameSpan = tag.querySelector(SEL.tagName);
                let tagName = nameSpan ? nameSpan.textContent.trim() : '';
                const href = tag.getAttribute('href');

                if (href.includes('/tag/')) return `tag:"${tagName}"`;
                if (href.includes('/artist/')) return `artist:"${tagName}"`;
                if (href.includes('/group/')) return `group:"${tagName}"`;
                if (href.includes('/parody/')) return `parody:"${tagName}"`;
                if (href.includes('/character/')) return `character:"${tagName}"`;
                if (href.includes('/language/')) return `language:"${tagName}"`;
                if (href.includes('/category/')) return `category:"${tagName}"`;
                return `"${tagName}"`;
            });
            searchInput.value = queryTerms.join(' ');
            searchInput.dispatchEvent(new Event('input'));
        }

        syncCommentsButton();
    }

    function syncFavoriteButtonState() {
        const btn = document.querySelector(SEL.favoriteButton);
        const id = window.location.pathname.match(/^\/g\/(\d+)/)?.[1];
        if (!btn || !id) return;

        const label = (btn.querySelector(SEL.favoriteLabel) || btn).textContent.trim().toLowerCase();
        if (label.includes('unfavorite')) addFav(id); else removeFav(id);
    }

    function initFavoriteButtonWatcher() {
        const btn = document.querySelector(SEL.favoriteButton);
        const id = window.location.pathname.match(/^\/g\/(\d+)/)?.[1];
        if (!btn || !id) return;

        if (btn.dataset.nfFavFor !== id) {
            btn.dataset.nfFavFor = id;
            syncFavoriteButtonState();
        }
        if (!btn.dataset.nfFavWatch) {
            btn.dataset.nfFavWatch = '1';
            new MutationObserver(syncFavoriteButtonState).observe(btn, { childList: true, characterData: true, subtree: true });
        }
    }

    // ==========================================================================
    // 10. SAVED SEARCHES
    // ==========================================================================

    let searchStorageHandler = null;

    function initSearchFlow() {
        if (!settings.enableSavedSearch) return;
        const form = document.querySelector(SEL.searchForm);
        const nav = form ? form.closest(SEL.nav) : document.querySelector(SEL.nav);

        if (!form || !nav || form.dataset.savedInit) return;
        form.dataset.savedInit = '1';

        const input = form.querySelector(SEL.searchInput);
        if (input) input.style.paddingRight = '40px';

        const SEP_MARKER = '---SEP---';
        let searchData = readJSON('nhentai_search_flow', { saved: [] });
        if (!searchData || !Array.isArray(searchData.saved)) searchData = { saved: [] };

        let isDeleteMode = false;
        let isReorderMode = false;
        let pendingDeletes = new Set();

        const saveSearch = () => localStorage.setItem('nhentai_search_flow', JSON.stringify(searchData));

        const toggleSavedItem = (query) => {
            if (!query) return;
            const idx = searchData.saved.indexOf(query);
            if (idx > -1) searchData.saved.splice(idx, 1);
            else searchData.saved.push(query);
            saveSearch();
            renderBar();
        };

        const addSeparator = () => {
            searchData.saved.push(SEP_MARKER);
            saveSearch();
            renderBar();

            const list = barContainer.querySelector('.sse-list');
            if (list) list.scrollTop = list.scrollHeight;
        };

        form.querySelectorAll('.search-saved-trigger').forEach(el => el.remove());
        document.querySelectorAll('.saved-search-extension').forEach(el => el.remove());

        const trigger = document.createElement('div');
        trigger.className = 'search-saved-trigger';
        trigger.innerHTML = '<i class="far fa-clipboard"></i>';
        trigger.title = "Saved Searches";
        form.appendChild(trigger);

        const barContainer = document.createElement('div');
        barContainer.className = 'saved-search-extension';
        nav.parentNode.insertBefore(barContainer, nav.nextSibling);

        const renderBar = () => {
            let prevScroll = 0;
            const existingList = barContainer.querySelector('.sse-list');
            if (existingList) prevScroll = existingList.scrollTop;

            const currentQ = input ? input.value.trim() : '';
            const baseQ = currentQ.replace(/(^|\s)-/g, '$1').trim();
            const isCurrentSaved = baseQ && searchData.saved.includes(baseQ);

            let listClass = 'sse-list';
            if (isDeleteMode) listClass += ' delete-mode';
            if (isReorderMode) listClass += ' reorder-mode';

            let actionButtons = '';
            let saveCurrentBtn = '';

            if (isDeleteMode) {
                actionButtons = `
                    ${pendingDeletes.size > 0 ? `
                        <button class="btn-sse-save" id="btn-confirm-del" style="margin-right: 5px;">Save Changes</button>
                    ` : ''}
                    <button class="btn-sse-edit" id="btn-cancel-del">Cancel</button>
                `;
            } else if (isReorderMode) {
                actionButtons = `
                    <button class="btn-sse-save" id="btn-finish-reorder" style="margin-right: 5px;">
                        Save
                    </button>
                    <button class="btn-sse-edit" id="btn-add-sep">Add Separator</button>
                `;
            } else {
                if (searchData.saved.length > 0) {
                    actionButtons = `
                        <button class="btn-sse-edit" id="btn-toggle-reorder" style="margin-right: 5px;" title="Organize items">
                            <i class="fa fa-sort"></i> Reorder
                        </button>
                        <button class="btn-sse-edit" id="btn-toggle-edit" title="Delete items">
                            <i class="fas fa-trash-alt"></i> Delete
                        </button>
                    `;
                }

                if (!isCurrentSaved && currentQ) {
                    saveCurrentBtn = `<button class="btn-sse-save" id="btn-save-curr-bar" style="margin-right:5px"><i class="fa fa-plus"></i> Save</button>`;
                }
            }

            let html = `
                <div class="sse-inner">
                    <div class="sse-header">
                        <div style="display:flex; gap:5px; align-items:center;">
                             ${saveCurrentBtn}
                        </div>
                        <div class="sse-actions">
                            ${actionButtons}
                        </div>
                    </div>
                    <div class="${listClass}">
            `;

            if (searchData.saved.length === 0) {
                html += `<div class="sse-empty">No saved searches yet. Build a search and click Save to add it here.</div>`;
            } else {
                searchData.saved.forEach((q, index) => {
                    if (q === SEP_MARKER) {
                        const isMarked = pendingDeletes.has(index.toString());
                        const deleteStyle = isMarked ? 'to-delete' : '';
                        const draggableAttr = isReorderMode ? 'draggable="true"' : '';

                        html += `<div class="ss-separator sse-item ${deleteStyle}" ${draggableAttr} data-index="${index}" data-type="sep"></div>`;
                    }
                    else {
                        const isCurrent = q === currentQ ? 'is-current' : '';
                        const isMarked = pendingDeletes.has(q);
                        const deleteStyle = isMarked ? 'to-delete' : '';
                        const safeQ = q.replace(/"/g, '&quot;');
                        const displayLabel = (q.match(/(?:[^\s"]+|"[^"]*")+/g) || [])
                        .map(term => term.replace(/\b[a-z-]+:/g, '').replace(/"/g, '').trim())
                        .filter(Boolean)
                        .join(' / ');
                        const categoryMatch = q.match(/^-?(tag|artist|parody|character|group|language|category):/i);
                        const pillCategory = categoryMatch ? categoryMatch[1].toLowerCase() : 'tag';

                        let tooltip = safeQ;
                        const draggableAttr = isReorderMode ? 'draggable="true"' : '';

                        const escapedTerm = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                        const positiveRegex = new RegExp(`(^|\\s)${escapedTerm}(?=\\s|$)`);
                        const negativeRegex = new RegExp(`(^|\\s)-${escapedTerm}(?=\\s|$)`);

                        let iconClass = 'fa-plus';
                        let btnStateClass = '';

                        if (negativeRegex.test(currentQ)) {
                            iconClass = 'fa-times';
                            btnStateClass = 'ss-add-negative';
                        } else if (positiveRegex.test(currentQ)) {
                            iconClass = 'fa-minus';
                            btnStateClass = 'ss-add-positive';
                        }

                        html += `
                            <div class="ss-pill sse-item ${isCurrent} ${deleteStyle}" ${draggableAttr} data-index="${index}" data-type="pill" data-category="${pillCategory}">
                                <div class="ss-part ss-text" data-full-query="${safeQ}" title="${tooltip}">${displayLabel}</div>
                                <div class="ss-part ss-add ${btnStateClass}" data-q="${safeQ}"><i class="fa ${iconClass}" style="font-size: 10px;"></i></div>
                            </div>
                        `;
                    }
                });
            }
            html += `</div></div>`;

            barContainer.innerHTML = html;

            const newList = barContainer.querySelector('.sse-list');
            if (newList) newList.scrollTop = prevScroll;


            if (isReorderMode) {
                const listElement = barContainer.querySelector('.sse-list');

                if (listElement.dataset.dndInit) return;
                listElement.dataset.dndInit = '1';

                let draggedIndex = null;
                let currentDropTarget = null;

                // 1.1 DESKTOP (Event Delegation)
                listElement.addEventListener('dragstart', (e) => {
                    const item = e.target.closest('.sse-item');
                    if (!item) return;
                    draggedIndex = parseInt(item.dataset.index);
                    item.classList.add('is-dragging');
                    e.dataTransfer.effectAllowed = 'move';
                });

                listElement.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    const item = e.target.closest('.sse-item');

                    if (item && item !== currentDropTarget) {
                        if (currentDropTarget) currentDropTarget.classList.remove('drag-over');

                        const targetIndex = parseInt(item.dataset.index);
                        if (targetIndex !== draggedIndex) {
                            item.classList.add('drag-over');
                            currentDropTarget = item;
                        } else {
                            currentDropTarget = null;
                        }
                    }
                });

                listElement.addEventListener('dragend', (e) => {
                    const item = e.target.closest('.sse-item');
                    if (item) item.classList.remove('is-dragging');
                    if (currentDropTarget) {
                        currentDropTarget.classList.remove('drag-over');
                        currentDropTarget = null;
                    }
                });

                listElement.addEventListener('drop', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    if (!currentDropTarget || draggedIndex === null) return;

                    const targetIndex = parseInt(currentDropTarget.dataset.index);
                    if (draggedIndex !== targetIndex) {
                        const itemToMove = searchData.saved.splice(draggedIndex, 1)[0];
                        searchData.saved.splice(targetIndex, 0, itemToMove);
                        saveSearch();
                        renderBar();
                    }
                    draggedIndex = null;
                });

                // 1.2 MOBILE (Event Delegation + rAF Throttling)
                let touchSourceItem = null;
                let longPressTimer = null;
                let isDragActive = false;
                let autoScrollRafId = null;
                let scrollDirection = 0;
                let lastTouchY = 0;
                let rafId = null;

                const scrollLoop = () => {
                    if (!isDragActive || !listElement || scrollDirection === 0) {
                        autoScrollRafId = null;
                        return;
                    }
                    const speed = 8; // 60fps, 8px = ~480px per second.
                    listElement.scrollTop += (speed * scrollDirection);

                    autoScrollRafId = requestAnimationFrame(scrollLoop);
                };

                const handleAutoScroll = () => {
                    if (!isDragActive || !listElement) return;

                    const rect = listElement.getBoundingClientRect();
                    const threshold = 50;

                    if (lastTouchY < rect.top + threshold) {
                        scrollDirection = -1;
                    } else if (lastTouchY > rect.bottom - threshold) {
                        scrollDirection = 1;
                    } else {
                        scrollDirection = 0;
                    }

                    if (scrollDirection !== 0 && !autoScrollRafId) {
                        autoScrollRafId = requestAnimationFrame(scrollLoop);
                    }
                };

                const stopAutoScroll = () => {
                    scrollDirection = 0;
                    if (autoScrollRafId) {
                        cancelAnimationFrame(autoScrollRafId);
                        autoScrollRafId = null;
                    }
                };

                listElement.addEventListener('touchstart', (e) => {
                    const item = e.target.closest('.sse-item');
                    if (!item) return;

                    touchSourceItem = item;
                    draggedIndex = parseInt(item.dataset.index);
                    item.classList.add('is-pressing');

                    longPressTimer = setTimeout(() => {
                        isDragActive = true;
                        item.classList.remove('is-pressing');
                        item.classList.add('is-dragging');
                        if (navigator.vibrate) navigator.vibrate(50);
                        document.body.style.overflow = 'hidden';
                    }, 500);
                }, { passive: true });

                listElement.addEventListener('touchmove', (e) => {
                    if (!isDragActive) {
                        clearTimeout(longPressTimer);
                        if (touchSourceItem) touchSourceItem.classList.remove('is-pressing');
                        return;
                    }

                    e.preventDefault();
                    const touch = e.touches[0];
                    lastTouchY = touch.clientY;
                    handleAutoScroll();

                    if (!rafId) {
                        rafId = requestAnimationFrame(() => {
                            const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
                            rafId = null;

                            if (!elementBelow) return;
                            const targetItem = elementBelow.closest('.sse-item');

                            if (targetItem !== currentDropTarget) {
                                if (currentDropTarget) currentDropTarget.classList.remove('drag-over');

                                if (targetItem && targetItem !== touchSourceItem) {
                                    targetItem.classList.add('drag-over');
                                    currentDropTarget = targetItem;
                                } else {
                                    currentDropTarget = null;
                                }
                            }
                        });
                    }
                }, { passive: false });

                const cleanupMobileDrag = () => {
                    clearTimeout(longPressTimer);
                    stopAutoScroll();
                    document.body.style.overflow = '';
                    if (rafId) { cancelAnimationFrame(rafId); rafId = null; }

                    if (touchSourceItem) touchSourceItem.classList.remove('is-pressing', 'is-dragging');
                    if (currentDropTarget) {
                        currentDropTarget.classList.remove('drag-over');
                        currentDropTarget = null;
                    }
                    isDragActive = false;
                    touchSourceItem = null;
                };

                listElement.addEventListener('touchend', (e) => {
                    if (isDragActive && currentDropTarget && draggedIndex !== null) {
                        const targetIndex = parseInt(currentDropTarget.dataset.index);
                        if (targetIndex !== draggedIndex) {
                            const itemToMove = searchData.saved.splice(draggedIndex, 1)[0];
                            searchData.saved.splice(targetIndex, 0, itemToMove);
                            saveSearch();
                            renderBar();
                        }
                    }
                    cleanupMobileDrag();
                    draggedIndex = null;
                });

                listElement.addEventListener('touchcancel', cleanupMobileDrag);
            }

            // 2. Control Buttons Handlers
            const btnDeleteMode = barContainer.querySelector('#btn-toggle-edit');
            if (btnDeleteMode) {
                btnDeleteMode.onclick = (e) => {
                    e.stopPropagation();
                    isDeleteMode = true;
                    isReorderMode = false;
                    pendingDeletes.clear();
                    renderBar();
                };
            }

            const btnReorderMode = barContainer.querySelector('#btn-toggle-reorder');
            if (btnReorderMode) {
                btnReorderMode.onclick = (e) => {
                    e.stopPropagation();
                    isReorderMode = true;
                    isDeleteMode = false;
                    renderBar();
                };
            }

            const btnFinishReorder = barContainer.querySelector('#btn-finish-reorder');
            if (btnFinishReorder) {
                btnFinishReorder.onclick = (e) => {
                    e.stopPropagation();
                    isReorderMode = false;
                    renderBar();
                };
            }

            const btnConfirm = barContainer.querySelector('#btn-confirm-del');
            if (btnConfirm) {
                btnConfirm.onclick = (e) => {
                    e.stopPropagation();
                    if (pendingDeletes.size > 0) {
                        const newSaved = [];
                        searchData.saved.forEach((item, idx) => {
                            if (item === SEP_MARKER) {
                                if (!pendingDeletes.has(idx.toString())) newSaved.push(item);
                            } else {
                                if (!pendingDeletes.has(item)) newSaved.push(item);
                            }
                        });
                        searchData.saved = newSaved;
                        saveSearch();
                    }
                    isDeleteMode = false;
                    pendingDeletes.clear();
                    renderBar();
                };
            }

            const btnCancel = barContainer.querySelector('#btn-cancel-del');
            if (btnCancel) {
                btnCancel.onclick = (e) => {
                    e.stopPropagation();
                    isDeleteMode = false;
                    pendingDeletes.clear();
                    renderBar();
                };
            }

            const btnSave = barContainer.querySelector('#btn-save-curr-bar');
            if (btnSave) {
                btnSave.onclick = (e) => {
                    e.stopPropagation();
                    toggleSavedItem(baseQ);
                };
            }

            const btnAddSep = barContainer.querySelector('#btn-add-sep');
            if (btnAddSep) {
                btnAddSep.onclick = (e) => {
                    e.stopPropagation();
                    addSeparator();
                };
            }

            // 3. Event Delegation
            barContainer.onclick = (e) => {
                const addBtn = e.target.closest('.ss-add');
                if (addBtn) {
                    e.stopPropagation();
                    if (isDeleteMode || isReorderMode) return;

                    const queryToAdd = addBtn.dataset.q.replace(/&quot;/g, '"');
                    let currentVal = input.value.trim();

                    const escapedTerm = queryToAdd.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    const positiveRegex = new RegExp(`(^|\\s)${escapedTerm}(?=\\s|$)`);
                    const negativeRegex = new RegExp(`(^|\\s)-${escapedTerm}(?=\\s|$)`);

                    if (negativeRegex.test(currentVal)) {
                        currentVal = currentVal.replace(negativeRegex, '').replace(/\s{2,}/g, ' ').trim();
                    } else if (positiveRegex.test(currentVal)) {
                        currentVal = currentVal.replace(positiveRegex, `$1-${queryToAdd}`).trim();
                    } else {
                        currentVal = currentVal ? currentVal + ' ' + queryToAdd : queryToAdd;
                    }

                    input.value = currentVal;
                    input.focus();

                    renderBar();
                    input.dispatchEvent(new Event('input'));
                    return;
                }

                const itemEl = e.target.closest('.sse-item');
                if (itemEl && !isReorderMode) {
                    e.stopPropagation();
                    const type = itemEl.dataset.type;
                    const index = itemEl.dataset.index;

                    if (isDeleteMode) {
                        const key = type === 'sep' ? index.toString() : itemEl.querySelector('.ss-text').dataset.fullQuery;
                        if (pendingDeletes.has(key)) pendingDeletes.delete(key);
                        else pendingDeletes.add(key);
                        renderBar();
                    } else if (type === 'pill') {
                        const fullQuery = itemEl.querySelector('.ss-text').dataset.fullQuery;
                        if(fullQuery) window.location.href = `/search/?q=${encodeURIComponent(fullQuery)}`;
                    }
                }
            };
        };

        if (searchStorageHandler) window.removeEventListener('storage', searchStorageHandler);
        searchStorageHandler = (e) => {
            if (e.key === 'nhentai_search_flow' && e.newValue) {
                searchData = JSON.parse(e.newValue);
                renderBar();
            }
        };
        window.addEventListener('storage', searchStorageHandler);

        trigger.onclick = (e) => {
            e.stopPropagation();
            const isVisible = barContainer.classList.contains('is-visible');

            if (!isVisible) {
                isDeleteMode = false;
                isReorderMode = false;
                pendingDeletes.clear();
                renderBar();
                barContainer.classList.add('is-visible');
                trigger.classList.add('is-active');
            } else {
                barContainer.classList.remove('is-visible');
                trigger.classList.remove('is-active');
            }
        };

        form.addEventListener('click', (e) => {
            if (!e.target.closest('button.ac-clear')) return;

            setTimeout(() => {
                clearTimeout(renderTimeout);
                renderBar();
            }, 0);
        }, true);

        let renderTimeout;
        if (input) {
            input.addEventListener('input', () => {
                if (!barContainer.classList.contains('is-visible')) return;

                clearTimeout(renderTimeout);
                renderTimeout = setTimeout(() => {
                    renderBar();
                }, 300);
            });
        }
    }

    // ==========================================================================
    // 11. CUSTOM CONTEXT MENU
    // ==========================================================================

    function initContextMenu() {
        if (document.body.dataset.contextMenuInit) return;
        document.body.dataset.contextMenuInit = '1';

        const menu = document.createElement('div');
        menu.className = 'nh-context-menu';
        document.body.appendChild(menu);

        let currentTargetId = null;
        let currentTargetUrl = null;
        let currentTargetTitle = '';

        document.addEventListener('contextmenu', (e) => {
            if (!settings.enableContextMenu) return;

            const gallery = e.target.closest(SEL.gallery);
            if (!gallery) {
                closeMenu();
                return;
            }

            const link = gallery.querySelector(SEL.cover);
            if (!link) return;

            e.preventDefault();

            currentTargetId = gallery.dataset.gid;
            currentTargetUrl = link.href;
            currentTargetTitle = gallery.querySelector(SEL.caption)?.textContent.trim()
            || metaCache.get(currentTargetId)?.title
            || '';

            renderMenuOptions(menu);
            menu.classList.add('is-visible');
            positionMenu(e.clientX, e.clientY);
        });

        window.addEventListener('click', (e) => {
            if (menu.contains(e.target)) return;
            closeMenu();
        }, true);

        ['scroll', 'resize'].forEach(evt => window.addEventListener(evt, closeMenu));

        function closeMenu() {
            menu.classList.remove('is-visible');
        }

        function positionMenu(x, y) {
            const margin = 8;
            const rect = menu.getBoundingClientRect();

            menu.style.left = `${Math.max(margin, Math.min(x, window.innerWidth - rect.width - margin))}px`;
            menu.style.top = `${Math.max(margin, Math.min(y, window.innerHeight - rect.height - margin))}px`;
        }

        function openSubmenu(group) {
            menu.querySelectorAll('.nh-cm-group').forEach(item => {
                const isOpen = item === group;
                item.classList.toggle('is-open', isOpen);
                item.querySelector('.nh-cm-parent').setAttribute('aria-expanded', String(isOpen));
            });

            const submenu = group.querySelector('.nh-cm-submenu');
            const row = group.getBoundingClientRect();
            const rect = submenu.getBoundingClientRect();
            const margin = 8;
            let left = row.right;

            if (left + rect.width > window.innerWidth - margin) {
                left = row.left - rect.width;
            }

            submenu.style.left = `${Math.max(margin, Math.min(left, window.innerWidth - rect.width - margin))}px`;
            submenu.style.top = `${Math.max(margin, Math.min(row.top - 10, window.innerHeight - rect.height - margin))}px`;
        }

        function renderMenuOptions(menuElement) {

            const galleryState = states.get(currentTargetId);

            const currentPage = galleryState ? galleryState.curr : 1;

            const readUrl = `/g/${currentTargetId}/${currentPage}/`;

            const readLabel = currentPage > 1 ? `Read from page ${currentPage}` : `Read now`;

            menuElement.innerHTML = `
            <div class="nh-cm-item" id="cm-new-tab">
                Open in new tab
            </div>
            <div class="nh-cm-item" id="cm-read-now">
                ${readLabel}
            </div>
            <div class="nh-cm-separator"></div>
            <div class="nh-cm-group" id="cm-explore">
                <button type="button" class="nh-cm-item nh-cm-parent"
                        aria-expanded="false" aria-controls="cm-explore-submenu">
                    <span>Explore</span>
                    <i class="fa fa-angle-right" aria-hidden="true"></i>
                </button>

                <div class="nh-cm-submenu nh-cm-explore-submenu" id="cm-explore-submenu">
                    <div class="nh-cm-status">Loading...</div>
                </div>
            </div>
            <div class="nh-cm-group" id="cm-copy">
                <button type="button" class="nh-cm-item nh-cm-parent"
                        aria-expanded="false" aria-controls="cm-copy-submenu">
                    <span>Copy</span>
                    <i class="fa fa-angle-right" aria-hidden="true"></i>
                </button>

                <div class="nh-cm-submenu" id="cm-copy-submenu">
                    <button type="button" class="nh-cm-item" data-copy="link">Link</button>
                    <button type="button" class="nh-cm-item" data-copy="title">Title</button>
                    <button type="button" class="nh-cm-item" data-copy="id">ID: ${currentTargetId}</button>
                </div>
            </div>
            <div class="nh-cm-footer" id="cm-close-hint">
                Shift + right-click to open system menu
            </div>
        `;

            menuElement.querySelector('#cm-read-now').onclick = () => {
                window.location.href = readUrl;
                closeMenu();
            };

            menuElement.querySelector('#cm-new-tab').onclick = () => {
                if (typeof GM_openInTab !== 'undefined') {
                    GM_openInTab(currentTargetUrl, { active: false, insert: true });
                } else {
                    window.open(currentTargetUrl, '_blank');
                }
                closeMenu();
            };

            const copyGroup = menuElement.querySelector('#cm-copy');
            const copyButton = copyGroup.querySelector('.nh-cm-parent');

            const copyValues = {
                link: currentTargetUrl,
                title: currentTargetTitle,
                id: currentTargetId
            };

            const closeCopy = () => {
                copyGroup.classList.remove('is-open');
                copyButton.setAttribute('aria-expanded', 'false');
            };

            copyGroup.onmouseenter = () => openSubmenu(copyGroup);

            copyGroup.onmouseleave = () => {
                if (!copyGroup.contains(document.activeElement)) closeCopy();
            };

            copyButton.onclick = copyButton.onfocus = () => openSubmenu(copyGroup);

            copyGroup.onfocusout = (e) => {
                if (!copyGroup.contains(e.relatedTarget)) closeCopy();
            };

            copyGroup.querySelectorAll('[data-copy]').forEach(button => {
                const text = copyValues[button.dataset.copy] || '';
                button.disabled = !text;

                button.onclick = async (e) => {
                    e.stopPropagation();
                    if (!text) return;

                    closeMenu();

                    try {
                        await navigator.clipboard.writeText(text);
                    } catch {
                        window.prompt('Copy manually:', text);
                    }
                };
            });

            const exploreGroup = menuElement.querySelector('#cm-explore');
            const exploreButton = exploreGroup.querySelector('.nh-cm-parent');
            const exploreSubmenu = exploreGroup.querySelector('.nh-cm-submenu');
            const exploreId = String(currentTargetId);

            let exploreLoading = false;
            let exploreLoaded = false;

            const exploreMessage = (text) => {
                const message = document.createElement('div');
                message.className = 'nh-cm-status';
                message.textContent = text;
                exploreSubmenu.replaceChildren(message);
            };

            const closeExplore = () => {
                exploreGroup.classList.remove('is-open');
                exploreButton.setAttribute('aria-expanded', 'false');
            };

            const showExplore = async () => {
                if (!exploreLoading && !exploreLoaded) exploreMessage('Loading...');

                openSubmenu(exploreGroup);

                if (exploreLoading || exploreLoaded) return;
                exploreLoading = true;

                try {
                    const meta = await getMeta(exploreId);

                    if (!exploreGroup.isConnected) return;
                    if (!meta || !Array.isArray(meta.tags)) {
                        throw new Error('Gallery metadata unavailable');
                    }

                    const fragment = document.createDocumentFragment();

                    for (const [type, label] of [['artist', 'Artist'], ['group', 'Group']]) {
                        const tags = meta.tags.filter(tag =>
                                                      tag.type === type && tag.name && tag.slug
                                                     );

                        if (!tags.length) continue;

                        const heading = document.createElement('div');
                        heading.className = 'nh-cm-section';
                        heading.textContent = label;
                        fragment.appendChild(heading);

                        for (const tag of tags) {
                            const link = document.createElement('a');
                            link.className = 'nh-cm-item nh-cm-explore-link';
                            link.textContent = tag.name;
                            link.href = `/${type}/${encodeURIComponent(tag.slug)}/`;

                            link.onclick = (e) => {
                                e.stopPropagation();
                                closeMenu();
                            };

                            fragment.appendChild(link);
                        }
                    }

                    if (fragment.childNodes.length) {
                        exploreSubmenu.replaceChildren(fragment);
                    } else {
                        exploreMessage('No artist or group listed.');
                    }

                    exploreLoaded = true;
                } catch {
                    if (exploreGroup.isConnected) {
                        exploreMessage('Unable to load. Try again later.');
                    }
                } finally {
                    exploreLoading = false;

                    if (exploreGroup.isConnected
                        && menuElement.classList.contains('is-visible')
                        && exploreGroup.classList.contains('is-open')) {
                        openSubmenu(exploreGroup);
                    }
                }
            };

            exploreGroup.onmouseenter = showExplore;
            exploreButton.onclick = exploreButton.onfocus = showExplore;

            exploreGroup.onmouseleave = () => {
                if (!exploreGroup.contains(document.activeElement)) closeExplore();
            };

            exploreGroup.onfocusout = (e) => {
                if (!exploreGroup.contains(e.relatedTarget)) closeExplore();
            };

            menuElement.querySelector('#cm-close-hint').onclick = () => {
                closeMenu();
            };
        }
    }

    // ==========================================================================
    // 12. GLOBAL SHORTCUTS
    // ==========================================================================

    let shortcutsInstalled = false;

    function getPrimarySearchInput() {
        return document.querySelector(`${SEL.nav} ${SEL.searchForm} ${SEL.searchInput}`) || document.querySelector(`${SEL.searchForm} ${SEL.searchInput}`);
    }

    function initGlobalShortcuts() {
        const input = getPrimarySearchInput();
        const form = input && input.closest('form');
        if (form && !form.querySelector('.search-slash-hint')) {
            const hint = document.createElement('div');
            hint.className = 'search-slash-hint';
            hint.textContent = 'Type / to search';
            form.insertBefore(hint, input.nextSibling);
        }

        if (shortcutsInstalled) return;
        shortcutsInstalled = true;

        document.addEventListener('keydown', (e) => {
            const target = e.target;
            if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) {
                if (e.key === 'Escape') target.blur();
                return;
            }
            if (e.ctrlKey || e.altKey || e.metaKey || e.isComposing) return;

            if (e.key === '/') {
                const current = getPrimarySearchInput();
                if (current) {
                    e.preventDefault();
                    current.focus();
                    current.select();
                }
            }
        });
    }

    // ==========================================================================
    // 13. DATA MANAGEMENT (BACKUP/RESTORE)
    // ==========================================================================

    function exportData() {
        const data = {
            settings: readJSON('nhentai_flow_settings', {}),
            queue: readJSON('nhentai_queue_v1', []),
            savedSearches: readJSON('nhentai_search_flow', { saved: [] })
        };

        const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        const timestamp = new Date().toISOString().slice(0, 10);
        a.href = url;
        a.download = `nh-flow-backup-${timestamp}.json`;
        document.body.appendChild(a);
        a.click();

        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    function importData() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json,application/json';

        const isObject = value =>
        value !== null &&
              typeof value === 'object' &&
              !Array.isArray(value);

        const has = (object, key) =>
        Object.prototype.hasOwnProperty.call(object, key);

        function validate(data) {
            if (!isObject(data)) {
                throw new Error('The file must contain a JSON object.');
            }

            if (!['settings', 'queue', 'savedSearches'].some(key => has(data, key))) {
                throw new Error('No backup categories were found.');
            }

            if (has(data, 'settings')) {
                if (!isObject(data.settings)) {
                    throw new Error('Invalid settings.');
                }

                for (const key of Object.keys(DEFAULT_SETTINGS)) {
                    if (!has(data.settings, key)) continue;

                    const value = data.settings[key];
                    const expected = typeof DEFAULT_SETTINGS[key];

                    if (
                        typeof value !== expected ||
                        (expected === 'number' && !Number.isFinite(value))
                    ) {
                        throw new Error(`Invalid setting: ${key}`);
                    }
                }
            }

            if (has(data, 'queue')) {
                if (
                    !Array.isArray(data.queue) ||
                    !data.queue.every(item =>
                                      isObject(item) &&
                                      ['string', 'number'].includes(typeof item.id) &&
                                      /^[1-9]\d*$/.test(String(item.id)) &&
                                      ['title', 'coverUrl', 'galleryUrl'].every(
                        key => typeof item[key] === 'string'
                    )
                                     )
                ) {
                    throw new Error('Invalid reading queue.');
                }
            }

            if (has(data, 'savedSearches')) {
                if (
                    !isObject(data.savedSearches) ||
                    !Array.isArray(data.savedSearches.saved) ||
                    !data.savedSearches.saved.every(item => typeof item === 'string')
                ) {
                    throw new Error('Invalid saved searches.');
                }
            }
        }

        function chooseMode() {
            return new Promise(resolve => {
                const previousFocus = document.activeElement;
                const overlay = document.createElement('div');
                overlay.className = 'nf-modal-overlay active';

                overlay.innerHTML = `
                <div class="nf-modal" role="dialog" aria-modal="true"
                     aria-labelledby="nf-import-title" tabindex="-1">
                    <div class="nf-modal-header">
                        <h2 id="nf-import-title">Import backup</h2>
                    </div>

                    <div class="nf-modal-body">
                        <p><strong>Merge:</strong> keeps current queue items and adds new ones without duplicate IDs. Imported searches, including separators and duplicates, are appended to the end. Current settings are preserved.</p>

                        <p><strong>Replace:</strong> overwrites your current reading queue, saved searches, and settings with the data from the backup.</p>

                        <p><strong>Note:</strong> Merging the same file again will duplicate the imported block of saved searches.</p>
                    </div>

                    <div class="nf-modal-footer"
                         style="justify-content:end">
                        <div class="nf-footer-actions"
                             style="flex-wrap:wrap">
                            <button type="button" class="nf-btn nf-btn-ghost"
                                    data-mode="cancel">Cancel</button>
                            <button type="button" class="nf-btn"
                                    data-mode="replace">Replace</button>
                            <button type="button" class="nf-btn nf-btn-primary"
                                    data-mode="merge">Merge</button>
                        </div>
                    </div>
                </div>
            `;

                function finish(mode) {
                    document.removeEventListener('keydown', onKey, true);
                    overlay.remove();

                    if (previousFocus && previousFocus.isConnected) {
                        previousFocus.focus({ preventScroll: true });
                    }

                    resolve(mode);
                }

                function onKey(event) {
                    if (event.key === 'Escape') {
                        event.preventDefault();
                        event.stopImmediatePropagation();
                        finish('cancel');
                    } else if (event.key === 'Tab') {
                        const buttons = [...overlay.querySelectorAll('button')];
                        const index = buttons.indexOf(document.activeElement);
                        const next = event.shiftKey
                        ? (index <= 0 ? buttons.length - 1 : index - 1)
                        : (index + 1) % buttons.length;

                        event.preventDefault();
                        event.stopImmediatePropagation();
                        buttons[next].focus();
                    }
                }

                overlay.querySelectorAll('[data-mode]').forEach(button => {
                    button.onclick = () => finish(button.dataset.mode);
                });

                overlay.addEventListener('mousedown', event => {
                    if (event.target === overlay) finish('cancel');
                });

                document.addEventListener('keydown', onKey, true);
                document.body.appendChild(overlay);
                overlay.querySelector('[data-mode="merge"]').focus();
            });
        }

        input.onchange = async () => {
            const file = input.files[0];
            if (!file) return;

            let data;

            try {
                data = JSON.parse(await file.text());
                validate(data);
            } catch (error) {
                alert(`Failed to read backup: ${error.message}`);
                return;
            }

            const mode = await chooseMode();
            if (mode === 'cancel') return;

            const merge = mode === 'merge';
            const updates = new Map();

            if (has(data, 'settings')) {
                const current = readJSON('nhentai_flow_settings', {});
                const imported = data.settings;

                const result = {};

                for (const key of Object.keys(DEFAULT_SETTINGS)) {
                    if (merge && isObject(current) && has(current, key)) {
                        result[key] = current[key];
                    } else if (has(imported, key)) {
                        result[key] = imported[key];
                    }
                }

                updates.set('nhentai_flow_settings', result);
            }

            if (has(data, 'queue')) {
                let queue = data.queue;

                if (merge) {
                    queue = [...readingQueue];
                    const ids = new Set(queue.map(item => String(item.id)));

                    for (const item of data.queue) {
                        const id = String(item.id);

                        if (!ids.has(id)) {
                            queue.push(item);
                            ids.add(id);
                        }
                    }
                }

                updates.set('nhentai_queue_v1', queue);
            }

            if (has(data, 'savedSearches')) {
                const current = readJSON('nhentai_search_flow', { saved: [] });
                const saved = isObject(current) && Array.isArray(current.saved)
                ? current.saved
                : [];

                updates.set('nhentai_search_flow', {
                    saved: merge
                    ? [...saved, ...data.savedSearches.saved]
                    : [...data.savedSearches.saved]
                });
            }

            const previous = new Map();
            const written = [];

            try {
                for (const key of updates.keys()) {
                    previous.set(key, localStorage.getItem(key));
                }

                for (const [key, value] of updates) {
                    localStorage.setItem(key, JSON.stringify(value));
                    written.push(key);
                }
            } catch (error) {
                let restored = true;

                for (const key of written.reverse()) {
                    try {
                        const oldValue = previous.get(key);

                        if (oldValue === null) localStorage.removeItem(key);
                        else localStorage.setItem(key, oldValue);
                    } catch {
                        restored = false;
                    }
                }

                console.error('[NHentai Flow] Import failed:', error);
                alert(
                    restored
                    ? 'Failed to save the backup. Your previous data has been preserved.'
                    : 'Import failed, and some previous data could not be restored.'
                );
                return;
            }

            if (updates.has('nhentai_queue_v1')) {
                clearTimeout(saveQueueTimeout);
                readingQueue = updates.get('nhentai_queue_v1');
            }

            alert('Backup imported successfully! The page will reload.');
            window.location.reload();
        };

        input.click();
    }

    // ==========================================================================
    // 14. SETTINGS MENU UI
    // ==========================================================================

    function initSettingsMenu() {
        if (document.getElementById('nf-settings-btn')) return;

        const nav = document.querySelector('.menu.right');
        if (!nav) return;

        const li = document.createElement('li');
        li.id = 'nf-settings-btn';
        li.innerHTML = `<a href="javascript:void(0)"><i class="fa fa-cog"></i> Flow</a>`;
        li.onclick = (e) => {
            e.preventDefault();
            openSettingsModal();
        };

        nav.prepend(li);
    }

    const SETTINGS_UI = [
        { title: 'Navigation', items: [
            ['smartNav', 'Scroll to next page', 'Keep scrolling (or swipe up) at the bottom of a page to go to the next one.'],
            { range: 'smartNavScroll', depends: 'smartNav', name: 'Scroll distance', desc: 'How much to keep scrolling with the mouse wheel before the page changes.', fmt: v => `≈ ${Math.round(v / 100)} wheel clicks` },
            { range: 'smartNavSwipe', depends: 'smartNav', name: 'Swipe distance', desc: 'How far to drag up on a touch screen before the page changes.', fmt: v => `${v} px` },
            ['previewNav', 'Card page flipping', 'Flip through a gallery’s pages directly on its card.'],
            ['paginationRight', 'Pagination on the right', 'Put the side pagination on the right instead of the left.']
        ]},
        { title: 'Cards', items: [
            ['highlightVisited', 'Highlight visited', 'Mark galleries you have already opened.'],
            ['showTagOverlay', 'Tag overlay', 'Show a gallery’s tags when hovering its card.'],
            ['enableContextMenu', 'Context menu', 'Right-click a card for quick actions.']
        ]},
        { title: 'Tools', items: [
            ['enableQueue', 'Reading queue', 'Add galleries to a queue and read them in order.'],
            ['enableTagSelect', 'Tag Select', 'Build a search by clicking tags on a gallery page.'],
            ['enableSavedSearch', 'Saved searches', 'Keep favorite searches one click away under the search bar.']
        ]}
    ];

    function openSettingsModal() {
        let overlay = document.getElementById('nf-settings-modal');

        if (!overlay) {
            overlay = document.createElement('div');
            overlay.id = 'nf-settings-modal';
            overlay.className = 'nf-modal-overlay';

            const esc = (t) => String(t).replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
            const row = (key, name, desc) => `
                <label class="nf-setting-row" for="nf-set-${key}">
                    <span class="nf-setting-text">
                        <span class="nf-setting-name">${esc(name)}</span>
                        ${desc ? `<span class="nf-setting-desc">${esc(desc)}</span>` : ''}
                    </span>
                    <input type="checkbox" id="nf-set-${key}" class="nf-setting-checkbox" data-key="${key}">
                </label>`;

            const rangeRow = (it) => {
                const [min, max, step] = NUM_LIMITS[it.range];
                return `
                <div class="nf-range-row" data-depends="${esc(it.depends || '')}">
                    <div class="nf-range-head">
                        <span class="nf-setting-name">${esc(it.name)}</span>
                        <span class="nf-range-right">
                            <span class="nf-range-value" data-for="${it.range}"></span>
                            <button type="button" class="nf-range-reset" data-for="${it.range}" title="Reset to default" aria-label="Reset ${esc(it.name)} to default"><i class="fa fa-undo"></i></button>
                        </span>
                    </div>
                    <span class="nf-setting-desc">${esc(it.desc)}</span>
                    <input type="range" id="nf-set-${it.range}" class="nf-setting-range" data-key="${it.range}" min="${min}" max="${max}" step="${step}">
                </div>`;
            };
            const itemKey = (it) => (Array.isArray(it) ? it[0] : it.range);

            const known = new Set();
            const rangeFmt = {};
            let settingsHtml = '';
            SETTINGS_UI.forEach(group => {
                const rows = group.items.filter(it => itemKey(it) in DEFAULT_SETTINGS);
                if (!rows.length) return;
                settingsHtml += `<div class="nf-section-title">${esc(group.title)}</div>`;
                rows.forEach(it => {
                    known.add(itemKey(it));
                    if (Array.isArray(it)) {
                        settingsHtml += row(it[0], it[1], it[2]);
                    } else {
                        rangeFmt[it.range] = it.fmt;
                        settingsHtml += rangeRow(it);
                    }
                });
            });
            const others = Object.keys(DEFAULT_SETTINGS).filter(k => !known.has(k) && typeof DEFAULT_SETTINGS[k] === 'boolean');
            if (others.length) {
                settingsHtml += `<div class="nf-section-title">Other</div>`;
                others.forEach(key => {
                    const name = key.replace(/([A-Z])/g, ' $1').replace(/^./, c => c.toUpperCase());
                    settingsHtml += row(key, name, '');
                });
            }

            const dataHtml = `
                <div class="nf-section-title">Data</div>
                <div class="nf-data-grid">
                    <button type="button" id="nf-sync-btn" class="nf-btn"><i class="fa fa-sync-alt"></i><span class="nf-btn-label">Sync Favs</span></button>
                    <button type="button" id="nf-export-btn" class="nf-btn"><i class="fa fa-download"></i><span class="nf-btn-label">Export</span></button>
                    <button type="button" id="nf-import-btn" class="nf-btn"><i class="fa fa-upload"></i><span class="nf-btn-label">Import</span></button>
                </div>`;

            overlay.innerHTML = `
                <div class="nf-modal" role="dialog" aria-modal="true" aria-labelledby="nf-modal-title" tabindex="-1">
                    <div class="nf-modal-header">
                        <h2 id="nf-modal-title"><i class="fa fa-cog"></i> Flow Settings</h2>
                        <button type="button" class="nf-modal-close" aria-label="Close"><i class="fa fa-times"></i></button>
                    </div>
                    <div class="nf-modal-body">
                        ${settingsHtml}
                        ${dataHtml}
                    </div>
                    <div class="nf-modal-footer">
                        <a class="nf-footer-hint" href="https://sleazyfork.org/scripts/557178" target="_blank" rel="noopener noreferrer">
                        <i class="fa fa-question-circle" aria-hidden="true"></i> Help
                        </a>
                        <div class="nf-footer-actions">
                            <button type="button" id="nf-cancel-btn" class="nf-btn nf-btn-ghost">Cancel</button>
                            <button type="button" id="nf-save-settings-btn" class="nf-btn nf-btn-primary">Save</button>
                        </div>
                    </div>
                </div>
            `;
            document.body.appendChild(overlay);

            const onKey = (e) => { if (e.key === 'Escape') closeModal(); };
            const closeModal = () => {
                overlay.classList.remove('active');
                document.removeEventListener('keydown', onKey, true);
            };
            overlay._nfOpen = () => {
                overlay.classList.add('active');
                document.addEventListener('keydown', onKey, true);
                const box = overlay.querySelector('.nf-modal');
                if (box) box.focus({ preventScroll: true });
            };

            overlay.querySelector('.nf-modal-close').onclick = closeModal;
            overlay.querySelector('#nf-cancel-btn').onclick = closeModal;
            overlay.addEventListener('mousedown', (e) => { if (e.target === overlay) closeModal(); });

            const showRange = (key, val) => {
                const out = overlay.querySelector(`.nf-range-value[data-for="${key}"]`);
                if (out) out.textContent = rangeFmt[key] ? rangeFmt[key](Number(val)) : String(val);
            };
            const syncRangeState = () => {
                overlay.querySelectorAll('.nf-range-row').forEach(r => {
                    const dep = r.dataset.depends;
                    const toggle = dep && overlay.querySelector(`.nf-setting-checkbox[data-key="${dep}"]`);
                    r.classList.toggle('is-disabled', !!toggle && !toggle.checked);
                });
            };
            overlay.addEventListener('input', (e) => {
                if (e.target.classList && e.target.classList.contains('nf-setting-range')) showRange(e.target.dataset.key, e.target.value);
            });
            overlay.addEventListener('change', (e) => {
                if (e.target.classList && e.target.classList.contains('nf-setting-checkbox')) syncRangeState();
            });
            overlay.addEventListener('click', (e) => {
                const btn = e.target.closest && e.target.closest('.nf-range-reset');
                if (!btn) return;
                const input = overlay.querySelector(`.nf-setting-range[data-key="${btn.dataset.for}"]`);
                if (input) { input.value = DEFAULT_SETTINGS[btn.dataset.for]; showRange(btn.dataset.for, input.value); }
            });
            overlay._nfSyncRanges = () => {
                overlay.querySelectorAll('.nf-setting-range').forEach(r => {
                    r.value = numSetting(r.dataset.key);
                    showRange(r.dataset.key, r.value);
                });
                syncRangeState();
            };

            overlay.querySelector('#nf-save-settings-btn').onclick = () => {
                let toggled = false;
                let tuned = false;
                overlay.querySelectorAll('.nf-setting-checkbox').forEach(cb => {
                    if (settings[cb.dataset.key] !== cb.checked) toggled = true;
                    settings[cb.dataset.key] = cb.checked;
                });
                overlay.querySelectorAll('.nf-setting-range').forEach(r => {
                    const v = Number(r.value);
                    if (numSetting(r.dataset.key) !== v) tuned = true;
                    settings[r.dataset.key] = v;
                });
                closeModal();
                if (!toggled && !tuned) return;
                saveSettings();
                if (toggled) window.location.reload();
            };

            overlay.querySelector('#nf-export-btn').onclick = () => {
                exportData();
            };
            overlay.querySelector('#nf-import-btn').onclick = () => {
                importData();
            };

            overlay.querySelector('#nf-sync-btn').onclick = async () => {
                const btn = overlay.querySelector('#nf-sync-btn');
                const label = btn.querySelector('.nf-btn-label');
                if (btn.disabled) return;
                btn.disabled = true;
                try {
                    const total = await syncFavorites((txt) => { label.textContent = txt; });
                    label.textContent = `Synced! (${total})`;
                } catch (err) {
                    console.error('[NHentai Flow] Sync failed:', err);
                    label.textContent = /Login/.test(err.message) ? 'Login required' : 'Sync failed';
                }
                setTimeout(() => { label.textContent = 'Sync Favs'; btn.disabled = false; }, 2500);
            };
        }

        overlay.querySelectorAll('.nf-setting-checkbox').forEach(cb => {
            cb.checked = !!settings[cb.dataset.key];
        });
        if (overlay._nfSyncRanges) overlay._nfSyncRanges();

        overlay._nfOpen();
    }

    // ==========================================================================
    // 15. SPA ROUTER (CLEANUP & ROUTING)
    // ==========================================================================

    let routerInstalled = false;

    function cleanupGhostElements() {
        document.querySelectorAll('.btn-next-queue, .btn-random-fav').forEach(el => el.remove());
        readerDirty = true;

        document.querySelectorAll(`${SEL.gallery}[data-init="1"]`).forEach(gal => {
            gal.removeAttribute('data-init');
            gal.querySelectorAll('.reading-marker, .inline-preview-ui, .btn-view-all-popular').forEach(m => m.remove());
        });
    }

    // ---------- Queue page: lives in its own container, Svelte keeps owning #content ----------
    function isQueueView() {
        return window.location.pathname === '/' && new URLSearchParams(window.location.search).get('view') === 'queue';
    }

    function getQueueRoot(create) {
        let root = document.getElementById('nf-queue-root');
        if (root && !root.isConnected) root = null;
        if (!root && create) {
            const content = document.querySelector(SEL.content);
            if (!content || !content.parentNode) return null;
            root = document.createElement('div');
            root.id = 'nf-queue-root';
            root.className = content.className;
            content.parentNode.insertBefore(root, content.nextSibling);
        }
        return root;
    }

    function syncQueueView() {
        if (isQueueView()) {
            document.body.classList.add('nf-queue-view');
            const root = getQueueRoot(true);
            if (root && (root.dataset.rendered !== window.location.search || root.dataset.pagSig !== pagSig())) renderQueuePage();
        } else {
            document.body.classList.remove('nf-queue-view');
            const root = document.getElementById('nf-queue-root');
            if (root) root.remove();
        }
    }

    function runRouteInit() {
        captureNativePagination();
        syncQueueView();
        syncCommentsButton();
        scan();
        if (window.location.pathname === '/') initPopularShortcut();
    }

    function initSPARouter() {
        if (routerInstalled) return;
        routerInstalled = true;

        let lastUrl = location.href;

        const onRouteChanged = () => {
            if (lastUrl === location.href) return;
            lastUrl = location.href;

            cleanupGhostElements();
            checkPage();
            smartNavOnRoute();

            runRouteInit();
            setTimeout(runRouteInit, 300);
            setTimeout(runRouteInit, 900);
        };

        ['pushState', 'replaceState'].forEach((fn) => {
            const original = history[fn];
            history[fn] = function() {
                const result = original.apply(this, arguments);
                setTimeout(onRouteChanged, 0);
                return result;
            };
        });
        window.addEventListener('popstate', () => setTimeout(onRouteChanged, 0));
        setInterval(onRouteChanged, 200);

        const observerWork = () => {
            if (hasUnsyncedCards()) {
                scan();
            } else {
                initFavoritesPage();
            }

            if (window.location.pathname === '/' && !document.querySelector('.btn-view-all-popular')) initPopularShortcut();
            if (!document.getElementById('nf-settings-btn')) initSettingsMenu();

            const form = document.querySelector(SEL.searchForm);
            if (form && settings.enableSavedSearch && (!form.dataset.savedInit || !document.querySelector('.saved-search-extension'))) {
                delete form.dataset.savedInit;
                initSearchFlow();
            }
            if (!document.querySelector('.search-slash-hint')) initGlobalShortcuts();

            captureNativePagination();
            if (isQueueView()) syncQueueView();
            syncCommentsButton();

            if (isReader) {
                const ic = document.querySelector(SEL.imageContainer);
                if (readerDirty || !document.querySelector('.btn-fullscreen-custom') || (ic && !ic.dataset.readerInit)) initReaderMode();
            } else if (settings.enableQueue && /^\/g\/\d+\/?$/.test(window.location.pathname)
                       && document.querySelector(SEL.infoButtons) && !document.querySelector('.btn-queue-add')) {
                initGalleryPageFeatures();
            }
        };

        let observerPending = false;
        const domObserver = new MutationObserver(() => {
            if (observerPending) return;
            observerPending = true;
            setTimeout(() => {
                observerPending = false;
                try { observerWork(); } catch (err) { console.error('[NHentai Flow] observer error:', err); }
            }, 120);
        });

        domObserver.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] });
    }


    // ==========================================================================
    // 16. INIT & OBSERVERS
    // ==========================================================================

    function scan() {
        const step = (fn) => {
            try { fn(); } catch (err) { console.error('[NHentai Flow] scan step failed:', fn.name || 'anonymous', err); }
        };

        step(initSearchFlow);
        step(() => document.querySelectorAll(SEL.gallery).forEach(initPreviewUI));
        step(initReaderMode);
        step(initGalleryPageFeatures);
        step(initFavoriteButtonWatcher);
        step(initGlobalShortcuts);
        step(initQueueWidget);
        step(initRandomContextual);
        step(initFavoritesPage);
        step(initContextMenu);
    }

    let lastArrowNav = 0;
    document.addEventListener('keydown', (e) => {
        if (hoveredGallery && !document.fullscreenElement) {
            if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
                e.preventDefault();
                const now = performance.now();
                if (now - lastArrowNav < 180) return;
                lastArrowNav = now;
                update(hoveredGallery, e.key === 'ArrowRight' ? 1 : -1);
            }
            else if (e.key === 'q') {
                if (!settings.enableQueue) return;
                const btn = hoveredGallery.querySelector('.queue-trigger');
                if (btn) btn.click();
            }
        }
    });

    let favWatcherInstalled = false;

    function cardIdFrom(el) {
        let node = el;
        for (let depth = 0; node && node !== document.body && depth < 8; depth++) {
            node = node.parentElement;
            if (!node) break;
            const ids = new Set();
            node.querySelectorAll(SEL.galleryLink).forEach(a => {
                const m = a.getAttribute('href').match(/\/g\/(\d+)/);
                if (m) ids.add(m[1]);
            });
            if (ids.size === 1) return [...ids][0];
            if (ids.size > 1) return null;
        }
        return null;
    }

    function initFavClickWatcher() {
        if (favWatcherInstalled) return;
        favWatcherInstalled = true;

        window.addEventListener('click', (e) => {
            if (!e.target || !e.target.closest) return;

            const removeBtn = e.target.closest(SEL.removeButton);
            if (!removeBtn) return;

            const id = cardIdFrom(removeBtn);
            if (!id) return;

            removeFav(id);
        }, true);
    }

    function boot() {
        const steps = [ensureCdn, initFavClickWatcher, initSettingsMenu, runRouteInit, initSPARouter, initSmartNavigation];
        steps.forEach(fn => {
            try { fn(); } catch (err) { console.error('[NHentai Flow] boot step failed:', fn.name, err); }
        });
    }

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