☰

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.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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