☰

NHentai Flow

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Violentmonkey installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey oder Userscripts installieren.

Um dieses Skript zu installieren, müssen Sie eine Erweiterung wie Tampermonkey installieren.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um dieses Design zu installieren, müssen Sie eine Erweiterung wie Stylus installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

Um diesen Stil zu installieren, müssen Sie eine Erweiterung für den Benutzerstil Verwaltung installieren.

(Ich habe bereits einen Benutzerstil Verwaltung, ich möchte ihn installieren!)

// ==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();
    }
})();