☰

Pawchive Downloader

Download images from pawchive.pw posts (ZIP / PDF / Strip). Also works on DoujinDesu, MangaToon, Kanzenin, KomikDewasa. JP title → romaji, multi-select, auto-scroll.

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         Pawchive Downloader
// @name:id      Pawchive Downloader
// @name:en      Pawchive Downloader
// @namespace    https://sleazyfork.org/users/qxyraxp
// @version      1.0.0
// @description  Download images from pawchive.pw posts (ZIP / PDF / Strip). Also works on DoujinDesu, MangaToon, Kanzenin, KomikDewasa. JP title → romaji, multi-select, auto-scroll.
// @description:id  Download gambar dari post pawchive.pw (ZIP / PDF / Strip). Juga support DoujinDesu, MangaToon, Kanzenin, KomikDewasa. Judul JP → romaji, multi-select, auto-scroll.
// @description:en  Download images from pawchive.pw posts (ZIP / PDF / Strip). Also supports DoujinDesu, MangaToon, Kanzenin, KomikDewasa. JP title → romaji, multi-select, auto-scroll.
// @author       qxyraxp
// @license      MIT
// @match        *://pawchive.pw/*
// @match        *://*.pawchive.pw/*
// @match        https://doujin.desu.xxx/*
// @match        http://doujin.desu.xxx/*
// @match        *://*.mangatoon.cc/*
// @match        *://mangatoon.cc/*
// @match        *://kanzenin.info/*
// @match        *://*.kanzenin.info/*
// @match        *://kanzenin.xyz/*
// @match        *://*.kanzenin.xyz/*
// @match        *://komikdewasa.art/*
// @match        *://*.komikdewasa.art/*
// @connect      translate.googleapis.com
// @connect      *
// @grant        GM_download
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @run-at       document-start
// @homepageURL  https://sleazyfork.org
// @supportURL   https://sleazyfork.org
// ==/UserScript==

(function () {
    'use strict';

    let imageURLs = [];
    let panelOpen = false;
    let autoScanInterval = null;
    let isDark = localStorage.getItem('dd_theme') !== 'light';
    let downloadHistory = JSON.parse(localStorage.getItem('dd_history') || '[]');
    let isDownloading = false;
    let thumbSize = localStorage.getItem('dd_thumb') || 'm';
    let isCompact = localStorage.getItem('dd_compact') === '1';
    let toolbarCollapsed = localStorage.getItem('dd_toolbar_collapsed') === '1';
    let showSelectedOnly = false;
    let accentColor = localStorage.getItem('dd_accent') || '#3b82f6';
    let hoverPreviewTimer = null;
    let downloadStartTime = 0;
    let downloadBytesDone = 0;
    let lastSelectedIndex = -1;
    let lastDownloadMode = localStorage.getItem('dd_last_mode') || 'zip';
    let isDraggingPanel = false;
    let estimatedSizes = {}; // url -> bytes
    let avgImageSize = 180000; // fallback ~180KB per image
    let originalDocTitle = document.title;
    let dlConcurrency = parseInt(localStorage.getItem('dd_concurrency') || '4', 10) || 4;
    let jpegQuality = parseFloat(localStorage.getItem('dd_jpeg_q') || '0.92') || 0.92;
    let autoOpenPanel = localStorage.getItem('dd_auto_open') !== '0';
    let fabPosition = localStorage.getItem('dd_fab_pos') || 'top-right'; // top-right | bottom-right
    let tabProgressEnabled = localStorage.getItem('dd_tab_progress') !== '0';
    let autoMinimizeOnDl = localStorage.getItem('dd_auto_minimize') === '1';
    let notifyEnabled = localStorage.getItem('dd_notify') !== '0';
    // default = Downloads | ask = Save As (handle dipilih di AWAL klik) | folder = pilih folder
    let saveMode = localStorage.getItem('dd_save_mode') || 'default';
    let sessionDirHandle = null;
    let sessionDirName = localStorage.getItem('dd_folder_label') || '';
    let pendingFileHandle = null; // Save As: dipilih di awal, ditulis di akhir
    let skipCoverCount = parseInt(localStorage.getItem('dd_skip_cover') || '2', 10);
    if (skipCoverCount < 0) skipCoverCount = 0;
    let uiLang = localStorage.getItem('dd_lang') || 'en'; // en | id — default English
    let ddLogs = [];
    const DD_LOG_MAX = 250;

    const I18N = {
        en: {
            fileName: 'File Name',
            saveFolder: 'Save Folder',
            filePlaceholder: 'ZIP / PDF file name...',
            refreshTitle: 'Fill from page title',
            pickFolder: 'Pick folder (Chrome/Edge)',
            resetFolder: 'Reset',
            downloadsDefault: 'Downloads (default)',
            folderSelected: 'Folder selected',
            saveAsDialog: 'Save As (dialog)',
            scan: 'Scan',
            scroll: 'Scroll',
            all: 'All',
            none: 'None',
            cover: 'Cover',
            ends: 'Ends',
            nextCh: 'Next Ch',
            download: 'Download',
            selected: 'Selected',
            selectedDesc: 'individual files',
            zipDesc: 'zip archive',
            pdfDesc: 'per page',
            stripDesc: 'long-strip PDF',
            images: 'images',
            selectedCount: 'selected',
            of: 'of',
            settings: 'Settings',
            language: 'Language',
            langId: 'ID',
            langEn: 'EN',
            save: 'Save',
            resetDefault: 'Reset default',
            log: 'Log',
            help: 'Help',
            statusHint: 'Shift+Click = range  •  R=Refresh  A=All  Z=ZIP  Esc=Close',
            emptyTitle: 'No images yet',
            emptyDesc: 'Scroll the page until images appear,<br>then press Scan or use Auto Scroll',
            emptyScroll: 'Auto Scroll',
            panel: 'Panel',
            close: 'Close (Esc)',
            minimize: 'Minimize',
            collapse: 'Collapse/Expand toolbar',
            filter: 'Show selected only',
            history: 'History',
            chapters: 'Chapters',
            theme: 'Theme',
            sizeS: 'Small',
            sizeM: 'Medium',
            sizeL: 'Large',
            compact: 'Compact',
            folderSaved: 'Folder saved: ',
            folderFail: 'Failed to pick folder.',
            folderUnsupported: 'This browser does not support folder picker.\nUse Chrome or Edge.',
            selectedOf: '{s} selected of {t} images',
            concurrent: 'Concurrent download',
            jpegQuality: 'JPEG quality (PDF)',
            defaultMode: 'Default download mode',
            skipCover: 'Skip cover (first panels)',
            fabPos: 'DL button position',
            saveLoc: 'Save location',
            autoOpen: 'Auto-open panel on reader pages',
            tabProg: 'Progress in browser tab title',
            autoMin: 'Auto-minimize panel on download',
            notify: 'Notify when download finishes',
            topRight: 'Top right',
            bottomRight: 'Bottom right',
            saveDefault: 'Downloads folder (default)',
            saveAsk: 'Ask location each file (Save As)',
            saveFolderMode: 'Custom folder (Chrome/Edge)',
            saveNote: 'Browser cannot force a fixed path. Folder mode: pick once per session.',
            langToggle: 'Language ID/EN'
        },
        id: {
            fileName: 'Nama File',
            saveFolder: 'Folder Simpan',
            filePlaceholder: 'Nama file ZIP / PDF...',
            refreshTitle: 'Isi ulang dari judul halaman',
            pickFolder: 'Pilih folder (Chrome/Edge)',
            resetFolder: 'Reset',
            downloadsDefault: 'Downloads (default)',
            folderSelected: 'Folder dipilih',
            saveAsDialog: 'Save As (dialog)',
            scan: 'Scan',
            scroll: 'Scroll',
            all: 'All',
            none: 'None',
            cover: 'Cover',
            ends: 'Ends',
            nextCh: 'Next Ch',
            download: 'Download',
            selected: 'Terpilih',
            selectedDesc: 'file satu-satu',
            zipDesc: 'arsip zip',
            pdfDesc: 'per halaman',
            stripDesc: 'PDF nyambung',
            images: 'gambar',
            selectedCount: 'terpilih',
            of: 'dari',
            settings: 'Pengaturan',
            language: 'Bahasa',
            langId: 'ID',
            langEn: 'EN',
            save: 'Simpan',
            resetDefault: 'Reset default',
            log: 'Log',
            help: 'Help',
            statusHint: 'Shift+Click = range  •  R=Refresh  A=All  Z=ZIP  Esc=Close',
            emptyTitle: 'Belum ada gambar',
            emptyDesc: 'Scroll halaman sampai gambar muncul,<br>lalu tekan Scan atau pakai Auto Scroll',
            emptyScroll: 'Auto Scroll',
            panel: 'Panel',
            close: 'Tutup (Esc)',
            minimize: 'Minimize',
            collapse: 'Collapse/Expand toolbar',
            filter: 'Tampilkan hanya terpilih',
            history: 'History',
            chapters: 'Chapter',
            theme: 'Theme',
            sizeS: 'Kecil',
            sizeM: 'Sedang',
            sizeL: 'Besar',
            compact: 'Compact',
            folderSaved: 'Folder tersimpan: ',
            folderFail: 'Gagal memilih folder.',
            folderUnsupported: 'Browser tidak mendukung pilih folder.\nGunakan Chrome atau Edge.',
            selectedOf: '{s} terpilih dari {t} gambar',
            concurrent: 'Concurrent download',
            jpegQuality: 'Kualitas JPEG (PDF)',
            defaultMode: 'Mode download default',
            skipCover: 'Skip cover (jumlah panel awal)',
            fabPos: 'Posisi tombol DL',
            saveLoc: 'Lokasi simpan file',
            autoOpen: 'Auto-buka panel di halaman reader',
            tabProg: 'Progress di title tab browser',
            autoMin: 'Minimize panel otomatis saat download',
            notify: 'Notifikasi selesai download',
            topRight: 'Pojok kanan atas',
            bottomRight: 'Pojok kanan bawah',
            saveDefault: 'Folder Downloads (default)',
            saveAsk: 'Tanya lokasi tiap file (Save As)',
            saveFolderMode: 'Pilih folder custom (Chrome/Edge)',
            saveNote: 'Browser tidak bisa memaksa path tetap. Mode folder: pilih sekali per sesi.',
            langToggle: 'Bahasa ID/EN'
        }
    };

    function t(key) {
        const pack = I18N[uiLang] || I18N.en;
        return pack[key] || I18N.en[key] || key;
    }

    function toggleLang() {
        uiLang = uiLang === 'en' ? 'id' : 'en';
        localStorage.setItem('dd_lang', uiLang);
        const btn = document.getElementById('dd-lang-btn');
        if (btn) btn.textContent = uiLang === 'en' ? 'EN' : 'ID';
        applyI18n();
    }

    function ddLog(level, msg, detail) {
        const entry = {
            t: new Date().toLocaleTimeString('id-ID', { hour12: false }),
            level: level || 'info',
            msg: String(msg || ''),
            detail: detail != null ? String(detail).slice(0, 600) : ''
        };
        ddLogs.push(entry);
        if (ddLogs.length > DD_LOG_MAX) ddLogs = ddLogs.slice(-DD_LOG_MAX);
        try {
            if (level === 'error') console.error('[DD]', msg, detail || '');
            else if (level === 'warn') console.warn('[DD]', msg, detail || '');
        } catch (e) {}
    }

    function showLogPanel() {
        if (document.getElementById('dd-log-box')) return;
        const box = document.createElement('div');
        box.id = 'dd-log-box';
        const rows = ddLogs.length
            ? ddLogs.slice().reverse().map(e => {
                const cls = e.level === 'error' ? 'dd-log-err' : (e.level === 'warn' ? 'dd-log-warn' : 'dd-log-info');
                return `<div class="dd-log-row ${cls}"><span class="dd-log-t">${e.t}</span><span class="dd-log-lv">${e.level}</span><span class="dd-log-msg">${escapeHtml(e.msg)}${e.detail ? ` <span class="dd-log-detail">${escapeHtml(e.detail)}</span>` : ''}</span></div>`;
            }).join('')
            : '<div class="dd-help-note">Belum ada log. Scan / download akan muncul di sini.</div>';
        box.innerHTML = `
            <div class="dd-modal-bg">
                <div class="dd-modal dd-modal-wide">
                    <div class="dd-modal-head">
                        <b>⌁ Log / Debug</b>
                        <button id="dd-log-close" type="button">✕</button>
                    </div>
                    <div class="dd-modal-body">
                        <div class="dd-ch-toolbar">
                            <button type="button" id="dd-log-copy" class="dd-ch-btn">Copy</button>
                            <button type="button" id="dd-log-clear" class="dd-ch-btn">Clear</button>
                            <span class="dd-ch-count">${ddLogs.length} entry</span>
                        </div>
                        <div class="dd-log-list">${rows}</div>
                        <div class="dd-help-note">URL di-skip, error fetch, dan hasil scan tercatat di sini. Max ${DD_LOG_MAX} baris.</div>
                    </div>
                </div>
            </div>`;
        document.body.appendChild(box);
        document.getElementById('dd-log-close').onclick = () => box.remove();
        box.querySelector('.dd-modal-bg').onclick = (e) => { if (e.target === e.currentTarget) box.remove(); };
        document.getElementById('dd-log-clear').onclick = () => {
            ddLogs = [];
            box.remove();
            showLogPanel();
        };
        document.getElementById('dd-log-copy').onclick = async () => {
            const text = ddLogs.map(e => `[${e.t}] ${e.level.toUpperCase()} ${e.msg}${e.detail ? ' | ' + e.detail : ''}`).join('\n');
            try {
                await navigator.clipboard.writeText(text);
                const st = document.getElementById('dd-status-text');
                if (st) st.textContent = 'Log disalin';
            } catch (e) {
                prompt('Copy log:', text);
            }
        };
    }

    function escapeHtml(s) {
        return String(s || '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    function getMimeForName(filename) {
        const f = (filename || '').toLowerCase();
        if (f.endsWith('.zip') || f.endsWith('.cbz')) return 'application/zip';
        if (f.endsWith('.pdf')) return 'application/pdf';
        if (f.endsWith('.png')) return 'image/png';
        if (f.endsWith('.webp')) return 'image/webp';
        if (f.endsWith('.avif')) return 'image/avif';
        return 'image/jpeg';
    }

    const DD_IDB_NAME = 'dd_downloader_db';
    const DD_IDB_STORE = 'handles';

    function idbOpen() {
        return new Promise((resolve, reject) => {
            const req = indexedDB.open(DD_IDB_NAME, 1);
            req.onupgradeneeded = () => {
                const db = req.result;
                if (!db.objectStoreNames.contains(DD_IDB_STORE)) db.createObjectStore(DD_IDB_STORE);
            };
            req.onsuccess = () => resolve(req.result);
            req.onerror = () => reject(req.error);
        });
    }

    async function idbSet(key, value) {
        try {
            const db = await idbOpen();
            return await new Promise((resolve, reject) => {
                const tx = db.transaction(DD_IDB_STORE, 'readwrite');
                tx.objectStore(DD_IDB_STORE).put(value, key);
                tx.oncomplete = () => resolve(true);
                tx.onerror = () => reject(tx.error);
            });
        } catch (e) { return false; }
    }

    async function idbGet(key) {
        try {
            const db = await idbOpen();
            return await new Promise((resolve, reject) => {
                const tx = db.transaction(DD_IDB_STORE, 'readonly');
                const req = tx.objectStore(DD_IDB_STORE).get(key);
                req.onsuccess = () => resolve(req.result);
                req.onerror = () => reject(req.error);
            });
        } catch (e) { return null; }
    }

    async function idbDel(key) {
        try {
            const db = await idbOpen();
            return await new Promise((resolve, reject) => {
                const tx = db.transaction(DD_IDB_STORE, 'readwrite');
                tx.objectStore(DD_IDB_STORE).delete(key);
                tx.oncomplete = () => resolve(true);
                tx.onerror = () => reject(tx.error);
            });
        } catch (e) { return false; }
    }

    async function persistDirHandle(handle, name) {
        sessionDirHandle = handle;
        sessionDirName = name || handle?.name || 'Folder';
        saveMode = 'folder';
        localStorage.setItem('dd_save_mode', 'folder');
        localStorage.setItem('dd_folder_label', sessionDirName);
        await idbSet('saveDir', handle);
        updateFolderUI();
    }

    async function clearDirHandle() {
        sessionDirHandle = null;
        sessionDirName = '';
        await idbDel('saveDir');
        localStorage.removeItem('dd_folder_label');
    }

    async function restoreDirHandle() {
        if (saveMode !== 'folder') return false;
        try {
            const handle = await idbGet('saveDir');
            if (!handle) return false;
            sessionDirHandle = handle;
            sessionDirName = handle.name || sessionDirName || 'Folder';
            localStorage.setItem('dd_folder_label', sessionDirName);
            updateFolderUI();
            return true;
        } catch (e) {
            return false;
        }
    }

    async function verifyDirPermission(handle) {
        if (!handle) return false;
        try {
            let p = await handle.queryPermission({ mode: 'readwrite' });
            if (p === 'granted') return true;
            p = await handle.requestPermission({ mode: 'readwrite' });
            return p === 'granted';
        } catch (e) {
            return false;
        }
    }

    function updateFolderUI() {
        const el = document.getElementById('dd-folder-label');
        if (!el) return;
        if (saveMode === 'folder' && (sessionDirHandle || sessionDirName)) {
            el.textContent = sessionDirName || sessionDirHandle?.name || t('folderSelected');
            el.title = el.textContent;
            el.classList.add('dd-folder-set');
        } else if (saveMode === 'ask') {
            el.textContent = t('saveAsDialog');
            el.title = t('saveAsDialog');
            el.classList.remove('dd-folder-set');
        } else {
            el.textContent = t('downloadsDefault');
            el.title = t('downloadsDefault');
            el.classList.remove('dd-folder-set');
        }
    }

    function applyI18n() {
        document.querySelectorAll('[data-i18n]').forEach(el => {
            const key = el.getAttribute('data-i18n');
            if (key) el.textContent = t(key);
        });
        document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
            const key = el.getAttribute('data-i18n-placeholder');
            if (key) el.setAttribute('placeholder', t(key));
        });
        document.querySelectorAll('[data-i18n-title]').forEach(el => {
            const key = el.getAttribute('data-i18n-title');
            if (key) el.setAttribute('title', t(key));
        });
        // bottom stats labels need dynamic rebuild
        updateSelectedUI();
        updateFolderUI();
        // menu descriptions
        const menuMap = [
            ['xamz-download-selected', 'selected', 'selectedDesc'],
            ['xamz-download-zip', null, 'zipDesc'],
            ['xamz-download-pdf', null, 'pdfDesc'],
            ['xamz-download-strip', null, 'stripDesc']
        ];
        menuMap.forEach(([id, labelKey, descKey]) => {
            const btn = document.getElementById(id);
            if (!btn) return;
            const desc = btn.querySelector('.dd-menu-desc');
            if (desc) desc.textContent = t(descKey);
            if (labelKey) {
                // keep structure: text + span
                const nodes = [...btn.childNodes].filter(n => n.nodeType === 3);
                if (nodes[0]) nodes[0].textContent = t(labelKey) + ' ';
            }
        });
        const nextBtn = document.getElementById('dd-nextch-btn');
        if (nextBtn) nextBtn.textContent = t('nextCh');
        const dlMain = document.getElementById('dd-download-main');
        if (dlMain) {
            const badge = dlMain.querySelector('#badge-dl');
            const badgeHtml = badge ? badge.outerHTML : '';
            dlMain.innerHTML = `${t('download')} ${badgeHtml} ▾`;
        }
        const toolbarMap = {
            'xamz-refresh': 'scan',
            'dd-autoscroll': 'scroll',
            'xamz-select-all': 'all',
            'xamz-select-none': 'none',
            'dd-skip-cover': 'cover',
            'dd-skip-ends': 'ends'
        };
        Object.entries(toolbarMap).forEach(([id, key]) => {
            const el = document.getElementById(id);
            if (el) el.textContent = t(key);
        });
        const langBtn = document.getElementById('dd-lang-btn');
        if (langBtn) {
            langBtn.textContent = uiLang === 'en' ? 'EN' : 'ID';
            langBtn.title = t('langToggle');
        }
        const sizeTitles = {
            'dd-size-s': 'sizeS',
            'dd-size-m': 'sizeM',
            'dd-size-l': 'sizeL',
            'dd-compact-btn': 'compact'
        };
        Object.entries(sizeTitles).forEach(([id, key]) => {
            const el = document.getElementById(id);
            if (el) el.title = t(key);
        });
        // re-render grid labels if open
        const grid = document.getElementById('xamz-grid');
        if (grid && grid.querySelector('.xamz-item')) {
            scheduleUpdatePanel();
        }
    }

    async function pickSaveFolder() {
        if (typeof window.showDirectoryPicker !== 'function') {
            alert(t('folderUnsupported'));
            return false;
        }
        try {
            const handle = await window.showDirectoryPicker({ mode: 'readwrite' });
            await persistDirHandle(handle, handle.name || 'Folder');
            const st = document.getElementById('dd-status-text');
            if (st) st.textContent = t('folderSaved') + sessionDirName;
            return true;
        } catch (e) {
            if (e && e.name === 'AbortError') return false;
            alert(t('folderFail'));
            return false;
        }
    }

    async function ensureSaveFolder() {
        if (saveMode !== 'folder') return true;
        if (!sessionDirHandle) await restoreDirHandle();
        if (sessionDirHandle) {
            if (await verifyDirPermission(sessionDirHandle)) return true;
            sessionDirHandle = null;
        }
        return await pickSaveFolder();
    }

    // Harus dipanggil di awal klik (sebelum await lama) supaya dialog diizinkan browser
    async function prepareSaveTarget(suggestedName) {
        pendingFileHandle = null;
        if (saveMode === 'folder') {
            return await ensureSaveFolder();
        }
        if (saveMode === 'ask') {
            if (typeof window.showSaveFilePicker !== 'function') {
                alert('Save As tidak didukung browser ini. Gunakan Chrome/Edge, atau mode folder/Downloads.');
                return false;
            }
            try {
                const ext = (suggestedName.split('.').pop() || 'bin').toLowerCase();
                pendingFileHandle = await window.showSaveFilePicker({
                    suggestedName: suggestedName,
                    types: [{
                        description: 'File',
                        accept: { [getMimeForName(suggestedName)]: ['.' + ext] }
                    }]
                });
                return true;
            } catch (e) {
                if (e && e.name === 'AbortError') return false;
                alert('Gagal membuka dialog Save As.');
                return false;
            }
        }
        return true;
    }

    async function saveBlob(blob, filename) {
        if (!blob) return false;
        // Save As: tulis ke handle yang sudah dipilih di awal
        if (pendingFileHandle) {
            try {
                const writable = await pendingFileHandle.createWritable();
                await writable.write(blob);
                await writable.close();
                pendingFileHandle = null;
                return true;
            } catch (e) {
                console.error(e);
                pendingFileHandle = null;
                return false;
            }
        }
        // Mode folder
        if (saveMode === 'folder') {
            if (!sessionDirHandle) return false;
            try {
                const fileHandle = await sessionDirHandle.getFileHandle(filename, { create: true });
                const writable = await fileHandle.createWritable();
                await writable.write(blob);
                await writable.close();
                return true;
            } catch (e) {
                console.error(e);
                return false;
            }
        }
        // Default: Downloads
        const a = document.createElement('a');
        a.href = URL.createObjectURL(blob);
        a.download = filename;
        a.click();
        URL.revokeObjectURL(a.href);
        return true;
    }

    // ==================== Minimal ZIP ====================
    function crc32(buf) {
        let c = ~0;
        for (let i = 0; i < buf.length; i++) {
            c ^= buf[i];
            for (let k = 0; k < 8; k++) c = (c >>> 1) ^ (0xedb88320 & -(c & 1));
        }
        return ~c >>> 0;
    }

    function createZip(files) {
        const localHeaders = [], fileDatas = [];
        let offset = 0;
        for (const f of files) {
            const nameBytes = new TextEncoder().encode(f.name);
            const data = f.data;
            const crc = crc32(data);
            const local = new Uint8Array(30 + nameBytes.length);
            const v = new DataView(local.buffer);
            v.setUint32(0, 0x04034b50, true);
            v.setUint16(4, 20, true);
            v.setUint16(8, 0, true);
            v.setUint32(14, crc, true);
            v.setUint32(18, data.length, true);
            v.setUint32(22, data.length, true);
            v.setUint16(26, nameBytes.length, true);
            local.set(nameBytes, 30);
            localHeaders.push({ header: local, offset, crc, size: data.length });
            fileDatas.push(data);
            offset += local.length + data.length;
        }
        const centralParts = [];
        let centralSize = 0;
        for (let i = 0; i < files.length; i++) {
            const h = localHeaders[i];
            const nameBytes = new TextEncoder().encode(files[i].name);
            const central = new Uint8Array(46 + nameBytes.length);
            const v = new DataView(central.buffer);
            v.setUint32(0, 0x02014b50, true);
            v.setUint16(4, 20, true);
            v.setUint16(6, 20, true);
            v.setUint32(16, h.crc, true);
            v.setUint32(20, h.size, true);
            v.setUint32(24, h.size, true);
            v.setUint16(28, nameBytes.length, true);
            v.setUint32(42, h.offset, true);
            central.set(nameBytes, 46);
            centralParts.push(central);
            centralSize += central.length;
        }
        const end = new Uint8Array(22);
        const ev = new DataView(end.buffer);
        ev.setUint32(0, 0x06054b50, true);
        ev.setUint16(8, files.length, true);
        ev.setUint16(10, files.length, true);
        ev.setUint32(12, centralSize, true);
        ev.setUint32(16, offset, true);
        const total = offset + centralSize + 22;
        const zip = new Uint8Array(total);
        let pos = 0;
        for (let i = 0; i < files.length; i++) {
            zip.set(localHeaders[i].header, pos); pos += localHeaders[i].header.length;
            zip.set(fileDatas[i], pos); pos += fileDatas[i].length;
        }
        for (const c of centralParts) { zip.set(c, pos); pos += c.length; }
        zip.set(end, pos);
        return new Blob([zip], { type: 'application/zip' });
    }

    // ==================== Ambil Judul ====================
    function getTitle() {
        try {
            const h1 = document.querySelector('h1.entry-title') ||
                       document.querySelector('h1.text-xl') ||
                       document.querySelector('h1.font-black') ||
                       document.querySelector('h1.post__title') ||
                       document.querySelector('.post__title') ||
                       document.querySelector('h1');
            if (h1) {
                let text = cleanTitleSuffix(h1.textContent.trim()
                    .replace(/[\\/:*?"<>|]/g, '')
                    .replace(/\s+/g, ' ')
                    .trim());
                if (text.length > 3) return text.slice(0, 120);
            }
            let pageTitle = document.title || '';
            pageTitle = pageTitle
                .replace(/\s*[-|–]\s*DoujinDesu.*$/i, '')
                .replace(/\s*[-|–]\s*Manga\s*Toon.*$/i, '')
                .replace(/\s*[-|–]\s*kanzenin.*$/i, '')
                .replace(/\s*[-|–]\s*komikdewasa.*$/i, '')
                .replace(/\s*[-|–]\s*pawchive.*$/i, '')
                .replace(/\s*\|\s*Kemono.*$/i, '')
                .trim();
            if (pageTitle.length > 3) {
                return cleanTitleSuffix(pageTitle.replace(/[\\/:*?"<>|]/g, '')).slice(0, 120);
            }
        } catch (e) {}
        return '';
    }

    function cleanTitleSuffix(text) {
        if (!text) return '';
        return String(text)
            .replace(/\s*\(\s*Pixiv\s*Fanbox\s*\)\s*/gi, ' ')
            .replace(/\s*\(\s*Fanbox\s*\)\s*/gi, ' ')
            .replace(/\s*\(\s*Patreon\s*\)\s*/gi, ' ')
            .replace(/\s*\(\s*Fantia\s*\)\s*/gi, ' ')
            .replace(/\s*[-|–]\s*Pixiv\s*Fanbox\s*$/i, '')
            .replace(/\s+/g, ' ')
            .trim();
    }

    // Kana → Romaji (Hepburn). Kanji di-handle via Google transliterate async.
    const KANA_ROMAJI = (() => {
        const m = {};
        const pairs = [
            ['きゃ','kya'],['きゅ','kyu'],['きょ','kyo'],['しゃ','sha'],['しゅ','shu'],['しょ','sho'],
            ['ちゃ','cha'],['ちゅ','chu'],['ちょ','cho'],['にゃ','nya'],['にゅ','nyu'],['にょ','nyo'],
            ['ひゃ','hya'],['ひゅ','hyu'],['ひょ','hyo'],['みゃ','mya'],['みゅ','myu'],['みょ','myo'],
            ['りゃ','rya'],['りゅ','ryu'],['りょ','ryo'],['ぎゃ','gya'],['ぎゅ','gyu'],['ぎょ','gyo'],
            ['じゃ','ja'],['じゅ','ju'],['じょ','jo'],['びゃ','bya'],['びゅ','byu'],['びょ','byo'],
            ['ぴゃ','pya'],['ぴゅ','pyu'],['ぴょ','pyo'],
            ['キャ','kya'],['キュ','kyu'],['キョ','kyo'],['シャ','sha'],['シュ','shu'],['ショ','sho'],
            ['チャ','cha'],['チュ','chu'],['チョ','cho'],['ニャ','nya'],['ニュ','nyu'],['ニョ','nyo'],
            ['ヒャ','hya'],['ヒュ','hyu'],['ヒョ','hyo'],['ミャ','mya'],['ミュ','myu'],['ミョ','myo'],
            ['リャ','rya'],['リュ','ryu'],['リョ','ryo'],['ギャ','gya'],['ギュ','gyu'],['ギョ','gyo'],
            ['ジャ','ja'],['ジュ','ju'],['ジョ','jo'],['ビャ','bya'],['ビュ','byu'],['ビョ','byo'],
            ['ピャ','pya'],['ピュ','pyu'],['ピョ','pyo'],
            ['あ','a'],['い','i'],['う','u'],['え','e'],['お','o'],
            ['か','ka'],['き','ki'],['く','ku'],['け','ke'],['こ','ko'],
            ['さ','sa'],['し','shi'],['す','su'],['せ','se'],['そ','so'],
            ['た','ta'],['ち','chi'],['つ','tsu'],['て','te'],['と','to'],
            ['な','na'],['に','ni'],['ぬ','nu'],['ね','ne'],['の','no'],
            ['は','ha'],['ひ','hi'],['ふ','fu'],['へ','he'],['ほ','ho'],
            ['ま','ma'],['み','mi'],['む','mu'],['め','me'],['も','mo'],
            ['や','ya'],['ゆ','yu'],['よ','yo'],
            ['ら','ra'],['り','ri'],['る','ru'],['れ','re'],['ろ','ro'],
            ['わ','wa'],['を','wo'],['ん','n'],
            ['が','ga'],['ぎ','gi'],['ぐ','gu'],['げ','ge'],['ご','go'],
            ['ざ','za'],['じ','ji'],['ず','zu'],['ぜ','ze'],['ぞ','zo'],
            ['だ','da'],['ぢ','ji'],['づ','zu'],['で','de'],['ど','do'],
            ['ば','ba'],['び','bi'],['ぶ','bu'],['べ','be'],['ぼ','bo'],
            ['ぱ','pa'],['ぴ','pi'],['ぷ','pu'],['ぺ','pe'],['ぽ','po'],
            ['ぁ','a'],['ぃ','i'],['ぅ','u'],['ぇ','e'],['ぉ','o'],
            ['ゃ','ya'],['ゅ','yu'],['ょ','yo'],['っ',''] ,
            ['ア','a'],['イ','i'],['ウ','u'],['エ','e'],['オ','o'],
            ['カ','ka'],['キ','ki'],['ク','ku'],['ケ','ke'],['コ','ko'],
            ['サ','sa'],['シ','shi'],['ス','su'],['セ','se'],['ソ','so'],
            ['タ','ta'],['チ','chi'],['ツ','tsu'],['テ','te'],['ト','to'],
            ['ナ','na'],['ニ','ni'],['ヌ','nu'],['ネ','ne'],['ノ','no'],
            ['ハ','ha'],['ヒ','hi'],['フ','fu'],['ヘ','he'],['ホ','ho'],
            ['マ','ma'],['ミ','mi'],['ム','mu'],['メ','me'],['モ','mo'],
            ['ヤ','ya'],['ユ','yu'],['ヨ','yo'],
            ['ラ','ra'],['リ','ri'],['ル','ru'],['レ','re'],['ロ','ro'],
            ['ワ','wa'],['ヲ','wo'],['ン','n'],
            ['ガ','ga'],['ギ','gi'],['グ','gu'],['ゲ','ge'],['ゴ','go'],
            ['ザ','za'],['ジ','ji'],['ズ','zu'],['ゼ','ze'],['ゾ','zo'],
            ['ダ','da'],['ヂ','ji'],['ヅ','zu'],['デ','de'],['ド','do'],
            ['バ','ba'],['ビ','bi'],['ブ','bu'],['ベ','be'],['ボ','bo'],
            ['パ','pa'],['ピ','pi'],['プ','pu'],['ペ','pe'],['ポ','po'],
            ['ァ','a'],['ィ','i'],['ゥ','u'],['ェ','e'],['ォ','o'],
            ['ャ','ya'],['ュ','yu'],['ョ','yo'],['ッ',''],
            ['ー','-'],['〜','-'],['・',' '],[' ',' ']
        ];
        for (const [k, v] of pairs) m[k] = v;
        return m;
    })();

    function kanaToRomaji(text) {
        if (!text) return '';
        let out = '';
        let i = 0;
        const s = String(text);
        while (i < s.length) {
            // sokuon っ/ッ → double next consonant
            if ((s[i] === 'っ' || s[i] === 'ッ') && i + 1 < s.length) {
                const next2 = s.slice(i + 1, i + 3);
                const next1 = s[i + 1];
                const rom = KANA_ROMAJI[next2] || KANA_ROMAJI[next1] || '';
                if (rom) {
                    out += rom[0] === 'c' ? 't' : rom[0];
                    i++;
                    continue;
                }
            }
            const two = s.slice(i, i + 2);
            if (KANA_ROMAJI[two] !== undefined) {
                out += KANA_ROMAJI[two];
                i += 2;
                continue;
            }
            if (KANA_ROMAJI[s[i]] !== undefined) {
                out += KANA_ROMAJI[s[i]];
                i++;
                continue;
            }
            out += s[i];
            i++;
        }
        return out
            .replace(/n([bmp])/gi, 'm$1')
            .replace(/\s+/g, ' ')
            .trim();
    }

    function hasJapanese(text) {
        return /[\u3040-\u30ff\u3400-\u9fff]/.test(text || '');
    }

    function hasKanji(text) {
        return /[\u3400-\u9fff]/.test(text || '');
    }

    // Sanitize for filename
    function sanitizeFilename(name) {
        return cleanTitleSuffix(
            (name || '')
                .replace(/[\\/:*?"<>|]/g, '')
                .replace(/\s+/g, ' ')
                .trim()
        ).slice(0, 120);
    }

    // Local-only romaji (kana). Kanji tetap jika ada.
    function toRomajiLocal(text) {
        if (!text) return '';
        return sanitizeFilename(kanaToRomaji(text));
    }

    // Full romaji via Google transliterate (dt=rm). Fallback ke local.
    function toRomajiAsync(text) {
        return new Promise((resolve) => {
            if (!text || !hasJapanese(text)) {
                resolve(sanitizeFilename(text));
                return;
            }
            // Tanpa kanji: cukup konversi lokal
            if (!hasKanji(text)) {
                resolve(toRomajiLocal(text));
                return;
            }
            try {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=ja&tl=en&dt=t&dt=rm&q=' + encodeURIComponent(text),
                    timeout: 8000,
                    onload: (res) => {
                        try {
                            const data = JSON.parse(res.responseText);
                            // Structure varies; romanization often in data[0][i][3] or separate field
                            let romaji = '';
                            if (Array.isArray(data) && Array.isArray(data[0])) {
                                // Collect any romanization snippets
                                for (const part of data[0]) {
                                    if (part && part[3]) romaji += part[3];
                                    else if (part && part[0] && !hasJapanese(part[0])) {
                                        // sometimes translation is used if no rm
                                    }
                                }
                            }
                            // Alternative: data[0].map(x => x[3]).join
                            if (!romaji && Array.isArray(data?.[0])) {
                                romaji = data[0].map(x => x && x[3] ? x[3] : '').join('');
                            }
                            if (romaji && romaji.trim()) {
                                resolve(sanitizeFilename(romaji));
                            } else {
                                // fallback: translate to English words (better than raw kanji for filename)
                                let eng = '';
                                if (Array.isArray(data?.[0])) {
                                    eng = data[0].map(x => x && x[0] ? x[0] : '').join('');
                                }
                                if (eng && !hasJapanese(eng)) resolve(sanitizeFilename(eng));
                                else resolve(toRomajiLocal(text));
                            }
                        } catch (e) {
                            resolve(toRomajiLocal(text));
                        }
                    },
                    onerror: () => resolve(toRomajiLocal(text)),
                    ontimeout: () => resolve(toRomajiLocal(text))
                });
            } catch (e) {
                resolve(toRomajiLocal(text));
            }
        });
    }

    async function fillZipNameRomaji() {
        const input = document.getElementById('xamz-zipname');
        if (!input) return;
        const raw = getTitle();
        if (!raw) return;
        input.value = hasJapanese(raw) ? await toRomajiAsync(raw) : sanitizeFilename(raw);
    }

    // ==================== Deteksi ====================
    function isTargetImage(url) {
        if (!url || typeof url !== 'string') return false;
        const u = url.toLowerCase();

        // Thumbnail WordPress: nama-file-218x300.jpg
        if (/\-\d{2,4}x\d{2,4}\.(jpe?g|webp|png|avif)(\?|$)/i.test(u)) return false;

        if (
            u.includes('discordapp.com') ||
            u.includes('discord.com') ||
            u.includes('cdn.discordapp') ||
            u.includes('google') ||
            u.includes('gstatic') ||
            u.includes('facebook') ||
            u.includes('fbcdn') ||
            u.includes('twitter') ||
            u.includes('twimg') ||
            u.includes('youtube') ||
            u.includes('ytimg') ||
            u.includes('gravatar') ||
            u.includes('emoji') ||
            u.includes('avatar') ||
            u.includes('icon') ||
            u.includes('logo') ||
            u.includes('favicon') ||
            u.includes('data:image') ||
            u.includes('/reactions/') ||
            u.includes('/comments/') ||
            u.includes('/content/web/') ||
            u.includes('/content/reaction') ||
            u.includes('/content/comment') ||
            u.includes('pic.desu.xxx/content/') ||
            u.includes('/sticker') ||
            u.includes('/badge') ||
            u.includes('/rank') ||
            u.includes('/emote') ||
            u.includes('?resize=') ||
            // thumbnail diblokir kecuali pawchive/kemono-style (akan di-upgrade ke original)
            (u.includes('thumbnail') && !u.includes('pawchive') && !u.includes('kemono') && !u.includes('coomer') && !u.includes('nekohouse')) ||
            u.includes('i1.wp.com') ||
            u.includes('i2.wp.com') ||
            u.includes('s.w.org') ||
            u.includes('ts-post-image') ||
            u.includes('attachment-medium') ||
            u.includes('attachment-thumbnail')
        ) return false;

        // Skip banner/ads GIF, logo situs, branding
        if (
            u.includes('.gif') ||
            u.includes('area-iklan') ||
            u.includes('bkp-') ||
            u.includes('komikdewasa-1.png') ||
            u.includes('komikdewasa-1.') ||
            // logo pendek: /KOMIKDEWASA-1.png (bukan chapter title panjang)
            /\/komikdewasa(-\d+)?\.(png|jpe?g|webp)(\?|$)/i.test(u) ||
            /\/uploads\/\d{4}\/\d{2}\/komikdewasa(-\d+)?\.(png|jpe?g|webp)/i.test(u)
        ) return false;

        const isImageExt = u.includes('.webp') || u.includes('.jpg') || u.includes('.jpeg') || u.includes('.png') || u.includes('.avif');
        if (!isImageExt) return false;

        // KomikDewasa chapter panels (nama panjang + nomor)
        // contoh: KOMIKDEWASA.ART-My-GFs-Mommy-Was-My-Sugar-Mommy-1.jpg
        const isKomikDewasaChapter =
            /komikdewasa\.art-[a-z0-9._-]{10,}-\d{1,3}\.(jpe?g|webp|png)(\?|$)/i.test(u) ||
            (/-[0-9]{1,3}\.(jpe?g|webp|png)(\?|$)/i.test(u) && u.length > 90 && u.includes('komikdewasa'));

        // Blokir wp-content di host situs sendiri (logo/assets), kecuali panel chapter KomikDewasa
        const isCdnHost = u.includes('cdnasu.xyz') || u.includes('manga.linksaya.com') ||
            u.includes('img.cdn.house') || u.includes('desu.pics') || u.includes('amz-ch');
        if (u.includes('wp-content') && !isCdnHost && !isKomikDewasaChapter) return false;

        if (isKomikDewasaChapter) return true;

        // MangaToon / Themesia / Kanzenin CDN
        if (
            u.includes('manga.linksaya.com') ||
            u.includes('cdnasu.xyz') ||
            u.includes('/uploads/manga-images/') ||
            u.includes('img.cdn.house') ||
            /\/chapter[-_]?\d+\/\d+\.(jpe?g|webp|png|avif)(\?|$)/i.test(u) ||
            (/\/\d+[\w\-]*\.(jpe?g|webp|png)(\?|$)/i.test(u) && (u.includes('/manga') || u.includes('/uploads/')))
        ) return true;

        // DoujinDesu CDN / signed S3 — query X-Amz-* harus tetap lolos
        if (
            u.includes('amz-ch.desu.pics') ||
            u.includes('desu.pics') ||
            u.includes('x-amz-') ||
            u.includes('storage/uploads')
        ) return true;

        // pawchive / kemono / coomer / nekohouse archives
        if (
            u.includes('pawchive') ||
            u.includes('file.pawchive') ||
            u.includes('img.pawchive') ||
            u.includes('kemono.') ||
            u.includes('coomer.') ||
            u.includes('nekohouse') ||
            /\/data\/[a-f0-9]{2}\/[a-f0-9]{2}\/[a-f0-9]{40,}\./i.test(u) ||
            (/\/[a-f0-9]{40,}\.(png|jpe?g|webp|avif)(\?|$)/i.test(u) && !u.includes('pic.desu.xxx'))
        ) return true;

        // Hash panjang (path lama)
        if (/\/[a-z0-9_-]{20,}\.(webp|jpg|jpeg|png|avif)(\?|$)/i.test(u) &&
            !u.includes('pic.desu.xxx') &&
            !u.includes('/content/')) {
            return true;
        }

        // Nama file aneh rilis baru: "4x (1).webp", "page 01.webp", dll di host gambar
        // (bukan di domain situs / content / reaction)
        if (
            /\.(webp|jpg|jpeg|png|avif)(\?|$)/i.test(u) &&
            !u.includes('pic.desu.xxx') &&
            !u.includes('/content/') &&
            !u.includes('doujin.desu.xxx') &&
            (u.includes('amazonaws') || u.includes('cloudfront') || u.includes('desu.pics') ||
             /\/[a-z0-9%._()\-\s]{8,}\.(webp|jpg|jpeg|png|avif)/i.test(u))
        ) {
            return true;
        }

        return false;
    }

    // Key dedup: pathname saja (tanpa query). JANGAN buang query di URL yang disimpan —
    // signed S3 (X-Amz-*) butuh query string, kalau di-strip gambar gagal load.
    function urlDedupKey(url) {
        if (!url || typeof url !== 'string') return '';
        try {
            const u = new URL(url);
            const host = u.hostname.toLowerCase().replace(/^www\./, '');
            return host + u.pathname.toLowerCase();
        } catch (e) {
            return url.split('?')[0].split('#')[0].toLowerCase()
                .replace(/^https?:\/\//i, '')
                .replace(/^www\./, '');
        }
    }

    // Simpan URL utuh (termasuk query signed). Hanya rapikan protocol/host.
    function normalizeUrl(url) {
        if (!url || typeof url !== 'string') return '';
        try {
            const u = new URL(url);
            // Pertahankan search (X-Amz-Algorithm, dll) — kritis untuk CDN signed
            return u.origin + u.pathname + u.search;
        } catch (e) {
            return url;
        }
    }

    function urlAlreadyHave(url, list) {
        const key = urlDedupKey(url);
        if (!key) return false;
        if (list.some(u => urlDedupKey(u) === key)) return true;
        // pawchive/kemono: dedupe by filename hash (thumbnail vs original beda host)
        const base = getBasename(url);
        if (base && /^[a-f0-9]{32,}\.(png|jpe?g|webp|avif)$/i.test(base)) {
            return list.some(u => getBasename(u) === base);
        }
        return false;
    }

    // pawchive / kemono-style: thumbnail → original full file
    // img.pawchive.pw/thumbnail/data/xx/yy/HASH.png → file.pawchive.pw/data/xx/yy/HASH.png
    function resolveArchiveImageUrl(url) {
        if (!url || typeof url !== 'string') return url;
        try {
            let u = url;
            // relative → absolute
            if (u.startsWith('//')) u = location.protocol + u;
            else if (u.startsWith('/')) u = location.origin + u;

            // pawchive thumbnail → original
            if (/img\.pawchive\.pw\/thumbnail\/data\//i.test(u)) {
                u = u.replace(/img\.pawchive\.pw\/thumbnail\/data\//i, 'file.pawchive.pw/data/');
            } else if (/\/\/img\./i.test(u) && /\/thumbnail\/data\//i.test(u) && /pawchive|kemono|coomer|nekohouse/i.test(u)) {
                u = u.replace(/\/\/img\./i, '//file.').replace(/\/thumbnail\/data\//i, '/data/');
            } else if (/\/thumbnail\/data\//i.test(u) && /pawchive|kemono|coomer/i.test(u)) {
                u = u.replace(/\/thumbnail\/data\//i, '/data/');
                u = u.replace(/\/\/img\./i, '//file.');
            }
            return u;
        } catch (e) {
            return url;
        }
    }

    function addUrl(url) {
        if (!url) return false;
        url = resolveArchiveImageUrl(url);
        if (urlAlreadyHave(url, imageURLs)) return false;
        if (isTargetImage(url)) {
            // Simpan FULL url (dengan query) supaya signed link tetap valid
            imageURLs.push(normalizeUrl(url) || url);
            return true;
        }
        return false;
    }

    function getPageNumFromUrl(url) {
        try {
            const path = new URL(url).pathname;
            const file = path.split('/').pop() || '';
            // Kanzenin: 01-7.jpg, 0001-7.jpg, 21-4.jpg → angka di depan (bukan setelah dash terakhir)
            const mK = file.match(/^(\d+)-\d+\.(jpe?g|webp|png|avif)$/i);
            if (mK) return parseInt(mK[1], 10);
            // MangaToon: 12.jpeg
            const m = file.match(/^(\d+)\.(jpe?g|webp|png|avif)$/i);
            if (m) return parseInt(m[1], 10);
            // KomikDewasa: Title-Name-1.jpg, Title-Name-17.jpg → angka di akhir
            const mEnd = file.match(/-(\d{1,3})\.(jpe?g|webp|png|avif)$/i);
            if (mEnd) return parseInt(mEnd[1], 10);
            // fallback
            const m2 = file.match(/^(\d+)/);
            if (m2) return parseInt(m2[1], 10);
        } catch (e) {}
        return null;
    }

    function isSortByFilenameSite() {
        return /mangatoon\.cc|kanzenin\.(info|xyz)|komikdewasa\.art/i.test(location.hostname);
    }

    // alias lama
    function isMangaToonSite() {
        return isSortByFilenameSite();
    }

    function getBasename(url) {
        try {
            const path = new URL(url).pathname;
            return path.split('/').pop().toLowerCase();
        } catch (e) {
            return (url || '').split('/').pop().split('?')[0].toLowerCase();
        }
    }

    function dedupeByBasename(urls) {
        const seen = new Set();
        const out = [];
        for (const u of urls) {
            const base = getBasename(u);
            if (!base || seen.has(base)) continue;
            seen.add(base);
            out.push(u);
        }
        return out;
    }

    function sortUrlsByFilename(urls) {
        return urls.slice().sort((a, b) => {
            const na = getPageNumFromUrl(a);
            const nb = getPageNumFromUrl(b);
            if (na !== null && nb !== null) return na - nb;
            if (na !== null) return -1;
            if (nb !== null) return 1;
            return a.localeCompare(b);
        });
    }

    function scanXAmz() {
        let changed = false;
        const newUrls = [];
        const before = imageURLs.length;

        function tryAdd(raw) {
            if (!raw) return;
            const resolved = resolveArchiveImageUrl(raw);
            if (!isTargetImage(resolved) && !isTargetImage(raw)) return;
            const full = normalizeUrl(resolved) || resolved;
            if (urlAlreadyHave(full, newUrls) || urlAlreadyHave(full, imageURLs)) return;
            newUrls.push(full);
        }

        // Prioritas: #readerarea / Themesia / Kanzenin reader content
        const readerImgs = document.querySelectorAll(
            '#readerarea img.ts-main-image, #readerarea img, .readercontent img.ts-main-image, .readercontent img, img.ts-main-image, #content.readercontent img'
        );
        const allImgs = readerImgs.length > 0 ? readerImgs : document.querySelectorAll('img');
        allImgs.forEach(img => {
            const candidates = [
                img.currentSrc, img.src,
                img.dataset.src, img.dataset.lazySrc, img.dataset.original,
                img.getAttribute('data-src'), img.getAttribute('data-full'),
                img.parentElement?.href
            ];
            for (const src of candidates) {
                if (src) tryAdd(src);
            }
        });

        // pawchive / kemono: link download original di .post__files / figure
        if (/pawchive|kemono|coomer|nekohouse/i.test(location.hostname)) {
            document.querySelectorAll(
                '.post__files a, .scrape__files a, article a, figure a, a.fileThumb, a.post__attachment-link'
            ).forEach(a => {
                const href = a.href || a.getAttribute('href');
                tryAdd(href);
            });
        }

        try {
            performance.getEntriesByType('resource').forEach(entry => {
                const resolved = resolveArchiveImageUrl(entry.name);
                if ((isTargetImage(resolved) || isTargetImage(entry.name)) &&
                    !urlAlreadyHave(resolved, newUrls) && !urlAlreadyHave(resolved, imageURLs)) {
                    const full = normalizeUrl(resolved) || resolved;
                    newUrls.push(full);
                    if (entry.transferSize > 0) {
                        estimatedSizes[full] = entry.transferSize;
                        estimatedSizes[entry.name] = entry.transferSize;
                    }
                }
            });
        } catch (e) {
            ddLog('warn', 'Performance scan gagal', e && e.message);
        }
        if (newUrls.length > 0) {
            imageURLs.push(...newUrls);
            changed = true;
            ddLog('info', `Scan +${newUrls.length} gambar`, `total ${imageURLs.length}`);
        }
        // MangaToon / Kanzenin: dedupe nama file + urutkan
        if (isSortByFilenameSite() && imageURLs.length > 1) {
            const deduped = dedupeByBasename(imageURLs);
            const sorted = sortUrlsByFilename(deduped);
            if (sorted.length !== imageURLs.length || !sorted.every((u, i) => u === imageURLs[i])) {
                const removed = imageURLs.length - sorted.length;
                imageURLs = sorted;
                changed = true;
                if (removed > 0) ddLog('info', `Dedupe -${removed}`, `sisa ${sorted.length}`);
            }
        }
        // pawchive / archive: selalu dedupe by basename (hash)
        if (/pawchive|kemono|coomer|nekohouse/i.test(location.hostname) && imageURLs.length > 1) {
            const beforeLen = imageURLs.length;
            // prefer file.* (original) over img.* (thumbnail)
            const byBase = new Map();
            for (const u of imageURLs) {
                const b = getBasename(u);
                if (!b) continue;
                const prev = byBase.get(b);
                if (!prev) byBase.set(b, u);
                else if (/file\./i.test(u) && !/file\./i.test(prev)) byBase.set(b, u);
                else if (!/thumbnail/i.test(u) && /thumbnail/i.test(prev)) byBase.set(b, u);
            }
            const deduped = [...byBase.values()];
            if (deduped.length !== beforeLen) {
                imageURLs = deduped;
                changed = true;
                ddLog('info', `Archive dedupe -${beforeLen - deduped.length}`, `sisa ${deduped.length}`);
            }
        }
        if (changed) scheduleUpdatePanel();
        else if (before === 0 && imageURLs.length === 0) {
            ddLog('warn', 'Scan: 0 gambar ditemukan', location.pathname);
        }
        return changed;
    }

    let _updatePanelTimer = null;
    function scheduleUpdatePanel() {
        if (_updatePanelTimer) return;
        _updatePanelTimer = setTimeout(() => {
            _updatePanelTimer = null;
            updatePanel();
        }, 400);
    }

    if ('PerformanceObserver' in window) {
        try {
            new PerformanceObserver((list) => {
                let changed = false;
                for (const entry of list.getEntries()) {
                    if (addUrl(entry.name)) changed = true;
                    if (entry.transferSize > 0) {
                        estimatedSizes[entry.name] = entry.transferSize;
                        estimatedSizes[normalizeUrl(entry.name)] = entry.transferSize;
                    }
                }
                if (changed && isSortByFilenameSite()) {
                    imageURLs = sortUrlsByFilename(dedupeByBasename(imageURLs));
                }
                // debounce — jangan rebuild grid tiap resource baru
                if (changed) scheduleUpdatePanel();
            }).observe({ type: 'resource', buffered: true });
        } catch (e) {}
    }

    // ==================== Estimasi Ukuran ====================
    function formatBytes(bytes) {
        if (!bytes || bytes <= 0) return '0 B';
        const units = ['B', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(1024));
        const val = bytes / Math.pow(1024, i);
        return (i === 0 ? val : val.toFixed(1)) + ' ' + units[i];
    }

    function getEstimatedSize(indexes) {
        if (!indexes || !indexes.length) return 0;
        let total = 0;
        let known = 0;
        indexes.forEach(idx => {
            const url = imageURLs[idx];
            if (url && estimatedSizes[url]) {
                total += estimatedSizes[url];
                known++;
            }
        });
        // kalau sebagian belum diketahui, pakai rata-rata
        if (known > 0 && known < indexes.length) {
            const avg = total / known;
            total += avg * (indexes.length - known);
        } else if (known === 0) {
            total = avgImageSize * indexes.length;
        }
        return Math.round(total);
    }

    function updateSizeEstimate() {
        const indexes = getSelectedIndexes();
        const el = document.getElementById('dd-size-estimate');
        if (!el) return;
        if (!indexes.length) {
            el.textContent = '';
            return;
        }
        const bytes = getEstimatedSize(indexes);
        el.textContent = `≈ ${formatBytes(bytes)}`;
        el.title = `Estimasi ukuran total ${indexes.length} gambar terpilih`;
    }

    // ==================== History ====================
    function addHistory(title, type, count) {
        const entry = {
            title: title || 'Untitled',
            type,
            count,
            time: new Date().toLocaleString('id-ID')
        };
        downloadHistory.unshift(entry);
        if (downloadHistory.length > 30) downloadHistory = downloadHistory.slice(0, 30);
        localStorage.setItem('dd_history', JSON.stringify(downloadHistory));
    }

    function showHistory() {
        const borderCol = isDark ? 'rgba(59,130,246,.2)' : 'rgba(59,130,246,.25)';
        const list = downloadHistory.map((h) =>
            `<div style="padding:8px 0;border-bottom:1px solid ${borderCol};font-size:13px;">
                <b>${h.title}</b><br>
                <span style="opacity:.7">${h.type} • ${h.count} gambar • ${h.time}</span>
            </div>`
        ).join('') || '<div style="padding:20px;text-align:center;opacity:.6">Belum ada history</div>';

        const box = document.createElement('div');
        box.id = 'dd-history-box';
        box.innerHTML = `
            <div style="position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.7);display:flex;align-items:center;justify-content:center;">
                <div style="background:${isDark ? '#0a0a0f' : '#fff'};color:${isDark ? '#fff' : '#111'};width:90%;max-width:420px;max-height:70vh;border-radius:12px;overflow:hidden;border:2px solid #3b82f6;">
                    <div style="padding:12px 16px;background:${isDark ? '#0c0c12' : '#dbeafe'};display:flex;align-items:center;justify-content:space-between;">
                        <b>📋 History Download</b>
                        <div>
                            <button id="dd-hist-clear" style="margin-right:8px;background:#3b82f6;color:#000;border:none;border-radius:6px;padding:5px 10px;cursor:pointer;font-weight:bold;">Hapus Semua</button>
                            <button id="dd-hist-close" style="background:#333;color:#fff;border:none;border-radius:6px;padding:5px 10px;cursor:pointer;">✕</button>
                        </div>
                    </div>
                    <div style="padding:12px 16px;overflow:auto;max-height:50vh;">${list}</div>
                </div>
            </div>
        `;
        document.body.appendChild(box);
        document.getElementById('dd-hist-close').onclick = () => box.remove();
        document.getElementById('dd-hist-clear').onclick = () => {
            downloadHistory = [];
            localStorage.setItem('dd_history', '[]');
            box.remove();
            showHistory();
        };
    }

    // ==================== Notification ====================
    function notify(title, body) {
        if (!notifyEnabled) return;
        try {
            if (typeof GM_notification === 'function') {
                GM_notification({ title, text: body, timeout: 4000 });
            } else if (Notification.permission === 'granted') {
                new Notification(title, { body });
            } else if (Notification.permission !== 'denied') {
                Notification.requestPermission().then(p => {
                    if (p === 'granted') new Notification(title, { body });
                });
            }
        } catch (e) {}
    }

    // ==================== Mini Progress ====================
    function ensureSpinnerStyle() {
        if (document.getElementById('dd-spinner-style')) return;
        const s = document.createElement('style');
        s.id = 'dd-spinner-style';
        s.textContent = `
            @keyframes ddSpin{to{transform:rotate(360deg)}}
            .dd-spinner{
                display:inline-block;width:12px;height:12px;
                border:2px solid rgba(59,130,246,.25);
                border-top-color:#3b82f6;border-radius:50%;
                animation:ddSpin .7s linear infinite;
                vertical-align:-2px;margin-right:4px;flex-shrink:0;
            }
            #dd-mini-title{display:flex;align-items:center;gap:2px}
        `;
        document.head.appendChild(s);
    }

    function spinnerHTML() {
        ensureSpinnerStyle();
        return '<span class="dd-spinner" aria-hidden="true"></span>';
    }

    function showMiniProgress() {
        let mini = document.getElementById('dd-mini-progress');
        if (mini) return mini;

        const bg = isDark ? '#0a0a0f' : '#eff6ff';
        const textColor = isDark ? '#fff' : '#1a1a1a';
        const muted = isDark ? '#93c5fd' : '#1d4ed8';

        mini = document.createElement('div');
        mini.id = 'dd-mini-progress';
        mini.innerHTML = `
            <div id="dd-mini-header">
                <span id="dd-mini-title">${spinnerHTML()} Downloading...</span>
                <button id="dd-mini-close">✕</button>
            </div>
            <div id="dd-mini-bar-wrap"><div id="dd-mini-bar"></div></div>
            <div id="dd-mini-text">0%</div>
        `;
        document.body.appendChild(mini);

        Object.assign(mini.style, {
            position: 'fixed', right: '15px', bottom: '70px', zIndex: '2147483647',
            width: '300px', background: bg, border: '2px solid #3b82f6',
            borderRadius: '10px', padding: '10px 12px', color: textColor,
            fontFamily: 'Arial, sans-serif', boxShadow: '0 4px 20px rgba(59,130,246,.4)', fontSize: '13px'
        });

        const header = mini.querySelector('#dd-mini-header');
        Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px', fontWeight: 'bold' });

        const closeBtn = mini.querySelector('#dd-mini-close');
        Object.assign(closeBtn.style, { background: 'transparent', border: 'none', color: muted, cursor: 'pointer', fontSize: '14px' });
        closeBtn.onclick = () => { if (!isDownloading) hideMiniProgress(); };

        const barWrap = mini.querySelector('#dd-mini-bar-wrap');
        Object.assign(barWrap.style, { height: '10px', background: 'rgba(59,130,246,.2)', borderRadius: '5px', overflow: 'hidden', marginBottom: '6px' });

        const bar = mini.querySelector('#dd-mini-bar');
        Object.assign(bar.style, { height: '100%', width: '0%', background: 'linear-gradient(90deg,#3b82f6,#60a5fa)', transition: 'width .25s', borderRadius: '5px' });

        const text = mini.querySelector('#dd-mini-text');
        Object.assign(text.style, { fontSize: '11px', color: muted, textAlign: 'center' });

        return mini;
    }

    function setMiniProgress(percent, text, speedInfo) {
        const pct = Math.min(100, Math.max(0, percent));
        let displayText = text || (Math.round(pct) + '%');

        // ETA + speed (MB/s)
        if (downloadStartTime && pct > 1 && pct < 99 && isDownloading) {
            const elapsed = (Date.now() - downloadStartTime) / 1000;
            if (elapsed > 0.5) {
                let speedStr = '';
                const bytes = (speedInfo && speedInfo.bytes) ? speedInfo.bytes : downloadBytesDone;
                if (bytes > 0) {
                    const mbPerSec = (bytes / (1024 * 1024)) / elapsed;
                    speedStr = ` • ${mbPerSec.toFixed(2)} MB/s`;
                }
                const totalEst = elapsed / (pct / 100);
                const remain = Math.max(0, totalEst - elapsed);
                if (remain > 0 && remain < 3600) {
                    const sec = Math.round(remain);
                    const etaStr = sec >= 60 ? `${Math.floor(sec / 60)}m ${sec % 60}s` : `${sec}s`;
                    displayText = `${text || Math.round(pct) + '%'}${speedStr} • sisa ~${etaStr}`;
                } else {
                    displayText = `${text || Math.round(pct) + '%'}${speedStr}`;
                }
            }
        }

        // Update mini progress HANYA jika sudah ada (user sudah pencet Minimize)
        const mini = document.getElementById('dd-mini-progress');
        if (mini) {
            const bar = mini.querySelector('#dd-mini-bar');
            const label = mini.querySelector('#dd-mini-text');
            const title = mini.querySelector('#dd-mini-title');
            if (bar) bar.style.width = pct + '%';
            if (label) label.textContent = displayText;
            if (title && text) {
                if (text.startsWith('✅') || text.startsWith('❌')) {
                    title.textContent = text;
                } else {
                    title.innerHTML = spinnerHTML() + ' ' + displayText;
                }
            }
        }

        // Panel progress & status selalu di-update
        const panelProg = document.getElementById('dd-panel-progress');
        const panelBar = document.getElementById('dd-panel-progress-bar');
        if (panelProg && panelBar) {
            panelProg.classList.remove('hidden');
            panelBar.style.width = pct + '%';
        }
        const statusText = document.getElementById('dd-status-text');
        if (statusText && displayText) statusText.textContent = displayText;

        // Progress di title tab browser
        if (tabProgressEnabled && isDownloading) {
            const short = (text || displayText || '').replace(/[•·].*$/, '').trim();
            const match = short.match(/(\d+)\s*\/\s*(\d+)/);
            if (match) {
                document.title = `(${match[1]}/${match[2]}) ${Math.round(pct)}% · DL`;
            } else if (pct >= 100) {
                document.title = `✅ Selesai · DL`;
            } else {
                document.title = `${Math.round(pct)}% · DL`;
            }
        }
    }

    function restoreDocTitle() {
        try {
            if (originalDocTitle) document.title = originalDocTitle;
        } catch (e) {}
    }

    function hideMiniProgress() {
        document.getElementById('dd-mini-progress')?.remove();
        const panelProg = document.getElementById('dd-panel-progress');
        if (panelProg) {
            panelProg.classList.add('hidden');
            const panelBar = document.getElementById('dd-panel-progress-bar');
            if (panelBar) panelBar.style.width = '0%';
        }
    }

    function minimizePanel() {
        const overlay = document.getElementById('xamz-overlay');
        if (overlay) overlay.style.display = 'none';
        const mini = showMiniProgress();
        // Sync status saat minimize (terutama saat download sedang jalan)
        if (isDownloading) {
            const statusText = document.getElementById('dd-status-text');
            const label = mini.querySelector('#dd-mini-text');
            const title = mini.querySelector('#dd-mini-title');
            const panelBar = document.getElementById('dd-panel-progress-bar');
            const miniBar = mini.querySelector('#dd-mini-bar');
            if (statusText && label) label.textContent = statusText.textContent || 'Downloading...';
            if (title) title.innerHTML = spinnerHTML() + ' ' + (statusText?.textContent || 'Downloading...');
            if (panelBar && miniBar) miniBar.style.width = panelBar.style.width || '0%';
        }
        const fab = document.getElementById('xamz-open-button');
        if (fab) fab.style.display = 'none';
    }

    function restorePanel() {
        const overlay = document.getElementById('xamz-overlay');
        if (overlay) overlay.style.display = '';
        const fab = document.getElementById('xamz-open-button');
        if (fab) fab.style.display = 'none';
        const box = document.getElementById('xamz-box');
        if (box) {
            box.classList.remove('dd-closing');
            box.classList.add('dd-open');
        }
    }

    // ==================== Next Chapter ====================
    // ==================== Chapter List ====================
    function collectChapters() {
        const map = new Map(); // url -> {url, title, current}
        const cur = location.href.split('#')[0];

        function add(url, title) {
            if (!url || typeof url !== 'string') return;
            try {
                const abs = new URL(url, location.href);
                if (abs.origin !== location.origin) return;
                const key = abs.href.split('#')[0];
                if (map.has(key)) return;
                const t = (title || abs.pathname.split('/').filter(Boolean).pop() || 'Chapter')
                    .replace(/\s+/g, ' ').trim().slice(0, 120);
                map.set(key, { url: key, title: t, current: key === cur });
            } catch (e) {}
        }

        // Themesia / MangaToon: select#chapter
        const sel = document.querySelector('select#chapter, select.chapter, select[name="chapter"]');
        if (sel) {
            [...sel.options].forEach(opt => {
                if (!opt.value) return;
                add(opt.value.startsWith('http') ? opt.value : opt.value, opt.textContent);
            });
        }

        // Themesia chapter list
        document.querySelectorAll(
            '#chapterlist a, .eplister a, .chapter-list a, .clstyle a, ul.clstyle a, .ch-list a, .listing-chapters_wrap a'
        ).forEach(a => {
            if (a.href) add(a.href, a.textContent);
        });

        // Generic: links yang mirip chapter
        document.querySelectorAll('a[href]').forEach(a => {
            const href = a.href || '';
            const text = (a.textContent || '').trim();
            if (!/chapter|ch\.?\s*\d|episode|bab\s*\d/i.test(href + ' ' + text)) return;
            if (/login|register|comment|discord|facebook|twitter|telegram|download\s*pdf/i.test(href + text)) return;
            add(href, text);
        });

        // DoujinDesu / reader: link ke /reader/
        if (/doujin\.desu/i.test(location.hostname)) {
            document.querySelectorAll('a[href*="/reader/"], a[href*="/manga/"]').forEach(a => {
                if (a.href) add(a.href, a.textContent);
            });
        }

        let list = [...map.values()];
        // Sort: coba angka chapter
        list.sort((a, b) => {
            const na = parseFloat((a.title.match(/(\d+(?:\.\d+)?)/) || [])[1]);
            const nb = parseFloat((b.title.match(/(\d+(?:\.\d+)?)/) || [])[1]);
            if (!isNaN(na) && !isNaN(nb)) return na - nb;
            return a.title.localeCompare(b.title);
        });
        return list;
    }

    function getChapterQueue() {
        try {
            return JSON.parse(sessionStorage.getItem('dd_ch_queue') || '[]');
        } catch (e) { return []; }
    }

    function getChapterQueueIndex() {
        return parseInt(sessionStorage.getItem('dd_ch_queue_i') || '0', 10) || 0;
    }

    function setChapterQueue(urls, index) {
        sessionStorage.setItem('dd_ch_queue', JSON.stringify(urls || []));
        sessionStorage.setItem('dd_ch_queue_i', String(index || 0));
    }

    function clearChapterQueue() {
        sessionStorage.removeItem('dd_ch_queue');
        sessionStorage.removeItem('dd_ch_queue_i');
    }

    function showChapterPanel() {
        if (document.getElementById('dd-chapter-box')) return;
        const chapters = collectChapters();
        const queue = getChapterQueue();
        const qIdx = getChapterQueueIndex();

        const box = document.createElement('div');
        box.id = 'dd-chapter-box';
        const rows = chapters.length
            ? chapters.map((ch, i) => `
                <label class="dd-ch-row${ch.current ? ' dd-ch-current' : ''}">
                    <input type="checkbox" class="dd-ch-cb" data-url="${ch.url.replace(/"/g, '&quot;')}" ${ch.current ? 'checked' : ''}>
                    <span class="dd-ch-num">${i + 1}</span>
                    <span class="dd-ch-title" title="${ch.title.replace(/"/g, '&quot;')}">${ch.title}${ch.current ? ' · sekarang' : ''}</span>
                </label>`).join('')
            : `<div class="dd-help-note">Tidak ada daftar chapter di halaman ini.<br>
               Buka halaman series / halaman yang punya list chapter, atau gunakan dropdown chapter situs.</div>`;

        box.innerHTML = `
            <div class="dd-modal-bg">
                <div class="dd-modal dd-modal-wide">
                    <div class="dd-modal-head">
                        <b>📑 Daftar Chapter</b>
                        <button id="dd-ch-close" type="button">✕</button>
                    </div>
                    <div class="dd-modal-body">
                        <div class="dd-ch-toolbar">
                            <button type="button" id="dd-ch-all" class="dd-ch-btn">Semua</button>
                            <button type="button" id="dd-ch-none" class="dd-ch-btn">Batal</button>
                            <button type="button" id="dd-ch-from" class="dd-ch-btn" title="Centang dari chapter saat ini ke bawah">Dari sini ↓</button>
                            <span class="dd-ch-count">${chapters.length} chapter</span>
                        </div>
                        <div class="dd-ch-list">${rows}</div>
                        ${queue.length ? `<div class="dd-help-note">Antrian aktif: ${Math.min(qIdx + 1, queue.length)}/${queue.length}
                            <button type="button" id="dd-ch-clear-q" class="dd-ch-btn" style="margin-left:8px">Hapus antrian</button></div>` : ''}
                        <div class="dd-set-actions" style="margin-top:12px">
                            <button type="button" id="dd-ch-open" class="dd-set-save">Buka terpilih</button>
                            <button type="button" id="dd-ch-queue" class="dd-set-reset">Antrian berurutan</button>
                        </div>
                        <div class="dd-help-note">Antrian berurutan: buka chapter satu per satu. Setelah selesai di satu chapter, pakai Next Ch / lanjut antrian.</div>
                    </div>
                </div>
            </div>`;
        document.body.appendChild(box);

        const listEl = box.querySelector('.dd-ch-list');
        document.getElementById('dd-ch-close').onclick = () => box.remove();
        box.querySelector('.dd-modal-bg').onclick = (e) => { if (e.target === e.currentTarget) box.remove(); };

        document.getElementById('dd-ch-all').onclick = () => {
            listEl.querySelectorAll('.dd-ch-cb').forEach(cb => { cb.checked = true; });
        };
        document.getElementById('dd-ch-none').onclick = () => {
            listEl.querySelectorAll('.dd-ch-cb').forEach(cb => { cb.checked = false; });
        };
        document.getElementById('dd-ch-from').onclick = () => {
            const cbs = [...listEl.querySelectorAll('.dd-ch-cb')];
            let start = cbs.findIndex(cb => cb.closest('.dd-ch-current'));
            if (start < 0) start = 0;
            cbs.forEach((cb, i) => { cb.checked = i >= start; });
        };
        const clearQBtn = document.getElementById('dd-ch-clear-q');
        if (clearQBtn) clearQBtn.onclick = () => { clearChapterQueue(); box.remove(); showChapterPanel(); };

        function selectedUrls() {
            return [...listEl.querySelectorAll('.dd-ch-cb:checked')].map(cb => cb.dataset.url).filter(Boolean);
        }

        document.getElementById('dd-ch-open').onclick = () => {
            const urls = selectedUrls();
            if (!urls.length) return alert('Pilih minimal 1 chapter');
            clearChapterQueue();
            box.remove();
            closePanel();
            location.href = urls[0];
        };

        document.getElementById('dd-ch-queue').onclick = () => {
            const urls = selectedUrls();
            if (!urls.length) return alert('Pilih minimal 1 chapter');
            setChapterQueue(urls, 0);
            box.remove();
            closePanel();
            location.href = urls[0];
        };
    }

    function goNextInQueue() {
        const queue = getChapterQueue();
        if (!queue.length) return false;
        let i = getChapterQueueIndex() + 1;
        if (i >= queue.length) {
            clearChapterQueue();
            notify('Pawchive DL', 'Antrian chapter selesai');
            return false;
        }
        setChapterQueue(queue, i);
        location.href = queue[i];
        return true;
    }

    function showQueueBanner() {
        const queue = getChapterQueue();
        if (!queue.length) return;
        if (document.getElementById('dd-queue-banner')) return;
        const i = getChapterQueueIndex();
        const ban = document.createElement('div');
        ban.id = 'dd-queue-banner';
        ban.innerHTML = `
            <span>📑 Antrian ${i + 1}/${queue.length}</span>
            <button type="button" id="dd-queue-next">Chapter berikutnya</button>
            <button type="button" id="dd-queue-stop">Stop</button>`;
        Object.assign(ban.style, {
            position: 'fixed', bottom: '16px', left: '16px', zIndex: '2147483646',
            display: 'flex', alignItems: 'center', gap: '8px',
            padding: '8px 12px', borderRadius: '10px',
            background: 'rgba(12,12,20,.92)', color: '#fff',
            border: '1px solid rgba(59,130,246,.5)',
            fontFamily: 'Segoe UI, Arial, sans-serif', fontSize: '12px',
            boxShadow: '0 4px 20px rgba(0,0,0,.4)'
        });
        document.body.appendChild(ban);
        document.getElementById('dd-queue-next').onclick = () => goNextInQueue();
        document.getElementById('dd-queue-stop').onclick = () => {
            clearChapterQueue();
            ban.remove();
        };
        // style buttons
        ban.querySelectorAll('button').forEach(b => {
            Object.assign(b.style, {
                border: 'none', borderRadius: '6px', padding: '5px 10px',
                cursor: 'pointer', fontWeight: '700', fontSize: '11px',
                background: '#3b82f6', color: '#000'
            });
        });
        document.getElementById('dd-queue-stop').style.background = '#333';
        document.getElementById('dd-queue-stop').style.color = '#fff';
    }

    function goNextChapter() {
        // Jika ada antrian chapter, prioritaskan
        if (getChapterQueue().length && goNextInQueue()) return;

        // Themesia / MangaToon: a.ch-next-btn
        const chNext = document.querySelector('a.ch-next-btn:not(.disabled)') ||
                       document.querySelector('.chnav a[rel="next"]') ||
                       document.querySelector('a.ch-next-btn');
        if (chNext && chNext.href && !chNext.classList.contains('disabled') && !chNext.href.includes('#/next')) {
            closePanel();
            location.href = chNext.href;
            return;
        }

        // Chapter select dropdown (Themesia): pilih option berikutnya
        const chapterSelect = document.querySelector('select#chapter');
        if (chapterSelect && chapterSelect.selectedIndex >= 0) {
            const nextOpt = chapterSelect.options[chapterSelect.selectedIndex + 1];
            if (nextOpt && nextOpt.value) {
                closePanel();
                if (nextOpt.value.startsWith('http')) {
                    location.href = nextOpt.value;
                } else {
                    chapterSelect.selectedIndex = chapterSelect.selectedIndex + 1;
                    chapterSelect.dispatchEvent(new Event('change', { bubbles: true }));
                }
                return;
            }
        }

        const candidates = [...document.querySelectorAll('a, button')].filter(el => {
            const t = (el.textContent || '').trim().toLowerCase();
            return t.includes('next ch') || t === 'next' || t.includes('next chapter');
        });

        const btn = candidates.find(el => !el.disabled && !el.classList.contains('disabled') && el.getAttribute('aria-disabled') !== 'true') || candidates[0];

        let nextUrl = null;
        if (btn) {
            if (btn.tagName === 'A' && btn.href && !btn.href.includes('#/next')) nextUrl = btn.href;
            else {
                const parentA = btn.closest('a');
                if (parentA && parentA.href && !parentA.href.includes('#/next')) nextUrl = parentA.href;
            }
        }

        const nextLink = document.querySelector('a[rel="next"]') ||
                         document.querySelector('a.next') ||
                         document.querySelector('[class*="next"] a');
        if (!nextUrl && nextLink && nextLink.href && !nextLink.href.includes('#/next')) nextUrl = nextLink.href;

        if (nextUrl) {
            closePanel();
            location.href = nextUrl;
            return;
        }

        if (btn) {
            closePanel();
            const oldUrl = location.href;
            btn.click();
            const check = setInterval(() => {
                if (location.href !== oldUrl) {
                    clearInterval(check);
                    setTimeout(() => {
                        imageURLs = [];
                        panelOpen = false;
                        if (!document.getElementById('xamz-overlay')) openPanel();
                    }, 1600);
                }
            }, 250);
            setTimeout(() => clearInterval(check), 12000);
            return;
        }

        alert('Tombol Next Chapter tidak ditemukan di halaman ini');
    }

    // ==================== UI ====================
    function applyFabPosition(btn) {
        if (!btn) btn = document.getElementById('xamz-open-button');
        if (!btn) return;
        btn.style.left = '';
        btn.style.right = '12px';
        if (fabPosition === 'bottom-right') {
            btn.style.top = '';
            btn.style.bottom = '12px';
        } else {
            btn.style.bottom = '';
            btn.style.top = '12px';
        }
    }

    function createButton() {
        if (document.getElementById('xamz-open-button')) return;
        const btn = document.createElement('button');
        btn.id = 'xamz-open-button';
        btn.title = 'Pawchive Downloader';
        btn.innerHTML = 'DL';
        Object.assign(btn.style, {
            position: 'fixed', zIndex: '2147483647',
            background: '#3b82f6', color: '#fff', border: 'none', borderRadius: '6px',
            padding: '5px 10px', fontSize: '11px', fontWeight: '700', cursor: 'pointer',
            boxShadow: '0 2px 10px rgba(59,130,246,.45)', letterSpacing: '0.5px',
            lineHeight: '1.2', opacity: '0.92', transition: 'opacity .15s, transform .15s'
        });
        applyFabPosition(btn);
        btn.onmouseenter = () => { btn.style.opacity = '1'; btn.style.transform = 'scale(1.05)'; };
        btn.onmouseleave = () => { btn.style.opacity = '0.92'; btn.style.transform = 'scale(1)'; };
        btn.onclick = openPanel;
        document.body.appendChild(btn);
    }

    function openPanel() {
        if (panelOpen && document.getElementById('xamz-overlay')) {
            restorePanel();
            return;
        }
        if (panelOpen) return;
        panelOpen = true;
        scanXAmz();
        autoScanInterval = setInterval(() => {
            if (panelOpen && !isDownloading) scanXAmz();
        }, 5000);

        const autoTitle = hasJapanese(getTitle()) ? toRomajiLocal(getTitle()) : getTitle();
        const overlay = document.createElement('div');
        overlay.id = 'xamz-overlay';
        const collapsedCls = toolbarCollapsed ? 'dd-toolbar-collapsed' : '';
        overlay.innerHTML = `
            <div id="xamz-box" class="${isCompact ? 'dd-compact' : ''} dd-thumb-${thumbSize} ${collapsedCls}" style="--dd-accent:${accentColor};--dd-accent-hover:${lightenAccent(accentColor)}">
                <div id="xamz-header">
                    <div class="dd-title-wrap">
                        <div class="dd-title">
                            <span class="dd-title-mark">PD</span>
                            <span class="dd-title-text">Pawchive</span>
                            <span class="dd-by">qxyraxp</span>
                        </div>
                    </div>
                    <div class="dd-header-actions">
                        <button id="dd-minimize-btn" class="dd-hbtn" title="${t('minimize')}" data-i18n-title="minimize">−</button>
                        <button id="dd-collapse-btn" class="dd-hbtn" title="${t('collapse')}" data-i18n-title="collapse">${toolbarCollapsed ? '▴' : '▾'}</button>
                        <span class="dd-hsep"></span>
                        <button id="dd-filter-btn" class="dd-hbtn" title="${t('filter')}" data-i18n-title="filter">⊆</button>
                        <button id="dd-history-btn" class="dd-hbtn" title="${t('history')}" data-i18n-title="history">☰</button>
                        <button id="dd-chapters-btn" class="dd-hbtn" title="${t('chapters')}" data-i18n-title="chapters">≡</button>
                        <button id="dd-settings-btn" class="dd-hbtn" title="${t('settings')}" data-i18n-title="settings">⚙</button>
                        <button id="dd-theme-btn" class="dd-hbtn" title="${t('theme')}" data-i18n-title="theme">${isDark ? '☀' : '☾'}</button>
                        <button id="dd-lang-btn" class="dd-hbtn dd-lang-btn" title="${t('langToggle')}" data-i18n-title="langToggle">${uiLang === 'en' ? 'EN' : 'ID'}</button>
                        <span class="dd-hsep"></span>
                        <button id="xamz-close" class="dd-hbtn dd-hbtn-close" title="${t('close')}" data-i18n-title="close">✕</button>
                    </div>
                </div>

                <div id="xamz-collapsible">
                    <div id="xamz-toolbar" class="dd-toolbar-row">
                        <button id="xamz-refresh" class="dd-tbtn" title="Refresh scan (R)">Scan</button>
                        <button id="dd-autoscroll" class="dd-tbtn" title="Auto Scroll">Scroll</button>
                        <span class="dd-hsep"></span>
                        <button id="xamz-select-all" class="dd-tbtn" title="Select All (A)">All</button>
                        <button id="xamz-select-none" class="dd-tbtn" title="Deselect All">None</button>
                        <button id="dd-skip-cover" class="dd-tbtn" title="Skip cover">Cover</button>
                        <button id="dd-skip-ends" class="dd-tbtn" title="Skip cover + ends">Ends</button>
                        <span class="dd-tool-spacer"></span>
                        <button id="dd-size-s" class="dd-tbtn dd-size-btn ${thumbSize==='s'?'dd-size-active':''}" title="Kecil">S</button>
                        <button id="dd-size-m" class="dd-tbtn dd-size-btn ${thumbSize==='m'?'dd-size-active':''}" title="Sedang">M</button>
                        <button id="dd-size-l" class="dd-tbtn dd-size-btn ${thumbSize==='l'?'dd-size-active':''}" title="Besar">L</button>
                        <button id="dd-compact-btn" class="dd-tbtn dd-size-btn ${isCompact?'dd-size-active':''}" title="Compact">⊞</button>
                    </div>

                    <div id="xamz-zipname-row">
                        <div class="dd-name-row">
                            <div class="dd-name-col">
                                <div class="dd-name-field dd-name-inline">
                                    <label class="dd-inline-label" data-i18n="fileName">${t('fileName')}</label>
                                    <input id="xamz-zipname" type="text" value="${autoTitle}" placeholder="${t('filePlaceholder')}" data-i18n-placeholder="filePlaceholder">
                                    <button id="dd-use-title" type="button" title="${t('refreshTitle')}" data-i18n-title="refreshTitle">↺</button>
                                </div>
                            </div>
                            <div class="dd-name-col">
                                <div class="dd-name-field dd-name-inline">
                                    <label class="dd-inline-label" data-i18n="saveFolder">${t('saveFolder')}</label>
                                    <div id="dd-folder-label" class="dd-folder-label" title="${t('pickFolder')}">${t('downloadsDefault')}</div>
                                    <button id="dd-pick-folder" type="button" title="${t('pickFolder')}" data-i18n-title="pickFolder">📁</button>
                                    <button id="dd-clear-folder" type="button" title="${t('resetFolder')}" data-i18n="resetFolder">${t('resetFolder')}</button>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                <div id="xamz-status"><span id="dd-status-text"></span></div>
                <div id="dd-panel-progress" class="dd-panel-progress hidden">
                    <div id="dd-panel-progress-bar"></div>
                </div>
                <div id="xamz-grid"></div>

                <!-- Bottom bar: single Download + Next Ch -->
                <div id="dd-bottom-bar">
                    <div class="dd-bottom-left">
                        <span id="xamz-count" class="dd-bottom-stat dd-bottom-total">0 gambar</span>
                        <span class="dd-bottom-sep">·</span>
                        <span id="dd-bottom-selected" class="dd-bottom-stat">0 terpilih</span>
                        <span id="dd-bottom-size" class="dd-bottom-stat"></span>
                    </div>
                    <div class="dd-bottom-right">
                        <button id="dd-nextch-btn" class="dd-bottom-btn dd-next-btn" title="Next Chapter">Next Ch</button>
                        <div class="dd-download-wrap">
                            <button id="dd-download-main" class="dd-bottom-btn dd-download-main dd-dl-btn" title="Download">
                                Download <span class="dd-badge" id="badge-dl">0</span> ▾
                            </button>
                            <div id="dd-download-menu" class="dd-download-menu hidden">
                                <button id="xamz-download-selected" class="dd-menu-item" data-mode="single">Terpilih <span class="dd-menu-desc">file satu-satu</span></button>
                                <button id="xamz-download-zip" class="dd-menu-item ${lastDownloadMode==='zip'?'dd-menu-active':''}" data-mode="zip">ZIP <span class="dd-menu-desc">arsip zip</span></button>
                                <button id="xamz-download-pdf" class="dd-menu-item ${lastDownloadMode==='pdf'?'dd-menu-active':''}" data-mode="pdf">PDF <span class="dd-menu-desc">per halaman</span></button>
                                <button id="xamz-download-strip" class="dd-menu-item ${lastDownloadMode==='strip'?'dd-menu-active':''}" data-mode="strip">Strip <span class="dd-menu-desc">PDF nyambung</span></button>
                            </div>
                        </div>
                    </div>
                </div>

                <div class="dd-resize-edge dd-resize-n" data-dir="n"></div>
                <div class="dd-resize-edge dd-resize-s" data-dir="s"></div>
                <div class="dd-resize-edge dd-resize-e" data-dir="e"></div>
                <div class="dd-resize-edge dd-resize-w" data-dir="w"></div>
                <div class="dd-resize-corner dd-resize-nw" data-dir="nw"></div>
                <div class="dd-resize-corner dd-resize-ne" data-dir="ne"></div>
                <div class="dd-resize-corner dd-resize-sw" data-dir="sw"></div>
                <div class="dd-resize-corner dd-resize-se" data-dir="se" title="Drag untuk ubah ukuran"></div>
                <div id="dd-hover-preview" class="dd-hover-preview hidden">
                    <img id="dd-hover-img" alt="preview">
                    <div id="dd-hover-meta" class="dd-hover-meta"></div>
                </div>
                <div id="dd-accent-picker" class="dd-accent-picker hidden">
                    <button data-color="#3b82f6" style="background:#3b82f6" title="Biru"></button>
                    <button data-color="#6366f1" style="background:#6366f1" title="Indigo"></button>
                    <button data-color="#0ea5e9" style="background:#0ea5e9" title="Sky"></button>
                    <button data-color="#1e40af" style="background:#1e40af" title="Navy"></button>
                    <button data-color="#1d4ed8" style="background:#1d4ed8" title="Blue"></button>
                    <button data-color="#2563eb" style="background:#2563eb" title="Azure"></button>
                </div>
            </div>
        `;
        document.body.appendChild(overlay);
        createStyles();
        applyTheme();
        const fab = document.getElementById('xamz-open-button');
        if (fab) fab.style.display = 'none';

        requestAnimationFrame(() => {
            const box = document.getElementById('xamz-box');
            if (box) box.classList.add('dd-open');
        });

        // Event bindings
        document.getElementById('xamz-close').onclick = closePanel;
        document.getElementById('dd-minimize-btn').onclick = minimizePanel;
        document.getElementById('xamz-refresh').onclick = () => {
            showSkeleton();
            scanXAmz();
            setTimeout(() => updatePanel(), 350);
        };
        document.getElementById('xamz-select-all').onclick = () => {
            document.querySelectorAll('#xamz-grid input[type="checkbox"]').forEach(cb => cb.checked = true);
            updateSelectedUI();
        };
        document.getElementById('xamz-select-none').onclick = () => {
            document.querySelectorAll('#xamz-grid input[type="checkbox"]').forEach(cb => cb.checked = false);
            updateSelectedUI();
        };
        document.getElementById('dd-skip-cover').onclick = skipCover;
        document.getElementById('dd-skip-ends').onclick = skipCoverAndEnd;
        document.getElementById('xamz-download-selected').onclick = () => { closeDownloadMenu(); downloadSelected(); };
        document.getElementById('xamz-download-zip').onclick = () => { closeDownloadMenu(); lastDownloadMode = 'zip'; localStorage.setItem('dd_last_mode', 'zip'); updateMenuActive(); downloadAsZip(); };
        document.getElementById('xamz-download-pdf').onclick = () => { closeDownloadMenu(); lastDownloadMode = 'pdf'; localStorage.setItem('dd_last_mode', 'pdf'); updateMenuActive(); downloadAsPdf(); };
        document.getElementById('xamz-download-strip').onclick = () => { closeDownloadMenu(); lastDownloadMode = 'strip'; localStorage.setItem('dd_last_mode', 'strip'); updateMenuActive(); downloadAsPdfStrip(); };
        document.getElementById('dd-history-btn').onclick = showHistory;
        document.getElementById('dd-chapters-btn').onclick = showChapterPanel;
        const logBtn = document.getElementById('dd-log-btn');
        if (logBtn) logBtn.onclick = showLogPanel;
        document.getElementById('dd-settings-btn').onclick = showSettings;
        const helpBtn = document.getElementById('dd-help-btn');
        if (helpBtn) helpBtn.onclick = showHelp;
        document.getElementById('dd-theme-btn').onclick = toggleTheme;
        const langBtn = document.getElementById('dd-lang-btn');
        if (langBtn) langBtn.onclick = toggleLang;
        document.getElementById('dd-autoscroll').onclick = autoScrollScan;
        const reloadBtn = document.getElementById('dd-reload-btn');
        if (reloadBtn) reloadBtn.onclick = () => location.reload();
        document.getElementById('dd-nextch-btn').onclick = goNextChapter;
        document.getElementById('dd-use-title').onclick = () => {
            fillZipNameRomaji();
        };
        document.getElementById('dd-pick-folder').onclick = () => { pickSaveFolder(); };
        document.getElementById('dd-clear-folder').onclick = async () => {
            await clearDirHandle();
            saveMode = 'default';
            localStorage.setItem('dd_save_mode', 'default');
            updateFolderUI();
            const st = document.getElementById('dd-status-text');
            if (st) st.textContent = 'Simpan ke Downloads (default)';
        };
        restoreDirHandle().then(() => updateFolderUI());

        // Download dropdown
        document.getElementById('dd-download-main').onclick = (e) => {
            e.stopPropagation();
            if (isDownloading) return;
            const menu = document.getElementById('dd-download-menu');
            if (menu) menu.classList.toggle('hidden');
        };

        document.getElementById('dd-size-s').onclick = () => setThumbSize('s');
        document.getElementById('dd-size-m').onclick = () => setThumbSize('m');
        document.getElementById('dd-size-l').onclick = () => setThumbSize('l');
        document.getElementById('dd-compact-btn').onclick = toggleCompact;
        document.getElementById('dd-collapse-btn').onclick = toggleToolbarCollapse;
        document.getElementById('dd-filter-btn').onclick = toggleSelectedOnly;

        const accentBtn = document.getElementById('dd-accent-btn');
        if (accentBtn) {
            accentBtn.onclick = (e) => {
                e.stopPropagation();
                document.getElementById('dd-accent-picker')?.classList.toggle('hidden');
            };
        }
        document.querySelectorAll('#dd-accent-picker button').forEach(btn => {
            btn.onclick = (e) => {
                e.stopPropagation();
                setAccentColor(btn.dataset.color);
                document.getElementById('dd-accent-picker')?.classList.add('hidden');
            };
        });
        document.getElementById('xamz-box')?.addEventListener('click', (e) => {
            if (!e.target.closest('#dd-accent-btn') && !e.target.closest('#dd-accent-picker')) {
                document.getElementById('dd-accent-picker')?.classList.add('hidden');
            }
            if (!e.target.closest('#dd-download-main') && !e.target.closest('#dd-download-menu')) {
                closeDownloadMenu();
            }
        });

        document.addEventListener('keydown', handleKeydown);

        updatePanel();
        initPanelResize();
        initPanelDrag();
        applyDownloadDisabledState();
        applyI18n();
        // Judul Jepang → romaji (async, overwrite setelah Google respond)
        fillZipNameRomaji();
    }

    function handleKeydown(e) {
        if (!panelOpen) return;
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') return;

        const key = e.key.toLowerCase();
        if (key === 'escape') {
            e.preventDefault();
            const modal = document.getElementById('dd-settings-box') || document.getElementById('dd-help-box') || document.getElementById('dd-history-box') || document.getElementById('dd-chapter-box') || document.getElementById('dd-log-box');
            if (modal) modal.remove();
            else closePanel();
        } else if (key === 'r' && !e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            document.getElementById('xamz-refresh')?.click();
        } else if (key === 'a' && !e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            document.getElementById('xamz-select-all')?.click();
        } else if (key === 'z' && !e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            document.getElementById('xamz-download-zip')?.click();
        } else if (key === '?' || (key === '/' && e.shiftKey)) {
            e.preventDefault();
            showHelp();
        } else if (key === 's' && !e.ctrlKey && !e.metaKey) {
            e.preventDefault();
            showSettings();
        }
    }

    function skipCover() {
        const n = Math.max(0, skipCoverCount | 0);
        const checkboxes = [...document.querySelectorAll('#xamz-grid input[type="checkbox"]')];
        checkboxes.forEach((cb, i) => {
            cb.checked = i >= n;
        });
        updateSelectedUI();
    }

    // Uncheck cover (awal) + credit/end page (akhir)
    function skipCoverAndEnd() {
        const nStart = Math.max(0, skipCoverCount | 0);
        const nEnd = 1; // 1 panel terakhir (biasanya credit)
        const checkboxes = [...document.querySelectorAll('#xamz-grid input[type="checkbox"]')];
        const total = checkboxes.length;
        if (!total) return;
        checkboxes.forEach((cb, i) => {
            const isCover = i < nStart;
            const isEnd = i >= total - nEnd;
            cb.checked = !isCover && !isEnd;
        });
        updateSelectedUI();
        const st = document.getElementById('dd-status-text');
        if (st) {
            const skipped = nStart + Math.min(nEnd, Math.max(0, total - nStart));
            st.textContent = `Skip ${nStart} cover + ${Math.min(nEnd, total)} akhir · ${getSelectedIndexes().length} terpilih`;
        }
    }

    function showHelp() {
        if (document.getElementById('dd-help-box')) return;
        const box = document.createElement('div');
        box.id = 'dd-help-box';
        box.innerHTML = `
            <div class="dd-modal-bg">
                <div class="dd-modal">
                    <div class="dd-modal-head">
                        <b>⌨ Shortcut Keyboard</b>
                        <button id="dd-help-close" type="button">✕</button>
                    </div>
                    <div class="dd-modal-body">
                        <div class="dd-help-row"><kbd>R</kbd> <span>Refresh scan gambar</span></div>
                        <div class="dd-help-row"><kbd>A</kbd> <span>Pilih semua panel</span></div>
                        <div class="dd-help-row"><kbd>Z</kbd> <span>Download sebagai ZIP</span></div>
                        <div class="dd-help-row"><kbd>S</kbd> <span>Buka Settings</span></div>
                        <div class="dd-help-row"><kbd>?</kbd> <span>Tampilkan bantuan ini</span></div>
                        <div class="dd-help-row"><kbd>Esc</kbd> <span>Tutup panel / modal</span></div>
                        <div class="dd-help-row"><kbd>Shift+Click</kbd> <span>Pilih range panel</span></div>
                        <div class="dd-help-row"><kbd>⏭ Cover</kbd> <span>Uncheck panel awal (jumlah di Settings)</span></div>
                        <div class="dd-help-row"><kbd>⏭ Ends</kbd> <span>Uncheck cover + 1 panel terakhir (credit)</span></div>
                        <div class="dd-help-note">Saat download, title tab browser menampilkan progress (bisa dimatikan di Settings).</div>
                    </div>
                </div>
            </div>`;
        document.body.appendChild(box);
        document.getElementById('dd-help-close').onclick = () => box.remove();
        box.querySelector('.dd-modal-bg').onclick = (e) => { if (e.target === e.currentTarget) box.remove(); };
    }

    function showSettings() {
        if (document.getElementById('dd-settings-box')) return;
        const box = document.createElement('div');
        box.id = 'dd-settings-box';
        box.innerHTML = `
            <div class="dd-modal-bg">
                <div class="dd-modal">
                    <div class="dd-modal-head">
                        <b>⚙ ${t('settings')}</b>
                        <button id="dd-settings-close" type="button">✕</button>
                    </div>
                    <div class="dd-modal-body">
                        <label class="dd-set-label">${t('language')}
                            <select id="dd-set-lang">
                                <option value="en"${uiLang==='en'?' selected':''}>English</option>
                                <option value="id"${uiLang==='id'?' selected':''}>Indonesia</option>
                            </select>
                        </label>
                        <label class="dd-set-label">${t('concurrent')}
                            <select id="dd-set-concurrency">
                                <option value="2"${dlConcurrency===2?' selected':''}>2</option>
                                <option value="3"${dlConcurrency===3?' selected':''}>3</option>
                                <option value="4"${dlConcurrency===4?' selected':''}>4 (default)</option>
                                <option value="5"${dlConcurrency===5?' selected':''}>5</option>
                                <option value="6"${dlConcurrency===6?' selected':''}>6</option>
                            </select>
                        </label>
                        <label class="dd-set-label">${t('jpegQuality')}
                            <select id="dd-set-jpeg">
                                <option value="0.8"${jpegQuality===0.8?' selected':''}>80%</option>
                                <option value="0.85"${jpegQuality===0.85?' selected':''}>85%</option>
                                <option value="0.92"${Math.abs(jpegQuality-0.92)<0.01?' selected':''}>92% (default)</option>
                                <option value="0.95"${jpegQuality===0.95?' selected':''}>95%</option>
                                <option value="1"${jpegQuality===1?' selected':''}>100%</option>
                            </select>
                        </label>
                        <label class="dd-set-label">${t('defaultMode')}
                            <select id="dd-set-mode">
                                <option value="zip"${lastDownloadMode==='zip'?' selected':''}>ZIP</option>
                                <option value="pdf"${lastDownloadMode==='pdf'?' selected':''}>PDF</option>
                                <option value="strip"${lastDownloadMode==='strip'?' selected':''}>Strip PDF</option>
                                <option value="single"${lastDownloadMode==='single'?' selected':''}>${t('selected')}</option>
                            </select>
                        </label>
                        <label class="dd-set-label">${t('skipCover')}
                            <select id="dd-set-skip">
                                <option value="0"${skipCoverCount===0?' selected':''}>0</option>
                                <option value="1"${skipCoverCount===1?' selected':''}>1</option>
                                <option value="2"${skipCoverCount===2?' selected':''}>2 (default)</option>
                                <option value="3"${skipCoverCount===3?' selected':''}>3</option>
                                <option value="4"${skipCoverCount===4?' selected':''}>4</option>
                            </select>
                        </label>
                        <label class="dd-set-label">${t('fabPos')}
                            <select id="dd-set-fab">
                                <option value="top-right"${fabPosition==='top-right'?' selected':''}>${t('topRight')}</option>
                                <option value="bottom-right"${fabPosition==='bottom-right'?' selected':''}>${t('bottomRight')}</option>
                            </select>
                        </label>
                        <label class="dd-set-label">${t('saveLoc')}
                            <select id="dd-set-savemode">
                                <option value="default"${saveMode==='default'?' selected':''}>${t('saveDefault')}</option>
                                <option value="ask"${saveMode==='ask'?' selected':''}>${t('saveAsk')}</option>
                                <option value="folder"${saveMode==='folder'?' selected':''}>${t('saveFolderMode')}</option>
                            </select>
                        </label>
                        <div class="dd-help-note" style="margin:0 0 12px;">${t('saveNote')}</div>
                        <label class="dd-set-check"><input type="checkbox" id="dd-set-autoopen"${autoOpenPanel?' checked':''}> ${t('autoOpen')}</label>
                        <label class="dd-set-check"><input type="checkbox" id="dd-set-tabprog"${tabProgressEnabled?' checked':''}> ${t('tabProg')}</label>
                        <label class="dd-set-check"><input type="checkbox" id="dd-set-automin"${autoMinimizeOnDl?' checked':''}> ${t('autoMin')}</label>
                        <label class="dd-set-check"><input type="checkbox" id="dd-set-notify"${notifyEnabled?' checked':''}> ${t('notify')}</label>
                        <div class="dd-set-actions">
                            <button type="button" id="dd-settings-save" class="dd-set-save">${t('save')}</button>
                            <button type="button" id="dd-settings-reset" class="dd-set-reset">${t('resetDefault')}</button>
                            <button type="button" id="dd-settings-log" class="dd-set-reset">${t('log')}</button>
                            <button type="button" id="dd-settings-help" class="dd-set-reset">${t('help')}</button>
                        </div>
                    </div>
                </div>
            </div>`;
        document.body.appendChild(box);
        document.getElementById('dd-settings-close').onclick = () => box.remove();
        box.querySelector('.dd-modal-bg').onclick = (e) => { if (e.target === e.currentTarget) box.remove(); };
        document.getElementById('dd-settings-log').onclick = () => { box.remove(); showLogPanel(); };
        document.getElementById('dd-settings-help').onclick = () => { box.remove(); showHelp(); };
        document.getElementById('dd-settings-save').onclick = () => {
            const newLang = document.getElementById('dd-set-lang')?.value || 'en';
            uiLang = (newLang === 'id') ? 'id' : 'en';
            localStorage.setItem('dd_lang', uiLang);
            dlConcurrency = parseInt(document.getElementById('dd-set-concurrency').value, 10) || 4;
            jpegQuality = parseFloat(document.getElementById('dd-set-jpeg').value) || 0.92;
            lastDownloadMode = document.getElementById('dd-set-mode').value || 'zip';
            skipCoverCount = parseInt(document.getElementById('dd-set-skip').value, 10) || 0;
            fabPosition = document.getElementById('dd-set-fab').value || 'top-right';
            autoOpenPanel = document.getElementById('dd-set-autoopen').checked;
            tabProgressEnabled = document.getElementById('dd-set-tabprog').checked;
            autoMinimizeOnDl = document.getElementById('dd-set-automin').checked;
            notifyEnabled = document.getElementById('dd-set-notify').checked;
            applyI18n();
            const newSaveMode = document.getElementById('dd-set-savemode').value || 'default';
            if (newSaveMode !== 'folder' && saveMode === 'folder') {
                clearDirHandle();
            }
            saveMode = newSaveMode;
            localStorage.setItem('dd_concurrency', String(dlConcurrency));
            localStorage.setItem('dd_jpeg_q', String(jpegQuality));
            localStorage.setItem('dd_last_mode', lastDownloadMode);
            localStorage.setItem('dd_skip_cover', String(skipCoverCount));
            localStorage.setItem('dd_fab_pos', fabPosition);
            localStorage.setItem('dd_auto_open', autoOpenPanel ? '1' : '0');
            localStorage.setItem('dd_tab_progress', tabProgressEnabled ? '1' : '0');
            localStorage.setItem('dd_auto_minimize', autoMinimizeOnDl ? '1' : '0');
            localStorage.setItem('dd_notify', notifyEnabled ? '1' : '0');
            localStorage.setItem('dd_save_mode', saveMode);
            applyFabPosition();
            updateMenuActive();
            updateFolderUI();
            box.remove();
            const st = document.getElementById('dd-status-text');
            if (st) st.textContent = 'Settings disimpan';
            setTimeout(() => { if (st && !isDownloading) updateSelectedUI(); }, 1500);
        };
        document.getElementById('dd-settings-reset').onclick = async () => {
            localStorage.removeItem('dd_concurrency');
            localStorage.removeItem('dd_jpeg_q');
            localStorage.removeItem('dd_skip_cover');
            localStorage.removeItem('dd_fab_pos');
            localStorage.removeItem('dd_auto_open');
            localStorage.removeItem('dd_tab_progress');
            localStorage.removeItem('dd_auto_minimize');
            localStorage.removeItem('dd_notify');
            localStorage.removeItem('dd_save_mode');
            await clearDirHandle();
            dlConcurrency = 4;
            jpegQuality = 0.92;
            skipCoverCount = 2;
            fabPosition = 'top-right';
            autoOpenPanel = true;
            tabProgressEnabled = true;
            autoMinimizeOnDl = false;
            notifyEnabled = true;
            saveMode = 'default';
            applyFabPosition();
            box.remove();
            showSettings();
        };
    }

    function closeDownloadMenu() {
        document.getElementById('dd-download-menu')?.classList.add('hidden');
    }

    function updateMenuActive() {
        document.querySelectorAll('#dd-download-menu .dd-menu-item').forEach(btn => {
            const mode = btn.dataset.mode;
            btn.classList.toggle('dd-menu-active', mode === lastDownloadMode);
        });
    }

    function lightenAccent(hex) {
        try {
            const n = parseInt(hex.slice(1), 16);
            let r = (n >> 16) & 255, g = (n >> 8) & 255, b = n & 255;
            r = Math.min(255, r + 40); g = Math.min(255, g + 40); b = Math.min(255, b + 40);
            return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
        } catch (e) { return '#60a5fa'; }
    }

    function setAccentColor(color) {
        accentColor = color;
        localStorage.setItem('dd_accent', color);
        const box = document.getElementById('xamz-box');
        if (box) {
            box.style.setProperty('--dd-accent', color);
            box.style.setProperty('--dd-accent-hover', lightenAccent(color));
        }
    }

    function toggleToolbarCollapse() {
        toolbarCollapsed = !toolbarCollapsed;
        localStorage.setItem('dd_toolbar_collapsed', toolbarCollapsed ? '1' : '0');
        const box = document.getElementById('xamz-box');
        if (box) box.classList.toggle('dd-toolbar-collapsed', toolbarCollapsed);
        const btn = document.getElementById('dd-collapse-btn');
        if (btn) btn.textContent = toolbarCollapsed ? '▴' : '▾';
    }

    function toggleSelectedOnly() {
        showSelectedOnly = !showSelectedOnly;
        const btn = document.getElementById('dd-filter-btn');
        if (btn) {
            btn.classList.toggle('dd-filter-active', showSelectedOnly);
            btn.title = showSelectedOnly ? 'Filter ON — tampilkan semua' : 'Tampilkan hanya terpilih';
        }
        applySelectedOnlyFilter();
    }

    function applySelectedOnlyFilter() {
        document.querySelectorAll('#xamz-grid .xamz-item').forEach(item => {
            const cb = item.querySelector('input[type="checkbox"]');
            if (!cb) return;
            if (showSelectedOnly && !cb.checked) {
                item.classList.add('dd-filtered-out');
            } else {
                item.classList.remove('dd-filtered-out');
            }
        });
    }

    function setThumbSize(size) {
        thumbSize = size;
        localStorage.setItem('dd_thumb', size);
        const box = document.getElementById('xamz-box');
        if (box) {
            box.classList.remove('dd-thumb-s', 'dd-thumb-m', 'dd-thumb-l');
            box.classList.add('dd-thumb-' + size);
        }
        ['s', 'm', 'l'].forEach(s => {
            const btn = document.getElementById('dd-size-' + s);
            if (btn) btn.classList.toggle('dd-size-active', s === size);
        });
    }

    function toggleCompact() {
        isCompact = !isCompact;
        localStorage.setItem('dd_compact', isCompact ? '1' : '0');
        const box = document.getElementById('xamz-box');
        if (box) box.classList.toggle('dd-compact', isCompact);
        const btn = document.getElementById('dd-compact-btn');
        if (btn) btn.classList.toggle('dd-size-active', isCompact);
    }

    function applyDownloadDisabledState() {
        document.querySelectorAll('.dd-dl-btn, .dd-menu-item').forEach(btn => {
            btn.disabled = isDownloading;
            btn.classList.toggle('dd-btn-disabled', isDownloading);
        });
        if (isDownloading) closeDownloadMenu();
    }

    function beginDownload() {
        isDownloading = true;
        downloadStartTime = Date.now();
        downloadBytesDone = 0;
        originalDocTitle = document.title;
        applyDownloadDisabledState();
        if (tabProgressEnabled) document.title = '⏳ 0% · DL';
        if (autoMinimizeOnDl && panelOpen) {
            try { minimizePanel(); } catch (e) {}
        }
        ddLog('info', 'Download mulai', `${getSelectedIndexes().length} panel · mode ${lastDownloadMode}`);
    }

    function endDownload() {
        isDownloading = false;
        downloadStartTime = 0;
        downloadBytesDone = 0;
        applyDownloadDisabledState();
        if (tabProgressEnabled) {
            document.title = '✅ Selesai · DL';
            setTimeout(restoreDocTitle, 2500);
        }
        ddLog('info', 'Download selesai');
    }

    function updateSelectedUI() {
        const selected = getSelectedIndexes().length;
        const total = imageURLs.length;

        const selCount = document.getElementById('xamz-selected-count');
        if (selCount) {
            selCount.textContent = selected + ' ' + t('selectedCount');
            selCount.classList.toggle('dd-has-selected', selected > 0);
        }

        const bottomTotal = document.getElementById('xamz-count');
        if (bottomTotal) bottomTotal.textContent = total + ' ' + t('images');

        const bottomSel = document.getElementById('dd-bottom-selected');
        if (bottomSel) bottomSel.textContent = selected + ' ' + t('selectedCount');
        const bottomSize = document.getElementById('dd-bottom-size');
        if (bottomSize) {
            if (selected > 0) {
                bottomSize.textContent = '≈ ' + formatBytes(getEstimatedSize(getSelectedIndexes()));
            } else {
                bottomSize.textContent = '';
            }
        }

        const badgeDl = document.getElementById('badge-dl');
        if (badgeDl) {
            badgeDl.textContent = selected;
            badgeDl.style.display = selected > 0 ? '' : 'none';
        }

        const badges = ['badge-selected', 'badge-zip', 'badge-pdf', 'badge-strip'];
        badges.forEach(id => {
            const el = document.getElementById(id);
            if (el) {
                el.textContent = selected;
                el.style.display = selected > 0 ? '' : 'none';
            }
        });

        updateSizeEstimate();

        const statusText = document.getElementById('dd-status-text');
        if (statusText && !isDownloading) {
            if (total === 0) statusText.textContent = '';
            else {
                statusText.textContent = t('selectedOf')
                    .replace('{s}', selected)
                    .replace('{t}', total) + '  •  ' + t('statusHint');
            }
        }

        if (showSelectedOnly) applySelectedOnlyFilter();
    }

    function closePanel() {
        document.removeEventListener('keydown', handleKeydown);
        const overlay = document.getElementById('xamz-overlay');
        const box = document.getElementById('xamz-box');
        if (box) {
            box.classList.remove('dd-open');
            box.classList.add('dd-closing');
        }
        setTimeout(() => {
            overlay?.remove();
            panelOpen = false;
            if (autoScanInterval) {
                clearInterval(autoScanInterval);
                autoScanInterval = null;
            }
            if (!isDownloading) hideMiniProgress();
            const fab = document.getElementById('xamz-open-button');
            if (fab) fab.style.display = '';
        }, 200);
    }

    function toggleTheme() {
        isDark = !isDark;
        localStorage.setItem('dd_theme', isDark ? 'dark' : 'light');
        applyTheme();
        const btn = document.getElementById('dd-theme-btn');
        if (btn) {
            btn.textContent = isDark ? '☀' : '☾';
            btn.title = isDark ? 'Mode Light' : 'Mode Dark';
        }
    }

    function applyTheme() {
        const root = document.getElementById('xamz-box');
        if (!root) return;
        if (isDark) {
            root.classList.remove('dd-light');
            root.classList.add('dd-dark');
        } else {
            root.classList.remove('dd-dark');
            root.classList.add('dd-light');
        }
    }

    // ==================== Panel Resize + Drag ====================
    function applySavedPanelSize() {
        const box = document.getElementById('xamz-box');
        if (!box) return;
        try {
            const saved = JSON.parse(localStorage.getItem('dd_panel_size') || 'null');
            if (saved && saved.w && saved.h) {
                box.style.left = 'auto';
                box.style.right = 'auto';
                box.style.top = 'auto';
                box.style.bottom = 'auto';
                box.style.width = saved.w + 'px';
                box.style.height = saved.h + 'px';
                if (typeof saved.x === 'number' && typeof saved.y === 'number') {
                    box.style.left = Math.max(8, saved.x) + 'px';
                    box.style.top = Math.max(8, saved.y) + 'px';
                } else {
                    const left = Math.max(8, (window.innerWidth - saved.w) / 2);
                    const top = Math.max(8, (window.innerHeight - saved.h) / 2);
                    box.style.left = left + 'px';
                    box.style.top = top + 'px';
                }
            }
        } catch (e) {}
    }

    function savePanelSize() {
        const box = document.getElementById('xamz-box');
        if (!box) return;
        const rect = box.getBoundingClientRect();
        localStorage.setItem('dd_panel_size', JSON.stringify({
            w: Math.round(rect.width),
            h: Math.round(rect.height),
            x: Math.round(rect.left),
            y: Math.round(rect.top)
        }));
    }

    function initPanelResize() {
        const box = document.getElementById('xamz-box');
        if (!box) return;
        applySavedPanelSize();

        const handles = box.querySelectorAll('.dd-resize-edge, .dd-resize-corner');
        if (!handles.length) return;

        let resizing = false;
        let dir = '';
        let startX, startY, startW, startH, startL, startT;

        const cursorMap = {
            n: 'ns-resize', s: 'ns-resize', e: 'ew-resize', w: 'ew-resize',
            nw: 'nwse-resize', se: 'nwse-resize', ne: 'nesw-resize', sw: 'nesw-resize'
        };

        handles.forEach(handle => {
            handle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                resizing = true;
                dir = handle.dataset.dir || 'se';
                const rect = box.getBoundingClientRect();
                startX = e.clientX;
                startY = e.clientY;
                startW = rect.width;
                startH = rect.height;
                startL = rect.left;
                startT = rect.top;
                box.style.left = startL + 'px';
                box.style.top = startT + 'px';
                box.style.right = 'auto';
                box.style.bottom = 'auto';
                box.style.width = startW + 'px';
                box.style.height = startH + 'px';
                document.body.style.userSelect = 'none';
                document.body.style.cursor = cursorMap[dir] || 'nwse-resize';
            });

            handle.addEventListener('dblclick', () => {
                box.style.left = '2.5%';
                box.style.right = '2.5%';
                box.style.top = '2.5%';
                box.style.bottom = '2.5%';
                box.style.width = '';
                box.style.height = '';
                localStorage.removeItem('dd_panel_size');
            });
        });

        window.addEventListener('mousemove', (e) => {
            if (!resizing) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            const minW = 420;
            const minH = 300;
            const maxW = window.innerWidth - 8;
            const maxH = window.innerHeight - 8;

            let newW = startW, newH = startH, newL = startL, newT = startT;

            if (dir.includes('e')) newW = Math.min(maxW, Math.max(minW, startW + dx));
            if (dir.includes('s')) newH = Math.min(maxH, Math.max(minH, startH + dy));
            if (dir.includes('w')) {
                newW = Math.min(maxW, Math.max(minW, startW - dx));
                newL = startL + (startW - newW);
            }
            if (dir.includes('n')) {
                newH = Math.min(maxH, Math.max(minH, startH - dy));
                newT = startT + (startH - newH);
            }

            // jaga tetap di dalam viewport
            if (newL < 0) { newW += newL; newL = 0; }
            if (newT < 0) { newH += newT; newT = 0; }
            if (newL + newW > window.innerWidth) newW = window.innerWidth - newL;
            if (newT + newH > window.innerHeight) newH = window.innerHeight - newT;

            box.style.width = Math.max(minW, newW) + 'px';
            box.style.height = Math.max(minH, newH) + 'px';
            box.style.left = Math.max(0, newL) + 'px';
            box.style.top = Math.max(0, newT) + 'px';
        });

        window.addEventListener('mouseup', () => {
            if (!resizing) return;
            resizing = false;
            document.body.style.userSelect = '';
            document.body.style.cursor = '';
            savePanelSize();
        });
    }

    function initPanelDrag() {
        const box = document.getElementById('xamz-box');
        const header = document.getElementById('xamz-header');
        if (!box || !header) return;

        let startX, startY, startLeft, startTop;

        header.addEventListener('mousedown', (e) => {
            if (e.target.closest('button')) return;
            e.preventDefault();
            isDraggingPanel = true;
            const rect = box.getBoundingClientRect();
            startX = e.clientX;
            startY = e.clientY;
            startLeft = rect.left;
            startTop = rect.top;
            box.style.left = startLeft + 'px';
            box.style.top = startTop + 'px';
            box.style.right = 'auto';
            box.style.bottom = 'auto';
            box.style.width = rect.width + 'px';
            box.style.height = rect.height + 'px';
            document.body.style.userSelect = 'none';
            header.style.cursor = 'grabbing';
        });

        window.addEventListener('mousemove', (e) => {
            if (!isDraggingPanel) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            box.style.left = Math.max(0, startLeft + dx) + 'px';
            box.style.top = Math.max(0, startTop + dy) + 'px';
        });

        window.addEventListener('mouseup', () => {
            if (!isDraggingPanel) return;
            isDraggingPanel = false;
            document.body.style.userSelect = '';
            header.style.cursor = 'grab';
            savePanelSize();
        });

        header.style.cursor = 'grab';
    }

    function createStyles() {
        if (document.getElementById('xamz-style')) return;
        const style = document.createElement('style');
        style.id = 'xamz-style';
        style.textContent = `
            @keyframes ddIn {
                from { opacity: 0; transform: scale(0.96) translateY(10px); }
                to { opacity: 1; transform: scale(1) translateY(0); }
            }
            @keyframes ddOut {
                from { opacity: 1; transform: scale(1) translateY(0); }
                to { opacity: 0; transform: scale(0.96) translateY(10px); }
            }
            @keyframes ddSpin {
                to { transform: rotate(360deg); }
            }
            .dd-spinner {
                display: inline-block;
                width: 12px; height: 12px;
                border: 2px solid rgba(59,130,246,.25);
                border-top-color: #3b82f6;
                border-radius: 50%;
                animation: ddSpin .7s linear infinite;
                vertical-align: -2px;
                margin-right: 4px;
                flex-shrink: 0;
            }
            #dd-mini-title {
                display: flex; align-items: center; gap: 2px;
            }

            #xamz-box {
                --dd-bg: #08080c;
                --dd-bg-header: #0c0c14;
                --dd-bg-toolbar: #0a0a12;
                --dd-bg-input-row: #08080e;
                --dd-bg-grid: #050508;
                --dd-bg-item: #0e0e14;
                --dd-bg-input: #0f0f16;
                --dd-text: #ffffff;
                --dd-text-muted: #93c5fd;
                --dd-accent: #3b82f6;
                --dd-accent-hover: #60a5fa;
                --dd-border: rgba(59,130,246,.45);
                --dd-border-soft: rgba(59,130,246,.25);
                --dd-shadow: 0 0 28px rgba(59,130,246,.22);
                --dd-item-border: rgba(59,130,246,.35);
            }
            #xamz-box.dd-light {
                --dd-bg: #eff6ff;
                --dd-bg-header: #dbeafe;
                --dd-bg-toolbar: #dbeafe;
                --dd-bg-input-row: #eff6ff;
                --dd-bg-grid: #f0f9ff;
                --dd-bg-item: #ffffff;
                --dd-bg-input: #ffffff;
                --dd-text: #1a1a1a;
                --dd-text-muted: #1d4ed8;
                --dd-accent: #3b82f6;
                --dd-accent-hover: #60a5fa;
                --dd-border: rgba(59,130,246,.5);
                --dd-border-soft: rgba(59,130,246,.3);
                --dd-shadow: 0 0 28px rgba(59,130,246,.15);
                --dd-item-border: rgba(59,130,246,.4);
            }

            #xamz-overlay{
                position:fixed;inset:0;z-index:2147483646;
                background:rgba(0,0,0,.82);
                font-family:'Segoe UI',Arial,sans-serif;
            }
            #xamz-box{
                position:absolute;left:2.5%;right:2.5%;top:2.5%;bottom:2.5%;
                background:var(--dd-bg);
                color:var(--dd-text);
                border:1.5px solid var(--dd-accent);
                border-radius:14px;
                display:flex;flex-direction:column;overflow:hidden;
                box-shadow:var(--dd-shadow);
                opacity:0;transform:scale(0.96) translateY(10px);
            }
            #xamz-box.dd-open{animation:ddIn .28s cubic-bezier(.22,1,.36,1) forwards}
            #xamz-box.dd-closing{animation:ddOut .22s ease-in forwards}

            #xamz-header{
                min-height:48px;display:flex;align-items:center;justify-content:space-between;
                padding:0 12px;gap:10px;
                background:var(--dd-bg-header);
                border-bottom:1px solid var(--dd-border-soft);
                position:sticky;top:0;z-index:5;
                user-select:none;
            }
            .dd-title-wrap{display:flex;align-items:center;gap:8px;min-width:0}
            .dd-title{
                display:flex;align-items:center;gap:8px;
                font-size:13px;line-height:1.2;
            }
            .dd-title-mark{
                display:inline-flex;align-items:center;justify-content:center;
                width:24px;height:24px;border-radius:6px;
                background:var(--dd-accent);
                color:#fff;font-weight:800;font-size:10px;letter-spacing:.4px;
            }
            .dd-title-text{
                font-weight:700;font-size:14px;letter-spacing:.2px;
                color:var(--dd-text);
            }
            .dd-title-sub{display:none}
            .dd-by{
                font-size:10px;font-weight:500;color:var(--dd-text-muted);
                opacity:.7;letter-spacing:.2px;
            }
            #xamz-box.dd-light .dd-title-text{color:#0f172a}
            .dd-hsep{
                width:1px;height:16px;background:var(--dd-border-soft);
                margin:0 4px;flex-shrink:0;
            }
            .dd-tool-spacer{flex:1}
            .dd-count{
                font-size:11px;color:var(--dd-text-muted);background:rgba(59,130,246,.15);
                padding:2px 9px;border-radius:999px;border:1px solid var(--dd-border-soft);
            }
            .dd-selected-count{
                font-size:11px;color:var(--dd-text-muted);background:rgba(59,130,246,.08);
                padding:2px 9px;border-radius:999px;border:1px solid transparent;
                transition: all .15s;
            }
            .dd-selected-count.dd-has-selected{
                color:#000;background:var(--dd-accent);border-color:var(--dd-accent);
                font-weight:700;
            }
            .dd-size-estimate{
                font-size:11px;color:var(--dd-text-muted);
                background:rgba(59,130,246,.1);
                padding:2px 9px;border-radius:999px;
                border:1px solid var(--dd-border-soft);
                font-weight:600;
            }
            .dd-badge{
                display:inline-block;min-width:16px;padding:1px 5px;margin-left:3px;
                font-size:10px;font-weight:800;border-radius:999px;
                background:rgba(0,0,0,.25);color:#fff;line-height:1.3;
            }
            .dd-size-btn{
                min-width:28px !important;padding:6px 8px !important;
                font-size:12px !important;
            }
            .dd-size-btn.dd-size-active{
                background:var(--dd-accent-hover) !important;
                box-shadow:0 0 0 2px rgba(59,130,246,.4);
            }
            .dd-mode-active{
                box-shadow:0 0 0 2px rgba(255,255,255,.35) !important;
            }
            .dd-panel-progress{
                height:5px;background:rgba(59,130,246,.15);overflow:hidden;
            }
            .dd-panel-progress.hidden{display:none}
            #dd-panel-progress-bar{
                height:100%;width:0%;background:linear-gradient(90deg,var(--dd-accent),var(--dd-accent-hover));
                transition:width .2s;
            }
            .dd-header-actions{
                display:flex;align-items:center;gap:2px;flex-shrink:0;
            }
            .dd-hbtn-group{
                display:contents;
            }
            .dd-header-actions .dd-hbtn{
                background:transparent;color:var(--dd-text-muted);border:none;border-radius:6px;
                width:28px;height:28px;min-width:28px;padding:0;cursor:pointer;
                font-weight:600;font-size:13px;line-height:1;
                display:flex;align-items:center;justify-content:center;
                transition:background .15s, color .15s, transform .12s;
            }
            .dd-header-actions .dd-hbtn:hover{
                background:rgba(59,130,246,.2);color:var(--dd-text);
            }
            .dd-header-actions .dd-hbtn.dd-filter-active,
            .dd-header-actions .dd-hbtn.dd-size-active{
                background:var(--dd-accent);color:#fff;
            }
            .dd-header-actions .dd-hbtn-close:hover{
                background:#ef4444;color:#fff;
            }
            .dd-lang-btn{
                font-size:11px !important;font-weight:700 !important;
                letter-spacing:.3px;min-width:32px !important;
            }

            .dd-toolbar-row{
                padding:6px 12px;display:flex;flex-wrap:wrap;gap:4px;align-items:center;
                background:var(--dd-bg-toolbar);border-bottom:1px solid var(--dd-border-soft);
            }
            .dd-toolbar-dl{background:var(--dd-bg-toolbar);}
            .dd-tool-group{display:contents}
            .dd-tool-size{display:contents}
            .dd-toolbar-row .dd-tbtn{
                background:transparent;color:var(--dd-text-muted);border:none;border-radius:6px;
                padding:5px 10px;cursor:pointer;font-weight:600;font-size:12px;
                transition:background .12s, color .12s;
                white-space:nowrap;line-height:1.2;
            }
            .dd-toolbar-row .dd-tbtn:hover{
                background:rgba(59,130,246,.18);color:var(--dd-text);
            }
            .dd-toolbar-row .dd-size-btn{
                min-width:28px;padding:5px 8px;text-align:center;
            }
            .dd-toolbar-row .dd-size-btn.dd-size-active{
                background:var(--dd-accent);color:#fff;
            }

            #xamz-zipname-row{
                padding:8px 12px;
                background:var(--dd-bg-input-row);
                border-bottom:1px solid var(--dd-border-soft);
            }
            .dd-name-row{
                display:flex;gap:8px;align-items:flex-end;flex-wrap:wrap;
            }
            .dd-name-col{
                flex:1 1 200px;min-width:0;display:flex;flex-direction:column;gap:0;
            }
            .dd-name-field{display:flex;gap:6px;align-items:center;}
            .dd-name-inline{width:100%}
            .dd-inline-label{
                flex-shrink:0;font-size:11px;font-weight:600;color:var(--dd-text-muted);
                white-space:nowrap;min-width:72px;
            }
            #xamz-zipname{
                flex:1;min-width:0;box-sizing:border-box;
                padding:7px 10px;border-radius:6px;
                border:1px solid var(--dd-border-soft);
                background:var(--dd-bg-input);color:var(--dd-text);font-size:13px;
            }
            #xamz-zipname:focus{
                outline:none;border-color:var(--dd-accent);
                box-shadow:0 0 0 2px rgba(59,130,246,.15);
            }
            .dd-name-field button{
                flex-shrink:0;padding:7px 9px;border-radius:6px;border:none;
                background:var(--dd-accent);color:#fff;font-weight:600;cursor:pointer;font-size:12px;
            }
            #dd-clear-folder{
                background:transparent !important;color:var(--dd-text-muted) !important;
                border:1px solid var(--dd-border-soft) !important;
            }
            .dd-folder-label{
                flex:1;min-width:0;box-sizing:border-box;
                padding:8px 11px;border-radius:8px;
                border:1px solid var(--dd-border-soft);
                background:var(--dd-bg-input);color:var(--dd-text-muted);
                font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
            }
            .dd-folder-label.dd-folder-set{
                color:var(--dd-text);border-color:var(--dd-accent);
                background:rgba(59,130,246,.12);
            }

            #xamz-status{
                min-height:18px;padding:4px 12px;
                color:var(--dd-text-muted);font-size:11px;
            }

            /* ===== BOTTOM BAR + DOWNLOAD MENU ===== */
            #dd-bottom-bar{
                display:flex;align-items:center;justify-content:space-between;
                gap:12px;padding:10px 14px;
                background:var(--dd-bg-header);
                border-top:1px solid var(--dd-border);
                flex-shrink:0;
            }
            .dd-bottom-left{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}
            .dd-bottom-sep{opacity:.35;font-weight:700;}
            .dd-bottom-total{
                color:var(--dd-accent) !important;
                border-color:rgba(59,130,246,.35) !important;
            }
            .dd-bottom-stat{
                font-size:12px;color:var(--dd-text-muted);
                background:rgba(59,130,246,.1);
                padding:4px 10px;border-radius:999px;
                border:1px solid var(--dd-border-soft);
                font-weight:600;
            }
            .dd-bottom-right{display:flex;align-items:center;gap:8px;}
            .dd-bottom-btn{
                background:var(--dd-accent);color:#000;border:none;border-radius:8px;
                padding:9px 16px;cursor:pointer;font-weight:700;font-size:13px;
                transition:background .15s, transform .15s, box-shadow .15s;
                white-space:nowrap;
            }
            .dd-bottom-btn:hover{
                background:var(--dd-accent-hover);transform:translateY(-1px);
                box-shadow:0 4px 12px rgba(59,130,246,.3);
            }
            .dd-next-btn{
                background:transparent;color:var(--dd-text-muted);
                border:1px solid var(--dd-border-soft);
            }
            .dd-next-btn:hover{
                background:rgba(59,130,246,.15);color:var(--dd-text);
                border-color:var(--dd-accent);
            }
            .dd-download-main{
                min-width:140px;
            }
            .dd-download-wrap{
                position:relative;
            }
            .dd-download-menu{
                position:absolute;bottom:calc(100% + 6px);right:0;
                min-width:220px;
                background:var(--dd-bg-header);
                border:1.5px solid var(--dd-accent);
                border-radius:10px;
                box-shadow:0 8px 28px rgba(0,0,0,.5);
                overflow:hidden;z-index:30;
                padding:4px;
            }
            .dd-download-menu.hidden{display:none}
            .dd-menu-item{
                display:flex;align-items:center;justify-content:space-between;
                width:100%;box-sizing:border-box;
                background:transparent;border:none;border-radius:7px;
                padding:10px 12px;cursor:pointer;
                color:var(--dd-text);font-size:13px;font-weight:600;
                text-align:left;gap:10px;
                transition:background .12s;
            }
            .dd-menu-item:hover{
                background:rgba(59,130,246,.2);
            }
            .dd-menu-item.dd-menu-active{
                background:rgba(59,130,246,.25);
                box-shadow:inset 3px 0 0 var(--dd-accent);
            }
            .dd-menu-desc{
                font-size:11px;font-weight:500;color:var(--dd-text-muted);
                opacity:.85;
            }

            #xamz-grid{
                flex:1;overflow:auto;padding:12px;
                display:grid;
                grid-template-columns:repeat(auto-fill,minmax(160px,1fr));
                gap:12px;align-content:start;
                background:var(--dd-bg-grid);
            }
            #xamz-grid::-webkit-scrollbar{width:8px}
            #xamz-grid::-webkit-scrollbar-track{background:transparent}
            #xamz-grid::-webkit-scrollbar-thumb{
                background:var(--dd-accent);border-radius:4px;
            }

            .dd-empty-msg{
                grid-column:1/-1;text-align:center;padding:40px 20px;
                color:var(--dd-text-muted);
                display:flex;flex-direction:column;align-items:center;gap:8px;
            }
            .dd-empty-icon{font-size:40px;line-height:1;opacity:.85}
            .dd-empty-title{font-size:15px;font-weight:700;color:var(--dd-text)}
            .dd-empty-desc{font-size:12px;line-height:1.5;opacity:.85;margin-bottom:6px}
            .dd-empty-btn{
                background:var(--dd-accent);color:#000;border:none;border-radius:8px;
                padding:9px 16px;font-weight:700;font-size:12px;cursor:pointer;
            }
            .dd-empty-btn:hover{background:var(--dd-accent-hover)}

            #xamz-box.dd-thumb-s #xamz-grid{grid-template-columns:repeat(auto-fill,minmax(110px,1fr));gap:8px}
            #xamz-box.dd-thumb-s .xamz-item img{height:130px}
            #xamz-box.dd-thumb-m #xamz-grid{grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}
            #xamz-box.dd-thumb-m .xamz-item img{height:220px}
            #xamz-box.dd-thumb-l #xamz-grid{grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:14px}
            #xamz-box.dd-thumb-l .xamz-item img{height:300px}

            #xamz-box.dd-compact #xamz-grid{gap:7px;padding:8px}
            #xamz-box.dd-compact .xamz-item{padding:4px;border-radius:7px}
            #xamz-box.dd-compact .xamz-item label{margin-top:4px;font-size:11px}
            #xamz-box.dd-compact .xamz-number{font-size:10px;padding:2px 5px;left:6px;top:6px}
            #xamz-box.dd-compact .dd-drag-handle{right:5px;top:5px;font-size:12px;padding:1px 3px}

            .xamz-item{
                position:relative;
                background:var(--dd-bg-item);
                border:1px solid var(--dd-item-border);
                border-radius:10px;padding:7px;
                transition:transform .15s, box-shadow .15s, border-color .15s, opacity .15s;
                cursor:grab;
            }
            .xamz-item:hover{
                border-color:var(--dd-accent-hover);
                box-shadow:0 5px 16px rgba(59,130,246,.25);
                transform:translateY(-2px);
            }
            .xamz-item.dd-dragging{ opacity:0.45; cursor:grabbing; transform:scale(0.97); }
            .xamz-item.dd-dragging-ghost{ opacity:0.3; }
            .xamz-item.dd-drag-over{
                border-color:var(--dd-accent);
                box-shadow:0 0 0 3px var(--dd-accent);
                transform:scale(1.03);
            }
            .xamz-item img{
                width:100%;height:220px;object-fit:contain;display:block;
                background:#000;border-radius:6px;cursor:pointer;
            }
            .xamz-item label{
                display:flex;align-items:center;gap:6px;margin-top:6px;
                font-size:11px;color:var(--dd-text-muted);cursor:pointer;user-select:none;
            }
            .dd-panel-label{flex:1}
            .dd-ext-badge{
                font-size:9px;font-weight:700;letter-spacing:.3px;
                color:var(--dd-text-muted);background:rgba(59,130,246,.12);
                padding:1px 5px;border-radius:4px;
                text-transform:uppercase;opacity:.7;
            }
            .xamz-number{
                position:absolute;left:10px;top:10px;z-index:2;
                background:var(--dd-accent);color:#000;
                padding:2px 7px;border-radius:5px;
                font-size:11px;font-weight:800;
                box-shadow:0 2px 6px rgba(0,0,0,.35);
                transition: background .15s, color .15s;
            }
            .xamz-item:has(input:checked) .xamz-number{
                background:#000;color:var(--dd-accent);
            }
            .dd-drag-handle{
                position:absolute;right:8px;top:8px;z-index:2;
                color:var(--dd-text-muted);font-size:14px;line-height:1;
                opacity:0.5;cursor:grab;user-select:none;
                background:rgba(0,0,0,.3);padding:2px 4px;border-radius:4px;
            }
            .xamz-item:hover .dd-drag-handle{opacity:1}
            .xamz-item:has(input:checked){
                border-color:var(--dd-accent);
                box-shadow:0 0 0 2px rgba(59,130,246,.45), 0 4px 12px rgba(59,130,246,.2);
                background:color-mix(in srgb, var(--dd-bg-item) 85%, var(--dd-accent) 15%);
            }

            .dd-resize-edge{
                position:absolute;z-index:12;background:transparent;
            }
            .dd-resize-edge:hover{background:rgba(59,130,246,.25)}
            .dd-resize-n{top:0;left:12px;right:12px;height:6px;cursor:ns-resize}
            .dd-resize-s{bottom:0;left:12px;right:12px;height:6px;cursor:ns-resize}
            .dd-resize-e{right:0;top:12px;bottom:12px;width:6px;cursor:ew-resize}
            .dd-resize-w{left:0;top:12px;bottom:12px;width:6px;cursor:ew-resize}
            .dd-resize-corner{
                position:absolute;width:14px;height:14px;z-index:13;
                background:transparent;
            }
            .dd-resize-nw{top:0;left:0;cursor:nwse-resize;border-radius:12px 0 0 0}
            .dd-resize-ne{top:0;right:0;cursor:nesw-resize;border-radius:0 12px 0 0}
            .dd-resize-sw{bottom:0;left:0;cursor:nesw-resize;border-radius:0 0 0 12px}
            .dd-resize-se{
                bottom:2px;right:2px;width:16px;height:16px;cursor:nwse-resize;
                background:
                    linear-gradient(135deg, transparent 50%, rgba(59,130,246,.35) 50%) no-repeat,
                    linear-gradient(135deg, transparent 60%, rgba(59,130,246,.55) 60%) no-repeat,
                    linear-gradient(135deg, transparent 70%, rgba(59,130,246,.85) 70%) no-repeat;
                background-size:100% 100%;opacity:.75;
            }
            .dd-resize-se:hover{opacity:1}
            .dd-resize-corner:hover{background:rgba(59,130,246,.2)}
            .dd-resize-se:hover{background:
                linear-gradient(135deg, transparent 50%, rgba(59,130,246,.5) 50%) no-repeat,
                linear-gradient(135deg, transparent 60%, rgba(59,130,246,.7) 60%) no-repeat,
                linear-gradient(135deg, transparent 70%, rgba(59,130,246,1) 70%) no-repeat;
                background-size:100% 100%;
            }

            #xamz-box.dd-toolbar-collapsed #xamz-collapsible{display:none}
            .xamz-item.dd-filtered-out{display:none !important}
            #dd-filter-btn.dd-filter-active{
                background:var(--dd-accent) !important;
                color:#000 !important;
                box-shadow:0 0 0 1px rgba(59,130,246,.4);
            }
            .dd-dl-btn.dd-btn-disabled, .dd-dl-btn:disabled{
                opacity:0.45 !important;cursor:not-allowed !important;
                transform:none !important;pointer-events:none;
            }

            .dd-skeleton{
                background:linear-gradient(90deg,
                    rgba(59,130,246,.08) 25%,
                    rgba(59,130,246,.18) 50%,
                    rgba(59,130,246,.08) 75%);
                background-size:200% 100%;
                animation:ddSkeleton 1.2s ease-in-out infinite;
                border-radius:10px;min-height:180px;
            }
            @keyframes ddSkeleton {
                0%{background-position:200% 0}
                100%{background-position:-200% 0}
            }

            .dd-hover-preview{
                position:fixed;z-index:2147483647;pointer-events:none;
                border:2px solid var(--dd-accent);border-radius:10px;overflow:hidden;
                box-shadow:0 8px 32px rgba(0,0,0,.55);background:#0a0a12;
                max-width:400px;
            }
            .dd-hover-preview.hidden{display:none}
            .dd-hover-preview img{display:block;max-width:380px;max-height:480px;object-fit:contain;margin:0 auto}
            .dd-hover-meta{
                padding:8px 10px;font-size:11px;line-height:1.45;
                color:#e2e8f0;background:rgba(10,10,18,.95);
                border-top:1px solid rgba(59,130,246,.3);
                word-break:break-all;
            }
            .dd-hover-meta b{color:#93c5fd;font-weight:600}
            .dd-panel-meta{
                display:flex;flex-wrap:wrap;gap:4px 8px;
                font-size:10px;color:var(--dd-text-muted);
                padding:2px 2px 0;line-height:1.3;
                opacity:.85;
            }
            .dd-panel-meta span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}

            .dd-accent-picker{
                position:absolute;top:48px;right:14px;z-index:20;
                display:flex;gap:5px;padding:7px;
                background:var(--dd-bg-header);border:1px solid var(--dd-border);
                border-radius:9px;box-shadow:0 4px 16px rgba(0,0,0,.3);
            }
            .dd-accent-picker.hidden{display:none}
            .dd-accent-picker button{
                width:24px;height:24px;border-radius:50%;
                border:2px solid transparent;cursor:pointer;padding:0;
                transition:transform .15s, border-color .15s;
            }
            .dd-accent-picker button:hover{transform:scale(1.15);border-color:#fff}

            /* ===== Settings / Help modal ===== */
            .dd-modal-bg{
                position:fixed;inset:0;z-index:2147483647;
                background:rgba(0,0,0,.72);
                display:flex;align-items:center;justify-content:center;
                font-family:'Segoe UI',Arial,sans-serif;
            }
            .dd-modal{
                background:var(--dd-bg-header,#0c0c14);color:var(--dd-text,#fff);
                width:90%;max-width:400px;max-height:80vh;border-radius:12px;overflow:hidden;
                border:2px solid #3b82f6;box-shadow:0 8px 32px rgba(0,0,0,.5);
            }
            .dd-modal-wide{max-width:480px;}
            .dd-ch-toolbar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:10px;}
            .dd-ch-btn{
                padding:6px 10px;border-radius:7px;border:1px solid rgba(59,130,246,.35);
                background:rgba(59,130,246,.12);color:#93c5fd;font-size:11px;font-weight:700;cursor:pointer;
            }
            .dd-ch-btn:hover{background:var(--dd-accent);color:#000;border-color:var(--dd-accent);}
            .dd-ch-count{margin-left:auto;font-size:11px;opacity:.7;}
            .dd-ch-list{
                max-height:42vh;overflow:auto;border:1px solid rgba(59,130,246,.2);
                border-radius:8px;padding:4px;
            }
            .dd-ch-row{
                display:flex;align-items:center;gap:8px;padding:7px 8px;
                border-radius:6px;cursor:pointer;font-size:12px;
            }
            .dd-ch-row:hover{background:rgba(59,130,246,.1);}
            .dd-ch-row.dd-ch-current{background:rgba(59,130,246,.18);}
            .dd-ch-num{
                min-width:28px;text-align:center;font-size:10px;font-weight:700;
                color:#93c5fd;opacity:.8;
            }
            .dd-ch-title{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
            .dd-ch-cb{width:15px;height:15px;accent-color:#3b82f6;}
            .dd-log-list{
                max-height:45vh;overflow:auto;border:1px solid rgba(59,130,246,.2);
                border-radius:8px;padding:4px;font-family:ui-monospace,Consolas,monospace;font-size:11px;
            }
            .dd-log-row{
                display:grid;grid-template-columns:64px 48px 1fr;gap:6px;
                padding:5px 6px;border-radius:5px;align-items:start;
            }
            .dd-log-row:nth-child(odd){background:rgba(59,130,246,.06);}
            .dd-log-t{opacity:.55;font-size:10px;}
            .dd-log-lv{font-weight:700;font-size:10px;text-transform:uppercase;}
            .dd-log-info .dd-log-lv{color:#93c5fd;}
            .dd-log-warn .dd-log-lv{color:#fbbf24;}
            .dd-log-err .dd-log-lv{color:#f87171;}
            .dd-log-msg{word-break:break-word;line-height:1.35;}
            .dd-log-detail{opacity:.65;font-size:10px;}
            .dd-modal-head{
                padding:12px 16px;display:flex;align-items:center;justify-content:space-between;
                background:rgba(59,130,246,.12);border-bottom:1px solid rgba(59,130,246,.3);
                font-size:14px;
            }
            .dd-modal-head button{
                background:#333;color:#fff;border:none;border-radius:6px;
                padding:4px 10px;cursor:pointer;font-weight:700;
            }
            .dd-modal-body{padding:14px 16px;overflow:auto;max-height:60vh;}
            .dd-help-row{
                display:flex;align-items:center;gap:12px;
                padding:8px 0;border-bottom:1px solid rgba(59,130,246,.15);font-size:13px;
            }
            .dd-help-row kbd{
                min-width:72px;text-align:center;
                background:rgba(59,130,246,.2);border:1px solid rgba(59,130,246,.4);
                border-radius:6px;padding:4px 8px;font-size:12px;font-weight:700;
                color:#93c5fd;
            }
            .dd-help-note{margin-top:12px;font-size:11px;opacity:.7;line-height:1.45;}
            .dd-set-label{
                display:flex;flex-direction:column;gap:4px;
                margin-bottom:12px;font-size:12px;font-weight:600;color:#93c5fd;
            }
            .dd-set-label select{
                padding:8px 10px;border-radius:8px;border:1px solid rgba(59,130,246,.4);
                background:#0a0a12;color:#fff;font-size:13px;
            }
            .dd-set-check{
                display:flex;align-items:center;gap:8px;
                margin-bottom:10px;font-size:13px;cursor:pointer;
            }
            .dd-set-check input{width:16px;height:16px;accent-color:#3b82f6;}
            .dd-set-actions{display:flex;gap:8px;margin-top:14px;}
            .dd-set-save,.dd-set-reset{
                flex:1;padding:10px;border:none;border-radius:8px;
                font-weight:700;font-size:13px;cursor:pointer;
            }
            .dd-set-save{background:#3b82f6;color:#000;}
            .dd-set-reset{background:#333;color:#fff;}

            #xamz-box button:focus-visible,
            #xamz-box input:focus-visible{
                outline:2px solid var(--dd-accent);outline-offset:2px;
            }
            .xamz-item:focus-within{
                border-color:var(--dd-accent);
                box-shadow:0 0 0 2px rgba(59,130,246,.35);
            }

            #xamz-box{border-color:var(--dd-accent)}
            .xamz-number, .dd-empty-btn{
                background:var(--dd-accent);
            }
        `;
        document.head.appendChild(style);
    }

    function showSkeleton() {
        const grid = document.getElementById('xamz-grid');
        if (!grid) return;
        const count = Math.min(8, Math.max(4, imageURLs.length || 6));
        grid.innerHTML = Array.from({ length: count }, () =>
            `<div class="dd-skeleton"></div>`
        ).join('');
    }

    function updatePanel() {
        const grid = document.getElementById('xamz-grid');
        if (!grid) return;
        // Simpan state checkbox sebelum rebuild
        const prevChecked = new Set();
        grid.querySelectorAll('input[type="checkbox"]').forEach(cb => {
            if (cb.checked) prevChecked.add(+cb.dataset.index);
        });
        const hadItems = grid.querySelectorAll('.xamz-item').length > 0;

        imageURLs = imageURLs.filter(u => isTargetImage(u));
        if (isSortByFilenameSite() && imageURLs.length > 0) {
            imageURLs = sortUrlsByFilename(dedupeByBasename(imageURLs));
        }
        const countEl = document.getElementById('xamz-count');
        if (countEl) countEl.textContent = imageURLs.length + ' ' + t('images');

        if (!imageURLs.length) {
            grid.innerHTML = `
                <div class="dd-empty-msg">
                    <div class="dd-empty-icon">📖</div>
                    <div class="dd-empty-title">${t('emptyTitle')}</div>
                    <div class="dd-empty-desc">${t('emptyDesc')}</div>
                    <button id="dd-empty-scroll" class="dd-empty-btn">${t('emptyScroll')}</button>
                </div>`;
            const emptyBtn = document.getElementById('dd-empty-scroll');
            if (emptyBtn) emptyBtn.onclick = autoScrollScan;
            updateSelectedUI();
            return;
        }

        // Default: semua tercentang kalau pertama kali; kalau rebuild, jaga state lama
        const defaultChecked = !hadItems || prevChecked.size === 0;

        const frag = document.createDocumentFragment();
        imageURLs.forEach((url, index) => {
            const item = document.createElement('div');
            item.className = 'xamz-item';
            item.draggable = true;
            item.dataset.index = index;
            const ext = getExtFromUrl(url).toUpperCase();
            const sizeBytes = estimatedSizes[url] || estimatedSizes[normalizeUrl(url)] || 0;
            const sizeStr = sizeBytes ? formatBytes(sizeBytes) : '…';
            const isChecked = defaultChecked || prevChecked.has(index);
            item.innerHTML = `
                <div class="xamz-number">${String(index + 1).padStart(3, '0')}</div>
                <div class="dd-drag-handle" title="Drag untuk ubah urutan">⠿</div>
                <img src="${url}" loading="lazy" decoding="async" data-index="${index}" draggable="false">
                <label>
                    <input type="checkbox" ${isChecked ? 'checked' : ''} data-index="${index}">
                    <span class="dd-panel-label">${t('panel')} ${index + 1}</span>
                    <span class="dd-ext-badge">${ext}</span>
                </label>
                <div class="dd-panel-meta" data-meta="${index}">
                    <span class="dd-meta-res">—</span>
                    <span class="dd-meta-size">${sizeStr}</span>
                </div>
            `;
            const img = item.querySelector('img');
            attachThumbFallback(img, url);
            const checkbox = item.querySelector('input[type="checkbox"]');
            const resEl = item.querySelector('.dd-meta-res');

            // Tidak ada HEAD request per-gambar (berat). Ukuran dari transferSize / saat download.
            img.addEventListener('load', () => {
                const w = img.naturalWidth;
                const h = img.naturalHeight;
                if (resEl && w && h) resEl.textContent = `${w}×${h}`;
                item.dataset.w = w;
                item.dataset.h = h;
            }, { once: true });

            img.addEventListener('click', (e) => {
                e.stopPropagation();
                if (e.shiftKey && lastSelectedIndex >= 0) {
                    const from = Math.min(lastSelectedIndex, index);
                    const to = Math.max(lastSelectedIndex, index);
                    document.querySelectorAll('#xamz-grid input[type="checkbox"]').forEach(cb => {
                        const i = +cb.dataset.index;
                        if (i >= from && i <= to) cb.checked = true;
                    });
                } else {
                    checkbox.checked = !checkbox.checked;
                    lastSelectedIndex = index;
                }
                updateSelectedUI();
            });

            checkbox.addEventListener('click', (e) => {
                if (e.shiftKey && lastSelectedIndex >= 0) {
                    e.preventDefault();
                    const from = Math.min(lastSelectedIndex, index);
                    const to = Math.max(lastSelectedIndex, index);
                    const targetState = !checkbox.checked;
                    document.querySelectorAll('#xamz-grid input[type="checkbox"]').forEach(cb => {
                        const i = +cb.dataset.index;
                        if (i >= from && i <= to) cb.checked = targetState;
                    });
                }
                lastSelectedIndex = index;
                updateSelectedUI();
                if (showSelectedOnly) applySelectedOnlyFilter();
            });

            img.addEventListener('mouseenter', () => {
                clearTimeout(hoverPreviewTimer);
                hoverPreviewTimer = setTimeout(() => {
                    const preview = document.getElementById('dd-hover-preview');
                    const pImg = document.getElementById('dd-hover-img');
                    const meta = document.getElementById('dd-hover-meta');
                    if (!preview || !pImg) return;
                    pImg.src = img.src || url;
                    const w = +item.dataset.w || img.naturalWidth || 0;
                    const h = +item.dataset.h || img.naturalHeight || 0;
                    const bytes = estimatedSizes[url] || sizeBytes || 0;
                    if (meta) {
                        const parts = [];
                        if (w && h) parts.push(`<b>${w}×${h}</b>`);
                        if (bytes) parts.push(`<b>${formatBytes(bytes)}</b>`);
                        meta.innerHTML = parts.join(' · ') || '—';
                    }
                    preview.classList.remove('hidden');
                    const rect = img.getBoundingClientRect();
                    let left = rect.right + 12;
                    let top = rect.top;
                    if (left + 400 > window.innerWidth) left = rect.left - 412;
                    if (top + 520 > window.innerHeight) top = window.innerHeight - 540;
                    if (top < 8) top = 8;
                    if (left < 8) left = 8;
                    preview.style.left = left + 'px';
                    preview.style.top = top + 'px';
                }, 400);
            });
            img.addEventListener('mouseleave', () => {
                clearTimeout(hoverPreviewTimer);
                document.getElementById('dd-hover-preview')?.classList.add('hidden');
            });

            item.addEventListener('dragstart', (e) => {
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', index);
                item.classList.add('dd-dragging');
                setTimeout(() => item.classList.add('dd-dragging-ghost'), 0);
            });
            item.addEventListener('dragend', () => {
                item.classList.remove('dd-dragging', 'dd-dragging-ghost');
                grid.querySelectorAll('.xamz-item').forEach(el => el.classList.remove('dd-drag-over'));
            });
            item.addEventListener('dragover', (e) => {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                if (!e.currentTarget.classList.contains('dd-dragging')) {
                    e.currentTarget.classList.add('dd-drag-over');
                }
            });
            item.addEventListener('dragleave', (e) => {
                e.currentTarget.classList.remove('dd-drag-over');
            });
            item.addEventListener('drop', (e) => {
                e.preventDefault();
                e.currentTarget.classList.remove('dd-drag-over');
                const fromIdx = parseInt(e.dataTransfer.getData('text/plain'), 10);
                const toIdx = parseInt(e.currentTarget.dataset.index, 10);
                if (isNaN(fromIdx) || isNaN(toIdx) || fromIdx === toIdx) return;
                const [moved] = imageURLs.splice(fromIdx, 1);
                imageURLs.splice(toIdx, 0, moved);
                updatePanel();
            });

            frag.appendChild(item);
        });
        grid.innerHTML = '';
        grid.appendChild(frag);
        updateSelectedUI();
    }

    function getSelectedIndexes() {
        return [...document.querySelectorAll('#xamz-grid input:checked')].map(cb => +cb.dataset.index);
    }

    // ==================== Auto Scroll ====================
    async function autoScrollScan() {
        showMiniProgress();
        setMiniProgress(0, 'Scrolling...');

        const step = Math.max(400, Math.floor(window.innerHeight * 0.75));
        let current = 0;
        let stableRounds = 0;       // berapa kali tinggi halaman tidak bertambah
        let lastHeight = 0;
        let lastCount = imageURLs.length;
        let iterations = 0;
        const MAX_ITER = 120;       // safety: jangan infinite loop
        const NEED_STABLE = 3;      // stop setelah 3x tinggi + count stabil di bawah

        // Mulai dari atas sekali
        window.scrollTo(0, 0);
        await new Promise(r => setTimeout(r, 200));

        while (iterations < MAX_ITER) {
            iterations++;
            const totalHeight = Math.max(
                document.body.scrollHeight,
                document.documentElement.scrollHeight,
                document.documentElement.offsetHeight
            );

            window.scrollTo(0, current);
            await new Promise(r => setTimeout(r, 350));
            scanXAmz();

            const newHeight = Math.max(
                document.body.scrollHeight,
                document.documentElement.scrollHeight
            );
            const newCount = imageURLs.length;

            // Progress berdasarkan posisi scroll (tinggi dinamis)
            const pct = Math.min(98, Math.round((current / Math.max(newHeight, 1)) * 100));
            setMiniProgress(pct, `Scrolling... ${pct}% • ${newCount} gambar`);

            // Deteksi stabil: tinggi & jumlah gambar tidak berubah
            if (newHeight <= lastHeight + 20 && newCount === lastCount && current + step >= newHeight) {
                stableRounds++;
            } else {
                stableRounds = 0;
            }
            lastHeight = newHeight;
            lastCount = newCount;

            if (stableRounds >= NEED_STABLE) break;

            current += step;
            // Kalau sudah melewati ujung, tarik ke bawah sekali lagi lalu cek stabil
            if (current >= newHeight) {
                current = newHeight;
                window.scrollTo(0, newHeight);
                await new Promise(r => setTimeout(r, 400));
                scanXAmz();
                // jangan reset ke 0 — itu yang bikin loop tanpa henti
                // cukup tunggu stabil di bawah
                if (imageURLs.length === lastCount &&
                    Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) <= lastHeight + 20) {
                    stableRounds++;
                } else {
                    lastHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight);
                    lastCount = imageURLs.length;
                    stableRounds = 0;
                    // konten baru muncul: lanjut sedikit dari posisi sekarang (bukan dari 0)
                    current = Math.max(0, lastHeight - step * 2);
                }
                if (stableRounds >= NEED_STABLE) break;
            }
        }

        window.scrollTo(0, 0);
        scanXAmz();
        updatePanel();
        setMiniProgress(100, `✅ Scroll selesai • ${imageURLs.length} gambar`);
        setTimeout(() => {
            hideMiniProgress();
            updateSelectedUI();
        }, 2000);
    }

    // ==================== Concurrent Helper ====================
    async function runConcurrent(items, concurrency, workerFn, onProgress) {
        let completed = 0;
        let nextIndex = 0;
        const results = new Array(items.length);

        async function worker() {
            while (nextIndex < items.length) {
                const i = nextIndex++;
                try {
                    results[i] = await workerFn(items[i], i);
                } catch (e) {
                    results[i] = null;
                }
                completed++;
                if (onProgress) onProgress(completed, items.length, results[i], i);
            }
        }

        const workers = [];
        const n = Math.min(concurrency, items.length);
        for (let c = 0; c < n; c++) workers.push(worker());
        await Promise.all(workers);
        return results;
    }

    // ==================== Download ====================
    function downloadSelected() {
        const indexes = getSelectedIndexes();
        if (!indexes.length) return alert('Pilih dulu panelnya');
        startDownloads(indexes);
    }

    async function startDownloads(indexes) {
        if (isDownloading) return;
        // Folder mode: pilih folder dulu (masih dalam user gesture)
        if (saveMode === 'folder' && !(await ensureSaveFolder())) {
            return;
        }
        beginDownload();

        let ok = 0, fail = 0;
        setMiniProgress(0, `0/${indexes.length}`, { done: 0, total: indexes.length, bytes: 0 });

        await runConcurrent(indexes, dlConcurrency, async (idx) => {
            const url = imageURLs[idx];
            const result = await downloadOne(idx);
            if (result && estimatedSizes[url]) {
                downloadBytesDone += estimatedSizes[url];
            } else if (result) {
                downloadBytesDone += avgImageSize;
            }
            return result;
        }, (done, total, success) => {
            if (success) ok++; else fail++;
            const pct = (done / total) * 100;
            setMiniProgress(pct, `${done}/${total}`, { done, total, bytes: downloadBytesDone });
        });

        setMiniProgress(100, `✅ ${ok} berhasil, ${fail} gagal`);
        addHistory(getTitle(), 'Single', ok);
        notify('Pawchive DL', `Download selesai: ${ok} berhasil, ${fail} gagal`);
        endDownload();
        setTimeout(() => hideMiniProgress(), 4000);
    }

    async function downloadAsZip() {
        const indexes = getSelectedIndexes();
        if (!indexes.length) return alert('Pilih dulu panelnya');
        if (isDownloading) return;

        let zipName = (document.getElementById('xamz-zipname')?.value || '').trim()
            .replace(/[\\/:*?"<>|]/g, '');
        if (!zipName) zipName = 'Pawchive';
        if (!zipName.toLowerCase().endsWith('.zip')) zipName += '.zip';

        // Pilih lokasi SEKARANG (sebelum download async) — penting untuk Save As
        if (!(await prepareSaveTarget(zipName))) return;

        beginDownload();

        const files = new Array(indexes.length);
        let ok = 0, fail = 0;
        setMiniProgress(0, `Gambar 0/${indexes.length}`, { done: 0, total: indexes.length, bytes: 0 });

        await runConcurrent(indexes, Math.max(2, dlConcurrency + 1), async (idx, i) => {
            try {
                const blob = await fetchImageAsBlob(imageURLs[idx]);
                if (blob) {
                    estimatedSizes[imageURLs[idx]] = blob.size;
                    downloadBytesDone += blob.size;
                    const ext = getExtFromUrl(imageURLs[idx]);
                    const name = String(i + 1).padStart(3, '0') + '.' + ext;
                    files[i] = { name, data: new Uint8Array(await blob.arrayBuffer()) };
                    return true;
                }
            } catch (e) {}
            return false;
        }, (done, total, success) => {
            if (success) ok++; else fail++;
            const pct = (done / total) * 90;
            setMiniProgress(pct, `Gambar ${done}/${total}`, { done, total, bytes: downloadBytesDone });
        });

        const validFiles = files.filter(Boolean);
        if (validFiles.length === 0) {
            setMiniProgress(0, '❌ Gagal ambil gambar');
            endDownload();
            setTimeout(() => hideMiniProgress(), 3000);
            return;
        }

        setMiniProgress(95, 'Membuat ZIP...');
        try {
            const zipBlob = createZip(validFiles);
            const saved = await saveBlob(zipBlob, zipName);
            if (!saved) {
                setMiniProgress(0, '❌ Simpan dibatalkan');
            } else {
                setMiniProgress(100, `✅ ZIP siap! ${ok} gambar • ${formatBytes(zipBlob.size)}`);
                addHistory(zipName.replace(/\.zip$/i, ''), 'ZIP', ok);
                notify('Pawchive DL', `ZIP berhasil: ${ok} gambar (${formatBytes(zipBlob.size)})`);
            }
        } catch (e) {
            setMiniProgress(0, '❌ Gagal buat ZIP');
        }
        endDownload();
        setTimeout(() => hideMiniProgress(), 4000);
    }

    async function downloadAsPdf() {
        const indexes = getSelectedIndexes();
        if (!indexes.length) return alert('Pilih dulu panelnya');
        if (isDownloading) return;

        let pdfName = (document.getElementById('xamz-zipname')?.value || '').trim()
            .replace(/[\\/:*?"<>|]/g, '');
        if (!pdfName) pdfName = 'Pawchive';
        if (!pdfName.toLowerCase().endsWith('.pdf')) pdfName += '.pdf';

        if (!(await prepareSaveTarget(pdfName))) return;

        beginDownload();

        const pages = new Array(indexes.length);
        let ok = 0, fail = 0;
        setMiniProgress(0, `Convert 0/${indexes.length}`, { done: 0, total: indexes.length, bytes: 0 });

        await runConcurrent(indexes, Math.min(3, Math.max(1, dlConcurrency - 1)), async (idx, i) => {
            try {
                const jpgBlob = await convertToJpg(imageURLs[idx]);
                if (jpgBlob) {
                    downloadBytesDone += jpgBlob.size;
                    pages[i] = jpgBlob;
                    return true;
                }
            } catch (e) {}
            return false;
        }, (done, total, success) => {
            if (success) ok++; else fail++;
            const pct = (done / total) * 80;
            setMiniProgress(pct, `Convert ${done}/${total}`, { done, total, bytes: downloadBytesDone });
        });

        const validPages = pages.filter(Boolean);
        if (validPages.length === 0) {
            setMiniProgress(0, '❌ Gagal convert');
            endDownload();
            setTimeout(() => hideMiniProgress(), 3000);
            return;
        }

        setMiniProgress(90, 'Membuat PDF...');
        try {
            const pageData = [];
            for (const b of validPages) {
                const buf = new Uint8Array(await b.arrayBuffer());
                const dims = getJpegSize(buf);
                pageData.push({ data: buf, width: dims.w, height: dims.h });
            }
            const pdfBlob = buildPdfBinary(pageData, pdfName.replace(/\.pdf$/i, ''));
            const saved = await saveBlob(pdfBlob, pdfName);
            if (!saved) {
                setMiniProgress(0, '❌ Simpan dibatalkan');
            } else {
                setMiniProgress(100, `✅ PDF siap! ${ok} halaman • ${formatBytes(pdfBlob.size)}`);
                addHistory(pdfName.replace(/\.pdf$/i, ''), 'PDF', ok);
                notify('Pawchive DL', `PDF berhasil: ${ok} halaman (${formatBytes(pdfBlob.size)})`);
            }
        } catch (e) {
            console.error(e);
            setMiniProgress(0, '❌ Gagal buat PDF');
        }
        endDownload();
        setTimeout(() => hideMiniProgress(), 4000);
    }

    async function downloadAsPdfStrip() {
        const indexes = getSelectedIndexes();
        if (!indexes.length) return alert('Pilih dulu panelnya');
        if (isDownloading) return;

        let pdfName = (document.getElementById('xamz-zipname')?.value || '').trim()
            .replace(/[\\/:*?"<>|]/g, '');
        if (!pdfName) pdfName = 'Pawchive';
        if (!pdfName.toLowerCase().endsWith('.pdf')) pdfName += '.pdf';
        if (!/_strip$/i.test(pdfName.replace(/\.pdf$/i, ''))) {
            pdfName = pdfName.replace(/\.pdf$/i, '') + '_strip.pdf';
        }

        if (!(await prepareSaveTarget(pdfName))) return;

        beginDownload();

        const bitmaps = new Array(indexes.length);
        let ok = 0, fail = 0;
        setMiniProgress(0, `Load 0/${indexes.length}`, { done: 0, total: indexes.length, bytes: 0 });

        await runConcurrent(indexes, dlConcurrency, async (idx, i) => {
            try {
                const blob = await fetchImageAsBlob(imageURLs[idx]);
                if (!blob) return false;
                estimatedSizes[imageURLs[idx]] = blob.size;
                downloadBytesDone += blob.size;
                const bmp = await createImageBitmap(blob);
                bitmaps[i] = bmp;
                return true;
            } catch (e) {
                return false;
            }
        }, (done, total, success) => {
            if (success) ok++; else fail++;
            const pct = (done / total) * 70;
            setMiniProgress(pct, `Load ${done}/${total}`, { done, total, bytes: downloadBytesDone });
        });

        const validBitmaps = bitmaps.filter(Boolean);
        if (validBitmaps.length === 0) {
            setMiniProgress(0, '❌ Gagal load gambar');
            endDownload();
            setTimeout(() => hideMiniProgress(), 3000);
            return;
        }

        setMiniProgress(75, 'Menyusun strip...');
        try {
            const maxW = Math.max(...validBitmaps.map(b => b.width));
            let totalH = 0;
            const scaled = validBitmaps.map(b => {
                const scale = maxW / b.width;
                const h = Math.round(b.height * scale);
                totalH += h;
                return { bmp: b, w: maxW, h };
            });

            const MAX_CANVAS = 16384;
            const pageData = [];

            async function canvasToJpegPage(items, height) {
                const canvas = document.createElement('canvas');
                canvas.width = maxW;
                canvas.height = height;
                const ctx = canvas.getContext('2d');
                ctx.fillStyle = '#FFFFFF';
                ctx.fillRect(0, 0, maxW, height);
                let y = 0;
                for (const s of items) {
                    ctx.drawImage(s.bmp, 0, y, s.w, s.h);
                    y += s.h;
                }
                const jpgBlob = await new Promise(res => canvas.toBlob(res, 'image/jpeg', jpegQuality));
                const buf = new Uint8Array(await jpgBlob.arrayBuffer());
                return { data: buf, width: maxW, height: height };
            }

            if (totalH > MAX_CANVAS) {
                const chunks = [];
                let cur = [], curH = 0;
                for (const s of scaled) {
                    if (curH + s.h > MAX_CANVAS && cur.length) {
                        chunks.push({ items: cur, h: curH });
                        cur = [];
                        curH = 0;
                    }
                    cur.push(s);
                    curH += s.h;
                }
                if (cur.length) chunks.push({ items: cur, h: curH });

                for (let ci = 0; ci < chunks.length; ci++) {
                    setMiniProgress(75 + (ci / chunks.length) * 15, `Strip part ${ci + 1}/${chunks.length}`);
                    pageData.push(await canvasToJpegPage(chunks[ci].items, chunks[ci].h));
                }
            } else {
                setMiniProgress(85, 'Render strip...');
                pageData.push(await canvasToJpegPage(scaled, totalH));
            }

            setMiniProgress(95, 'Membuat PDF strip...');
            const pdfBlob = buildPdfBinary(pageData, pdfName.replace(/\.pdf$/i, ''));
            const saved = await saveBlob(pdfBlob, pdfName);
            validBitmaps.forEach(b => { try { b.close(); } catch (e) {} });
            if (!saved) {
                setMiniProgress(0, '❌ Simpan dibatalkan');
            } else {
                setMiniProgress(100, `✅ Strip PDF siap! ${ok} gambar • ${formatBytes(pdfBlob.size)}`);
                addHistory(pdfName.replace(/\.pdf$/i, ''), 'PDF Strip', ok);
                notify('Pawchive DL', `PDF Strip berhasil: ${ok} gambar (${formatBytes(pdfBlob.size)})`);
            }
        } catch (e) {
            console.error(e);
            setMiniProgress(0, '❌ Gagal buat strip PDF');
        }
        endDownload();
        setTimeout(() => hideMiniProgress(), 4000);
    }

    function convertToJpg(url) {
        return new Promise(async (resolve) => {
            try {
                const blob = await fetchImageAsBlob(url);
                if (!blob) return resolve(null);

                const img = new Image();
                img.crossOrigin = 'anonymous';
                const objectUrl = URL.createObjectURL(blob);
                img.onload = () => {
                    const canvas = document.createElement('canvas');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    const ctx = canvas.getContext('2d');
                    ctx.fillStyle = '#FFFFFF';
                    ctx.fillRect(0, 0, canvas.width, canvas.height);
                    ctx.drawImage(img, 0, 0);
                    canvas.toBlob((b) => {
                        URL.revokeObjectURL(objectUrl);
                        resolve(b);
                    }, 'image/jpeg', jpegQuality);
                };
                img.onerror = () => {
                    URL.revokeObjectURL(objectUrl);
                    resolve(null);
                };
                img.src = objectUrl;
            } catch (e) {
                resolve(null);
            }
        });
    }

    function getJpegSize(buf) {
        let i = 2;
        while (i < buf.length - 8) {
            if (buf[i] !== 0xFF) break;
            const marker = buf[i + 1];
            if (marker === 0xC0 || marker === 0xC1 || marker === 0xC2) {
                const h = (buf[i + 5] << 8) | buf[i + 6];
                const w = (buf[i + 7] << 8) | buf[i + 8];
                return { w, h };
            }
            const len = (buf[i + 2] << 8) | buf[i + 3];
            i += 2 + len;
        }
        return { w: 800, h: 1200 };
    }

    function buildPdfBinary(pages, title) {
        const encoder = new TextEncoder();
        const parts = [];
        const offsets = [0];
        let pos = 0;

        const pushStr = (str) => {
            const data = encoder.encode(str);
            parts.push(data);
            pos += data.length;
        };
        const pushBin = (data) => {
            parts.push(data);
            pos += data.length;
        };

        pushStr('%PDF-1.4\n%\xE2\xE3\xCF\xD3\n');

        offsets.push(pos);
        pushStr('1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n');

        offsets.push(pos);
        let kids = '';
        for (let i = 0; i < pages.length; i++) kids += (3 + i * 3) + ' 0 R ';
        pushStr(`2 0 obj\n<< /Type /Pages /Kids [${kids}] /Count ${pages.length} >>\nendobj\n`);

        let objNum = 3;
        for (let i = 0; i < pages.length; i++) {
            const p = pages[i];
            const pageObj = objNum;
            const contentObj = objNum + 1;
            const imageObj = objNum + 2;

            offsets.push(pos);
            pushStr(`${pageObj} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${p.width} ${p.height}] /Contents ${contentObj} 0 R /Resources << /XObject << /Im${i} ${imageObj} 0 R >> /ProcSet [/PDF /ImageC] >> >>\nendobj\n`);

            const stream = `q\n${p.width} 0 0 ${p.height} 0 0 cm\n/Im${i} Do\nQ\n`;
            offsets.push(pos);
            pushStr(`${contentObj} 0 obj\n<< /Length ${stream.length} >>\nstream\n${stream}endstream\nendobj\n`);

            offsets.push(pos);
            pushStr(`${imageObj} 0 obj\n<< /Type /XObject /Subtype /Image /Width ${p.width} /Height ${p.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${p.data.length} >>\nstream\n`);
            pushBin(p.data);
            pushStr('\nendstream\nendobj\n');

            objNum += 3;
        }

        const infoObj = objNum;
        offsets.push(pos);
        const safeTitle = (title || 'Pawchive').replace(/[()\\]/g, '');
        pushStr(`${infoObj} 0 obj\n<< /Title (${safeTitle}) /Producer (Pawchive Downloader) /Creator (qxyraxp) >>\nendobj\n`);
        objNum++;

        const xrefPos = pos;
        let xref = `xref\n0 ${objNum}\n0000000000 65535 f \n`;
        for (let i = 1; i < offsets.length; i++) {
            xref += String(offsets[i]).padStart(10, '0') + ' 00000 n \n';
        }
        pushStr(xref);
        pushStr(`trailer\n<< /Size ${objNum} /Root 1 0 R /Info ${infoObj} 0 R >>\nstartxref\n${xrefPos}\n%%EOF\n`);

        const totalLen = parts.reduce((s, p) => s + p.length, 0);
        const result = new Uint8Array(totalLen);
        let offset = 0;
        for (const p of parts) {
            result.set(p, offset);
            offset += p.length;
        }
        return new Blob([result], { type: 'application/pdf' });
    }

    // ==================== Helper ====================
    function getExtFromUrl(url) {
        try {
            const pathname = new URL(url).pathname.toLowerCase();
            if (pathname.endsWith('.webp')) return 'webp';
            if (pathname.endsWith('.avif')) return 'avif';
            if (pathname.endsWith('.jpg') || pathname.endsWith('.jpeg')) return 'jpg';
            if (pathname.endsWith('.png')) return 'png';
            const u = url.toLowerCase();
            if (u.includes('.webp')) return 'webp';
            if (u.includes('.avif')) return 'avif';
            if (u.includes('.png')) return 'png';
        } catch (e) {}
        return 'jpg';
    }

    // Fallback thumbnail panel: kalau gambar original diblok (mis. ERR_BLOCKED_BY_CLIENT
    // oleh AdBlock) atau gagal load, coba via GM_xmlhttpRequest -> blob, lalu versi thumbnail.
    function attachThumbFallback(img, url) {
        let step = 0;
        const markFailed = () => {
            img.style.opacity = '0.35';
            img.title = 'Gagal load preview (download tetap dicoba lewat GM_xmlhttpRequest)';
            ddLog('warn', 'Preview gagal', url);
        };
        img.addEventListener('error', () => {
            step++;
            if (step === 1) {
                try {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        responseType: 'blob',
                        headers: { 'Referer': location.href },
                        onload: (res) => {
                            if (res.status >= 200 && res.status < 300 && res.response) {
                                img.src = URL.createObjectURL(res.response);
                            } else {
                                img.dispatchEvent(new Event('error'));
                            }
                        },
                        onerror: () => img.dispatchEvent(new Event('error')),
                        ontimeout: () => img.dispatchEvent(new Event('error'))
                    });
                    return;
                } catch (e) { step++; }
            }
            if (step === 2) {
                const thumb = url.replace(/\/\/file\.pawchive\.pw\/data\//i, '//img.pawchive.pw/thumbnail/data/');
                if (thumb !== url) { img.src = thumb; return; }
                step++;
            }
            if (step >= 3) markFailed();
        });
    }

    function fetchImageAsBlob(url) {
        return new Promise((resolve) => {
            const img = [...document.querySelectorAll('#xamz-grid img')].find(i => i.src === url);
            if (img && img.complete && img.naturalWidth > 0) {
                try {
                    const canvas = document.createElement('canvas');
                    canvas.width = img.naturalWidth;
                    canvas.height = img.naturalHeight;
                    canvas.getContext('2d').drawImage(img, 0, 0);
                    const ext = getExtFromUrl(url);
                    let mime = 'image/jpeg';
                    if (ext === 'webp') mime = 'image/webp';
                    else if (ext === 'png') mime = 'image/png';
                    canvas.toBlob(b => {
                        if (b) estimatedSizes[url] = b.size;
                        resolve(b);
                    }, mime, 0.92);
                    return;
                } catch (e) {}
            }
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                responseType: 'arraybuffer',
                headers: {
                    'Referer': location.href,
                    'Origin': location.origin,
                    'Accept': 'image/avif,image/webp,image/jpeg,image/png,image/*,*/*;q=0.8'
                },
                onload: (res) => {
                    if (res.status >= 200 && res.status < 300) {
                        let mime = 'image/jpeg';
                        const ct = (res.responseHeaders || '').toLowerCase();
                        if (ct.includes('image/webp')) mime = 'image/webp';
                        else if (ct.includes('image/avif')) mime = 'image/avif';
                        else if (ct.includes('image/png')) mime = 'image/png';
                        else {
                            const ul = url.toLowerCase();
                            if (ul.includes('.webp')) mime = 'image/webp';
                            else if (ul.includes('.avif')) mime = 'image/avif';
                            else if (ul.includes('.png')) mime = 'image/png';
                        }
                        const blob = new Blob([res.response], { type: mime });
                        estimatedSizes[url] = blob.size;
                        resolve(blob);
                    } else resolve(null);
                },
                onerror: () => resolve(null),
                ontimeout: () => resolve(null)
            });
        });
    }

    async function downloadOne(index) {
        const url = imageURLs[index];
        const ext = getExtFromUrl(url);
        const filename = String(index + 1).padStart(3, '0') + '.' + ext;
        try {
            const blob = await fetchImageAsBlob(url);
            if (blob) {
                return await saveBlob(blob, filename);
            }
        } catch (e) {}
        try {
            await new Promise((resolve, reject) => {
                GM_download({
                    url,
                    name: filename,
                    saveAs: saveMode === 'ask',
                    onload: resolve,
                    onerror: reject
                });
            });
            return true;
        } catch (e) {}
        return false;
    }

    function isReaderPage() {
        if (/\/reader\//i.test(location.pathname)) return true;
        if (document.querySelectorAll('img[src*="desu.pics"], img[src*="amz-ch"]').length > 2) return true;
        // MangaToon / Themesia / Kanzenin
        if (document.querySelector('#readerarea img, .readercontent img, img.ts-main-image')) return true;
        if (/mangatoon\.cc|kanzenin\.(info|xyz)|komikdewasa\.art/i.test(location.hostname) &&
            /chapter/i.test(location.pathname + document.title)) return true;
        // pawchive: HANYA halaman post (.../post/123), bukan list user /fanbox/user/...
        if (/pawchive\.pw/i.test(location.hostname) && /\/post\/\d+/i.test(location.pathname)) return true;
        return false;
    }

    function init() {
        if (!document.body) return setTimeout(init, 300);
        createButton();
        ddLog('info', 'Script aktif', location.hostname + location.pathname);
        // Restore folder handle dari IndexedDB (permission dicek lagi saat download)
        if (saveMode === 'folder') restoreDirHandle();
        setTimeout(scanXAmz, 1500);
        setTimeout(scanXAmz, 4000);
        setTimeout(showQueueBanner, 1200);

        if (autoOpenPanel && isReaderPage()) {
            setTimeout(() => {
                if (!panelOpen) openPanel();
            }, 1800);
        }
    }
    init();
})();