Greasy Fork is available in English.

Toki Mark

비로그인으로 뉴토끼 북마크 관리를 가능하게 해주는 스크립트입니다.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Toki Mark
// @namespace    http://tampermonkey.net/
// @version      260523012800
// @description  비로그인으로 뉴토끼 북마크 관리를 가능하게 해주는 스크립트입니다.
// @license      MIT
// @include      *://sbxh*.com/*
// @include      *://*.sbxh*.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=newtoki.com
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    'use strict';

    // Strict Hostname Guard to prevent execution on unrelated .com/.net domains
    const isTokiSite = /^(.*\.)?(sbxh\d*|newtoki\d*|manatoki\d*)\.(com|net)$/i.test(location.hostname);
    if (!isTokiSite) return;

    // DevTools Shortcut Bypass logic to override site's keydown restriction
    (function bypassDevToolsBlocker() {
        const isDevToolsKey = (e) => {
            if (!(e instanceof KeyboardEvent)) return false;
            const keyCode = e.keyCode || e.which;
            const key = e.key ? e.key.toLowerCase() : '';

            if (keyCode === 123 || key === 'f12') return true;
            if ((e.ctrlKey || e.metaKey) && e.shiftKey && (keyCode === 73 || keyCode === 67 || keyCode === 74 || key === 'i' || key === 'c' || key === 'j')) return true;
            if ((e.ctrlKey || e.metaKey) && (keyCode === 85 || key === 'u')) return true;

            return false;
        };

        window.addEventListener('keydown', function(e) {
            if (isDevToolsKey(e)) {
                e.stopImmediatePropagation();
            }
        }, true);
    })();

    const STORAGE_KEY = 'toki_local_bookmarks_v10';
    const PREF_KEY = 'toki_sort_pref_v10';
    const PIN_UP_KEY = 'toki_pin_up_v10';
    const PIN_CLR_KEY = 'toki_pin_clr_v10'; // Pin clear key
    const SORT_ASC_KEY = 'toki_sort_asc_v10';
    const SCAN_QUEUE_KEY = 'toki_scan_queue_v20'; // Reset scan queue key
    const SYNC_AT_KEY = 'toki_last_sync_at_v10';
    const REMOTE_AT_KEY = 'toki_last_remote_at_v10';
    let pushDebounceTimer = null; // Timer for sync debouncing


    const DIRTY_KEY = 'toki_dirty_count';
    const syncChannel = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('toki_update_sync_v20') : null;

    // Constants for UI stability
    const ID_BOOKMARKS_WRAPPER = 'my-local-bookmarks';
    const ID_MANHWA_BTN = 'my-local-btn';
    const ID_VIEWER_UI = 'toki-go-bookmark'; // Using bookmark link as UI presence indicator

    // Encapsulated State
    const tokiState = {
        filters: { states: [], genres: [] },
        filterMode: 'AND',
        searchQuery: '',
        activeTab: (() => {
            let saved = localStorage.getItem('toki_fav_last_tab');
            if (!saved || saved === 'all') saved = 'manhwa';
            return saved;
        })(),
        isScanning: false,
        isSyncing: false,
        btnTimeout: null,
        debounceTimer: null,
        isPushing: false
    };

    // Detection for mobile environments
    const isMobile = /Android|iPhone|iPad/i.test(navigator.userAgent);

    /**
     * Shared Utilities
     */
    const TokiUtils = {
        // [LWW: Last Write Wins] Unified timestamp extraction for conflict resolution
        getEffectiveTime: (m) => Math.max(m.exactUpdatedAt || 0, m.parsedUpdatedAt || 0, m.latestUpdatedAt || 0, m.lastReadAt || 0, m.addedAt || 0),

        formatDateTime: (ts) => {
            if(!ts) return "";
            const d = new Date(ts); const pad = n => n.toString().padStart(2, '0');
            return `${String(d.getFullYear()).slice(2)}.${pad(d.getMonth()+1)}.${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
        },

        getCategoryFromUrl: (url) => {
            if (!url) return 'manhwa';
            const m = url.match(/\/(manhwa|webtoon|novel)\//i) || url.match(/\/(manhwa|webtoon|novel)$/i);
            return m ? m[1].toLowerCase() : 'manhwa';
        },

        extractAuthor: (htmlString, docTitle) => {
            let rawTitle = docTitle || "";
            if (htmlString) {
                const titleMatch = htmlString.match(/<title>([^<]+)<\/title>/i);
                if (titleMatch) rawTitle = titleMatch[1];
            }
            const match = rawTitle.match(/^(.*?)\s*-\s*([^|]+?)\s*\|\s*뉴토끼$/);
            if (match) {
                const candidate = match[2].trim();
                if (candidate.match(/화$|프롤로그|완결|단편|예고편|^\d/)) {
                    return "";
                }
                return candidate;
            }
            return "";
        },

        ensureV10Structure: (data) => {
            // Return a safe empty structure if data is empty, not an object, or an array (data corruption state)
            if (!data || typeof data !== 'object' || Array.isArray(data)) {
                return { manhwa: {}, webtoon: {}, novel: {} };
            }

            // Perform migration if it is an old version (single object without category separation)
            if (!data.manhwa && !data.webtoon && !data.novel) {
                const migrated = { manhwa: {}, webtoon: {}, novel: {} };
                let migratedCount = 0;
                for (const [id, item] of Object.entries(data)) {
                    if (!item || typeof item !== 'object') continue; // Skip corrupted individual item
                    const cat = TokiUtils.getCategoryFromUrl(item.url || '');
                    migrated[cat][id] = item;
                    migratedCount++;
                }

                // CRITICAL SAFETY RESCUE: If migration resulted in 0 items but the original data had elements,
                // fallback everything into 'manhwa' to prevent catastrophic data loss!
                const originalKeysCount = Object.keys(data).length;
                if (migratedCount === 0 && originalKeysCount > 0) {
                    console.warn('[TokiUtils] Migration guard: 0 items migrated but original data had', originalKeysCount, 'keys. Rescuing data to manhwa...');
                    for (const [id, item] of Object.entries(data)) {
                        if (item && typeof item === 'object') {
                            migrated.manhwa[id] = item;
                        }
                    }
                }
                return migrated;
            }

            // If it already has the new version structure (fill in missing categories if any)
            return {
                manhwa: data.manhwa || {},
                webtoon: data.webtoon || {},
                novel: data.novel || {}
            };
        }
    };

    // ==========================================
    // 🛡️ TokiSafetyGuard (Triple Data Protection)
    // ==========================================
    const TokiSafetyGuard = {
        performEmergencyRecovery: function() {
            try {
                const legacyKey = 'toki_local_bookmarks';
                const currentKey = STORAGE_KEY;
                const backupKey = 'toki_bookmarks_backup_safe';

                const legacyData = localStorage.getItem(legacyKey);
                const currentData = localStorage.getItem(currentKey);

                // 1. Emergency Recovery: Migration from legacy plain token structure
                const isEmpty = !currentData || currentData === '{}' || currentData === '{"manhwa":{},"webtoon":{},"novel":{}}';
                if (legacyData && isEmpty) {
                    const parsed = JSON.parse(legacyData);
                    const structured = TokiUtils.ensureV10Structure(parsed);
                    saveBookmarks(structured, { silent: true });
                    console.log('[TokiSafetyGuard] Emergency Recovery Success: Legacy bookmarks restored.');
                    if (typeof TokiUI !== 'undefined') {
                        TokiUI.toast('💡 이전 버전의 북마크가 안전하게 복구되었습니다.', 'success');
                    }
                }

                // 2. Local Safe Backup: Take a periodic snapshot of stable local data
                const activeData = localStorage.getItem(currentKey);
                if (activeData && activeData !== '{}' && activeData !== '{"manhwa":{},"webtoon":{},"novel":{}}') {
                    localStorage.setItem(backupKey, activeData);
                }
            } catch (e) {
                console.error('[TokiSafetyGuard] Emergency recovery failed:', e);
            }
        },

        validateSchema: function(data) {
            if (!data || typeof data !== 'object') return false;
            // Strict Schema Assertion: Must contain at least one of core categories
            const hasManhwa = 'manhwa' in data && typeof data.manhwa === 'object' && !Array.isArray(data.manhwa);
            const hasWebtoon = 'webtoon' in data && typeof data.webtoon === 'object' && !Array.isArray(data.webtoon);
            const hasNovel = 'novel' in data && typeof data.novel === 'object' && !Array.isArray(data.novel);
            return hasManhwa || hasWebtoon || hasNovel;
        }
    };

    // ==========================================
    // CSS Injection
    // ==========================================
    const style = document.createElement('style');
    style.innerHTML = `

        /* Theme Variables */
        :root, [data-theme="light"] {
          --toki-bg: #fff; --toki-bg-sub: #f8f9fa; --toki-bg-input: #fff;
          --toki-border: #e9ecef; --toki-border-light: #eee; --toki-border-dashed: #dcdde1;
          --toki-text: #2d3436; --toki-text-sub: #636e72; --toki-text-muted: #a4b0be; --toki-text-label: #b2bec3;
          --toki-card-shadow: 0 1px 3px rgba(0,0,0,0.04);
          --toki-highlight-bg: #ffebeb; --toki-highlight-border: #ffcccc;
          --toki-empty-bg: #f8f9fa; --toki-delete-bg: #f1f2f6;
          --toki-clr-pin-bg: #f1f2f6; --toki-clr-pin-border: #dfe6e9; --toki-clr-pin-hover: #e2e6e9;
          --toki-pin-bg: #ffebeb; --toki-pin-hover: #ffe3e3;
          --toki-genre-bg: #f8f9fa; --toki-genre-border: #e9ecef; --toki-genre-hover: #e2e6e9;
          --toki-filter-bg: #fff; --toki-filter-border: #dcdde1; --toki-filter-hover: #f1f2f6;
          --toki-dropdown-bg: white; --toki-dropdown-shadow: 0 4px 12px rgba(0,0,0,0.15);
          --toki-done-bg: #dfe6e9; --toki-done-text: #636e72;
          --toki-modal-bg: #fff; --toki-modal-cancel: #dfe6e9;
        }
        [data-theme="dark"] {
          --toki-bg: #1e1e2e; --toki-bg-sub: #181825; --toki-bg-input: #11111b;
          --toki-border: #313244; --toki-border-light: #313244; --toki-border-dashed: #45475a;
          --toki-text: #cdd6f4; --toki-text-sub: #a6adc8; --toki-text-muted: #6c7086; --toki-text-label: #585b70;
          --toki-card-shadow: 0 1px 3px rgba(0,0,0,0.3);
          --toki-highlight-bg: #3b1c2a; --toki-highlight-border: #5a3040;
          --toki-empty-bg: #181825; --toki-delete-bg: #313244;
          --toki-clr-pin-bg: #313244; --toki-clr-pin-border: #45475a; --toki-clr-pin-hover: #45475a;
          --toki-pin-bg: #3b1c2a; --toki-pin-hover: #3a2030;
          --toki-genre-bg: #181825; --toki-genre-border: #313244; --toki-genre-hover: #45475a;
          --toki-filter-bg: #1e1e2e; --toki-filter-border: #45475a; --toki-filter-hover: #313244;
          --toki-dropdown-bg: #1e1e2e; --toki-dropdown-shadow: 0 4px 12px rgba(0,0,0,0.5);
          --toki-done-bg: #313244; --toki-done-text: #a6adc8;
          --toki-modal-bg: #1e1e2e; --toki-modal-cancel: #313244;
        }

        @keyframes pulseUP { 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7); } 70% { transform: scale(1.05); box-shadow: 0 0 0 4px rgba(255, 71, 87, 0); } 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 71, 87, 0); } }
        @keyframes toastFadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
        @keyframes modalFadeIn { from { opacity: 0; } to { opacity: 1; } }
        @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }

        .toki-up-badge { display: inline-flex; align-items: center; justify-content: center; background-color: #ff4757; color: white; border-radius: 4px; padding: 0 5px; height: 18px; font-size: 11px; font-weight: 900; animation: pulseUP 2s infinite; flex-shrink: 0; white-space: nowrap; line-height: 1; }
        .toki-clr-badge { display: inline-flex; align-items: center; justify-content: center; background-color: #636e72; color: white; border-radius: 4px; padding: 0 5px; height: 18px; font-size: 11px; font-weight: 900; flex-shrink: 0; white-space: nowrap; line-height: 1; }
        .toki-control-bar { display: flex; justify-content: space-between; align-items: center; background: var(--toki-bg-sub); padding: 12px 16px; border-radius: 8px; margin: 15px 0 25px 0; border: 1px solid var(--toki-border); box-shadow: var(--toki-card-shadow); flex-wrap: wrap; gap: 10px; }
        .toki-card { background: var(--toki-bg); border-radius: 8px; margin-bottom: 12px; padding: 12px; border: 1px solid var(--toki-border-light); box-shadow: var(--toki-card-shadow); display: flex; flex-direction: column; gap: 12px; }
        .toki-card-top { display: flex; text-decoration: none; color: inherit; align-items: center; }
        .toki-thumb { border-radius: 6px; overflow: hidden; width: 80px; height: 100px; flex-shrink: 0; background: var(--toki-delete-bg); }
        .toki-thumb img { width: 100%; height: 100%; object-fit: cover; }
        .toki-info { width: 100%; padding-left: 15px; display: flex; flex-direction: column; justify-content: center; }
        .toki-title { font-size: 16px; margin-bottom: 4px; display: flex; align-items: center; font-weight: 800; color: var(--toki-text); gap: 6px; }
        .toki-genre { font-size: 11.5px; color: var(--toki-text-muted); margin-bottom: 4px; font-weight: 500; }
        .toki-author { font-size: 12px; color: var(--toki-text-sub); margin-bottom: 4px; font-weight: 600; }
        .toki-meta-box { display: flex; flex-direction: column; gap: 4px; }
        .toki-meta-row { font-size: 13px; color: var(--toki-text-sub); display: flex; align-items: center; }
        .toki-date-sub { font-size: 11px; color: var(--toki-text-muted); margin-left: 6px; }
        .toki-actions { display: flex; gap: 8px; width: 100%; }
        .toki-btn { padding: 10px 0; border-radius: 6px; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none; cursor: pointer; border: none; transition: filter 0.2s; display: flex; justify-content: center; align-items: center; }
        .toki-btn:hover { filter: brightness(0.95); color: white; }

        #toki-toast-container { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 10000001; display: flex; flex-direction: column-reverse; gap: 10px; align-items: center; }
        .toki-toast { background: rgba(45, 52, 54, 0.95); color: white; padding: 12px 24px; border-radius: 50px; font-size: 14px; font-weight: bold; box-shadow: 0 4px 15px rgba(0,0,0,0.2); animation: toastFadeIn 0.3s ease-out; pointer-events: none; backdrop-filter: blur(5px); border: 1px solid rgba(255,255,255,0.1); white-space: nowrap; transition: opacity 0.2s, transform 0.2s; width: max-content; min-width: 160px; text-align: center; }
        .toki-toast.success { background: rgba(0, 184, 148, 0.95); }
        .toki-toast.error { background: rgba(255, 71, 87, 0.95); }
        .toki-toast.hiding { opacity: 0; transform: translateY(10px); }

        .toki-modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 10000000; display: flex; align-items: center; justify-content: center; animation: modalFadeIn 0.2s; backdrop-filter: blur(2px); }
        .toki-modal { background: var(--toki-modal-bg); border-radius: 12px; width: 320px; max-width: 90%; padding: 24px; box-shadow: 0 10px 25px rgba(0,0,0,0.2); animation: modalSlideIn 0.3s forwards; text-align: center; }
        .toki-modal-title { font-size: 18px; font-weight: 800; color: var(--toki-text); margin-bottom: 10px; }
        .toki-modal-desc { font-size: 14px; color: var(--toki-text-sub); margin-bottom: 20px; line-height: 1.5; }
        .toki-modal-btns { display: flex; gap: 10px; justify-content: center; }
        .toki-modal-btn { flex: 1; padding: 10px; border-radius: 8px; font-size: 14px; font-weight: bold; cursor: pointer; border: none; }
        .toki-modal-btn.cancel { background: #dfe6e9; color: #2d3436; }
        .toki-modal-btn.cancel:hover { background: #b2bec3; }
        .toki-modal-btn.confirm { background: #ff4757; color: white; }
        .toki-modal-btn.primary { background: #0984e3; color: white; }

        .toki-filter-bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
        .toki-filter-btn { padding: 6px 12px; border-radius: 20px; font-size: 13px; font-weight: bold; cursor: pointer; border: 1px solid var(--toki-filter-border); background: var(--toki-filter-bg); color: var(--toki-text-sub); transition: 0.2s; display: flex; align-items: center; gap: 6px; user-select: none; }
        .toki-filter-btn:hover { background: var(--toki-filter-hover); }
        .toki-filter-btn.active { background: #0984e3; color: white; border-color: #0984e3; }
        .toki-filter-btn.reset { background: #ff7675; color: white; border-color: #ff7675; }
        .toki-filter-btn.reset:hover { background: #d63031; }
        .toki-dropdown { position: relative; }
        .toki-dropdown-menu { position: absolute; top: 100%; left: 0; margin-top: 5px; background: var(--toki-dropdown-bg); border: 1px solid var(--toki-filter-border); border-radius: 8px; box-shadow: var(--toki-dropdown-shadow); width: 600px; max-width: 90vw; max-height: 400px; overflow-y: auto; z-index: 100; display: none; padding: 12px; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 6px; }
        .toki-dropdown-menu.show { display: grid; }
        .toki-genre-item { padding: 6px 8px; font-size: 12px; font-weight: bold; cursor: pointer; border-radius: 6px; display: flex; align-items: center; justify-content: center; gap: 4px; background: var(--toki-genre-bg); border: 1px solid var(--toki-genre-border); color: var(--toki-text-sub); transition: 0.15s; }
        .toki-genre-item:hover { background: var(--toki-genre-hover); }
        .toki-genre-item.active { background: #0984e3; color: white; border-color: #0984e3; }
        .toki-genre-item.active .chk { display: inline-block; }
        .toki-genre-item .chk { display: none; margin-right: 2px; }
        .toki-mode-btn { padding: 6px 12px; border-radius: 20px; font-size: 13px; font-weight: 900; cursor: pointer; border: 1px solid #2d3436; background: #2d3436; color: white; transition: 0.2s; display: flex; align-items: center; gap: 6px; user-select: none; }
        .toki-mode-btn:hover { background: #636e72; border-color: #636e72; }
        .toki-mode-btn.or-mode { background: #00b894; border-color: #00b894; }
        .toki-mode-btn.or-mode:hover { background: #55efc4; border-color: #55efc4; }

        .toki-pin-toggle { font-size:13px; font-weight:800; color:#ff4757; display:flex; align-items:center; gap:4px; cursor:pointer; background:var(--toki-pin-bg); padding:6px 10px; border-radius:6px; border:1px solid var(--toki-highlight-border); user-select:none; transition:0.2s; }
        .toki-pin-toggle:hover { background:var(--toki-pin-hover); }
        .toki-pin-toggle input { cursor: pointer; margin: 0; }
        .toki-pin-toggle.clr-pin { color:var(--toki-text-sub); background:var(--toki-clr-pin-bg); border-color:var(--toki-clr-pin-border); }
        .toki-pin-toggle.clr-pin:hover { background:var(--toki-clr-pin-hover); }
        .toki-pin-toggle:has(input:checked) { border-color: #ff4757; background: rgba(255, 71, 87, 0.05); }
        .toki-pin-toggle.clr-pin:has(input:checked) { border-color: #00b894; background: rgba(0, 184, 148, 0.05); }

        /* [Sync Modal] */
        .toki-sync-modal-bg { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.6); z-index: 2000001; display: flex; align-items: center; justify-content: center; backdrop-filter: blur(4px); animation: modalFadeIn 0.2s; }
        .toki-sync-modal-box { position: relative; background: var(--toki-modal-bg); border-radius: 16px; width: 380px; max-width: 90%; padding: 28px; box-shadow: 0 15px 35px rgba(0,0,0,0.3); animation: modalSlideIn 0.3s forwards; }
        .toki-sync-title { font-size: 20px; font-weight: 900; color: var(--toki-text); margin-bottom: 12px; display: flex; align-items: center; justify-content: space-between; }
        .toki-sync-title .status-toggle { font-size: 11px; padding: 4px 12px; border-radius: 20px; background: #dfe6e9; color: #636e72; cursor: pointer; transition: 0.2s; border: 1px solid #b2bec3; user-select: none; }
        .toki-sync-title .status-toggle:hover { background: #b2bec3; }
        .toki-sync-title .status-toggle.on { background: #00b894; color: white; border-color: #00b894; }
        .toki-sync-title .status-toggle.on:hover { background: #00a383; }
        .toki-sync-desc { font-size: 13.5px; color: var(--toki-text-sub); margin-bottom: 20px; line-height: 1.6; text-align: left; }
        .toki-sync-input { width: 100%; padding: 12px; border-radius: 8px; border: 1px solid var(--toki-filter-border); background: var(--toki-bg-input); color: var(--toki-text); margin-bottom: 20px; font-family: monospace; font-size: 13px; box-sizing: border-box; }
        .toki-sync-btns { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
        .toki-sync-btn { padding: 12px 5px; border-radius: 8px; font-size: 13px; font-weight: bold; cursor: pointer; border: none; transition: 0.2s; background: var(--toki-bg-sub); color: var(--toki-text-sub); border: 1px solid var(--toki-border); }
        .toki-sync-btn.primary { background: #0984e3; color: white; border-color: #0984e3; }
        .toki-sync-btn.success { background: #00b894; color: white; border-color: #00b894; }
        .toki-sync-btn.cancel { background: #f1f2f6; color: #2d3436; border: 1px solid #dfe6e9; }
        .toki-sync-btn:hover { opacity: 0.9; transform: translateY(-1px); }
        .toki-sync-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }

        /* [Stats Grid] */
        .toki-stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 15px; padding: 0 5px; }
        .toki-stat-item { background: var(--toki-bg-sub); border: 1px solid var(--toki-border); border-radius: 10px; padding: 10px 5px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; }
        .toki-stat-item .label { font-size: 11px; font-weight: 800; color: var(--toki-text-muted); }
        .toki-stat-item .value { font-size: 15px; font-weight: 900; color: var(--toki-text); }

        /* [PC Specific Styles] */
        .toki-pc .toki-card { border-radius: 8px; transition: all 0.2s; }
        .toki-pc .toki-card:hover { border-color: #0984e3; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
        .toki-pc .toki-actions { display: flex; gap: 10px; width: 100%; margin-top: 5px; }
        .toki-pc .toki-btn-main { flex: 1; } /* View from first episode / Resume (PC) */
        .toki-pc .toki-btn-sub { flex: 1; }  /* Next episode / Latest episode (PC) */
        .toki-pc .toki-btn-del { flex: 0 0 85px; background: var(--toki-delete-bg) !important; color: var(--toki-text-sub) !important; }
        .toki-pc .btn-import, .toki-pc .btn-export { background: #2d3436 !important; color: white !important; }
        .toki-pc .toki-title .title-text { flex: none; }
        .toki-pc #sortSelect { height: 32px; font-size: 13px; border-color: var(--toki-filter-border); transition: border-color 0.2s; }
        .toki-pc #sortSelect:focus { border-color: #0984e3; }

        /* [Mobile Specific Styles] */
        .toki-mobile .toki-category-tabs { gap: 6px; padding: 4px; margin-bottom: 12px; border-radius: 10px; }
        .toki-mobile .toki-tab-btn { padding: 10px 4px; font-size: 13px; gap: 4px; border-radius: 8px; }
        .toki-mobile .tab-badge.has-up { font-size: 9.5px; padding: 1px 4px; }
        .toki-mobile .toki-stats-grid { grid-template-columns: repeat(4, 1fr); }
        .toki-mobile .toki-stat-item.total { grid-column: span 4; flex-direction: row; justify-content: space-between; padding: 12px 20px; }
        .toki-mobile .toki-control-bar { padding: 14px; margin: 10px 0; flex-direction: column; align-items: stretch; gap: 12px; border-radius: 12px; }
        .toki-mobile .toki-filter-bar { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
        .toki-mobile .toki-filter-btn, .toki-mobile .toki-mode-btn, .toki-mobile .toki-dropdown { width: 100%; justify-content: center; padding: 10px 0; font-size: 12px; }
        .toki-mobile #btnFilterReset { grid-column: span 1; }
        .toki-mobile #btnFilterMode { grid-column: span 1; }
        .toki-mobile .toki-dropdown { grid-column: span 2; }
        .toki-mobile .state-toggle { grid-column: span 1; }
        .toki-mobile #sortDirBtn .toki-icon-blue { color: #0984e3 !important; font-weight: 900; }

        .toki-mobile .toki-sort-row { display: flex; flex-direction: column; gap: 10px; padding-top: 5px; border-top: 1px dashed var(--toki-border); }
        .toki-mobile .toki-sort-main { display: flex; gap: 8px; align-items: center; }
        .toki-mobile #sortSelect { flex: 1; height: 44px; border-radius: 6px; border: 1px solid var(--toki-filter-border); background: var(--toki-bg-input); color: var(--toki-text); font-weight: bold; }
        .toki-mobile #sortDirBtn { width: 100px; height: 44px; margin-right: 0 !important; }
        .toki-mobile .toki-pin-group { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
        .toki-mobile .toki-pin-toggle { justify-content: center; padding: 12px; height: 48px; font-size: 12px; box-sizing: border-box; border-width: 2px; }
        .toki-mobile .toki-pin-toggle input { width: 18px; height: 18px; }
        .toki-mobile .toki-sync-group { display: flex; flex-direction: column; gap: 8px; }
        .toki-mobile .toki-action-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
        .toki-mobile .toki-action-row button { height: 48px; font-size: 13px; border-radius: 8px; font-weight: 800; border: none; color: white; cursor: pointer; }
        .toki-mobile .btn-import, .toki-mobile .btn-export { background: #2d3436 !important; }
        .toki-mobile #syncSettingsBtn { background: #4b6584 !important; }
        .toki-mobile #checkUpdatesBtn { background: #0984e3 !important; box-shadow: 0 2px 5px rgba(9, 132, 227, 0.3); }
        .toki-mobile .toki-sync-group button { width: 100%; margin: 0; }

        .toki-mobile .toki-card { padding: 12px; border-radius: 12px; }
        .toki-mobile .toki-thumb { width: 75px; height: 100px; }
        .toki-mobile .toki-title { font-size: 16px; line-height: 1.4; }
        .toki-mobile .toki-actions { display: flex !important; gap: 8px !important; align-items: stretch !important; margin-top: 12px; }
        .toki-mobile .toki-actions .toki-btn { padding: 14px 0 !important; border-radius: 10px !important; font-size: 12px !important; height: 52px !important; display: flex !important; align-items: center !important; justify-content: center !important; margin: 0 !important; }
        .toki-mobile .toki-actions .toki-btn-main, .toki-mobile .toki-actions .toki-btn-sub { flex: 1 !important; }
        .toki-mobile .toki-actions .toki-btn-del { flex: 0 0 75px !important; background: var(--toki-delete-bg) !important; border: 1px solid var(--toki-border) !important; color: var(--toki-text-sub) !important; font-weight: bold; }
        .toki-mobile .toki-dropdown-menu { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 90%; max-width: 420px; max-height: 85vh; grid-template-columns: repeat(2, 1fr); gap: 12px; padding: 24px; border-radius: 24px; z-index: 2000000; box-shadow: 0 0 0 9999px rgba(0,0,0,0.75), 0 25px 50px rgba(0,0,0,0.5); overflow-y: auto; }
        .toki-mobile .toki-genre-item { padding: 14px 8px; font-size: 14px; border-radius: 10px; }
        .toki-mobile-close { grid-column: 1 / -1 !important; margin-top: 15px !important; background: #2d3436 !important; color: white !important; padding: 16px !important; border-radius: 12px !important; text-align: center !important; font-weight: 800 !important; font-size: 15px !important; box-shadow: 0 4px 10px rgba(0,0,0,0.2) !important; cursor: pointer; }

        /* [Custom Sort Dropdown] */
        .toki-sort-item { padding: 10px 15px; font-size: 13px; font-weight: 700; cursor: pointer; color: var(--toki-text-sub); border-radius: 6px; transition: 0.2s; }
        .toki-sort-item:hover { background: var(--toki-bg-sub); color: #0984e3; }
        .toki-sort-item.active { background: #0984e3; color: white; }
        .toki-icon-blue { display: none; } /* Hide the custom text icons as we revert to emojis */
        .toki-title { display: flex; align-items: center; gap: 6px; word-break: keep-all; line-height: 1.4; }
        .toki-title .title-text { flex: 1; }

        /* Search Bar Styles */
        .toki-search-wrap {
          position: relative;
          display: flex;
          align-items: center;
          background: var(--toki-bg-input);
          border: 1px solid var(--toki-filter-border);
          border-radius: 6px;
          padding: 0 10px;
          height: 34px;
          transition: border-color 0.2s, box-shadow 0.2s;
        }
        .toki-search-wrap:focus-within {
          border-color: #0984e3;
          box-shadow: 0 0 0 3px rgba(9, 132, 227, 0.15);
        }
        .toki-search-icon {
          font-size: 13px;
          color: var(--toki-text-muted);
          margin-right: 8px;
          user-select: none;
        }
        #toki-search-input {
          border: none;
          background: transparent;
          color: var(--toki-text);
          font-size: 13px;
          font-weight: 600;
          outline: none;
          width: 180px;
          padding: 0;
          height: 100%;
        }
        #toki-search-input::placeholder {
          color: var(--toki-text-muted);
          font-weight: 500;
        }
        .toki-search-clear {
          border: none;
          background: transparent;
          color: var(--toki-text-muted);
          font-size: 12px;
          font-weight: bold;
          cursor: pointer;
          padding: 0 4px;
          margin-left: 6px;
          display: flex;
          align-items: center;
          justify-content: center;
          transition: color 0.2s;
        }
        .toki-search-clear:hover {
          color: #ff4757;
        }

        /* Mobile Search Bar Overrides */
        .toki-mobile .toki-search-wrap {
          height: 44px;
          border-radius: 8px;
          margin-bottom: 8px;
          width: 100%;
          box-sizing: border-box;
        }
        .toki-mobile #toki-search-input {
          font-size: 14px;
          width: 100%;
        }
        .toki-mobile .toki-search-icon {
          font-size: 15px;
        }
        .toki-mobile .toki-search-clear {
          font-size: 14px;
          padding: 0 8px;
        }

        /* Custom Thumbnail Bookmark Button Overrides */
        .my-local-fav-heart {
          display: flex !important;
          align-items: center !important;
          justify-content: center !important;
          background: rgba(0, 0, 0, 0.4) !important;
          border-radius: 50% !important;
          width: 38px !important;
          height: 38px !important;
          padding: 0 !important;
          transition: background-color 0.2s, transform 0.2s !important;
          z-index: 10 !important;
        }
        .my-local-fav-heart:hover {
          background: rgba(0, 0, 0, 0.6) !important;
          transform: scale(1.1) !important;
        }
        .my-local-fav-heart svg {
          transition: transform 0.2s, fill 0.2s, stroke 0.2s !important;
        }
        .my-local-fav-heart:active svg {
          transform: scale(0.8) !important;
        }

        /* Premium Category Tabs - Bubble Button Style */
        .toki-category-tabs {
            display: flex;
            gap: 12px;
            margin-bottom: 20px;
            padding: 6px;
            background: var(--toki-bg-sub);
            border-radius: 12px;
            border: 1px solid var(--toki-border);
        }
        .toki-tab-btn {
            flex: 1;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            padding: 12px 16px;
            border: none;
            background: transparent;
            color: var(--toki-text-sub);
            font-size: 14.5px;
            font-weight: 800;
            cursor: pointer;
            border-radius: 9px;
            transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
        }
        .toki-tab-btn:hover {
            background: var(--toki-bg);
            color: var(--toki-text);
            transform: translateY(-1px);
        }
        .toki-tab-btn.active {
            background: rgba(9, 132, 227, 0.12);
            color: #0984e3 !important;
            box-shadow: 0 4px 12px rgba(9, 132, 227, 0.2);
            border: 1px solid rgba(9, 132, 227, 0.4);
        }
        .tab-badge {
            display: none;
            font-size: 10px;
            font-weight: 900;
            padding: 2px 6px;
            border-radius: 10px;
            background: var(--toki-border);
            color: var(--toki-text-muted);
        }
        .tab-badge.has-up {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            background: #ff4757;
            color: white !important;
            font-size: 10.5px;
            border-radius: 10px;
            box-shadow: 0 2px 6px rgba(255, 71, 87, 0.4);
            animation: pulseUP 2s infinite;
        }
    `;
    document.head.appendChild(style);

    // ==========================================
    // Custom UI and Utilities
    // ==========================================
    const TokiUI = {
        toast: function(msg, type = 'default') {
            let container = document.getElementById('toki-toast-container');
            if (!container) { container = document.createElement('div'); container.id = 'toki-toast-container'; document.body.appendChild(container); }
            const toast = document.createElement('div'); toast.className = `toki-toast ${type}`; toast.innerText = msg;
            container.appendChild(toast);
            setTimeout(() => {
                toast.classList.add('hiding');
                setTimeout(() => toast.remove(), 200);
            }, 2500);
        },
        confirm: function(title, desc = "") {
            return new Promise(resolve => {
                const backdrop = document.createElement('div'); backdrop.className = 'toki-modal-backdrop';
                backdrop.innerHTML = `<div class="toki-modal"><div class="toki-modal-title">${title}</div>${desc ? `<div class="toki-modal-desc">${desc}</div>` : ''}<div class="toki-modal-btns"><button class="toki-modal-btn cancel" id="toki-modal-cancel">취소</button><button class="toki-modal-btn confirm" id="toki-modal-ok">확인</button></div></div>`;
                document.body.appendChild(backdrop);
                document.getElementById('toki-modal-ok').onclick = () => { backdrop.remove(); resolve(true); };
                document.getElementById('toki-modal-cancel').onclick = () => { backdrop.remove(); resolve(false); };
            });
        },
        importDialog: function() {
            return new Promise(resolve => {
                const backdrop = document.createElement('div'); backdrop.className = 'toki-modal-backdrop';
                backdrop.innerHTML = `<div class="toki-modal" style="width:360px;"><div class="toki-modal-title">가져오기 설정</div><div class="toki-modal-desc"><b>병합</b>: 기존 북마크를 유지하며 합침<br><b>덮어쓰기</b>: 기존 북마크를 모두 삭제함</div><div class="toki-modal-btns" style="flex-direction:column;"><button class="toki-modal-btn primary" id="toki-import-merge">기존 데이터에 병합</button><button class="toki-modal-btn confirm" id="toki-import-overwrite">기존 데이터 덮어쓰기</button><button class="toki-modal-btn cancel" id="toki-import-cancel">취소</button></div></div>`;
                document.body.appendChild(backdrop);
                document.getElementById('toki-import-merge').onclick = () => { backdrop.remove(); resolve('merge'); };
                document.getElementById('toki-import-overwrite').onclick = () => { backdrop.remove(); resolve('overwrite'); };
                document.getElementById('toki-import-cancel').onclick = () => { backdrop.remove(); resolve(null); };
            });
        }
    };

    function getBookmarks() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            return TokiUtils.ensureV10Structure(raw ? JSON.parse(raw) : null);
        } catch (e) {
            return { manhwa: {}, webtoon: {}, novel: {} };
        }
    }
    function saveBookmarks(data, opts) {
        const newStructured = TokiUtils.ensureV10Structure(data);
        const oldRaw = localStorage.getItem(STORAGE_KEY);
        let hasCoreChanges = false;

        if (oldRaw && !(opts && opts.silent)) {
            try {
                const oldStructured = TokiUtils.ensureV10Structure(JSON.parse(oldRaw));
                const categories = ['manhwa', 'webtoon', 'novel'];
                
                for (const cat of categories) {
                    const oldCat = oldStructured[cat] || {};
                    const newCat = newStructured[cat] || {};
                    const oldKeys = Object.keys(oldCat);
                    const newKeys = Object.keys(newCat);

                    // 1. Number of items changed (addition/deletion)
                    if (oldKeys.length !== newKeys.length) {
                        hasCoreChanges = true;
                        break;
                    }

                    // 2. Core field diff check
                    for (const id of newKeys) {
                        if (!oldCat[id]) {
                            hasCoreChanges = true;
                            break;
                        }

                        const oldItem = oldCat[id];
                        const newItem = newCat[id];

                        if (
                            oldItem.title !== newItem.title ||
                            oldItem.author !== newItem.author ||
                            oldItem.status !== newItem.status ||
                            oldItem.lastReadEp !== newItem.lastReadEp ||
                            oldItem.lastReadEpLink !== newItem.lastReadEpLink ||
                            oldItem.latestEp !== newItem.latestEp ||
                            oldItem.latestEpLink !== newItem.latestEpLink ||
                            oldItem.nextEp !== newItem.nextEp ||
                            oldItem.nextEpLink !== newItem.nextEpLink ||
                            oldItem.url !== newItem.url
                        ) {
                            hasCoreChanges = true;
                            break;
                        }
                    }
                    if (hasCoreChanges) break;
                }
            } catch (diffErr) {
                console.error('[saveBookmarks] Diff calculation failed:', diffErr);
                hasCoreChanges = true; // Fallback to safety mark on comparison failure
            }
        } else if (!oldRaw) {
            hasCoreChanges = true;
        }

        localStorage.setItem(STORAGE_KEY, JSON.stringify(newStructured));

        // Only mark dirty and trigger push/sync when real contents change
        if (hasCoreChanges && !(opts && opts.silent)) {
            const dc = parseInt(localStorage.getItem(DIRTY_KEY) || '0');
            localStorage.setItem(DIRTY_KEY, (dc + 1).toString());
            localStorage.setItem('toki_unpushed_changes', 'true');
            if (typeof GistSync !== 'undefined' && GistSync.isFallbackMode()) {
                localStorage.setItem('toki_offline_dirty', 'true');
            }
        }
    }
    function getSortPref() { return localStorage.getItem(PREF_KEY) || 'siteUpdateDesc'; }

    // [TokiCrypt] Obfuscation envelope for secure storage protection
    const TokiCrypt = {
        encrypt: function(text) {
            if (!text) return '';
            try { return btoa(text); } catch(e) { return ''; }
        },
        decrypt: function(encoded) {
            if (!encoded) return '';
            try { 
                const decoded = atob(encoded);
                if (/[^\x20-\x7E]/.test(decoded)) return ''; // Return empty string if old method is corrupted (forces re-login)
                return decoded;
            } catch(e) { return ''; }
        }
    };

    const GistSync = {
        getToken: () => TokiCrypt.decrypt(localStorage.getItem('toki_gh_token')),
        getGistId: () => localStorage.getItem('toki_gist_id'),
        getMode: () => localStorage.getItem('toki_sync_mode') === 'true',

        setCredentials: function(token, gistId) {
            localStorage.setItem('toki_gh_token', TokiCrypt.encrypt(token));
            if(gistId) localStorage.setItem('toki_gist_id', gistId);
            // Clear existing unauthenticated/stale Rate Limit cache when setting new credentials (prevents 98/100 bug)
            localStorage.removeItem('toki_rate_limit_remaining');
            localStorage.removeItem('toki_rate_limit_reset');
            localStorage.removeItem('toki_rate_limit_limit');
        },
        enable: () => localStorage.setItem('toki_sync_mode', 'true'),
        disable: () => { 
            localStorage.removeItem('toki_sync_mode'); 
            localStorage.removeItem('toki_gh_token'); 
            localStorage.removeItem('toki_gist_id'); 
            localStorage.removeItem('toki_sync_fallback');
            localStorage.removeItem('toki_offline_dirty');
            localStorage.removeItem('toki_rate_limit_remaining');
            localStorage.removeItem('toki_rate_limit_reset');
            localStorage.removeItem('toki_rate_limit_limit');
        },

        headers: function() {
            return {
                'Authorization': `Bearer ${this.getToken()}`,
                'Accept': 'application/vnd.github.v3+json',
                'Content-Type': 'application/json',
                'Cache-Control': 'no-cache, no-store, must-revalidate',
                'Pragma': 'no-cache',
                'Expires': '0',
                'X-GitHub-Api-Version': '2022-11-28'
            };
        },

        // Rate Limit Tracer Utility
        updateRateLimit: function(headersStr) {
            if (!headersStr) return;
            const headers = {};
            headersStr.split(/[\r\n]+/).forEach(line => {
                const parts = line.split(':');
                if (parts.length >= 2) {
                    const key = parts[0].trim().toLowerCase();
                    const val = parts.slice(1).join(':').trim();
                    headers[key] = val;
                }
            });
            
            const remaining = headers['x-ratelimit-remaining'];
            const reset = headers['x-ratelimit-reset'];
            const limit = headers['x-ratelimit-limit'];

            // Safety Guard: If sync mode is active but limit is unauthenticated level (<= 100),
            // ignore it as it is an unauthenticated rate limit caused by invalid token (e.g. 401 Unauthorized)
            if (limit !== undefined) {
                const limitVal = parseInt(limit);
                if (this.getMode() && this.getToken() && limitVal <= 100) {
                    return;
                }
                localStorage.setItem('toki_rate_limit_limit', limit);
            }

            if (remaining !== undefined) localStorage.setItem('toki_rate_limit_remaining', remaining);
            if (reset !== undefined) localStorage.setItem('toki_rate_limit_reset', reset);

            // Automatically enter temporary local mode if Remaining is 0
            if (remaining === '0') {
                this.enterOfflineFallback(true);
            }
        },

        getRateLimit: function() {
            const rawRemaining = localStorage.getItem('toki_rate_limit_remaining');
            const rawReset = localStorage.getItem('toki_rate_limit_reset');
            const rawLimit = localStorage.getItem('toki_rate_limit_limit');
            
            return {
                remaining: rawRemaining ? parseInt(rawRemaining) : -1,
                reset: rawReset ? parseInt(rawReset) * 1000 : 0,
                limit: rawLimit ? parseInt(rawLimit) : -1
            };
        },

        isFallbackMode: () => localStorage.getItem('toki_sync_fallback') === 'true',

        enterOfflineFallback: function(isRateLimit = false) {
            localStorage.setItem('toki_sync_fallback', 'true');
            if (isRateLimit) {
                console.warn('[GistSync] Gist API Rate Limit exceeded. Dynamic offline fallback engaged.');
            } else {
                console.warn('[GistSync] Server/Network failure detected. Temporary offline fallback engaged.');
            }
        },

        exitOfflineFallback: function() {
            localStorage.removeItem('toki_sync_fallback');
            console.log('[GistSync] Restored to online sync mode.');
        },

        checkAndRecoverSync: async function() {
            if (!this.getMode() || !this.getToken()) return;
            
            const rate = this.getRateLimit();
            const now = Date.now();

            if (this.isFallbackMode()) {
                if (now >= rate.reset || rate.remaining > 0) {
                    this.exitOfflineFallback();
                    if (localStorage.getItem('toki_offline_dirty') === 'true') {
                        console.log('[GistSync] Offline local changes detected on recovery. Invoking smart push reconciliation.');
                        localStorage.removeItem('toki_offline_dirty');
                        await this.push(false);
                    } else {
                        await this.pull();
                    }
                }
            }
        },

        // GM_xmlhttpRequest Wrapper for CSP Bypass
        request: function(opts) {
            return new Promise((resolve, reject) => {
                if (typeof GM_xmlhttpRequest === 'undefined') {
                    return reject(new Error('GM_xmlhttpRequest is not available. Please check @grant.'));
                }
                GM_xmlhttpRequest({
                    method: opts.method || 'GET',
                    url: opts.url,
                    headers: this.headers(),
                    data: opts.data ? JSON.stringify(opts.data) : null,
                    onload: (res) => {
                        if (res.responseHeaders) {
                            GistSync.updateRateLimit(res.responseHeaders);
                        }
                        if (res.status >= 200 && res.status < 300) {
                            try { resolve(JSON.parse(res.responseText)); }
                            catch(e) { reject(new Error('Invalid JSON response from Gist API')); }
                        } else {
                            if (res.status === 403 || res.status === 429) {
                                GistSync.enterOfflineFallback(true);
                            } else if (res.status >= 500) {
                                GistSync.enterOfflineFallback(false);
                            }
                            reject({ status: res.status, message: res.statusText || 'API Error', responseHeaders: res.responseHeaders });
                        }
                    },
                    onerror: (err) => {
                        GistSync.enterOfflineFallback(false);
                        reject(err);
                    }
                });
            });
        },

        pull: async function(options = { forceOverwrite: false }) {
            if (!this.getMode() || !this.getToken()) return false;
            
            // Self-healing check
            await this.checkAndRecoverSync();
            
            // Abort network request if in fallback mode
            const rate = this.getRateLimit();
            if (this.isFallbackMode() && Date.now() < rate.reset) {
                console.warn('[GistSync] Pull aborted: Switch to Offline Fallback is active (Rate Limit).');
                return false;
            }

            try {
                let gistId = this.getGistId();
                if (!gistId) {
                    const gists = await this.request({ url: 'https://api.github.com/gists' });
                    const found = gists.find(g => g.files['toki_bookmarks_sync.json']);
                    if (found) {
                        gistId = found.id;
                        this.setCredentials(this.getToken(), gistId);
                    } else {
                        const newGist = await this.request({
                            method: 'POST',
                            url: 'https://api.github.com/gists',
                            data: { description: "Newtoki Bookmark Sync", public: false, files: { 'toki_bookmarks_sync.json': { content: JSON.stringify(getBookmarks()) } } }
                        });
                        this.setCredentials(this.getToken(), newGist.id);
                        localStorage.setItem(REMOTE_AT_KEY, newGist.updated_at);
                        return true;
                    }
                }

                const data = await this.request({ url: `https://api.github.com/gists/${gistId}` });
                const file = data.files['toki_bookmarks_sync.json'];
                if (!file) throw new Error('File not found in Gist');

                let content = file.content;
                if (file.truncated || !content) {
                    content = await this.request({ url: `${file.raw_url}?cb=${Date.now()}` });
                }

                let remoteData = {};
                try {
                    remoteData = TokiUtils.ensureV10Structure(JSON.parse(content || '{}'));
                    if (!TokiSafetyGuard.validateSchema(remoteData)) {
                        throw new Error('Gist data does not match the valid bookmarks schema');
                    }
                } catch(parseErr) {
                    console.error('[GistSync] Broken JSON parsing or validation failed:', parseErr);
                    if (typeof TokiUI !== 'undefined') {
                        TokiUI.toast('❌ 서버 동기화 실패: 원격 데이터 구조가 올바르지 않습니다.', 'error');
                    }
                    throw parseErr;
                }
                const remoteUpdatedAt = data.updated_at;

                const localData = getBookmarks();
                const localNovelCount = Object.keys(localData.novel || {}).length;
                const localManhwaCount = Object.keys(localData.manhwa || {}).length;
                const localWebtoonCount = Object.keys(localData.webtoon || {}).length;
                const isLocalEmpty = (localNovelCount + localManhwaCount + localWebtoonCount) === 0;

                let merged = {
                    manhwa: { ...localData.manhwa },
                    webtoon: { ...localData.webtoon },
                    novel: { ...localData.novel }
                };
                let hasChanges = false;
                const lastSyncAt = parseInt(localStorage.getItem(SYNC_AT_KEY) || '0');
                const categories = ['manhwa', 'webtoon', 'novel'];

                for (const cat of categories) {
                    const remoteCatData = remoteData[cat] || {};
                    const localCatData = localData[cat] || {};

                    // 1. Process remote updates and additions
                    for (const [key, remoteItem] of Object.entries(remoteCatData)) {
                        const localItem = localCatData[key];
                        if (!localItem) {
                            const remoteTime = TokiUtils.getEffectiveTime(remoteItem);
                            // CRITICAL FIX: If forceOverwrite is enabled or local database is empty, bypass timestamp check!
                            if (!options.forceOverwrite && isLocalEmpty === false && lastSyncAt > 0 && remoteTime <= lastSyncAt) {
                                hasChanges = true;
                            } else {
                                merged[cat][key] = remoteItem;
                                hasChanges = true;
                            }
                        } else {
                            if (options.forceOverwrite) {
                                merged[cat][key] = remoteItem;
                                hasChanges = true;
                            } else {
                                const localReadTime = localItem.lastReadAt || 0;
                                const remoteReadTime = remoteItem.lastReadAt || 0;
                                const localUpdateTime = Math.max(localItem.exactUpdatedAt || 0, localItem.parsedUpdatedAt || 0, localItem.latestUpdatedAt || 0, localItem.addedAt || 0);
                                const remoteUpdateTime = Math.max(remoteItem.exactUpdatedAt || 0, remoteItem.parsedUpdatedAt || 0, remoteItem.latestUpdatedAt || 0, remoteItem.addedAt || 0);

                                let itemChanged = false;
                                const mergedItem = { ...localItem };

                                if (remoteReadTime > localReadTime) {
                                    mergedItem.lastReadEp = remoteItem.lastReadEp;
                                    mergedItem.lastReadEpLink = remoteItem.lastReadEpLink;
                                    mergedItem.lastReadAt = remoteItem.lastReadAt;
                                    mergedItem.nextEp = remoteItem.nextEp;
                                    mergedItem.nextEpLink = remoteItem.nextEpLink;
                                    itemChanged = true;
                                }

                                if (remoteUpdateTime > localUpdateTime) {
                                    mergedItem.latestEp = remoteItem.latestEp;
                                    mergedItem.latestEpLink = remoteItem.latestEpLink;
                                    mergedItem.latestDate = remoteItem.latestDate;
                                    mergedItem.exactUpdatedAt = remoteItem.exactUpdatedAt;
                                    mergedItem.parsedUpdatedAt = remoteItem.parsedUpdatedAt;
                                    mergedItem.latestUpdatedAt = remoteItem.latestUpdatedAt;
                                    mergedItem.status = remoteItem.status || localItem.status;
                                    mergedItem.genres = remoteItem.genres || localItem.genres;
                                    mergedItem.author = remoteItem.author || localItem.author;
                                    mergedItem.thumb = remoteItem.thumb || localItem.thumb;
                                    itemChanged = true;
                                }

                                if (itemChanged) {
                                    merged[cat][key] = mergedItem;
                                    hasChanges = true;
                                }
                            }
                        }
                    }

                    // 2. Process deletions from remote (LWW Deletion Sync)
                    // Skip remote deletions if forceOverwrite is active or local database is empty
                    if (!options.forceOverwrite && !isLocalEmpty) {
                        for (const key of Object.keys(localCatData)) {
                            if (!remoteCatData[key]) {
                                const localItem = localCatData[key];
                                const addedTime = localItem.addedAt || 0;
                                if (lastSyncAt > 0 && addedTime < lastSyncAt) {
                                    delete merged[cat][key];
                                    hasChanges = true;
                                }
                            }
                        }
                    }
                }

                if (hasChanges || options.forceOverwrite) saveBookmarks(merged, { silent: true });

                localStorage.setItem(REMOTE_AT_KEY, remoteUpdatedAt);   

                if (localStorage.getItem('toki_unpushed_changes') !== 'true') {
                    localStorage.setItem(SYNC_AT_KEY, Date.now().toString());
                }
                return true;
            } catch(e) { console.error('Gist Pull Error:', e); return false; }
        },

        checkNeedPull: async function() {
            if (!this.getMode() || !this.getToken() || !this.getGistId()) return false;
            
            // Abort checking if in offline fallback
            const rate = this.getRateLimit();
            if (this.isFallbackMode() && Date.now() < rate.reset) {
                return false;
            }

            try {
                // Light metadata check via Gist updated_at timestamp
                const data = await this.request({ url: `https://api.github.com/gists/${this.getGistId()}` });
                const remoteUpdatedAt = data.updated_at;
                const lastRemoteAt = localStorage.getItem(REMOTE_AT_KEY);

                // Trigger pull if remote version is newer than last known remote sync timestamp
                return remoteUpdatedAt !== lastRemoteAt;
            } catch (e) {
                console.error('[GistSync] checkNeedPull Network/API failure:', e);
                return false;
            }
        },

        push: function(force = false) {
            const self = this;
            if (!self.getMode() || !self.getToken()) return;

            // Abort pushing if in offline fallback
            const rate = self.getRateLimit();
            if (self.isFallbackMode() && Date.now() < rate.reset) {
                const diff = rate.reset - Date.now();
                const mins = Math.floor(diff / 60000);
                const secs = Math.floor((diff % 60000) / 1000);
                if (force) {
                    TokiUI.toast(`⚠️ API 할당량 초과: 대기 중 (${mins}분 ${secs}초 남음)`, 'error');
                }
                return;
            }

            if (pushDebounceTimer) clearTimeout(pushDebounceTimer);

            const executePush = async (isManual = false) => {
                if (tokiState.isPushing) {
                    pushDebounceTimer = setTimeout(() => self.push(false), 5000);
                    return;
                }
                tokiState.isPushing = true;

                try {
                    let remoteBookmarkCount = 0;
                    let hasFetchedRemote = false;

                    if (!self.getGistId()) {
                        const success = await self.pull();
                        if (!success) {
                            if (isManual) TokiUI.toast('❌ Gist를 찾거나 생성할 수 없습니다.', 'error');
                            return;
                        }
                    } else {
                        // [Optimization] Run pull first only when new remote changes are found via checkNeedPull, significantly reducing API usage
                        const needPull = await self.checkNeedPull();
                        if (needPull) {
                            await self.pull();
                        }

                        // Fetch remote data to check bookmark count for Write-Zero protection
                        try {
                            const data = await self.request({ url: `https://api.github.com/gists/${self.getGistId()}` });
                            const file = data.files['toki_bookmarks_sync.json'];
                            if (file) {
                                let content = file.content;
                                if (file.truncated || !content) {
                                    content = await self.request({ url: `${file.raw_url}?cb=${Date.now()}` });
                                }
                                const parsed = JSON.parse(content || '{}');
                                const remoteNovel = Object.keys(parsed.novel || {}).length;
                                const remoteManhwa = Object.keys(parsed.manhwa || {}).length;
                                const remoteWebtoon = Object.keys(parsed.webtoon || {}).length;
                                remoteBookmarkCount = remoteNovel + remoteManhwa + remoteWebtoon;
                                hasFetchedRemote = true;
                            }
                        } catch (remoteErr) {
                            console.error('[GistSync] Write-Zero checking: remote fetch failed:', remoteErr);
                        }
                    }

                    const localData = getBookmarks();
                    const localNovel = Object.keys(localData.novel || {}).length;
                    const localManhwa = Object.keys(localData.manhwa || {}).length;
                    const localWebtoon = Object.keys(localData.webtoon || {}).length;
                    const localBookmarkCount = localNovel + localManhwa + localWebtoon;

                    // WRITE-ZERO SAFETY VALVE: Prevent wiping remote bookmarks if local is empty
                    if (localBookmarkCount === 0 && hasFetchedRemote && remoteBookmarkCount > 0) {
                        if (isManual) {
                            const userConfirmed = await TokiUI.confirm(
                                '⚠️ 빈 데이터 업로드 경고',
                                `현재 로컬(기기)의 북마크가 0개이지만, 백업 서버(Gist)에는 ${remoteBookmarkCount}개의 북마크가 저장되어 있습니다.\n\n정말로 서버의 백업 데이터를 모두 삭제하고 초기화하시겠습니까?`
                            );
                            if (!userConfirmed) {
                                TokiUI.toast('❌ 업로드가 취소되었습니다. 서버 데이터 보호됨.', 'error');
                                return;
                            }
                        } else {
                            console.warn('[GistSync] Background push aborted: Local bookmarks are 0, but remote contains', remoteBookmarkCount, 'items. Safety guard active.');
                            return;
                        }
                    }

                    if (isManual) TokiUI.toast('☁️ 서버에 북마크를 업로드 중...');

                    const data = await self.request({
                        method: 'PATCH',
                        url: `https://api.github.com/gists/${self.getGistId()}`,
                        data: { files: { 'toki_bookmarks_sync.json': { content: JSON.stringify(localData) } } }
                    });
                    
                    localStorage.setItem(REMOTE_AT_KEY, data.updated_at);
                    localStorage.setItem(DIRTY_KEY, '0');
                    localStorage.setItem('toki_last_push_time', Date.now().toString());
                    localStorage.setItem(SYNC_AT_KEY, Date.now().toString());
                    
                    // Clear unpushed changes marker since push succeeded
                    localStorage.removeItem('toki_unpushed_changes');

                    if (isManual) TokiUI.toast('✅ 업로드 완료!', 'success');
                } catch(e) {
                    console.error('[TokiMark] Gist Push Error:', e);
                    if (isManual) {
                        if (e.status === 403 || e.status === 429) {
                            const r = self.getRateLimit();
                            const diff = r.reset - Date.now();
                            const mins = Math.max(0, Math.floor(diff / 60000));
                            const secs = Math.max(0, Math.floor((diff % 60000) / 1000));
                            TokiUI.toast(`⚠️ 할당량 소진: ${mins}분 ${secs}초 후 다시 시도하세요.`, 'error');
                        }
                        else TokiUI.toast('❌ 업로드 실패: 토큰 또는 권한 확인', 'error');
                    }
                } finally {
                    tokiState.isPushing = false;
                }
            };

            const now = Date.now();
            const lastPush = parseInt(localStorage.getItem('toki_last_push_time') || '0');
            const MIN_PUSH_INTERVAL = 10000;

            if (force) {
                executePush(true);
            } else {
                const remaining = MIN_PUSH_INTERVAL - (now - lastPush);
                if (remaining <= 0) {
                    pushDebounceTimer = setTimeout(() => executePush(false), 1000);
                } else {
                    pushDebounceTimer = setTimeout(() => executePush(false), Math.max(3000, remaining));
                }
            }
        },


        showSettingsModal: function() {
            // Remove existing modal (prevent duplication)
            const oldModal = document.querySelector('.toki-sync-modal-bg');
            if (oldModal) oldModal.remove();

            const isSync = this.getMode();
            const bg = document.createElement('div');
            bg.className = 'toki-sync-modal-bg';
            bg.innerHTML = `
                <div class="toki-sync-modal-box">
                    <div class="toki-sync-title">
                        ☁️ 웹 동기화
                        <span class="status-toggle ${isSync ? 'on' : ''}" id="toki-sync-toggle" title="클릭하여 연결 상태 전환">
                            ${isSync ? '연결됨' : '미연결'}
                        </span>
                    </div>
                    <div class="toki-sync-desc">
                        GitHub Gist를 활용해 기기간 북마크를 공유합니다.
                    </div>
                    <details style="margin-bottom:15px; font-size:12px; line-height:1.6; cursor:pointer;">
                        <summary style="font-weight:bold; margin-bottom:4px; color:var(--toki-text-sub);">📖 토큰 발급 방법 (Classic)</summary>
                        <ol style="padding-left:18px; margin:8px 0; color:var(--toki-text-sub); font-size:11px;">
                            <li>GitHub Settings → Developer settings</li>
                            <li>Personal access tokens → Tokens (classic)</li>
                            <li>Generate new token → <b>gist</b> 권한 체크 ✅</li>
                        </ol>
                    </details>
                    <input type="password" id="toki-sync-token" class="toki-sync-input" placeholder="GitHub Access Token (classic)" value="${this.getToken() || ''}">
                    
                    <!-- Rate Limit / Offline Status Display -->
                    <div id="toki-sync-rate-status" style="margin-bottom:15px; padding:12px; background:var(--toki-bg-sub); border:1px solid var(--toki-border); border-radius:8px; font-size:12px; display:none; box-sizing:border-box; transition: all 0.3s ease;">
                        <div style="display:flex; justify-content:space-between; margin-bottom:6px; font-weight:bold; color:var(--toki-text-sub); white-space:nowrap; gap:8px;">
                            <span style="flex-shrink:0;">요청 제한 잔여:</span>
                            <span id="toki-rate-remaining-val" style="white-space:nowrap; text-align:right;">-</span>
                        </div>
                        <div style="display:flex; justify-content:space-between; font-weight:bold; color:var(--toki-text-sub); white-space:nowrap; gap:8px;">
                            <span style="flex-shrink:0;">제한 리셋 대기:</span>
                            <span id="toki-rate-reset-val" style="white-space:nowrap; text-align:right;">-</span>
                        </div>
                    </div>

                    <div style="margin-bottom:20px; display:flex; align-items:center; justify-content:space-between; font-size:13px; font-weight:bold; color:var(--toki-text-sub);">
                        <span>🔔 자동 스캔 알림 받기</span>
                        <label class="toki-pin-toggle" style="padding:4px 8px; border-radius:6px; font-size:12px; margin:0;">
                            <input type="checkbox" id="toki-auto-notice-toggle" ${localStorage.getItem('toki_auto_scan_notice') !== 'false' ? 'checked' : ''}> 활성화
                        </label>
                    </div>
                    <div class="toki-sync-btns">
                        <button class="toki-sync-btn cancel" id="toki-sync-close">닫기</button>
                        <button class="toki-sync-btn primary" id="toki-sync-pull" ${!isSync ? 'disabled' : ''}>지금 풀</button>
                        <button class="toki-sync-btn success" id="toki-sync-push" ${!isSync ? 'disabled' : ''}>지금 푸시</button>
                    </div>
                </div>
            `;
            document.body.appendChild(bg);

            let rateTimer = null;

            const close = () => {
                if (rateTimer) clearInterval(rateTimer);
                bg.remove();
            };
            bg.onclick = (e) => { if(e.target === bg) close(); };
            document.getElementById('toki-sync-close').onclick = close;

            const formatDuration = (ms) => {
                const totalSecs = Math.max(0, Math.floor(ms / 1000));
                const hrs = Math.floor(totalSecs / 3600);
                const mins = Math.floor((totalSecs % 3600) / 60);
                const secs = totalSecs % 60;
                const pad = n => n.toString().padStart(2, '0');
                if (hrs > 0) return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
                return `${pad(mins)}:${pad(secs)}`;
            };

            const updateRateUI = () => {
                const statusDiv = document.getElementById('toki-sync-rate-status');
                const remSpan = document.getElementById('toki-rate-remaining-val');
                const resetSpan = document.getElementById('toki-rate-reset-val');
                const btnPull = document.getElementById('toki-sync-pull');
                const btnPush = document.getElementById('toki-sync-push');
                if (!statusDiv || !remSpan || !resetSpan) return;

                const isSyncEnabled = this.getMode();
                if (!isSyncEnabled || !this.getToken()) {
                    statusDiv.style.display = 'none';
                    return;
                }

                const rate = this.getRateLimit();
                if (rate.remaining === -1) {
                    statusDiv.style.display = 'none';
                    return;
                }

                statusDiv.style.display = 'block';
                remSpan.innerText = `${rate.remaining} / ${rate.limit}`;
                
                const now = Date.now();
                if (this.isFallbackMode() && now < rate.reset) {
                    statusDiv.style.borderLeft = '4px solid #ff4757';
                    const diff = rate.reset - now;
                    resetSpan.innerHTML = `<span style="color:#ff4757; font-weight:900;">⌛ ~ ${formatDuration(diff)}</span> (로컬 모드)`;
                    if (btnPull) btnPull.disabled = true;
                    if (btnPush) btnPush.disabled = true;
                } else {
                    statusDiv.style.borderLeft = '4px solid #0984e3';
                    if (rate.reset > now) {
                        const diff = rate.reset - now;
                        resetSpan.innerHTML = `<span style="color:#2ed573; font-weight:900;">✅ 정상</span> (⌛ ~ ${formatDuration(diff)})`;
                    } else {
                        resetSpan.innerHTML = `<span style="color:#2ed573; font-weight:900;">✅ 충전 완료</span>`;
                    }
                    if (this.isFallbackMode()) {
                        this.checkAndRecoverSync();
                    }
                    if (btnPull) btnPull.disabled = false;
                    if (btnPush) btnPush.disabled = false;
                }
            };

            updateRateUI();
            rateTimer = setInterval(updateRateUI, 1000);

            document.getElementById('toki-auto-notice-toggle').onchange = (e) => {
                localStorage.setItem('toki_auto_scan_notice', e.target.checked ? 'true' : 'false');
                TokiUI.toast(e.target.checked ? '🔔 자동 스캔 알림이 활성화되었습니다.' : '🔕 자동 스캔 알림이 비활성화되었습니다.', 'success');
            };

            document.getElementById('toki-sync-toggle').onclick = async () => {
                const tokenInput = document.getElementById('toki-sync-token').value.trim();
                if (this.getMode()) {
                    if (await TokiUI.confirm('⚠️ 동기화 해제', '연결을 해제하시겠습니까? 토큰 정보가 삭제됩니다.')) {
                        this.disable();
                        TokiUI.toast('동기화 비활성화됨');
                        close();
                    }
                } else {
                    if (!tokenInput) return TokiUI.toast('토큰을 먼저 입력하세요.', 'error');

                    const btnToggle = document.getElementById('toki-sync-toggle');
                    btnToggle.innerText = '연결 중...';
                    
                    // Temporary bind credentials for searching
                    this.setCredentials(tokenInput, null);

                    try {
                        // Check if Gist list contains a bookmark file
                        const gists = await this.request({ url: 'https://api.github.com/gists' });
                        const foundGist = gists.find(g => g.files['toki_bookmarks_sync.json']);
                        
                        let remoteBookmarkCount = 0;
                        if (foundGist) {
                            const data = await this.request({ url: `https://api.github.com/gists/${foundGist.id}` });
                            const file = data.files['toki_bookmarks_sync.json'];
                            if (file) {
                                let content = file.content;
                                if (file.truncated || !content) {
                                    content = await this.request({ url: file.raw_url });
                                }
                                const parsed = JSON.parse(content || '{}');
                                const remoteNovel = Object.keys(parsed.novel || {}).length;
                                const remoteManhwa = Object.keys(parsed.manhwa || {}).length;
                                const remoteWebtoon = Object.keys(parsed.webtoon || {}).length;
                                remoteBookmarkCount = remoteNovel + remoteManhwa + remoteWebtoon;
                            }
                        }

                        if (foundGist && remoteBookmarkCount > 0) {
                            // Prompt user for synchronization direction
                            const modeChoice = await TokiUI.importDialog();
                            if (modeChoice === 'merge') {
                                this.setCredentials(tokenInput, foundGist.id);
                                this.enable();
                                const pullSuccess = await this.pull({ forceOverwrite: false });
                                if (pullSuccess) {
                                    this.push(true);
                                    TokiUI.toast('웹 동기화 연결 성공 (데이터 병합 완료)!', 'success');
                                    localStorage.setItem(DIRTY_KEY, '1');
                                    if (typeof renderFavoritesList === 'function') renderFavoritesList();
                                    close();
                                } else {
                                    this.disable();
                                    TokiUI.toast('❌ 데이터 병합 실패: 네트워크 확인', 'error');
                                    btnToggle.innerText = '미연결';
                                }
                            } else if (modeChoice === 'overwrite') {
                                this.setCredentials(tokenInput, foundGist.id);
                                this.enable();
                                const pullSuccess = await this.pull({ forceOverwrite: true });
                                if (pullSuccess) {
                                    TokiUI.toast('웹 동기화 연결 성공 (로컬 데이터 덮어쓰기 완료)!', 'success');
                                    localStorage.setItem(DIRTY_KEY, '1');
                                    if (typeof renderFavoritesList === 'function') renderFavoritesList();
                                    close();
                                } else {
                                    this.disable();
                                    TokiUI.toast('❌ 데이터 오버라이트 실패: 네트워크 확인', 'error');
                                    btnToggle.innerText = '미연결';
                                }
                            } else {
                                // Cancel and rollback credentials
                                this.disable();
                                btnToggle.innerText = '미연결';
                                TokiUI.toast('연동이 취소되었습니다.');
                            }
                        } else {
                            // Fresh integration or empty remote Gist
                            this.enable();
                            const success = await this.pull();
                            if (success) {
                                TokiUI.toast('웹 동기화 연결 성공!', 'success');
                                this.push(true); 
                                localStorage.setItem(DIRTY_KEY, '1');
                                if (typeof renderFavoritesList === 'function') renderFavoritesList();
                                close();
                            } else {
                                this.disable();
                                TokiUI.toast('오류: 토큰 권한 또는 네트워크 확인', 'error');
                                btnToggle.innerText = '미연결';
                            }
                        }
                    } catch (err) {
                        console.error('[GistSync] Initial bind failed:', err);
                        this.disable();
                        TokiUI.toast('오류: 토큰 권한 또는 네트워크 확인', 'error');
                        btnToggle.innerText = '미연결';
                    }
                }
            };

            document.getElementById('toki-sync-pull').onclick = async () => {
                const token = document.getElementById('toki-sync-token').value.trim();
                if(token) this.setCredentials(token, null);

                TokiUI.toast('☁️ 서버에서 데이터를 가져오는 중...');
                const success = await this.pull();
                if (success) {
                    TokiUI.toast('✅ 다운로드 완료!', 'success');
                    if (typeof renderFavoritesList === 'function') renderFavoritesList();
                } else {
                    TokiUI.toast('❌ 다운로드 실패', 'error');
                }
            };

            document.getElementById('toki-sync-push').onclick = () => {
                const token = document.getElementById('toki-sync-token').value.trim();
                if(token) this.setCredentials(token, null);
                this.push(true);
            };
        }
    };
    function saveSortPref(pref) { localStorage.setItem(PREF_KEY, pref); }

    // SWR Dynamic Caching (Dynamic TTL)
    function getTTL(m) {
        let lastUpdated = m.exactUpdatedAt || m.parsedUpdatedAt || m.latestUpdatedAt || m.addedAt;
        if (!lastUpdated) lastUpdated = Date.now(); // Fallback for NaN error prevention

        const daysSinceUpdate = (Date.now() - lastUpdated) / (1000 * 60 * 60 * 24);

        if (m.status === 'completed') {
            if (daysSinceUpdate <= 90) return 7 * 24 * 60 * 60 * 1000;
            if (daysSinceUpdate <= 365) return 30 * 24 * 60 * 60 * 1000;
            return Infinity;
        }

        const isReading = m.lastReadEp != null;

        if (isReading) {
            if (daysSinceUpdate <= 1) return 1 * 60 * 60 * 1000;
            if (daysSinceUpdate <= 7) return 6 * 60 * 60 * 1000;
            return 24 * 60 * 60 * 1000;
        } else {
            if (daysSinceUpdate <= 7) return 12 * 60 * 60 * 1000;
            return 24 * 60 * 60 * 1000;
        }
    }

    // Extract Exact Timestamp inside JSON (availableAt > publishedAt+createdAt > createdAt)
    // Next.js JSON inside HTML is escaped as \\\" -> regex matches both \\\\?"
    function parseExactUpdatedAt(htmlText) {
        try {
            const Q = '[\\\\"]+'; // Flexibly support escaped or normal quotes
            const reAvail = new RegExp('availableAt' + Q + ':' + Q + '(\\d{4}-[^"\\\\]+?)' + Q);
            const rePub = new RegExp('publishedAt' + Q + ':' + Q + '([^"\\\\]+?)' + Q);
            const reCrt = new RegExp('createdAt' + Q + ':' + Q + '([^"\\\\]+?)' + Q);

            const availMatch = htmlText.match(reAvail);
            const pubMatch = htmlText.match(rePub);
            const crtMatch = htmlText.match(reCrt);

            let bestDate = null;
            if (availMatch) {
                bestDate = new Date(availMatch[1]);
                // Replace with createdAt time if hour and minute are 00:00
                if (bestDate.getUTCHours() === 0 && bestDate.getUTCMinutes() === 0 && crtMatch) {
                    const crtDate = new Date(crtMatch[1]);
                    bestDate.setUTCHours(crtDate.getUTCHours(), crtDate.getUTCMinutes(), 0, 0);
                }
                return bestDate.getTime();
            }
            if (pubMatch && crtMatch) {
                const pubDate = new Date(pubMatch[1]);
                const crtDate = new Date(crtMatch[1]);
                pubDate.setUTCHours(crtDate.getUTCHours(), crtDate.getUTCMinutes(), 0, 0);
                return pubDate.getTime();
            }
            if (crtMatch) return new Date(crtMatch[1]).getTime();
        } catch (e) {}
        return null;
    }

    function parseStatus(doc) {
        const statusEl = doc.querySelector('.hero-v2-badges .pill-status');
        if (statusEl) {
            return statusEl.classList.contains('completed') ? 'completed' : 'ongoing';
        }
        return 'ongoing';
    }

    function parseSiteDateToTimestamp(dateStr) {
        if (!dateStr) return 0;
        const now = Date.now();
        if (dateStr.includes('초 전')) return now - parseInt(dateStr) * 1000;
        if (dateStr.includes('분 전')) return now - parseInt(dateStr) * 60 * 1000;
        if (dateStr.includes('시간 전')) return now - parseInt(dateStr) * 3600 * 1000;
        if (dateStr.includes('일 전')) return now - parseInt(dateStr) * 86400 * 1000;
        if (dateStr.includes('어제')) return now - 86400 * 1000;

        const parts = dateStr.split(/[\.\-]/);
        if (parts.length >= 2) {
            const d = new Date();
            if (parts.length === 3) {
                let year = parseInt(parts[0]);
                if (year < 100) year += 2000;
                d.setFullYear(year, parseInt(parts[1]) - 1, parseInt(parts[2]));
            } else {
                d.setMonth(parseInt(parts[0]) - 1, parseInt(parts[1]));
            }
            return d.getTime();
        }
        return now;
    }

    function formatDateTime(ts) {
        return TokiUtils.formatDateTime(ts);
    }

    function extractShortEp(text) {
        if (!text) return "";
        
        const specialMatch = text.match(/(프롤로그|에필로그|후기|최종화|마지막화)/);
        if (specialMatch) return specialMatch[1];

        const prefixMatch = text.match(/(외전|특별편|단편|번외|시즌\s*\d+)(?:\s*(\d+(?:[\.\-]\d+)?)\s*(화|권)?)?/);
        if (prefixMatch) {
            const prefix = prefixMatch[1].replace(/\s+/g, '');
            if (prefixMatch[2]) {
                const unit = prefixMatch[3] || '화';
                return prefix + ' ' + prefixMatch[2] + unit;
            }
            return prefix;
        }

        const match = text.match(/(\d+(?:[\.\-]\d+)?)\s*(화|권)/);
        if (match) return match[1] + match[2];

        const lastWord = text.trim().split(' ').pop();
        if (/^\d+(?:[\.\-]\d+)?$/.test(lastWord)) return lastWord + '화';
        
        const fallbackText = text.split('-')[0].split('|')[0].trim();
        return fallbackText.length > 20 ? fallbackText.substring(0, 20) + '...' : fallbackText;
    }

    const removeTokiLocalBookmark = async function(id, category, e) {
        if(e && e.preventDefault) e.preventDefault();
        const isConfirmed = await TokiUI.confirm('북마크 삭제', '해당 작품을 로컬 북마크에서 완전히 삭제합니다.');
        if(isConfirmed) {
            const marks = getBookmarks();
            const cat = category || 'manhwa';
            if (marks[cat] && marks[cat][id]) {
                delete marks[cat][id];
                saveBookmarks(marks);
                if(typeof GistSync !== 'undefined') GistSync.push();
                TokiUI.toast('작품이 삭제되었습니다.');
            }

            // [Fix] Surgical UI Update: Instead of removing the entire UI, just re-render to reflect changes
            if(typeof renderFavoritesList === 'function') renderFavoritesList();
            localStorage.setItem(DIRTY_KEY, '0');
        }
    };

    function exportData() {
        const data = localStorage.getItem(STORAGE_KEY) || '{}';
        const blob = new Blob([data], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url; a.download = `toki_bookmarks_${new Date().toISOString().slice(2, 10).replace(/-/g, '')}.json`;
        document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
        TokiUI.toast('데이터가 백업되었습니다.', 'success');
    }

    function importData() {
        const input = document.createElement('input'); input.type = 'file'; input.accept = '.json';
        input.onchange = e => {
            const file = e.target.files[0]; if (!file) return;
            const reader = new FileReader();
            reader.onload = async (event) => {
                try {
                    const parsed = JSON.parse(event.target.result);
                    if (typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error();

                    const sanitizedData = TokiUtils.ensureV10Structure(parsed);
                    const now = Date.now();
                    const categories = ['manhwa', 'webtoon', 'novel'];

                    for (const cat of categories) {
                        for (const key of Object.keys(sanitizedData[cat])) {
                            const item = sanitizedData[cat][key];
                            if (!item.addedAt) item.addedAt = now;
                            if (!item.latestUpdatedAt) item.latestUpdatedAt = now;
                        }
                    }

                    const action = await TokiUI.importDialog();
                    if (!action) return;
                    if (action === 'overwrite') {
                        saveBookmarks(sanitizedData);
                        TokiUI.toast('데이터 덮어쓰기 완료', 'success');
                    } else if (action === 'merge') {
                        const local = getBookmarks();
                        const merged = {
                            manhwa: Object.assign({}, local.manhwa, sanitizedData.manhwa),
                            webtoon: Object.assign({}, local.webtoon, sanitizedData.webtoon),
                            novel: Object.assign({}, local.novel, sanitizedData.novel)
                        };
                        saveBookmarks(merged);
                        TokiUI.toast('데이터 병합 완료', 'success');
                    }
                    renderFavoritesList();
                } catch(err) { TokiUI.toast('올바르지 않은 파일입니다.', 'error'); }
            };
            reader.readAsText(file);
        };
        input.click();
    }


    function isViewerPage() {
        return /^\/(manhwa|webtoon|novel)\/[^/]+\/[^/?#]+/.test(location.pathname);
    }

    function isViewerUIApplied() {
        const botActions = document.querySelector('.vw-bot-actions');
        if (!botActions) return true;

        const siteBookmarkBtn = Array.from(botActions.children).find(el =>
            el.innerText.includes('책갈피') ||
            el.getAttribute('aria-label')?.includes('책갈피') ||
            el.title?.includes('책갈피')
        );
        if (siteBookmarkBtn) return false;

        const favLink = botActions.querySelector(`#${ID_VIEWER_UI}`);
        if (!favLink) return false;

        const children = Array.from(botActions.children);
        const prevBtn = children.find(el => el.innerText.includes('이전화') || el.getAttribute('aria-label')?.includes('이전화') || el.title?.includes('이전화'));
        const listBtn = children.find(el => el.innerText.includes('목록') || el.getAttribute('aria-label')?.includes('목록') || el.title?.includes('목록'));
        const nextBtn = children.find(el => el.innerText.includes('다음화') || el.getAttribute('aria-label')?.includes('다음화') || el.title?.includes('다음화'));

        if (prevBtn && children.indexOf(prevBtn) > children.indexOf(favLink)) return false;
        if (listBtn && children.indexOf(favLink) > children.indexOf(listBtn)) return false;
        if (nextBtn && children.indexOf(listBtn) > children.indexOf(nextBtn)) return false;

        return true;
    }

    // ==========================================
    // 2. Viewer Tracking
    // ==========================================
    function injectViewerUI() {
        if (!location.pathname.match(/^\/(manhwa|webtoon|novel)\/[^/]+\/[^/?#]+/)) return;

        const match = location.pathname.match(/^\/(manhwa|webtoon|novel)\/([^/]+)\/([^/?#]+)$/);
        if (match) {
            const category = match[1];
            const workId = match[2];
            let marks = getBookmarks();

            if (marks[category] && marks[category][workId]) {
                const fallbackTitle = document.title.split('|')[0].trim();
                const shortEp = extractShortEp(fallbackTitle);

                marks[category][workId].lastReadEp = shortEp;
                marks[category][workId].lastReadEpLink = location.pathname;
                marks[category][workId].lastReadAt = Date.now();

                const nextBtn = Array.from(document.querySelectorAll('a')).find(a =>
                    a.innerText.includes('다음화') || a.innerText.includes('다음 화') || a.id === 'goNextBtn' || a.className.includes('btn-next')
                );

                if (nextBtn && nextBtn.getAttribute('href') && nextBtn.getAttribute('href') !== '#') {
                    marks[category][workId].nextEpLink = nextBtn.getAttribute('href');
                    marks[category][workId].nextEp = "다음화";
                } else {
                    marks[category][workId].nextEpLink = null;
                    marks[category][workId].nextEp = null;
                }
                saveBookmarks(marks);
                if(typeof GistSync !== 'undefined') GistSync.push();

                fetch(`/${category}/${workId}`).then(res => res.text()).then(text => {
                    const doc = new DOMParser().parseFromString(text, 'text/html');
                    const epList = Array.from(doc.querySelectorAll('.ep-list-v2 .ep-row-v2-link, .novel-eps li a'));
                    
                    const currentPath = location.pathname;
                    let lastIdx = epList.findIndex(el => {
                        const href = el.getAttribute('href');
                        return href && href.includes(currentPath);
                    });

                    const getEpNode = (el) => {
                        if (category === 'novel') {
                            return el.querySelector('.ne-title') || el.querySelector('.ne-num') || el;
                        }
                        return el.querySelector('.ne-title, .ep-row-v2-title strong, .ep-row-v2-title, .ne-num') || el;
                    };

                    if (lastIdx === -1) {
                        lastIdx = epList.findIndex(el => {
                            const titleNode = getEpNode(el);
                            return extractShortEp(titleNode.textContent) === shortEp;
                        });
                    }

                    const updatedMarks = getBookmarks();
                    if(updatedMarks[category] && updatedMarks[category][workId]) {
                        if (lastIdx !== -1) {
                            const currNode = getEpNode(epList[lastIdx]);
                            updatedMarks[category][workId].lastReadEp = extractShortEp(currNode.textContent);
                        }

                        if (lastIdx > 0) {
                            const nextEl = epList[lastIdx - 1];
                            const nextNode = getEpNode(nextEl);

                            updatedMarks[category][workId].nextEp = extractShortEp(nextNode.textContent);
                            updatedMarks[category][workId].nextEpLink = nextEl.getAttribute('href');
                        } else if (lastIdx === 0) {
                            updatedMarks[category][workId].nextEp = null;
                            updatedMarks[category][workId].nextEpLink = null;
                        }
                        saveBookmarks(updatedMarks);
                        if(typeof GistSync !== 'undefined') GistSync.push();
                    }
                }).catch(err => console.log("다음화 탐색 중단"));
            }

            // [Task 4] Novel bookmark button hijacking
            const novelNav = document.querySelector('.ne-nav');
            if (novelNav && category === 'novel') {
                const originalBtn = novelNav.querySelector('.bookmark-btn');
                if (originalBtn) {
                    originalBtn.style.display = 'none';
                    
                    let customBtn = document.getElementById('toki-novel-bookmark-btn');
                    if (!customBtn) {
                        customBtn = document.createElement('button');
                        customBtn.id = 'toki-novel-bookmark-btn';
                        customBtn.type = 'button';
                        customBtn.className = 'btn btn--outline bookmark-btn-custom';
                        customBtn.style.borderColor = '#ff4757';
                        customBtn.style.color = '#ff4757';
                        customBtn.style.display = 'inline-flex';
                        customBtn.style.alignItems = 'center';
                        customBtn.style.gap = '6px';
                        customBtn.style.cursor = 'pointer';
                        
                        const marks = getBookmarks();
                        if (!marks['novel']) marks['novel'] = {};
                        const novelMark = marks['novel'][workId];
                        const fallbackTitle = document.title.split('|')[0].trim();
                        const currentEpText = document.querySelector('.ne-h1')?.innerText.trim() || fallbackTitle;
                        const isCurrentEpMarked = novelMark && (novelMark.lastReadEp === extractShortEp(currentEpText) || novelMark.lastReadEpLink === location.pathname);

                        customBtn.innerHTML = `
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="${isCurrentEpMarked ? '#ff4757' : 'none'}" stroke="#ff4757" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
                            </svg>
                            <span>${isCurrentEpMarked ? '책갈피 완료' : '책갈피 저장'}</span>
                        `;
                        
                        originalBtn.parentNode.insertBefore(customBtn, originalBtn.nextSibling);
                        
                        customBtn.onclick = async () => {
                            const _marks = getBookmarks();
                            if (!_marks['novel']) _marks['novel'] = {};
                            const now = Date.now();
                            const _currentEpText = document.querySelector('.ne-h1')?.innerText.trim() || extractShortEp(document.title);
                            
                            if (!_marks['novel'][workId]) {
                                const novelTitle = document.querySelector('.crumb strong')?.innerText.trim() || document.title.split('-')[0].trim();
                                
                                _marks['novel'][workId] = {
                                    id: workId,
                                    title: novelTitle,
                                    author: TokiUtils.extractAuthor(document.documentElement.innerHTML, document.title),
                                    genres: "",
                                    url: `/novel/${workId}`,
                                    thumb: "", 
                                    latestEp: _currentEpText,
                                    latestEpLink: location.pathname,
                                    latestDate: TokiUtils.formatDateTime(now),
                                    firstEpLink: `/novel/${workId}`,
                                    lastReadEp: _currentEpText,
                                    lastReadEpLink: location.pathname,
                                    lastReadAt: now,
                                    nextEp: null,
                                    nextEpLink: null,
                                    addedAt: now,
                                    latestUpdatedAt: now,
                                    status: 'ongoing'
                                };
                                TokiUI.toast('소설을 로컬 북마크에 추가하고 책갈피를 꽂았습니다!', 'success');
                            } else {
                                _marks['novel'][workId].lastReadEp = extractShortEp(_currentEpText);
                                _marks['novel'][workId].lastReadEpLink = location.pathname;
                                _marks['novel'][workId].lastReadAt = now;
                                TokiUI.toast('회차 책갈피 저장이 완료되었습니다.', 'success');
                            }
                            
                            saveBookmarks(_marks);
                            if (typeof GistSync !== 'undefined') GistSync.push(); // Trigger push
                            
                            // UI update
                            customBtn.querySelector('svg').setAttribute('fill', '#ff4757');
                            customBtn.querySelector('span').innerText = '책갈피 완료';
                        };
                    }
                }
            }
        }



        const botActions = document.querySelector('.vw-bot-actions');
        if (botActions) {
            // Find original bookmark button if exists
            const siteBookmarkBtn = Array.from(botActions.children).find(el => el.innerText.includes('책갈피') || el.getAttribute('aria-label')?.includes('책갈피') || el.title?.includes('책갈피'));

            // Create or get custom bookmark link
            let favLink = botActions.querySelector(`#${ID_VIEWER_UI}`);
            if (!favLink) {
                const globalFavLink = document.getElementById(ID_VIEWER_UI);
                if (globalFavLink && globalFavLink.isConnected) {
                    favLink = globalFavLink;
                } else {
                    favLink = document.createElement('a');
                    favLink.className = 'vw-act';
                    favLink.id = ID_VIEWER_UI;
                    favLink.href = '/favorites';
                    favLink.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg><span>북마크</span>`;
                }
            }

            if (siteBookmarkBtn) {
                botActions.replaceChild(favLink, siteBookmarkBtn);
            } else if (!favLink.parentNode) {
                const listBtn = Array.from(botActions.children).find(el => el.innerText.includes('목록') || el.getAttribute('aria-label')?.includes('목록') || el.title?.includes('목록'));
                if (listBtn) botActions.insertBefore(favLink, listBtn);
                else botActions.appendChild(favLink);
            }
        }
    }

    // ==========================================
    // 3. Title Detail Page (Bookmark Button & Main CTA Hijack)
    // ==========================================
    function injectManhwaButton() {
        if (!/^\/(manhwa|webtoon|novel)\/[^/]+$/i.test(location.pathname)) return;
        const pathParts = location.pathname.split('/');
        if (pathParts.length !== 3) return;
        const category = pathParts[1];
        const workId = pathParts[2];
        const isNovel = category === 'novel';

        const actionBoxSelector = isNovel ? '.nd-actions' : '.hero-v2-actions';
        const thumbContainerSelector = isNovel ? '.nd-thumb' : '.hero-v2-thumb';

        // 1. Inject main CTA button
        let localBtn = document.getElementById(ID_MANHWA_BTN);
        const actionBox = document.querySelector(actionBoxSelector);
        if (actionBox && !localBtn) {
            const originalFav = actionBox.querySelector('.cta-fav, .nd-fav');
            if (originalFav && originalFav.style.display !== 'none') originalFav.style.display = 'none';

            const mainCtaBtn = actionBox.querySelector('.cta-primary, .nd-primary, .btn-primary');
            const marks = getBookmarks();
            if (mainCtaBtn && marks[category] && marks[category][workId]) {
                const item = marks[category][workId];
                if (item.nextEpLink) {
                    mainCtaBtn.setAttribute('href', item.nextEpLink);
                    const epText = item.nextEp === '다음화' ? '다음화' : item.nextEp;
                    mainCtaBtn.innerHTML = `<span class="ic">▶</span><span class="t">이어보기</span><span class="s">${epText}</span>`;
                } else if (item.lastReadEpLink) {
                    mainCtaBtn.setAttribute('href', item.lastReadEpLink);
                    mainCtaBtn.innerHTML = `<span class="ic">▶</span><span class="t">이어보기</span><span class="s">${item.lastReadEp}</span>`;
                }
            }

            localBtn = document.createElement('button');
            localBtn.id = ID_MANHWA_BTN;
            localBtn.type = 'button';
            localBtn.className = 'cta cta-fav';
            localBtn.style.border = isNovel ? '1px solid var(--toki-border)' : 'none';
            localBtn.style.cursor = 'pointer';
            actionBox.appendChild(localBtn);
        }

        // 2. Inject thumbnail button
        const ID_THUMB_BTN = 'my-local-thumb-btn';
        let thumbBtn = document.getElementById(ID_THUMB_BTN);
        const thumbContainer = document.querySelector(thumbContainerSelector);
        if (thumbContainer && !thumbBtn) {
            const originalFavHeart = thumbContainer.querySelector('.fav-heart, .nd-fav-heart');
            if (originalFavHeart) originalFavHeart.style.display = 'none';

            thumbBtn = document.createElement('button');
            thumbBtn.id = ID_THUMB_BTN;
            thumbBtn.type = 'button';
            // Apply same circular translucent button class as manhwa/webtoon
            thumbBtn.className = 'fav-heart my-local-fav-heart';
            thumbBtn.style.border = 'none';
            thumbBtn.style.cursor = 'pointer';
            
            // Fix location to bottom-right (original style) for novels
            if (isNovel) {
                thumbBtn.style.position = 'absolute';
                thumbBtn.style.bottom = '10px';
                thumbBtn.style.right = '10px';
                if (window.getComputedStyle(thumbContainer).position === 'static') {
                    thumbContainer.style.position = 'relative';
                }
            }
            
            thumbContainer.appendChild(thumbBtn);
        }

        // 3. Keep both in sync with a single update function
        const updateAllButtonsUI = () => {
            const _marks = getBookmarks();
            const isMarked = !!(_marks[category] && _marks[category][workId]);

            if (localBtn) {
                if (isNovel) {
                    localBtn.className = 'cta cta-fav';
                    localBtn.innerHTML = `
                        <span class="ic">
                            <svg width="20" height="20" viewBox="0 0 24 24" fill="${isMarked ? '#ff4757' : 'none'}" stroke="${isMarked ? '#ff4757' : 'currentColor'}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                                <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
                            </svg>
                        </span>
                        <span class="t">${isMarked ? '북마크 완료' : '북마크'}</span>
                    `;
                    localBtn.style.color = isMarked ? '#ff4757' : 'inherit';
                    localBtn.style.backgroundColor = isMarked ? 'rgba(255, 71, 87, 0.05)' : 'transparent';
                    localBtn.style.border = isMarked ? '1px solid #ff4757' : '1px solid var(--toki-border)';
                } else {
                    localBtn.className = isMarked ? 'cta cta-primary' : 'cta cta-ghost';
                    localBtn.innerHTML = `<span class="ic">${isMarked ? '⭐' : '☆'}</span><span class="t">${isMarked ? '북마크 취소' : '북마크 추가'}</span>`;
                    localBtn.style.backgroundColor = isMarked ? '#ff4757' : '';
                    localBtn.style.color = isMarked ? '#ffffff' : '';
                    localBtn.style.border = isMarked ? 'none' : '';
                }
            }

            if (thumbBtn) {
                thumbBtn.innerHTML = isMarked ? `
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="#ffd700" stroke="#ffd700" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                        <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
                    </svg>
                ` : `
                    <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
                        <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon>
                    </svg>
                `;
            }
        };
        updateAllButtonsUI();

        // 4. Shared click logic
        const toggleBookmark = async () => {
            const _marks = getBookmarks();
            if (_marks[category] && _marks[category][workId]) {
                const confirmDelete = await TokiUI.confirm('북마크 해제', '해당 작품을 로컬 북마크에서 제거합니다.');
                if(confirmDelete) {
                    delete _marks[category][workId];
                    TokiUI.toast('작품이 북마크에서 제외되었습니다.');
                } else return;
            } else {
                const titleEl = document.querySelector('.hero-v2-title, .nd-title, .crumb strong');
                const title = titleEl ? titleEl.innerText.trim() : document.title.split('|')[0].trim();
                const thumbEl = document.querySelector('.hero-v2-thumb img, .nd-thumb img');
                const thumb = thumbEl ? thumbEl.src : '';
                const genreEls = document.querySelectorAll('.hero-v2-tags .hero-v2-tag, .nd-tags .nd-tag, .nd-genres .nd-genre');
                const genres = Array.from(genreEls).map(el => el.innerText.trim().replace(/\s+/g, '')).join(' ');

                const latestEpEl = document.querySelector('.ep-list-v2 .ep-row-v2-title strong, .nd-ep-list .nd-ep-row strong, .ep-row-v2-link strong, .novel-eps li a .ne-title, .novel-eps li a .ne-num');
                const latestEp = latestEpEl ? extractShortEp(latestEpEl.textContent) : '';
                const latestEpLinkEl = document.querySelector('.ep-list-v2 .ep-row-v2-link, .nd-ep-list .nd-ep-row-link, .ep-row-v2-link, .novel-eps li a');
                const latestEpLink = latestEpLinkEl ? latestEpLinkEl.getAttribute('href') : '';
                const latestDateEl = document.querySelector('.ep-list-v2 .ep-row-v2-date, .nd-ep-list .nd-ep-row-date, .ep-row-v2-date, .novel-eps .ne-date');
                const latestDate = latestDateEl ? latestDateEl.innerText.trim() : '';
                const firstEpBtn = document.querySelector('.cta-ghost, .nd-first-ep, .nd-actions .btn--primary');
                const firstEpLink = firstEpBtn ? firstEpBtn.getAttribute('href') : '';

                // Premium Author Metadata!
                const author = TokiUtils.extractAuthor(document.documentElement.innerHTML, document.title);

                const now = Date.now();
                const exactTime = parseExactUpdatedAt(document.documentElement.innerHTML);
                _marks[category][workId] = {
                    id: workId, title: title, author: author, genres: genres, url: location.pathname, thumb: thumb,
                    latestEp: latestEp, latestEpLink: latestEpLink, latestDate: latestDate, firstEpLink: firstEpLink,
                    lastReadEp: null, lastReadEpLink: null, lastReadAt: null, nextEp: null, nextEpLink: null,
                    exactUpdatedAt: exactTime, parsedUpdatedAt: exactTime || parseSiteDateToTimestamp(latestDate),
                    addedAt: now, latestUpdatedAt: now, status: parseStatus(document)
                };
                TokiUI.toast('성공적으로 북마크에 등록되었습니다.', 'success');
            }
            saveBookmarks(_marks);
            if(typeof GistSync !== 'undefined') GistSync.push();
            updateAllButtonsUI();
        };

        if (localBtn) localBtn.onclick = toggleBookmark;
        if (thumbBtn) thumbBtn.onclick = toggleBookmark;
    }

    // ==========================================
    // 4. Background Scanning (BroadcastChannel, SWR)
    // ==========================================
    async function runBackgroundScan(scanType = 'auto') { // 'auto', 'smart', 'force'
        if (tokiState.isScanning) {
            if (scanType !== 'auto') TokiUI.toast('이미 스캔 중입니다.', 'error');
            return;
        }
        tokiState.isScanning = true;
        try {
            if (!navigator.locks) {
                await executeScan(scanType);
            } else {
                await new Promise(resolve => {
                    navigator.locks.request('toki_bg_scan_lock', { ifAvailable: true }, async (lock) => {
                        if (!lock) {
                            if (scanType !== 'auto') TokiUI.toast('다른 탭에서 이미 스캔 중입니다.', 'error');
                            resolve();
                            return;
                        }
                        await executeScan(scanType);
                        resolve();
                    });
                });
            }
        } finally {
            tokiState.isScanning = false;
        }
    }

    async function executeScan(scanType) {
        tokiState.isScanning = true;
        const currentMarks = getBookmarks();
        let queue = [];
        try {
            queue = JSON.parse(localStorage.getItem(SCAN_QUEUE_KEY) || '[]');
        } catch(e) { queue = []; }

        if (queue.length === 0) {
            const now = Date.now();
            const categories = ['manhwa', 'webtoon', 'novel'];
            for (const cat of categories) {
                const catMarks = currentMarks[cat] || {};
                for (const key of Object.keys(catMarks)) {
                    const m = catMarks[key];
                    if (scanType === 'force') {
                        queue.push({ id: key, category: cat });
                        continue;
                    }

                    if (scanType === 'smart') {
                        if (m.status === 'completed') continue;
                        const isReading = m.lastReadEp != null;
                        if (isReading) {
                            queue.push({ id: key, category: cat });
                            continue;
                        }
                    }

                    const nextScan = (m.lastScannedAt || 0) + getTTL(m);
                    if (now >= nextScan) {
                        queue.push({ id: key, category: cat });
                    }
                }
            }
            localStorage.setItem(SCAN_QUEUE_KEY, JSON.stringify(queue));
        }

        const totalKeys = queue.length;
        if (totalKeys === 0) {
            dispatchSync({ type: 'DONE', count: 0, force: scanType === 'force', scanType: scanType, totalKeys: 0 });
            return;
        }

        let updateCount = 0;
        let originalTotal = parseInt(localStorage.getItem('toki_scan_total_v20') || totalKeys);
        if (queue.length === totalKeys) {
            localStorage.setItem('toki_scan_total_v20', totalKeys);
            originalTotal = totalKeys;
        }

        const processItem = async () => {
            let q = [];
            try {
                q = JSON.parse(localStorage.getItem(SCAN_QUEUE_KEY) || '[]');
            } catch(e) { q = []; }
            if (q.length === 0) return;
            const item = q.shift();
            localStorage.setItem(SCAN_QUEUE_KEY, JSON.stringify(q));

            const latestMarks = getBookmarks();
            const m = latestMarks[item.category]?.[item.id];
            if (!m) return;

            const scannedIdx = originalTotal - q.length;
            dispatchSync({ type: 'PROGRESS', current: scannedIdx, total: originalTotal });

            try {
                const res = await fetch(m.url, { cache: 'no-store' });
                const text = await res.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(text, 'text/html');

                // Handle novel structure support
                const epList = Array.from(doc.querySelectorAll('.ep-list-v2 .ep-row-v2-link, .novel-eps li a'));
                const latestDateEl = doc.querySelector('.ep-list-v2 .ep-row-v2-date, .novel-eps .ne-date');
                const genreEls = doc.querySelectorAll('.hero-v2-tags .hero-v2-tag');

                m.status = parseStatus(doc);
                m.lastScannedAt = Date.now();
                if (!m.genres && genreEls.length > 0) m.genres = Array.from(genreEls).map(el => el.innerText.trim().replace(/\s+/g, '')).join(' ');

                // Auto extract premium author if not present
                if (!m.author) {
                    m.author = TokiUtils.extractAuthor(text, doc.title);
                }

                if (epList.length > 0) {
                    const getEpNode = (el) => {
                        if (item.category === 'novel') {
                            return el.querySelector('.ne-title') || el.querySelector('.ne-num') || el;
                        }
                        return el.querySelector('.ne-title, .ep-row-v2-title strong, .ep-row-v2-title, .ne-num') || el;
                    };

                    const epNode = getEpNode(epList[0]);
                    const fetchedLatestEp = epNode ? extractShortEp(epNode.textContent) : '';
                    const fetchedLatestLink = epList[0].getAttribute('href');
                    const fetchedDate = latestDateEl ? latestDateEl.innerText.trim() : m.latestDate;

                    const exactTime = parseExactUpdatedAt(text);
                    const newParsedDate = exactTime || parseSiteDateToTimestamp(fetchedDate);

                    const isNewEp = (fetchedLatestEp !== m.latestEp);

                    if (scanType === 'force' || isNewEp || !m.exactUpdatedAt) {
                        m.latestEp = fetchedLatestEp;
                        m.latestEpLink = fetchedLatestLink;
                        m.latestDate = fetchedDate;
                        if (exactTime !== null) m.exactUpdatedAt = exactTime;
                        m.parsedUpdatedAt = newParsedDate;
                        m.latestUpdatedAt = Date.now();
                        if (isNewEp) updateCount++;
                    }

                    if (m.lastReadEpLink || m.lastReadEp) {
                        let lastIdx = -1;
                        if (m.lastReadEpLink) {
                            lastIdx = epList.findIndex(el => {
                                const href = el.getAttribute('href');
                                return href && href.includes(m.lastReadEpLink);
                            });
                        }
                        
                        if (lastIdx === -1 && m.lastReadEp) {
                            lastIdx = epList.findIndex(el => {
                                const titleNode = getEpNode(el);
                                return extractShortEp(titleNode.textContent) === m.lastReadEp;
                            });
                        }

                        if (lastIdx !== -1) {
                            m.lastReadEp = extractShortEp(getEpNode(epList[lastIdx]).textContent);
                        }

                        if (lastIdx > 0) {
                            const nextEl = epList[lastIdx - 1];
                            const nextNode = getEpNode(nextEl);
                            
                            m.nextEp = extractShortEp(nextNode.textContent);
                            m.nextEpLink = nextEl.getAttribute('href');
                        } else if (lastIdx === 0) {
                            m.nextEp = null; m.nextEpLink = null;
                        }
                    }
                }
            } catch(e) { console.error('체크 실패:', m.title); }

            await new Promise(resolve => setTimeout(resolve, 300));

            const currentLatestMarks = getBookmarks();
            if (currentLatestMarks[item.category]?.[item.id]) {
                currentLatestMarks[item.category][item.id] = {
                    ...currentLatestMarks[item.category][item.id],
                    status: m.status,
                    lastScannedAt: m.lastScannedAt,
                    genres: m.genres,
                    author: m.author,
                    latestEp: m.latestEp,
                    latestEpLink: m.latestEpLink,
                    latestDate: m.latestDate,
                    exactUpdatedAt: m.exactUpdatedAt,
                    parsedUpdatedAt: m.parsedUpdatedAt,
                    latestUpdatedAt: m.latestUpdatedAt,
                    lastReadEp: m.lastReadEp,
                    nextEp: m.nextEp,
                    nextEpLink: m.nextEpLink
                };
                saveBookmarks(currentLatestMarks);
            }
        };

        const enqueue = async () => {
            let q = [];
            try {
                q = JSON.parse(localStorage.getItem(SCAN_QUEUE_KEY) || '[]');
            } catch(e) { q = []; }
            if (q.length === 0) return;
            await processItem();
            await enqueue();
        };

        const activePromises = [];
        const CONCURRENCY_LIMIT = 15;
        for (let i = 0; i < CONCURRENCY_LIMIT; i++) { activePromises.push(enqueue()); }
        await Promise.all(activePromises);

        let finalQ = [];
        try {
            finalQ = JSON.parse(localStorage.getItem(SCAN_QUEUE_KEY) || '[]');
        } catch(e) { finalQ = []; }
        if (finalQ.length === 0) {
            localStorage.removeItem('toki_scan_total_v20');
            if (GistSync.getMode()) GistSync.push();
            tokiState.isScanning = false;
            dispatchSync({ type: 'DONE', count: updateCount, force: scanType === 'force', scanType: scanType, totalKeys: totalKeys });
        }
    }

    // Broadcast Sync UI Status Updates
    function updateScanUI(msg) {
        const btn = document.getElementById('checkUpdatesBtn');
        const showAutoNotice = localStorage.getItem('toki_auto_scan_notice') !== 'false';

        if (msg.type === 'PROGRESS') {
            tokiState.isScanning = true;
            if (btn) {
                if (tokiState.btnTimeout) { clearTimeout(tokiState.btnTimeout); tokiState.btnTimeout = null; }
                btn.disabled = true;
                btn.style.background = '#b2bec3';
                btn.innerText = `스캔 중... (${msg.current}/${msg.total})`;
            }
        } else if (msg.type === 'DONE') {
            tokiState.isScanning = false;
            const isAuto = msg.scanType === 'auto';

            if (msg.count > 0) {
                if (!isAuto || showAutoNotice) {
                    TokiUI.toast(`${msg.count}개의 작품에 새로운 회차가 있습니다.`, 'success');
                }
                if (btn) {
                    if (tokiState.btnTimeout) clearTimeout(tokiState.btnTimeout);
                    btn.innerText = `완료! (${msg.count}건 갱신) 🎉`;
                    btn.style.background = '#00b894';
                    tokiState.btnTimeout = setTimeout(() => { renderFavoritesList(); }, 1500);
                }
            } else {
                if (msg.force) {
                    TokiUI.toast('모든 북마크가 최신 상태입니다.');
                } else if (!isAuto) {
                    if (location.pathname !== '/favorites') {
                        TokiUI.toast('백그라운드 북마크 스캔 완료 (최신상태 유지중)', 'success');
                    }
                }

                if (btn) {
                    if (tokiState.btnTimeout) clearTimeout(tokiState.btnTimeout);
                    btn.innerText = '모두 최신입니다 ✨';
                    btn.style.background = '#0984e3';
                    tokiState.btnTimeout = setTimeout(() => { btn.innerText = '🔄 스마트 스캔'; btn.disabled = false; }, 3000);
                }
            }
        }
    }

    function dispatchSync(msg) {
        updateScanUI(msg);
        if (syncChannel) syncChannel.postMessage(msg);
    }

    if (syncChannel) {
        syncChannel.onmessage = (e) => updateScanUI(e.data);
    }




    // ==========================================
    // 5. Bookmarks Tab Rendering
    // ==========================================
    // [State Handled in tokiState]

    function applyFilters() {
        const searchInput = document.getElementById('toki-search-input');
        const cards = document.querySelectorAll('.toki-card');
        const { states, genres } = tokiState.filters;

        const normalizeString = (str) => (str || '').toLowerCase().replace(/\s+/g, '');
        const searchQueryNormalized = normalizeString(tokiState.searchQuery);

        let visibleGenres = new Set();
        let matchCount = 0; // Track matched card counts

        const evaluateFilter = (f, isUnread, isReading, isCompleted, cardGenres) => {
            if (f.type === 'state') {
                if (f.val === 'unread') return isUnread;
                if (f.val === 'reading') return isReading;
                if (f.val === 'ongoing') return !isCompleted;
                if (f.val === 'completed') return isCompleted;
            }
            return cardGenres.includes(f.val);
        };

        const filters = [
            ...states.map(s => ({ type: 'state', val: s })),
            ...genres.map(g => ({ type: 'genre', val: g }))
        ];

        cards.forEach(card => {
            const isCompleted = card.dataset.status === 'completed';
            const isReading = card.dataset.reading === 'true';
            const isUnread = card.dataset.reading === 'false';
            const cardGenres = card.dataset.genres ? card.dataset.genres.split(' ') : [];

            const cardTitleNormalized = normalizeString(card.dataset.title);
            const cardAuthorNormalized = normalizeString(card.dataset.author);
            const matchesSearch = !searchQueryNormalized || cardTitleNormalized.includes(searchQueryNormalized) || cardAuthorNormalized.includes(searchQueryNormalized);

            const finalMatch = filters.length === 0 || (
                tokiState.filterMode === 'AND'
                    ? filters.every(f => evaluateFilter(f, isUnread, isReading, isCompleted, cardGenres))
                    : filters.some(f => evaluateFilter(f, isUnread, isReading, isCompleted, cardGenres))
            );

            if (finalMatch && matchesSearch) {
                card.style.display = 'flex';
                cardGenres.forEach(g => { if(g) visibleGenres.add(g); });
                matchCount++;
            } else {
                card.style.display = 'none';
            }
        });

        // Toggle "No Search Results" state UI
        const emptyState = document.getElementById('toki-search-empty');
        if (emptyState) {
            emptyState.style.display = matchCount === 0 ? 'block' : 'none';
        }

        const btnGenre = document.getElementById('btnFilterGenre');
        if (btnGenre) {
            if (genres.length > 0) {
                btnGenre.classList.add('active');
                btnGenre.innerHTML = `장르 선택 (${genres.length}) ▾`;
            } else {
                btnGenre.classList.remove('active');
                btnGenre.innerHTML = `장르 선택 ▾`;
            }
        }

        document.querySelectorAll('.toki-genre-item').forEach(item => {
            const val = item.dataset.val;
            if (!visibleGenres.has(val) && !tokiState.filters.genres.includes(val)) {
                item.style.display = 'none';
            } else {
                item.style.display = 'flex';
            }
        });
    }

    async function renderFavoritesPage() {
        if (location.pathname !== '/favorites') return;
        if (tokiState.isScanning) return;
        const mainContainer = document.querySelector('main.container');
        if (!mainContainer) return;

        const emptyState = document.querySelector('.ep-empty');
        if (emptyState && emptyState.style.display !== 'none') emptyState.style.display = 'none';
        if (document.getElementById('toki-sync-loading')) return;

        // Auto PULL sync
        if (GistSync.getMode() && !tokiState.isSyncing) {
            tokiState.isSyncing = true;
            await GistSync.checkAndRecoverSync();
            const needPull = await GistSync.checkNeedPull();
            if (needPull) {
                TokiUI.toast('☁️ 새로운 데이터가 확인되어 동기화합니다...');
                const success = await GistSync.pull();
                if (success) {
                    TokiUI.toast('✅ 동기화 완료!', 'success');
                    localStorage.setItem(DIRTY_KEY, '1');
                    renderFavoritesList();
                }
            }
            tokiState.isSyncing = false;
        }

        const dirtyCount = parseInt(localStorage.getItem(DIRTY_KEY) || '-1');
        if (document.getElementById('my-local-bookmarks') && dirtyCount === 0) {
            localStorage.setItem('toki_fav_last_viewed', Date.now());
            return;
        }
        localStorage.setItem(DIRTY_KEY, '0');
        localStorage.setItem('toki_fav_last_viewed', Date.now());

        document.querySelector('.page-top h1').innerHTML = '<span class="em" style="color:#ff4757;">★</span>로컬 북마크';
        const pageDesc = document.querySelector('.page-top .desc');
        if(pageDesc) pageDesc.innerText = '개인정보 보호를 위해 기기에 안전하게 저장된 트래커입니다.';

        renderFavoritesList();
    }

    function renderFavoritesList() {
        const lastFavViewAt = parseInt(localStorage.getItem('toki_fav_last_viewed') || '0');
        const ONE_DAY = 24 * 60 * 60 * 1000;

        const mainContainer = document.querySelector('main.container');
        let wrapper = document.getElementById(ID_BOOKMARKS_WRAPPER);
        if (!wrapper) {
            wrapper = document.createElement('div');
            wrapper.id = ID_BOOKMARKS_WRAPPER;
            if (isMobile) wrapper.classList.add('toki-mobile');
            else wrapper.classList.add('toki-pc');
            wrapper.style.maxWidth = isMobile ? '100%' : '1200px';
            mainContainer.appendChild(wrapper);
        }

        const allMarks = getBookmarks();
        const counts = { all: 0, manhwa: 0, webtoon: 0, novel: 0 };
        const upCounts = { all: 0, manhwa: 0, webtoon: 0, novel: 0 };

        const listByCategory = { manhwa: [], webtoon: [], novel: [] };
        for (const cat of ['manhwa', 'webtoon', 'novel']) {
            const list = Object.values(allMarks[cat] || {});
            counts[cat] = list.length;
            counts.all += list.length;

            list.forEach(m => {
                m.category = cat; // Category tag
                const lastInteraction = m.lastReadAt || m.addedAt || 0;
                const updateTime = m.exactUpdatedAt || m.parsedUpdatedAt || m.latestUpdatedAt || m.addedAt || 0;
                const isRead = !!m.lastReadEp;

                if (isRead) {
                    m.hasNewUp = updateTime > lastInteraction && m.latestEp !== m.lastReadEp;
                } else {
                    const hasSeenUpdate = lastFavViewAt > updateTime;
                    const isOneDayOld = (Date.now() - updateTime) >= ONE_DAY;
                    if (updateTime > lastInteraction) {
                        m.hasNewUp = !(hasSeenUpdate && isOneDayOld);
                    } else {
                        m.hasNewUp = false;
                    }
                }
                m.isClr = m.latestEp && m.latestEp === m.lastReadEp && !m.hasNewUp;

                if (m.hasNewUp) {
                    upCounts[cat]++;
                    upCounts.all++;
                }
                listByCategory[cat].push(m);
            });
        }

        // Active tab filtering
        let markList = listByCategory[tokiState.activeTab] || [];

        // Sort configuration
        const currentSort = getSortPref();
        const pinUpMode = localStorage.getItem(PIN_UP_KEY) !== 'false';
        const clrKeyVal = localStorage.getItem(PIN_CLR_KEY);
        const pinClrMode = clrKeyVal === null ? true : (clrKeyVal === 'true');
        const sortAsc = localStorage.getItem(SORT_ASC_KEY) === 'true';

        markList.sort((a, b) => {
            if (pinUpMode) {
                if (a.hasNewUp && !b.hasNewUp) return -1;
                if (!a.hasNewUp && b.hasNewUp) return 1;
            }
            if (pinClrMode) {
                if (a.isClr && !b.isClr) return 1;
                if (!a.isClr && b.isClr) return -1;
            }

            const getSortTime = (x) => x.exactUpdatedAt || x.parsedUpdatedAt || x.latestUpdatedAt || x.addedAt || 0;

            let cmp = 0;
            if (currentSort === 'siteUpdateDesc') {
                cmp = getSortTime(a) - getSortTime(b);
            } else if (currentSort === 'readDesc') {
                cmp = (a.lastReadAt || 0) - (b.lastReadAt || 0);
            } else if (currentSort === 'addedDesc') {
                cmp = (a.addedAt || 0) - (b.addedAt || 0);
            } else if (currentSort === 'titleAsc') {
                cmp = b.title.localeCompare(a.title);
            }
            return sortAsc ? cmp : -cmp;
        });

        // Genres extraction from filtered list
        const allGenres = new Set();
        markList.forEach(m => { if(m.genres) m.genres.split(' ').forEach(g => { if(g) allGenres.add(g); }) });
        const sortedGenres = Array.from(allGenres).sort();

        // Calculate active tab stats
        const stats = { total: markList.length, ongoing: 0, completed: 0, unread: 0, reading: 0 };
        markList.forEach(m => {
            if (m.status === 'completed') stats.completed++; else stats.ongoing++;
            if (m.lastReadEp) stats.reading++; else stats.unread++;
        });

        // Stats grid
        let html = `
            <div class="toki-stats-grid">
                <div class="toki-stat-item total"><span class="label">📚 전체</span><span class="value">${stats.total}</span></div>
                <div class="toki-stat-item ongoing"><span class="label">▶ 연재중</span><span class="value" style="color:#0984e3">${stats.ongoing}</span></div>
                <div class="toki-stat-item completed"><span class="label">🏁 완결</span><span class="value" style="color:#e1b12c">${stats.completed}</span></div>
                <div class="toki-stat-item reading"><span class="label">📖 읽는중</span><span class="value" style="color:#00b894">${stats.reading}</span></div>
                <div class="toki-stat-item unread"><span class="label">📦 안읽음</span><span class="value" style="color:#ff7675">${stats.unread}</span></div>
            </div>
        `;

        // Premium Category Tabs UI
        html += `
            <div class="toki-category-tabs">
                <button class="toki-tab-btn ${tokiState.activeTab === 'manhwa' ? 'active' : ''}" data-category="manhwa">
                    🌸 만화 ${upCounts.manhwa > 0 ? `<span class="tab-badge has-up">${upCounts.manhwa}</span>` : ''}
                </button>
                <button class="toki-tab-btn ${tokiState.activeTab === 'webtoon' ? 'active' : ''}" data-category="webtoon">
                    ⚡ 웹툰 ${upCounts.webtoon > 0 ? `<span class="tab-badge has-up">${upCounts.webtoon}</span>` : ''}
                </button>
                <button class="toki-tab-btn ${tokiState.activeTab === 'novel' ? 'active' : ''}" data-category="novel">
                    ✍️ 소설 ${upCounts.novel > 0 ? `<span class="tab-badge has-up">${upCounts.novel}</span>` : ''}
                </button>
            </div>
        `;

        if (isMobile) {
            // [Mobile UI Template]
            html += `
                <div class="toki-control-bar" style="margin-bottom:10px;">
                    <div class="toki-search-wrap">
                        <span class="toki-search-icon">🔍</span>
                        <input type="text" id="toki-search-input" placeholder="제목/작가 검색..." value="${tokiState.searchQuery || ''}" autocomplete="off">
                        <button id="toki-search-clear" class="toki-search-clear" style="display: ${tokiState.searchQuery ? 'flex' : 'none'};">✕</button>
                    </div>
                    <div class="toki-filter-bar">
                        <button id="btnFilterReset" class="toki-filter-btn reset">↺ 초기화</button>
                        <button id="btnFilterMode" class="toki-mode-btn ${tokiState.filterMode === 'OR' ? 'or-mode' : ''}">${tokiState.filterMode} 🔄</button>
                        <div class="toki-dropdown">
                            <button id="btnFilterGenre" class="toki-filter-btn">장르 선택 ▾</button>
                            <div id="genreDropdown" class="toki-dropdown-menu">
                                ${sortedGenres.map(g => `<div class="toki-genre-item" data-val="${g}"><span class="chk">✓</span> ${g}</div>`).join('')}
                                <div class="toki-mobile-close" id="btnCloseGenre">선택 완료 및 닫기</div>
                            </div>
                        </div>
                        <button class="toki-filter-btn state-toggle" data-val="unread"><div class="chk"></div>안읽음</button>
                        <button class="toki-filter-btn state-toggle" data-val="reading"><div class="chk"></div>읽는중</button>
                        <button class="toki-filter-btn state-toggle" data-val="ongoing"><div class="chk"></div>연재</button>
                        <button class="toki-filter-btn state-toggle" data-val="completed"><div class="chk"></div>완결</button>
                    </div>
                    <div class="toki-sort-row">
                        <div class="toki-sort-main">
                            <select id="sortSelect">
                                <option value="siteUpdateDesc" ${currentSort === 'siteUpdateDesc' ? 'selected' : ''}>최근 업데이트순</option>
                                <option value="readDesc" ${currentSort === 'readDesc' ? 'selected' : ''}>최근 열람순</option>
                                <option value="addedDesc" ${currentSort === 'addedDesc' ? 'selected' : ''}>최근 등록순</option>
                                <option value="titleAsc" ${currentSort === 'titleAsc' ? 'selected' : ''}>가나다순</option>
                            </select>
                            <button id="sortDirBtn" class="toki-btn">
                                ${sortAsc ? '🔼 오름차순' : '🔽 내림차순'}
                            </button>
                        </div>
                        <div class="toki-pin-group">
                            <label class="toki-pin-toggle">
                                <input type="checkbox" id="pinUpCheck" ${pinUpMode ? 'checked' : ''}> 📌 UP 고정
                            </label>
                            <label class="toki-pin-toggle clr-pin">
                                <input type="checkbox" id="pinClrCheck" ${pinClrMode ? 'checked' : ''}> ⬇️ CLR 고정
                            </label>
                        </div>
                    </div>
                    <div class="toki-sync-group">
                        <div class="toki-action-row">
                            <button id="importDataBtn" class="btn-import">가져오기</button>
                            <button id="exportDataBtn" class="btn-export">내보내기</button>
                        </div>
                        <div class="toki-action-row">
                            <button id="syncSettingsBtn">☁️ 웹 동기화</button>
                            <button id="checkUpdatesBtn">🔄 스마트 스캔</button>
                        </div>
                    </div>
                </div>
            `;
        } else {
            // [PC UI Template]
            html += `
                <div class="toki-control-bar" style="margin-bottom:10px;">
                    <div class="toki-filter-bar">
                        <button id="btnFilterReset" class="toki-filter-btn reset">↺ 초기화</button>
                        <button id="btnFilterMode" class="toki-mode-btn ${tokiState.filterMode === 'OR' ? 'or-mode' : ''}">${tokiState.filterMode} 🔄</button>
                        <div class="toki-dropdown">
                            <button id="btnFilterGenre" class="toki-filter-btn">장르 선택 ▾</button>
                            <div id="genreDropdown" class="toki-dropdown-menu">
                                ${sortedGenres.map(g => `<div class="toki-genre-item" data-val="${g}"><span class="chk">✓</span> ${g}</div>`).join('')}
                            </div>
                        </div>
                        <button class="toki-filter-btn state-toggle" data-val="unread"><div class="chk"></div>안읽음</button>
                        <button class="toki-filter-btn state-toggle" data-val="reading"><div class="chk"></div>읽는중</button>
                        <button class="toki-filter-btn state-toggle" data-val="ongoing"><div class="chk"></div>연재</button>
                        <button class="toki-filter-btn state-toggle" data-val="completed"><div class="chk"></div>완결</button>
                    </div>
                    <div style="flex-grow:1"></div>
                    <div class="toki-search-wrap">
                        <span class="toki-search-icon">🔍</span>
                        <input type="text" id="toki-search-input" placeholder="제목/작가 검색..." value="${tokiState.searchQuery || ''}" autocomplete="off">
                        <button id="toki-search-clear" class="toki-search-clear" style="display: ${tokiState.searchQuery ? 'flex' : 'none'};">✕</button>
                    </div>
                </div>
                <div class="toki-control-bar" style="margin-top:0;">
                    <div class="toki-dropdown">
                        <button id="btnSortSelect" class="toki-filter-btn" style="min-width:140px; justify-content:space-between;">
                            ${({siteUpdateDesc:'최근 업데이트순', readDesc:'최근 열람순', addedDesc:'최근 등록순', titleAsc:'가나다순'})[currentSort]} ▾
                        </button>
                        <div id="sortDropdown" class="toki-dropdown-menu" style="width:160px; grid-template-columns: 1fr;">
                            <div class="toki-sort-item ${currentSort === 'siteUpdateDesc' ? 'active' : ''}" data-val="siteUpdateDesc">최근 업데이트순</div>
                            <div class="toki-sort-item ${currentSort === 'readDesc' ? 'active' : ''}" data-val="readDesc">최근 열람순</div>
                            <div class="toki-sort-item ${currentSort === 'addedDesc' ? 'active' : ''}" data-val="addedDesc">최근 등록순</div>
                            <div class="toki-sort-item ${currentSort === 'titleAsc' ? 'active' : ''}" data-val="titleAsc">가나다순</div>
                        </div>
                    </div>
                    <button id="sortDirBtn" class="toki-btn" style="padding:6px 10px; font-size:12px; background:var(--toki-bg); border:1px solid var(--toki-filter-border); color:var(--toki-text); margin:0 5px;">
                        ${sortAsc ? '🔼 오름차순' : '🔽 내림차순'}
                    </button>
                    <label class="toki-pin-toggle" style="margin-right:5px;">
                        <input type="checkbox" id="pinUpCheck" ${pinUpMode ? 'checked' : ''}> 📌 UP 고정
                    </label>
                    <label class="toki-pin-toggle clr-pin">
                        <input type="checkbox" id="pinClrCheck" ${pinClrMode ? 'checked' : ''}> ⬇️ CLR 고정
                    </label>
                    <div style="flex-grow:1"></div>
                    <div class="toki-sync-group" style="display:flex; gap:8px;">
                        <button id="syncSettingsBtn" style="padding:8px 12px; background:#4b6584; color:white; border:none; border-radius:6px; font-weight:bold; cursor:pointer; font-size:12px;">☁️ 웹 동기화</button>
                        <button id="importDataBtn" class="btn-import" style="padding:8px 12px; background:#2d3436; color:white; border:none; border-radius:6px; font-weight:bold; cursor:pointer; font-size:12px;">가져오기</button>
                        <button id="exportDataBtn" class="btn-export" style="padding:8px 12px; background:#2d3436; color:white; border:none; border-radius:6px; font-weight:bold; cursor:pointer; font-size:12px;">내보내기</button>
                        <button id="checkUpdatesBtn" style="padding:8px 16px; background:#0984e3; color:white; border:none; border-radius:6px; font-weight:bold; cursor:pointer; box-shadow:0 2px 5px rgba(9, 132, 227, 0.3);">🔄 스마트 스캔</button>
                    </div>
                </div>
            `;
        }

        // Top-level container wrapper to prevent layout collapse during category switching or filtering
        html += `<div style="min-height: 85vh;">`;

        if (markList.length === 0) {
            html += `<div style="text-align:center; padding:80px 0; background:var(--toki-empty-bg); border-radius:12px; border:1px dashed var(--toki-border-dashed);">
                <div style="font-size:50px; margin-bottom:15px; opacity:0.5;">📁</div>
                <div style="font-size:18px; font-weight:bold; color:var(--toki-text-sub);">표시할 북마크가 비어있습니다.</div>
            </div>`;
        } else {
            // Remove min-height from the list wrapper to allow the empty state to float up when the list is hidden
            html += `<div id="toki-card-list-wrap" style="display:flex; flex-direction:column;">`;
            markList.forEach(m => {
                const badgeHtml = m.hasNewUp ? `<span class="toki-up-badge">UP</span>` : (m.isClr ? `<span class="toki-clr-badge">CLR</span>` : '');
                const genreHtml = m.genres ? `<div class="toki-genre">${m.genres}</div>` : '';
                const authorHtml = m.author ? `<div class="toki-author">✍️ ${m.author}</div>` : '';
                const displayTime = m.exactUpdatedAt || m.parsedUpdatedAt;
                const siteDateText = displayTime ? `(${formatDateTime(displayTime)})` : (m.latestDate ? `(${m.latestDate})` : '');
                const readDateText = m.lastReadAt ? `(${formatDateTime(m.lastReadAt)})` : '';
                const statusBadge = m.status === 'completed'
                    ? `<span style="font-size:10px; background:#e1b12c; color:white; border-radius:4px; padding:0 5px; height:18px; display:inline-flex; align-items:center; justify-content:center; font-weight:800; line-height:1; flex-shrink:0;">완결</span>`
                    : `<span style="font-size:10px; background:#0984e3; color:white; border-radius:4px; padding:0 5px; height:18px; display:inline-flex; align-items:center; justify-content:center; font-weight:800; line-height:1; flex-shrink:0;">연재</span>`;

                let actionBtns = '';
                if (!m.lastReadEpLink) {
                    actionBtns = `<a href="${m.firstEpLink || m.url}" class="toki-btn toki-btn-main" style="background:#00b894; color:white;">첫화부터 보기</a>`;
                } else if (m.nextEpLink) {
                    const nextText = m.nextEp === '다음화' ? '다음화 보기' : `다음화 보기 (${m.nextEp})`;
                    actionBtns = `
                        <a href="${m.lastReadEpLink}" class="toki-btn toki-btn-main" style="background:#0984e3; color:white;">이어보기 (${m.lastReadEp})</a>
                        <a href="${m.nextEpLink}" class="toki-btn toki-btn-sub" style="background:#ff4757; color:white;">${nextText}</a>
                    `;
                } else if (m.latestEp !== m.lastReadEp) {
                    actionBtns = `
                        <a href="${m.lastReadEpLink}" class="toki-btn toki-btn-main" style="background:#0984e3; color:white;">이어보기 (${m.lastReadEp})</a>
                        <a href="${m.latestEpLink}" class="toki-btn toki-btn-sub" style="background:#ff4757; color:white;">최신화 보기 (${m.latestEp})</a>
                    `;
                } else {
                    actionBtns = `
                        <a href="${m.lastReadEpLink}" class="toki-btn toki-btn-main" style="background:#0984e3; color:white;">이어보기 (${m.lastReadEp})</a>
                        <a href="javascript:void(0);" class="toki-btn toki-btn-sub" style="background:var(--toki-done-bg); color:var(--toki-done-text); cursor:default;">정주행 완료</a>
                    `;
                }

                html += `
                <div class="toki-card" id="bookmark-${m.id}" data-title="${m.title.replace(/"/g, '&quot;')}" data-author="${m.author ? m.author.replace(/"/g, '&quot;') : ''}" data-status="${m.status}" data-reading="${m.lastReadEp != null}" data-genres="${m.genres}" style="${m.hasNewUp ? 'background:var(--toki-highlight-bg); border-color:var(--toki-highlight-border);' : ''}">
                    <a class="toki-card-top" href="${m.url}">
                        <div class="toki-thumb">
                            <img src="${m.thumb}" alt="thumb">
                        </div>
                        <div class="toki-info">
                            <div class="toki-title">
                                ${statusBadge}
                                <span class="title-text" style="word-break:keep-all;">${m.title}</span>
                                ${badgeHtml}
                            </div>
                            ${genreHtml}
                            ${authorHtml}
                            <div class="toki-meta-box">
                                <div class="toki-meta-row">📡 업데이트: <strong style="color:#ff4757; margin-left:5px;">${m.latestEp || '없음'}</strong> <span class="toki-date-sub">${siteDateText}</span></div>
                                <div class="toki-meta-row">👀 마지막 열람: <strong style="color:#00b894; margin-left:5px;">${m.lastReadEp || '기록없음'}</strong> <span class="toki-date-sub">${readDateText}</span></div>
                            </div>
                        </div>
                    </a>
                    <div class="toki-actions">
                        ${actionBtns}
                        <a href="javascript:void(0);" data-id="${m.id}" data-category="${m.category}" class="toki-btn toki-btn-del toki-del-btn">삭제</a>
                    </div>
                </div>`;
            });
            html += `</div>`;
            
            // No Search Results state UI (placed below list; displays when list has display: none)
            html += `<div id="toki-search-empty" style="display:none; text-align:center; padding:80px 0; background:var(--toki-empty-bg); border-radius:12px; border:1px dashed var(--toki-border-dashed);">
                <div style="font-size:50px; margin-bottom:15px; opacity:0.5;">🔍</div>
                <div style="font-size:18px; font-weight:bold; color:var(--toki-text-sub);">검색 결과가 없습니다.</div>
            </div>`;
        }

        html += `</div>`; // Close top-level container wrapper
        wrapper.innerHTML = html;

        // Event Delegation for deletion buttons (Sandbox safe)
        if (!wrapper.dataset.listener) {
            wrapper.addEventListener('click', async (e) => {
                const delBtn = e.target.closest('.toki-del-btn');
                if (delBtn) {
                    const id = delBtn.getAttribute('data-id');
                    const category = delBtn.getAttribute('data-category');
                    await removeTokiLocalBookmark(id, category, e);
                }
            });
            wrapper.dataset.listener = 'true';
        }

        applyFilters(); // Apply filter states immediately

        // Tab click bindings
        document.querySelectorAll('.toki-tab-btn').forEach(btn => {
            btn.addEventListener('click', function() {
                tokiState.activeTab = this.dataset.category;
                localStorage.setItem('toki_fav_last_tab', tokiState.activeTab);
                renderFavoritesList();
            });
        });

        // Search Bar Event Handlers
        const searchInput = document.getElementById('toki-search-input');
        const searchClear = document.getElementById('toki-search-clear');

        if (searchInput && searchClear) {
            // [Core Fix] Stop site-native keyboard shortcuts/scroll behaviors from intercepting input entries
            const preventSiteInterference = (e) => e.stopPropagation();
            searchInput.addEventListener('keydown', (e) => {
                preventSiteInterference(e);
                if (e.key === 'Enter') e.target.blur();
            });
            searchInput.addEventListener('keyup', preventSiteInterference);
            searchInput.addEventListener('keypress', preventSiteInterference);

            searchInput.addEventListener('input', (e) => {
                preventSiteInterference(e);
                tokiState.searchQuery = e.target.value;
                searchClear.style.display = tokiState.searchQuery ? 'flex' : 'none';
                
                // Prevent page jumps by preserving scroll position during filtering
                const currentScrollY = window.scrollY;
                applyFilters();
                window.scrollTo(0, currentScrollY);
            });

            searchClear.addEventListener('click', (e) => {
                e.preventDefault();
                searchInput.value = '';
                tokiState.searchQuery = '';
                searchClear.style.display = 'none';
                
                const currentScrollY = window.scrollY;
                applyFilters();
                window.scrollTo(0, currentScrollY);
                
                if (!isMobile) searchInput.focus();
            });
        }

        // Filters and event configurations
        const btnFilterMode = document.getElementById('btnFilterMode');
        btnFilterMode.addEventListener('click', () => {
            tokiState.filterMode = tokiState.filterMode === 'AND' ? 'OR' : 'AND';
            btnFilterMode.innerText = `${tokiState.filterMode} 🔄`;
            if (tokiState.filterMode === 'OR') btnFilterMode.classList.add('or-mode');
            else btnFilterMode.classList.remove('or-mode');
            applyFilters();
        });

        document.querySelectorAll('.state-toggle').forEach(btn => {
            const val = btn.dataset.val;
            if (tokiState.filters.states.includes(val)) btn.classList.add('active');
            btn.addEventListener('click', function() {
                this.classList.toggle('active');
                if (this.classList.contains('active')) tokiState.filters.states.push(val);
                else tokiState.filters.states = tokiState.filters.states.filter(v => v !== val);
                applyFilters();
            });
        });

        const genreDropdown = document.getElementById('genreDropdown');
        const genreBtn = document.getElementById('btnFilterGenre');
        genreBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            genreDropdown.classList.toggle('show');
        });

        const btnCloseGenre = document.getElementById('btnCloseGenre');
        if (btnCloseGenre) {
            btnCloseGenre.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                genreDropdown.classList.remove('show');
            });
        }

        document.querySelectorAll('.toki-genre-item').forEach(item => {
            const val = item.dataset.val;
            if (tokiState.filters.genres.includes(val)) item.classList.add('active');

            item.addEventListener('click', (e) => {
                e.stopPropagation();
                item.classList.toggle('active');
                const isActive = item.classList.contains('active');
                if (isActive && !tokiState.filters.genres.includes(val)) tokiState.filters.genres.push(val);
                else if (!isActive) tokiState.filters.genres = tokiState.filters.genres.filter(v => v !== val);
                applyFilters();
            });
        });

        document.getElementById('btnFilterReset').addEventListener('click', () => {
            tokiState.filters = { states: [], genres: [] };
            tokiState.searchQuery = '';
            const sInput = document.getElementById('toki-search-input');
            const sClear = document.getElementById('toki-search-clear');
            if (sInput) sInput.value = '';
            if (sClear) sClear.style.display = 'none';
            document.querySelectorAll('.state-toggle').forEach(b => b.classList.remove('active'));
            document.querySelectorAll('.toki-genre-item').forEach(b => b.classList.remove('active'));
            applyFilters();
        });

        // PC custom sort dropdown
        const sortDropdown = document.getElementById('sortDropdown');
        const sortBtn = document.getElementById('btnSortSelect');
        if (sortBtn && sortDropdown) {
            sortBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                sortDropdown.classList.toggle('show');
            });
            document.querySelectorAll('.toki-sort-item').forEach(item => {
                item.addEventListener('click', () => {
                    saveSortPref(item.dataset.val);
                    renderFavoritesList();
                });
            });
        }

        const sortSelect = document.getElementById('sortSelect');
        if (sortSelect) sortSelect.addEventListener('change', function() { saveSortPref(this.value); renderFavoritesList(); });
        document.getElementById('sortDirBtn').addEventListener('click', function() { localStorage.setItem(SORT_ASC_KEY, !sortAsc); renderFavoritesList(); });
        document.getElementById('pinUpCheck').addEventListener('change', function() { localStorage.setItem(PIN_UP_KEY, this.checked); renderFavoritesList(); });
        document.getElementById('pinClrCheck').addEventListener('change', function() { localStorage.setItem(PIN_CLR_KEY, this.checked); renderFavoritesList(); });
        document.getElementById('syncSettingsBtn').addEventListener('click', () => GistSync.showSettingsModal());
        document.getElementById('checkUpdatesBtn').addEventListener('click', (e) => {
            const isForce = e.shiftKey;
            if (isForce) {
                localStorage.removeItem(SCAN_QUEUE_KEY);
                runBackgroundScan('force');
            } else {
                localStorage.removeItem(SCAN_QUEUE_KEY);
                runBackgroundScan('smart');
            }
        });
        document.getElementById('exportDataBtn').addEventListener('click', exportData);
        document.getElementById('importDataBtn').addEventListener('click', importData);
    }

    // ==========================================
    // 6. SPA Routing Observer
    // ==========================================
    let currentPath = location.pathname;

    const observer = new MutationObserver(() => {
        clearTimeout(tokiState.debounceTimer);
        tokiState.debounceTimer = setTimeout(() => {
            const newPath = location.pathname;
            const isDetail = /^\/(manhwa|webtoon|novel)\/[^/]+$/i.test(newPath);

            if (newPath !== currentPath) {
                currentPath = newPath;
                injectViewerUI();
                if (isDetail) injectManhwaButton();
                if (newPath === '/favorites') renderFavoritesPage();
                return;
            }

            if (isViewerPage()) {
                if (!isViewerUIApplied()) injectViewerUI();
            } else if (isDetail) {
                if (!document.getElementById(ID_MANHWA_BTN)) injectManhwaButton();
            } else if (newPath === '/favorites') {
                const dirty = localStorage.getItem(DIRTY_KEY);
                if (!document.getElementById(ID_BOOKMARKS_WRAPPER) || dirty !== '0') {
                    renderFavoritesPage();
                }
            }
        }, 150);
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Close dropdowns when clicking outside
    document.addEventListener('click', (e) => {
        const genreDropdown = document.getElementById('genreDropdown');
        const genreBtn = document.getElementById('btnFilterGenre');
        const sortDropdown = document.getElementById('sortDropdown');
        const sortBtn = document.getElementById('btnSortSelect');

        if (genreDropdown && genreBtn && !genreDropdown.contains(e.target) && e.target !== genreBtn) {
            genreDropdown.classList.remove('show');
        }
        if (sortDropdown && sortBtn && !sortDropdown.contains(e.target) && e.target !== sortBtn) {
            sortDropdown.classList.remove('show');
        }
    });

    // Run emergency recovery and periodic backup before initializing layout
    TokiSafetyGuard.performEmergencyRecovery();

    injectViewerUI();
    if (/^\/(manhwa|webtoon|novel)\/[^/]+$/i.test(location.pathname)) injectManhwaButton();
    if (location.pathname === '/favorites') renderFavoritesPage();

    // Trigger Automatic Background Scan on page load
    runBackgroundScan('auto');

    // ==========================================
    // 🧪 Test Automation Bridge for Test Suite
    // ==========================================
    if (typeof window !== 'undefined') {
        window.tokiState = tokiState;
        window.renderFavoritesList = renderFavoritesList;
        window.renderFavoritesPage = renderFavoritesPage;
        window.injectViewerUI = injectViewerUI;
        window.getBookmarks = getBookmarks;
        window.saveBookmarks = saveBookmarks;
        window.extractShortEp = extractShortEp;
        window.TokiUtils = TokiUtils;
        window.TokiSafetyGuard = TokiSafetyGuard;
    }
    window.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'hidden' && typeof GistSync !== 'undefined' && GistSync.getMode()) {
            if (localStorage.getItem('toki_unpushed_changes') === 'true') {
                GistSync.push(true);
            }
        }
    });
})();