Toki Mark

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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