토끼 마커

비로그인 상태에서 뉴토끼의 북마크를 로컬스토리지 및 Gist와 연동하여 관리하는 스크립트입니다.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name           토끼 마커
// @name:ko        토끼 마커
// @name:en        Toki Marker
// @version        260524022100
// @description    비로그인 상태에서 뉴토끼의 북마크를 로컬스토리지 및 Gist와 연동하여 관리하는 스크립트입니다.
// @description:ko 비로그인 상태에서 뉴토끼의 북마크를 로컬스토리지 및 Gist와 연동하여 관리하는 스크립트입니다.
// @description:en Manage Newtoki bookmarks without logging in, syncing them with localStorage and GitHub Gist.
// @author         gemini
// @license        MIT
// @include        *://sbxh*.com/*
// @include        *://*.sbxh*.com/*
// @icon           https://www.google.com/s2/favicons?sz=64&domain=newtoki.com
// @grant          GM_xmlhttpRequest
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function() {
    'use strict';

    const isTokiSite = /^(.*\.)?(sbxh\d*|newtoki\d*|manatoki\d*)\.(com|net)$/i.test(location.hostname);
    if (!isTokiSite) return;

    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';
    const SORT_ASC_KEY = 'toki_sort_asc_v10';
    const SCAN_QUEUE_KEY = 'toki_scan_queue_v20';
    const SYNC_AT_KEY = 'toki_last_sync_at_v10';
    const REMOTE_AT_KEY = 'toki_last_remote_at_v10';
    const DIRTY_KEY = 'toki_dirty_count';
    
    let pushDebounceTimer = null;
    const syncChannel = typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel('toki_update_sync_v20') : null;

    const ID_BOOKMARKS_WRAPPER = 'my-local-bookmarks';
    const ID_MANHWA_BTN = 'my-local-btn';
    const ID_VIEWER_UI = 'toki-go-bookmark';

    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
    };

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

    const TokiUtils = {
        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) => {
            if (!data || typeof data !== 'object' || Array.isArray(data)) {
                return { manhwa: {}, webtoon: {}, novel: {} };
            }

            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;
                    const cat = TokiUtils.getCategoryFromUrl(item.url || '');
                    migrated[cat][id] = item;
                    migratedCount++;
                }

                const originalKeysCount = Object.keys(data).length;
                if (migratedCount === 0 && originalKeysCount > 0) {
                    for (const [id, item] of Object.entries(data)) {
                        if (item && typeof item === 'object') {
                            migrated.manhwa[id] = item;
                        }
                    }
                }
                return migrated;
            }

            return {
                manhwa: data.manhwa || {},
                webtoon: data.webtoon || {},
                novel: data.novel || {}
            };
        }
    };

    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);

                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 });
                    if (typeof TokiUI !== 'undefined') {
                        TokiUI.toast('💡 이전 버전의 북마크가 안전하게 복구되었습니다.', 'success');
                    }
                }

                const activeData = localStorage.getItem(currentKey);
                if (activeData && activeData !== '{}' && activeData !== '{"manhwa":{},"webtoon":{},"novel":{}}') {
                    localStorage.setItem(backupKey, activeData);
                }
            } catch (e) {
                console.error('Emergency recovery failed:', e);
            }
        },

        validateSchema: function(data) {
            if (!data || typeof data !== 'object') return false;
            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;
        }
    };

    const style = document.createElement('style');
    style.innerHTML = `
        :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); }

        .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; }

        .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); }

        .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; }
        .toki-pc .toki-btn-sub { flex: 1; }
        .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; }

        .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; }

        .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; }
        .toki-title { display: flex; align-items: center; gap: 6px; word-break: keep-all; line-height: 1.4; }
        .toki-title .title-text { flex: 1; }

        .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;
        }

        .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;
        }

        .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;
        }

        .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);

    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);

                    if (oldKeys.length !== newKeys.length) {
                        hasCoreChanges = true;
                        break;
                    }

                    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('Diff calculation failed:', diffErr);
                hasCoreChanges = true;
            }
        } else if (!oldRaw) {
            hasCoreChanges = true;
        }

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

        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'; 
    }

    // Base64 obfuscation for token fallback safety
    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 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);
            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'
            };
        },

        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'];

            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);

            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 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') {
                        localStorage.removeItem('toki_offline_dirty');
                        await this.push(false);
                    } else {
                        await this.pull();
                    }
                }
            }
        },

        request: function(opts) {
            return new Promise((resolve, reject) => {
                if (typeof GM_xmlhttpRequest === 'undefined') {
                    return reject(new Error('GM_xmlhttpRequest is not available. Check script permissions.'));
                }
                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;
            
            await this.checkAndRecoverSync();
            
            const rate = this.getRateLimit();
            if (this.isFallbackMode() && Date.now() < rate.reset) {
                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('Invalid schema format');
                    }
                } catch(parseErr) {
                    console.error('Broken Gist JSON data:', 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] || {};

                    for (const [key, remoteItem] of Object.entries(remoteCatData)) {
                        const localItem = localCatData[key];
                        if (!localItem) {
                            const remoteTime = TokiUtils.getEffectiveTime(remoteItem);
                            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;
                                }
                            }
                        }
                    }

                    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;
            
            const rate = this.getRateLimit();
            if (this.isFallbackMode() && Date.now() < rate.reset) {
                return false;
            }

            try {
                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);
                return remoteUpdatedAt !== lastRemoteAt;
            } catch (e) {
                console.error('Failed to check remote update status:', e);
                return false;
            }
        },

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

            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 {
                        const needPull = await self.checkNeedPull();
                        if (needPull) {
                            await self.pull();
                        }

                        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 this.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('Failed to get remote counts:', 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;

                    // Safety Guard: Avoid overwriting existing remote backup with 0 local items
                    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('Background push cancelled to protect remote data.');
                            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());
                    localStorage.removeItem('toki_unpushed_changes');

                    if (isManual) TokiUI.toast('✅ 업로드 완료!', 'success');
                } catch(e) {
                    console.error('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() {
            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() || ''}">
                    
                    <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 = '연결 중...';
                    
                    this.setCredentials(tokenInput, null);

                    try {
                        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) {
                            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 {
                                this.disable();
                                btnToggle.innerText = '미연결';
                                TokiUI.toast('연동이 취소되었습니다.');
                            }
                        } else {
                            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('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); 
    }

    // Dynamic SWR Cache TTL calculation
    function getTTL(m) {
        let lastUpdated = m.exactUpdatedAt || m.parsedUpdatedAt || m.latestUpdatedAt || m.addedAt;
        if (!lastUpdated) lastUpdated = Date.now();

        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 precise update timestamps from JSON embedded inside HTML
    function parseExactUpdatedAt(htmlText) {
        try {
            const Q = '[\\\\"]+';
            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]);
                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('작품이 삭제되었습니다.');
            }

            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;
    }

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

            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();
                            
                            customBtn.querySelector('svg').setAttribute('fill', '#ff4757');
                            customBtn.querySelector('span').innerText = '책갈피 완료';
                        };
                    }
                }
            }
        }

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

            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);
            }
        }
    }

    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';

        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);
        }

        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';
            thumbBtn.className = 'fav-heart my-local-fav-heart';
            thumbBtn.style.border = 'none';
            thumbBtn.style.cursor = 'pointer';
            
            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);
        }

        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();

        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') : '';

                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;
    }

    async function runBackgroundScan(scanType = 'auto') {
        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');

                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(' ');

                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('Scan failed for title:', 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 });
        }
    }

    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);
    }

    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;

        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';
            }
        });

        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;

        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;
                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);
            });
        }

        let markList = listByCategory[tokiState.activeTab] || [];

        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;
        });

        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();

        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++;
        });

        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>
        `;

        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) {
            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 {
            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>
            `;
        }

        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 {
            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>`;
            
            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>`;
        wrapper.innerHTML = html;

        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();

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

        const searchInput = document.getElementById('toki-search-input');
        const searchClear = document.getElementById('toki-search-clear');

        if (searchInput && searchClear) {
            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';
                
                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();
            });
        }

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

        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);
    }

    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 });

    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');
        }
    });

    TokiSafetyGuard.performEmergencyRecovery();

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

    runBackgroundScan('auto');

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