Library Sync for GeForce NOW

Automatically extract games from Epic Games or Amazon Luna and auto-sync them directly to your NVIDIA GeForce NOW library!

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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

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

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Library Sync for GeForce NOW
// @namespace    https://github.com/arpitdev99/geforce-now-library-sync
// @version      3.0.0
// @description  Automatically extract games from Epic Games or Amazon Luna and auto-sync them directly to your NVIDIA GeForce NOW library!
// @author       AV (arpitdev99)
// @license      MIT
// @match        *://www.epicgames.com/account/transactions*
// @match        *://gaming.amazon.com/my-collection*
// @match        *://play.geforcenow.com/*
// @grant        none
// ==/UserScript==

/**
 * Library Sync for GeForce NOW - Automatic Library Sync Script
 * 
 * Hey there! This script automatically pulls your games from Epic Games or Amazon Luna
 * and syncs them directly to your GeForce NOW account.
 * 
 * Library Sync for GeForce NOW | Created by AV
 */

// Configuration Object: Defines global execution parameters.
const CONFIG = {
    // Store priority order (e.g. prioritize Epic over Steam if a game exists on both).
    TARGET_STORE_PRIORITY: ["Epic Games", "Steam", "Xbox", "Ubisoft Connect", "GOG"],
    // Only sync games if the primary store (e.g. Epic Games) is available on GFN. Skips other store variants.
    REQUIRE_PRIMARY_STORE_ONLY: true,
    // Limit how many games to sync at once. Set to 0 to sync your entire library.
    MAX_GAMES_TO_SYNC: 0, 
    // Selection mode for limiting games: 'first', 'last', 'random', 'alphabetical', 'reverse-alphabetical'
    MAX_GAMES_MODE: 'first',
    // Too lazy to extract? Just paste your game names here directly! (e.g. ["Cyberpunk 2077"])
    MANUAL_GAME_LIST: [
        // "Cyberpunk 2077",
    ],

    // Auto-starts the sync immediately upon pasting the script (requires games in MANUAL_GAME_LIST).
    AUTO_START_SYNC_ON_LOAD: false,
    // Skips games you already own on GeForce NOW to save time.
    SKIP_ALREADY_OWNED: true, 
    // Gives games that failed to sync (due to lag or GFN glitches) a second chance at the end.
    AUTO_RETRY_FAILED: true,         

    // Shows the Auto-Sync UI when you visit your Epic Games account page.
    ENABLE_EPIC_EXTRACTION: true,    
    // Shows the Auto-Sync UI when you visit your Amazon Luna library.
    ENABLE_AMAZON_EXTRACTION: false, 
    
    // Amazon Luna specific parameters:
    AMAZON_EXTRACT_LUNA_GAMES: true,  // Extract native Luna catalog.
    AMAZON_EXTRACT_EPIC_GAMES: true,  // grab epic games that are linked to amazon
    AMAZON_EXTRACT_GOG_GAMES: false,        // Grab GOG games (GFN barely supports them, but just in case). ️ 4. Advanced Settings (leave these alone if you don't know what they do)
    ENABLE_ADVANCED_OPTIONS: false, // Default hides the advanced UI settings for normal users
    CUSTOM_NAME_ALIASES: {        // epic names things weirdly sometimes. map them to gfn's actual names here so the search works! "Marvel's Guardians of the Galaxy": "Guardians of the Galaxy",
    },
    DRY_RUN_MODE: false, // Simulates the entire process without actually clicking "Mark as Owned"
    VERBOSE_LOGGING: false, // Dumps raw network and DOM data to the terminal
    HEADLESS_MODE: false,        // Automatically hides the UI while syncing to save screen space how many typos or missing letters we're allowed to ignore when matching titles (0-10)
    FUZZY_MATCH_THRESHOLD: 4, 
    // how long (in seconds) we're willing to wait for gfn's slow popup windows to load
    PANEL_TIMEOUT_SECONDS: 20,       
    // give gfn's search bar 3 seconds to actually load results before we give up
    API_WAIT_TIME_MS: 3000, 
    // delay between clicking "show more" on epic. lower = faster but might break
    EPIC_PAGINATION_DELAY_MS: 300,
    // delay for scrolling down amazon's infinite page
    AMAZON_SCROLL_DELAY_MS: 500,

    // UI Customization
    ENABLE_UI: true,        // Set to false to run completely silently in the console using MANUAL_GAME_LIST where do you want the floating window? (center, top-left, top-right, bottom-left, bottom-right)
    UI_POSITION: "bottom-right", 
    // color for the buttons and text. kept it nvidia green by default.
    UI_THEME_COLOR: "#76B900", 
    // Light mode vs dark mode theme
    LIGHT_MODE: false,
    // should the whole window just delete itself when the sync is completely done?
    UI_AUTO_CLOSE_ON_FINISH: false,
    // automatically save extracted or pasted games to browser local storage
    AUTO_SAVE_GAMES: true
};

const DEFAULT_CONFIG = JSON.parse(JSON.stringify(CONFIG));

(function initAutoSyncPro() {
    const CONSTANTS = {
        STORAGE_KEYS: {
            CONFIG: "_gfn_sync_pro_config",
            TEXTAREA: "_gfn_sync_textarea",
            HISTORY: "_gfn_sync_history",
            BACKUP: "_gfn_sync_backup"
        },
        FILE_NAMES: {
            LOG_TXT: "NVIDIA_Library_Sync_Logs.txt",
            REPORT_JSON: "NVIDIA_Sync_Report.json",
            REPORT_TXT: "NVIDIA_Sync_Report.txt"
        },
        DEFAULT_PRIMARY_STORE: "Epic Games",
        STORES: {
            EPIC: "EPIC",
            STEAM: "STEAM",
            XBOX: "XBOX",
            UBISOFT: "UBISOFT",
            GOG: "GOG"
        },
        EXTERNAL_LINK_KEYWORDS: [
            "BUY ON", "GET ON", "VIEW IN STORE", "STORE PAGE", "VISIT STORE", "PURCHASE"
        ],
        URLS: {
            EPIC_API: '/account/v2/payment/ajaxGetOrderHistory',
            GFN_GRAPHQL: 'graphql',
            GFN_PLAY: 'play.geforcenow.com',
            EPIC_STORE: 'epicgames.com',
            AMAZON_STORE: 'amazon.com',
            GFN_MALL: 'https://play.geforcenow.com/mall/'
        },
        SELECTORS: {
            ROOT: "#gfn-sync-pro-root",
            SETTINGS_PANEL: "#gfn-settings-panel",
            EPIC: { PURCHASED_REGEX: /Purchased[\r\n]+([^\r\n]+)/g, PAGINATION_BTN: "#next-btn" },
            AMAZON: { LOAD_MORE_BTN: "button", ACCORDION_ENTRY: '[data-a-target="accordion_entry"]', TITLE: "h3[title]" },
            GFN: {
                SEARCH_INPUT: "input.search-input",
                GAME_TILE: "gfn-game-tile",
                DIALOG_CLOSE: 'button[aria-label="Close"], button.close-button, .dialog-close',
                BUTTON: "button",
                STORE_CHIPS: "button, [role='button'], mat-chip, .chip, [role='listitem']"
            }
        },
        STRINGS: {
            BTN_DONE: "DONE", BTN_YES: "YES", BTN_CONFIRM: "CONFIRM", BTN_CONTINUE: "CONTINUE", BTN_LOAD_MORE: "Load more",
            STATUS_NOT_OWNED: "NOT_OWNED", API_APPS_PAYLOAD: '"apps"',
            ADD_BUTTON_LABELS: ["MARK AS OWNED", "+ MARK AS OWNED", "GET FREE", "ADD TO LIBRARY", "PLAY"]
        },
        DELAYS: {
            RETRY_BACKOFF_BASE: 1500, RETRY_BACKOFF_MAX: 10000, DIALOG_WAIT: 1500,
            POLL_INTERVAL: 1000, AMAZON_SCROLL: 1500, DOM_SETTLE: 2000, AUTO_START: 1000,
            EPIC_RETRY_RELOAD: 2000, NETWORK_CATCHUP: 3000
        }
    };

    // Reusable utility methods for storage, clipboard, downloads, and DOM safety
    const Utils = {
        safeStorageGet(key, fallback = null) {
            try {
                const val = localStorage.getItem(key);
                return val !== null ? val : fallback;
            } catch (e) {
                return fallback;
            }
        },
        safeStorageSet(key, value) {
            try {
                localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value));
            } catch (e) {}
        },
        safeStorageRemove(key) {
            try {
                localStorage.removeItem(key);
            } catch (e) {}
        },
        safeJsonParse(raw, fallback = null) {
            if (typeof raw !== 'string') return fallback;
            try {
                return JSON.parse(raw);
            } catch (e) {
                return fallback;
            }
        },
        escapeHTML(str) {
            if (typeof str !== 'string') return '';
            return str.replace(/[&<>"']/g, (m) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' }[m]));
        },
        async copyToClipboard(text) {
            if (!text) return false;
            try {
                await navigator.clipboard.writeText(text);
                return true;
            } catch (e) {
                try {
                    const ta = document.createElement("textarea");
                    ta.value = text;
                    ta.style.position = "fixed";
                    ta.style.opacity = "0";
                    document.body.appendChild(ta);
                    ta.focus();
                    ta.select();
                    const ok = document.execCommand("copy");
                    document.body.removeChild(ta);
                    return ok;
                } catch (err) {
                    return false;
                }
            }
        },
        downloadFile(content, filename, mimeType = "text/plain") {
            try {
                const blob = new Blob([content], { type: mimeType });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = filename;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                return true;
            } catch (e) {
                return false;
            }
        },
        getRandomJitterDelay(baseMs) {
            const jitter = CONFIG.ENABLE_DELAY_JITTER !== false ? (Math.random() * 1000 - 500) : 0;
            return Math.max(500, Math.round(baseMs + jitter));
        },
        cleanGameTitle(title) {
            if (!title || typeof title !== 'string') return '';
            let cleaned = title
                .replace(/[™®©]/g, '')
                .replace(/\s*-\s*(Standard|Deluxe|Ultimate|GOTY|Game of the Year|Complete|Gold|Definitive|Collector's|Digital Deluxe)\s*Edition\b/gi, '')
                .replace(/\s*\(?(Standard|Deluxe|Ultimate|GOTY|Game of the Year|Complete|Gold|Definitive|Collector's)\s*Edition\)?/gi, '')
                .replace(/\s*\([^)]*Add-?[O|o]n[^)]*\)/gi, '')
                .replace(/\s*\([^)]*Bonus Content[^)]*\)/gi, '')
                .trim();
            return cleaned || title.trim();
        },
        parseGameListInput(raw) {
            if (!raw || typeof raw !== 'string') return [];
            let trimmed = raw.trim();
            if (!trimmed) return [];
            
            if ((trimmed.startsWith('[') && trimmed.endsWith(']')) || (trimmed.startsWith('{') && trimmed.endsWith('}'))) {
                try {
                    const parsed = JSON.parse(trimmed);
                    if (Array.isArray(parsed)) {
                        return [...new Set(parsed.map(x => (typeof x === 'string' ? x.trim() : (x?.name || x?.title || '')).trim()).filter(Boolean))];
                    }
                } catch(e) {}
            }
            
            const lines = trimmed.split(/[\r\n]+/);
            const result = [];
            
            lines.forEach(line => {
                let text = line.trim();
                if (!text) return;
                text = text.replace(/^[-*•\d+.\s"']+|["',]+$/g, '').trim();
                if (text) result.push(text);
            });
            
            return [...new Set(result)];
        },
        exportBackup(gameList) {
            const data = {
                app: "Library Sync for GeForce NOW",
                exportedAt: new Date().toISOString(),
                totalGames: gameList.length,
                games: gameList
            };
            return this.downloadFile(JSON.stringify(data, null, 2), "GFN_Library_Backup.json", "application/json");
        },
        async importBackup() {
            return new Promise((resolve) => {
                const input = document.createElement("input");
                input.type = "file";
                input.accept = ".json,application/json";
                input.onchange = (e) => {
                    const file = e.target.files[0];
                    if (!file) return resolve(null);
                    const reader = new FileReader();
                    reader.onload = (evt) => {
                        try {
                            const parsed = JSON.parse(evt.target.result);
                            if (parsed && Array.isArray(parsed.games)) resolve(parsed.games);
                            else if (Array.isArray(parsed)) resolve(parsed);
                            else resolve(null);
                        } catch (err) { resolve(null); }
                    };
                    reader.readAsText(file);
                };
                input.click();
            });
        }
    };

    // Scalable UI Component Builders
    const UIComponents = {
        renderToggle(id, label, checked, tooltip = "") {
            return `<label class="gfn-toggle-label" style="width: 48%;"><span title="${Utils.escapeHTML(tooltip)}">${label}</span>
                <input type="checkbox" class="gfn-toggle-input" id="${id}" ${checked ? 'checked' : ''}>
                <div class="gfn-toggle-track"><div class="gfn-toggle-thumb"></div></div>
            </label>`;
        },
        renderHeaderBtn(id, label, icon, title = "") {
            return `<button class="gfn-header-btn" id="${id}" aria-label="${label}" title="${title || label}">${icon}</button>`;
        },
        renderModalSection(title, items) {
            const content = items.map(i => typeof i === 'string' ? `<p style="margin:4px 0;">${i}</p>` : `<h4 style="color:#ccc; margin:10px 0 2px 0;">${i.h}</h4><p style="margin-top:0;">${i.p}</p>`).join('');
            return `<h3 style="color:var(--theme-color); margin:16px 0 4px 0; border-bottom:1px solid #333; padding-bottom:4px;">${title}</h3>${content}`;
        }
    };

    try {
        // Prevent injecting duplicate script UI instances
        if (document.querySelector(CONSTANTS.SELECTORS.ROOT)) {
            console.log("Initialization aborted: UI instance already exists in the DOM.");
            return;
        }
    
        const saved = Utils.safeStorageGet(CONSTANTS.STORAGE_KEYS.CONFIG);
        if (saved) Object.assign(CONFIG, Utils.safeJsonParse(saved, {}));

        // Defensive Sanitization: Ensure TARGET_STORE_PRIORITY is ALWAYS a valid Array
        if (!Array.isArray(CONFIG.TARGET_STORE_PRIORITY) || CONFIG.TARGET_STORE_PRIORITY.length === 0) {
            CONFIG.TARGET_STORE_PRIORITY = ["Epic Games", "Steam", "Xbox", "Ubisoft Connect", "GOG"];
        }

        // Step 1: Inject all our custom styles directly into the page. I prefer doing this in JS so you only have to copy one file, not a separate CSS file.
    const style = document.createElement('style');
    style.innerHTML = `
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
        #gfn-sync-pro-root { --theme-color: ${CONFIG.UI_THEME_COLOR}; --bg-color: rgba(10, 10, 10, 0.75); --text-color: #ffffff; --panel-bg: rgba(0,0,0,0.6); --border-color: rgba(255,255,255,0.1); position: fixed; z-index: 999999; width: 850px; background: var(--bg-color); backdrop-filter: blur(25px) saturate(180%); -webkit-backdrop-filter: blur(25px) saturate(180%); border: 1px solid color-mix(in srgb, var(--theme-color) 40%, transparent); border-top: 1px solid color-mix(in srgb, var(--theme-color) 60%, transparent); border-radius: 20px; box-shadow: 0 30px 60px rgba(0,0,0,0.8), 0 0 30px color-mix(in srgb, var(--theme-color) 15%, transparent), inset 0 1px 0 var(--border-color); font-family: 'Inter', system-ui, -apple-system, sans-serif; color: var(--text-color); display: ${CONFIG.ENABLE_UI ? 'flex' : 'none'}; flex-direction: column; transition: width 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275), transform 0.3s ease; animation: slideIn 0.5s ease forwards; }
        #gfn-sync-pro-root.minimized { width: 650px !important; border-radius: 16px !important; box-shadow: 0 15px 40px rgba(0,0,0,0.85), 0 0 25px color-mix(in srgb, var(--theme-color) 20%, transparent); }
        #gfn-sync-pro-root.minimized #gfn-header { padding: 12px 16px; }
        #gfn-sync-pro-root.minimized #gfn-header h3 { font-size: 15px; }
        #gfn-header-mini-status { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 140px; margin: 0 10px; background: rgba(0,0,0,0.45); padding: 6px 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.08); box-sizing: border-box; overflow: hidden; }
        #gfn-sync-pro-root.light-mode #gfn-header-mini-status { background: rgba(241, 245, 249, 0.95); border-color: #cbd5e1; }
        #gfn-sync-pro-root.light-mode #gfn-header-mini-status span { color: #0f172a !important; }
        #gfn-sync-pro-root.light-mode #gfn-mini-percent-text { color: #475569 !important; }
        #gfn-sync-pro-root.light-mode, #gfn-settings-panel.light-mode { --bg-color: #ffffff; --text-color: #0f172a; --panel-bg: #ffffff; --border-color: #cbd5e1; }
        #gfn-sync-pro-root.light-mode { background: #ffffff !important; border: 1px solid #cbd5e1 !important; box-shadow: 0 20px 50px rgba(0,0,0,0.15), 0 0 30px color-mix(in srgb, var(--theme-color) 20%, transparent) !important; color: #0f172a !important; }
        #gfn-settings-panel.light-mode { color: #0f172a !important; background: #ffffff !important; border: 1px solid #cbd5e1 !important; box-shadow: 0 20px 50px rgba(0,0,0,0.25) !important; }
        #gfn-settings-panel.light-mode .gfn-toggle-label, #gfn-sync-pro-root.light-mode .gfn-toggle-label { color: #0f172a !important; font-weight: 700; }
        #gfn-settings-panel.light-mode label, #gfn-settings-panel.light-mode span, #gfn-settings-panel.light-mode div { color: #0f172a; }
        #gfn-sync-pro-root.light-mode label, #gfn-sync-pro-root.light-mode span, #gfn-sync-pro-root.light-mode div { color: #0f172a; }
        #gfn-sync-pro-root.light-mode #gfn-export-backup-btn, #gfn-sync-pro-root.light-mode #gfn-import-backup-btn, #gfn-sync-pro-root.light-mode #gfn-clear-cache-btn-gfn { background: #e2e8f0 !important; border-color: #cbd5e1 !important; }
        #gfn-sync-pro-root.light-mode div[style*="background:rgba(255,255,255,0.04)"] { background: #f8fafc !important; border-color: #cbd5e1 !important; }
        #gfn-sync-pro-root.light-mode #gfn-search-results-preview { background: #f8fafc !important; border-color: #cbd5e1 !important; color: #0f172a !important; }
        #gfn-sync-pro-root.light-mode [style*="color:#aaa"], #gfn-sync-pro-root.light-mode [style*="color: #aaa"], #gfn-sync-pro-root.light-mode [style*="color:#ccc"], #gfn-sync-pro-root.light-mode [style*="color: #ccc"], #gfn-sync-pro-root.light-mode [style*="color:#ddd"] { color: #475569 !important; }
        #gfn-settings-panel.light-mode [style*="color:#aaa"], #gfn-settings-panel.light-mode [style*="color: #aaa"], #gfn-settings-panel.light-mode [style*="color:#ccc"], #gfn-settings-panel.light-mode [style*="color: #ccc"] { color: #334155 !important; }
        #gfn-settings-panel.light-mode [style*="color: var(--theme-color)"] { color: var(--theme-color) !important; }
        #gfn-settings-panel.light-mode #gfn-settings-close-btn { color: #334155 !important; }
        #gfn-settings-panel.light-mode #gfn-settings-close-btn:hover { color: #dc2626 !important; }
        #gfn-settings-panel.light-mode .gfn-toggle-track, #gfn-sync-pro-root.light-mode .gfn-toggle-track { background: #cbd5e1 !important; border-color: #94a3b8 !important; box-shadow: inset 0 2px 4px rgba(0,0,0,0.15) !important; }
        #gfn-settings-panel.light-mode .gfn-toggle-thumb, #gfn-sync-pro-root.light-mode .gfn-toggle-thumb { background: #475569 !important; box-shadow: 0 2px 4px rgba(0,0,0,0.3) !important; }
        #gfn-settings-panel.light-mode .gfn-toggle-input:checked + .gfn-toggle-track, #gfn-sync-pro-root.light-mode .gfn-toggle-input:checked + .gfn-toggle-track { background: var(--theme-color) !important; border-color: var(--theme-color) !important; }
        #gfn-settings-panel.light-mode .gfn-toggle-input:checked + .gfn-toggle-track .gfn-toggle-thumb, #gfn-sync-pro-root.light-mode .gfn-toggle-input:checked + .gfn-toggle-track .gfn-toggle-thumb { background: #ffffff !important; box-shadow: 0 2px 5px rgba(0,0,0,0.4) !important; }
        #gfn-settings-panel.light-mode input[type=range]::-webkit-slider-runnable-track { background: #cbd5e1 !important; }
        #gfn-settings-panel.light-mode input[type=range]::-webkit-slider-thumb { background: #0f172a !important; border-color: var(--theme-color) !important; }
        #gfn-settings-panel.light-mode select option { background: #ffffff !important; color: #0f172a !important; }
        #gfn-settings-panel.light-mode #gfn-reset-settings-btn { background: #fee2e2 !important; color: #dc2626 !important; border: 1px solid #fca5a5 !important; }
        #gfn-settings-panel.light-mode #gfn-reset-settings-btn:hover { background: #fca5a5 !important; color: #991b1b !important; }
        #gfn-sync-pro-root.light-mode #gfn-header { background: linear-gradient(135deg, #f1f5f9 0%, color-mix(in srgb, var(--theme-color) 20%, transparent) 100%); color: #0f172a; border-bottom: 1px solid #cbd5e1; }
        #gfn-sync-pro-root.light-mode #gfn-header h3 { background: none; -webkit-text-fill-color: #0f172a; color: #0f172a; }
        #gfn-sync-pro-root.light-mode .gfn-header-btn { color: #334155; background: #e2e8f0; border-color: #cbd5e1; }
        #gfn-sync-pro-root.light-mode .gfn-header-btn:hover { background: #cbd5e1; color: #0f172a; }
        #gfn-sync-pro-root.light-mode #gfn-textarea { background: #ffffff !important; color: #0f172a !important; border-color: #cbd5e1 !important; box-shadow: inset 0 1px 3px rgba(0,0,0,0.05); }
        #gfn-sync-pro-root.light-mode #gfn-terminal { background: #f8fafc !important; border-color: #cbd5e1 !important; color: #0f172a !important; box-shadow: inset 0 2px 5px rgba(0,0,0,0.03); }
        #gfn-sync-pro-root.light-mode .gfn-btn-secondary { background: #e2e8f0; color: #0f172a; border-color: #cbd5e1; }
        #gfn-sync-pro-root.light-mode .gfn-btn-secondary:hover { background: #cbd5e1; }
        #gfn-sync-pro-root.light-mode input, #gfn-settings-panel.light-mode input, #gfn-sync-pro-root.light-mode select, #gfn-settings-panel.light-mode select, #gfn-sync-pro-root.light-mode textarea, #gfn-settings-panel.light-mode textarea { background: #ffffff !important; color: #0f172a !important; border-color: #cbd5e1 !important; }
        #gfn-sync-pro-root.light-mode .gfn-label, #gfn-settings-panel.light-mode .gfn-label { color: #475569 !important; }
        #gfn-sync-pro-root.light-mode .gfn-hint { color: #64748b !important; }
        #gfn-sync-pro-root.light-mode #gfn-footer { color: #64748b !important; border-top-color: #cbd5e1 !important; }
        #gfn-sync-pro-root.light-mode strong[style*="color:#fff"], #gfn-settings-panel.light-mode strong[style*="color:#fff"] { color: #0f172a !important; }
        #gfn-sync-pro-root.light-mode div[style*="background: rgba(255,255,255,0.05)"], #gfn-settings-panel.light-mode div[style*="background: rgba(255,255,255,0.05)"] { background: #f1f5f9 !important; color: #0f172a !important; }
        #gfn-sync-pro-root.light-mode .gfn-log-info { color: #0284c7 !important; font-weight: 600; }
        #gfn-sync-pro-root.light-mode .gfn-log-success { color: #16a34a !important; font-weight: 600; }
        #gfn-sync-pro-root.light-mode .gfn-log-warn { color: #d97706 !important; font-weight: 600; }
        #gfn-sync-pro-root.light-mode .gfn-log-error { color: #dc2626 !important; font-weight: 600; }
        #gfn-sync-pro-root.light-mode #gfn-modal { background: #ffffff !important; color: #0f172a !important; border: 1px solid #cbd5e1 !important; box-shadow: 0 20px 50px rgba(0,0,0,0.2) !important; }
        #gfn-sync-pro-root.light-mode #gfn-modal-content { color: #334155 !important; }
        #gfn-sync-pro-root.light-mode #gfn-modal-close-top { color: #334155 !important; }
        #gfn-sync-pro-root.light-mode #gfn-modal-close-top:hover { color: #dc2626 !important; }
        @keyframes slideIn { from { opacity: 0; transform: translateY(20px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
        #gfn-sync-pro-root.bottom-right { bottom: 24px; right: 24px; }
        #gfn-sync-pro-root.bottom-left { bottom: 24px; left: 24px; }
        #gfn-sync-pro-root.top-right { top: 24px; right: 24px; }
        #gfn-sync-pro-root.top-left { top: 24px; left: 24px; }
        #gfn-sync-pro-root.center { top: 50%; left: 50%; transform: translate(-50%, -50%); animation: none; }
        #gfn-header { background: linear-gradient(135deg, rgba(0,0,0,0.7) 0%, color-mix(in srgb, var(--theme-color) 10%, transparent) 100%); padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; cursor: grab; }
        #gfn-header:active { cursor: grabbing; }
        #gfn-header h3 { margin: 0; font-size: 18px; font-weight: 800; background: linear-gradient(90deg, #fff, var(--theme-color)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: 0.5px; display: flex; align-items: center; gap: 10px; white-space: nowrap !important; flex-shrink: 0; }
        .gfn-header-btn { background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.1); color: #fff; cursor: pointer; width: 32px; height: 32px; border-radius: 8px; display: flex; align-items: center; justify-content: center; transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1); padding: 6px; box-sizing: border-box; }
        .gfn-header-btn:hover { background: rgba(255,255,255,0.15); transform: translateY(-1px); box-shadow: 0 4px 10px rgba(0,0,0,0.2); }
        .gfn-header-btn:active { transform: translateY(1px); scale: 0.95; }
        #gfn-close-btn:hover { background: #ff4d4d; border-color: #ff4d4d; color: #fff; }
        #gfn-body { padding: 20px; display: flex; flex-direction: row; gap: 20px; flex-wrap: wrap; max-height: 75vh; overflow-y: auto; overflow-x: hidden; }
        #gfn-body::-webkit-scrollbar { width: 6px; }
        #gfn-body::-webkit-scrollbar-track { background: transparent; }
        #gfn-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.2); border-radius: 4px; }
        #gfn-body::-webkit-scrollbar-thumb:hover { background: var(--theme-color); }
        .gfn-pane-left { flex: 1; min-width: 300px; max-width: 380px; display: flex; flex-direction: column; gap: 12px; }
        .gfn-pane-right { flex: 1.2; min-width: 350px; display: flex; flex-direction: column; gap: 12px; }
        .gfn-label { font-size: 12px; color: #a0a0a0; text-transform: uppercase; font-weight: 600; margin-bottom: 6px; display: block; letter-spacing: 1px; }
        .gfn-hint { font-size: 11px; color: #777; margin-top: 4px; display: block; font-style: italic; }
        #gfn-textarea { width: 100%; height: 90px; background: rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 8px; color: #fff; padding: 12px; font-family: 'Consolas', monospace; font-size: 12px; resize: vertical; box-sizing: border-box; transition: border 0.3s; }
        #gfn-textarea:focus { outline: none; border-color: var(--theme-color); box-shadow: 0 0 10px color-mix(in srgb, var(--theme-color) 20%, transparent); }
        #gfn-textarea::placeholder { color: #555; }
        .gfn-btn { background: linear-gradient(135deg, var(--theme-color) 0%, color-mix(in srgb, var(--theme-color) 50%, black) 100%); color: #000; border: none; padding: 14px 20px; border-radius: 8px; font-weight: 800; font-size: 14px; cursor: pointer; transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1); text-transform: uppercase; letter-spacing: 1px; box-shadow: 0 4px 15px color-mix(in srgb, var(--theme-color) 40%, transparent); position: relative; overflow: hidden; }
        .gfn-btn::after { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: rgba(255,255,255,0.1); transform: rotate(45deg); transition: 0.5s; }
        .gfn-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px color-mix(in srgb, var(--theme-color) 60%, transparent); }
        .gfn-btn:hover::after { left: 100%; }
        .gfn-btn:active { transform: scale(0.97); }
        .gfn-btn:disabled { background: #333; color: #666; cursor: not-allowed; box-shadow: none; transform: none; }
        .gfn-btn:disabled::after { display: none; }
        .gfn-btn:focus-visible { outline: 2px solid var(--theme-color); outline-offset: 2px; }
        .gfn-btn-secondary { background: rgba(255,255,255,0.1); color: #fff; box-shadow: none; }
        .gfn-btn-secondary:hover { background: rgba(255,255,255,0.2); box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
        #gfn-terminal { flex-grow: 1; min-height: 180px; max-height: 350px; background: #0a0a0a; border: 1px solid rgba(255,255,255,0.05); border-radius: 8px; padding: 10px; font-family: 'Consolas', monospace; font-size: 11px; color: #ccc; overflow-y: auto; display: flex; flex-direction: column; gap: 6px; box-shadow: inset 0 2px 10px rgba(0,0,0,0.5); user-select: text !important; -webkit-user-select: text !important; cursor: text; }
        #gfn-terminal * { user-select: text !important; -webkit-user-select: text !important; }
        #gfn-terminal::-webkit-scrollbar { width: 6px; }
        #gfn-terminal::-webkit-scrollbar-track { background: #111; border-radius: 4px; }
        #gfn-terminal::-webkit-scrollbar-thumb { background: #333; border-radius: 4px; }
        #gfn-terminal::-webkit-scrollbar-thumb:hover { background: var(--theme-color); }
        .gfn-log-info { color: #5bc0de; } .gfn-log-success { color: #5cb85c; } .gfn-log-warn { color: #f0ad4e; } .gfn-log-error { color: #d9534f; }
        #gfn-footer { text-align: center; font-size: 10px; color: #666; margin-top: -5px; padding-bottom: 10px; font-weight: 600; letter-spacing: 0.5px; }
        #gfn-footer a, #gfn-footer span { color: var(--theme-color); text-decoration: none; }
        #gfn-footer a:hover { text-decoration: underline; }
        .gfn-hidden { display: none !important; }
        .gfn-fade-in { animation: fadeIn 0.3s ease forwards; }
        @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
        #gfn-settings-panel { position: fixed; width: 400px; max-width: 92vw; z-index: 1000000; box-sizing: border-box; background: rgba(14, 14, 14, 0.98); backdrop-filter: blur(25px) saturate(180%); -webkit-backdrop-filter: blur(25px) saturate(180%); border-radius: 16px; font-size: 11px; padding: 18px; margin: 0; border: 1px solid color-mix(in srgb, var(--theme-color) 45%, transparent); box-shadow: 0 25px 60px rgba(0,0,0,0.95), 0 0 30px color-mix(in srgb, var(--theme-color) 15%, transparent); top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.9); opacity: 0; pointer-events: none; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); font-family: 'Inter', system-ui, -apple-system, sans-serif; color: #fff; max-height: 85vh; overflow-y: auto; }
        #gfn-settings-panel.open { transform: translate(-50%, -50%) scale(1); opacity: 1; pointer-events: all; }
        #gfn-settings-panel::-webkit-scrollbar { width: 4px; }
        #gfn-settings-panel::-webkit-scrollbar-thumb { background: var(--theme-color); border-radius: 2px; }
        .gfn-toggle-label { display: flex; align-items: center; cursor: pointer; justify-content: space-between; user-select: none; font-size: 11px; font-weight: 600; color: #eee; gap: 8px; }
        .gfn-toggle-input { display: none; }
        .gfn-toggle-track { width: 40px; height: 22px; background: rgba(255,255,255,0.12); border-radius: 22px; position: relative; transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); box-shadow: inset 0 2px 4px rgba(0,0,0,0.6); border: 1px solid rgba(255,255,255,0.25); flex-shrink: 0; }
        .gfn-toggle-thumb { width: 16px; height: 16px; background: #999999; border-radius: 50%; position: absolute; top: 2px; left: 2px; transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1); box-shadow: 0 2px 4px rgba(0,0,0,0.4); }
        .gfn-toggle-input:checked + .gfn-toggle-track { background: var(--theme-color) !important; box-shadow: inset 0 1px 3px rgba(0,0,0,0.2), 0 0 12px color-mix(in srgb, var(--theme-color) 60%, transparent); border-color: rgba(255,255,255,0.4); }
        .gfn-toggle-input:checked + .gfn-toggle-track .gfn-toggle-thumb { left: 20px; background: #ffffff !important; box-shadow: 0 2px 5px rgba(0,0,0,0.5); }
        .gfn-toggle-input:focus-visible + .gfn-toggle-track { outline: 2px solid #fff; outline-offset: 2px; }
        input[type=range] { -webkit-appearance: none; width: 100px; background: transparent; }
        input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; height: 14px; width: 14px; border-radius: 50%; background: #fff; cursor: pointer; margin-top: -5px; box-shadow: 0 1px 3px rgba(0,0,0,0.5); border: 2px solid var(--theme-color); transition: transform 0.1s; }
        input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2); }
        input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 4px; cursor: pointer; background: rgba(255,255,255,0.1); border-radius: 2px; }
        input[type=range]:focus { outline: none; }
        select { outline: none; transition: border-color 0.3s; color-scheme: dark; position: relative; z-index: 100; pointer-events: auto !important; cursor: pointer; }
        select option { background: #1a1a1a !important; color: #ffffff !important; font-size: 12px; }
        select:focus-visible { border-color: var(--theme-color) !important; box-shadow: 0 0 10px color-mix(in srgb, var(--theme-color) 20%, transparent); }
        #gfn-progress-container { width: 100%; height: 6px; background: #222; border-radius: 3px; margin-bottom: 8px; overflow: hidden; }
        #gfn-progress-bar { height: 100%; width: 0%; background: var(--theme-color); transition: width 0.3s ease; }
        #gfn-modal { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: #000000; z-index: 10; display: flex; flex-direction: column; padding: 20px; box-sizing: border-box; border-radius: 16px; }
        #gfn-modal-content { overflow-y: auto; flex-grow: 1; font-size: 12px; line-height: 1.5; color: #ddd; padding-right: 5px; }
        #gfn-modal-content::-webkit-scrollbar { width: 4px; }
        #gfn-modal-content::-webkit-scrollbar-thumb { background: var(--theme-color); border-radius: 2px; }
        #gfn-modal-content h4 { color: var(--theme-color); margin: 0 0 8px 0; font-size: 14px; text-transform: uppercase; }
        .gfn-settings-btn:hover { background: rgba(255,255,255,0.2); transform: scale(1.05); }
    `;
    document.head.appendChild(style);

    // SVG Icons
    const iconSettings = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:18px;height:18px;"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`;
    const iconGuide = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`;
    const iconFeatures = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><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>`;
    const iconMin = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`;
    const iconClose = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="width:16px;height:16px;"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`;

    // Step 2: Build the HTML skeleton
    const root = document.createElement("div");
    root.id = "gfn-sync-pro-root";
    root?.setAttribute("role", "dialog");
    root?.setAttribute("aria-modal", "false");
    root?.setAttribute("aria-label", "Library Sync for GeForce NOW");
    root.className = CONFIG.UI_POSITION;
    
    let htmlContent = `
        <div id="gfn-header">
            <h3>
                Library Sync for GeForce NOW 
                <button id="gfn-settings-btn" aria-label="Toggle Settings" class="gfn-header-btn" style="border:none; padding:4px;" title="Toggle Settings">${iconSettings}</button>
            </h3>
            <div id="gfn-header-mini-status">
                <div style="flex:1; display:flex; flex-direction:column; gap:3px; min-width:0; overflow:hidden;">
                    <div style="display:flex; justify-content:space-between; align-items:center; font-size:10px; font-weight:600; color:#aaa; gap:8px;">
                        <span id="gfn-mini-title-text" style="color:var(--theme-color); font-weight:700; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; flex:1;">Ready</span>
                        <span id="gfn-mini-percent-text" style="font-family:'Consolas', monospace; font-size:10px; color:#ddd; white-space:nowrap; flex-shrink:0;">0%</span>
                    </div>
                    <div style="width:100%; height:4px; background:rgba(255,255,255,0.15); border-radius:2px; overflow:hidden;">
                        <div id="gfn-mini-progress-bar" style="width:0%; height:100%; background:var(--theme-color); transition:width 0.3s ease;"></div>
                    </div>
                </div>
                <button id="gfn-mini-stop-btn" class="gfn-hidden" style="background:linear-gradient(135deg, #d9534f 0%, #c9302c 100%); border:none; color:#fff; font-weight:800; font-size:10px; padding:4px 8px; border-radius:5px; cursor:pointer; display:flex; align-items:center; gap:4px; flex-shrink:0;" title="Stop Syncing">
                    <svg viewBox="0 0 24 24" fill="currentColor" style="width:11px;height:11px;"><rect x="4" y="4" width="16" height="16"></rect></svg> STOP
                </button>
            </div>
            <div style="display:flex; gap:6px;">
                ${UIComponents.renderHeaderBtn("gfn-features-btn", "Features", iconFeatures, "All Features")}
                ${UIComponents.renderHeaderBtn("gfn-guide-btn", "Guide", iconGuide, "How to use")}
                ${UIComponents.renderHeaderBtn("gfn-min-btn", "Minimize", iconMin, "Minimize")}
                ${UIComponents.renderHeaderBtn("gfn-close-btn", "Close", iconClose, "Close")}
            </div>
        </div>
        <div id="gfn-modal" class="gfn-hidden">
            <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; border-bottom:1px solid color-mix(in srgb, var(--theme-color) 30%, transparent); padding-bottom:8px;">
                <h4 id="gfn-modal-title" style="color: var(--theme-color); margin: 0; font-size: 16px; text-transform: uppercase;"> Title</h4>
                <button id="gfn-modal-close-top" style="background:none; border:none; color:#fff; font-size:18px; cursor:pointer; padding:0;">&times;</button>
            </div>
            <div id="gfn-modal-content"></div>
            <button id="gfn-modal-close" class="gfn-btn" style="margin-top: 15px; padding: 10px; flex-shrink: 0;">Got It, Let's Go!</button>
        </div>
        <div id="gfn-body">
            <div class="gfn-pane-left">
    `;
    
    // Create the settings panel entirely separately so it floats over everything
    const settingsPanelDiv = document.createElement("div");
    settingsPanelDiv.id = "gfn-settings-panel";
    settingsPanelDiv.innerHTML = `
                <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; border-bottom:1px solid color-mix(in srgb, var(--theme-color) 30%, transparent); padding-bottom:8px;">
                    <div style="font-weight: 800; color: var(--theme-color); text-transform: uppercase; letter-spacing: 0.5px; font-size: 13px;">General Settings</div>
                    <button id="gfn-settings-close-btn" style="background:none; border:none; color:#aaa; font-size:18px; cursor:pointer; padding:0; transition:color 0.2s;">&times;</button>
                </div>
                
                <div style="display:flex; justify-content:space-between; margin-bottom:10px;">
                    ${UIComponents.renderToggle("cfg-skip", "Skip Owned Games", CONFIG.SKIP_ALREADY_OWNED, "Automatically skips games you already own on GeForce NOW.")}
                    ${UIComponents.renderToggle("cfg-light-mode", "Light Mode UI", CONFIG.LIGHT_MODE, "Switch between Dark and Light mode aesthetics.")}
                </div>
                <div style="display:flex; justify-content:space-between; margin-bottom:10px;">
                    ${UIComponents.renderToggle("cfg-strict-store", "Strict Store Only", CONFIG.REQUIRE_PRIMARY_STORE_ONLY, "Only syncs games if selected Primary Store is available.")}
                    ${UIComponents.renderToggle("cfg-retry", "Auto-Retry Failed", CONFIG.AUTO_RETRY_FAILED, "Automatically re-attempts any failed games at the end.")}
                </div>
                <div style="display:flex; justify-content:space-between; margin-bottom:14px; border-bottom:1px solid rgba(255,255,255,0.1); padding-bottom:12px;">
                    ${UIComponents.renderToggle("cfg-close", "Auto-Close UI", CONFIG.UI_AUTO_CLOSE_ON_FINISH, "Closes the dashboard UI automatically when done.")}
                </div>
                
                <div style="display:flex; flex-direction:column; gap:10px;">
                    <label style="display:flex; justify-content:space-between; align-items:center;">Primary Store: 
                        <select id="cfg-store" style="background:#111; color:#fff; border:1px solid #444; padding:4px 8px; border-radius:6px; cursor:pointer;">
                            ${CONFIG.TARGET_STORE_PRIORITY.map(s => `<option value="${s}">${s}</option>`).join('')}
                        </select>
                    </label>
                    <label style="display:flex; justify-content:space-between; align-items:center;">Dashboard Position: 
                        <select id="cfg-pos" style="background:#111; color:#fff; border:1px solid #444; padding:4px 8px; border-radius:6px; cursor:pointer;">
                            <option value="bottom-right" ${CONFIG.UI_POSITION === 'bottom-right' ? 'selected' : ''}>Bottom Right</option>
                            <option value="bottom-left" ${CONFIG.UI_POSITION === 'bottom-left' ? 'selected' : ''}>Bottom Left</option>
                            <option value="top-right" ${CONFIG.UI_POSITION === 'top-right' ? 'selected' : ''}>Top Right</option>
                            <option value="top-left" ${CONFIG.UI_POSITION === 'top-left' ? 'selected' : ''}>Top Left</option>
                            <option value="center" ${CONFIG.UI_POSITION === 'center' ? 'selected' : ''}>Center</option>
                        </select>
                    </label>
                    <label style="display:flex; justify-content:space-between; align-items:center;">Sync Speed / Delay: 
                        <input type="range" id="cfg-speed" min="500" max="5000" step="100" value="${CONFIG.API_WAIT_TIME_MS}" style="width:120px;" title="Adjust wait time between games.">
                    </label>
                    <label style="display:flex; justify-content:space-between; align-items:center;">Match Tolerance: 
                        <input type="range" id="cfg-fuzzy" min="0" max="10" value="${CONFIG.FUZZY_MATCH_THRESHOLD}" style="width:120px;" title="Adjust title matching strictness.">
                    </label>
                    <label style="display:flex; justify-content:space-between; align-items:center;">Theme Color: 
                        <div style="display:flex; align-items:center; gap:8px;">
                            ${[["#76B900", "Nvidia Green"], ["#00F0FF", "Cyberpunk Neon"], ["#9D4EDD", "Epic Purple"], ["#FF5722", "Sunset Orange"]].map(([h, n]) => `<button type="button" class="gfn-preset-theme" data-color="${h}" style="background:${h}; border:1px solid rgba(255,255,255,0.3); width:18px; height:18px; border-radius:50%; cursor:pointer;" title="${n}"></button>`).join('')}
                            <input type="color" id="cfg-color" value="${CONFIG.UI_THEME_COLOR}" style="background:none; border:none; width:24px; height:24px; cursor:pointer; padding:0;">
                        </div>
                    </label>
                </div>
                
                <button id="gfn-adv-toggle-btn" style="background:none; border:none; color:#aaa; font-weight:800; cursor:pointer; width:100%; text-align:left; padding:12px 0 8px 0; border-top:1px solid rgba(255,255,255,0.1); margin-top:10px; text-transform:uppercase; letter-spacing:0.5px; transition: color 0.2s; display:flex; justify-content:space-between; align-items:center;">
                    <span>Advanced Options</span> <span id="gfn-adv-arrow">▼</span>
                </button>
                <div id="gfn-adv-options" class="${CONFIG.ENABLE_ADVANCED_OPTIONS ? '' : 'gfn-hidden'}">
                    <div style="display:flex; justify-content:space-between; margin-bottom:10px;">
                        ${UIComponents.renderToggle("cfg-dry", "Simulation Mode", CONFIG.DRY_RUN_MODE, "Simulates sync without modifying library.")}
                        ${UIComponents.renderToggle("cfg-verbose", "Detailed Logging", CONFIG.VERBOSE_LOGGING, "Outputs raw network diagnostic log data.")}
                    </div>
                    <div style="display:flex; justify-content:space-between; margin-bottom:12px;">
                        ${UIComponents.renderToggle("cfg-headless", "Background Mode", CONFIG.HEADLESS_MODE, "Hides dashboard while syncing in background.")}
                        ${UIComponents.renderToggle("cfg-autosave", "Auto-Save Cache", CONFIG.AUTO_SAVE_GAMES, "Saves extracted game list to browser storage.")}
                    </div>
                    <label style="display:block; margin-bottom:6px; font-weight:600; color:#aaa; font-size:10px; text-transform:uppercase;">Custom Name Aliases (Epic=GFN):</label>
                    <textarea id="cfg-aliases" placeholder="Marvel's Guardians of the Galaxy=Guardians of the Galaxy" style="width:100%; height:45px; background:rgba(0,0,0,0.5); color:#fff; border:1px solid rgba(255,255,255,0.1); border-radius:6px; font-family:'Consolas', monospace; font-size:11px; padding:6px; resize:vertical; transition:border 0.3s;">${(() => {
                        if (typeof CONFIG.CUSTOM_NAME_ALIASES === 'string') return CONFIG.CUSTOM_NAME_ALIASES;
                        return Object.entries(CONFIG.CUSTOM_NAME_ALIASES || {}).map(([k,v]) => k + "=" + v).join('\n');
                    })()}</textarea>
                    <button id="gfn-reset-settings-btn" style="width: 100%; margin-top: 10px; background: rgba(217, 83, 79, 0.2); color: #d9534f; border: 1px solid rgba(217, 83, 79, 0.5); padding: 8px; border-radius: 6px; cursor: pointer; font-weight: bold; font-size: 11px; text-transform: uppercase; transition: all 0.2s;">Reset to Defaults</button>
                </div>
    `;
    document.body.appendChild(settingsPanelDiv);

    // Figure out where the user is running the script.
    const isEpic = window.location.hostname.includes(CONSTANTS.URLS.EPIC_STORE);
    const isAmazon = window.location.hostname.includes(CONSTANTS.URLS.AMAZON_STORE);
    const isGFN = window.location.hostname.includes(CONSTANTS.URLS.GFN_PLAY);
    
    // A nice clean info box for instructions
    const infoBoxStyle = `background: rgba(255,255,255,0.05); border-left: 3px solid ${CONFIG.UI_THEME_COLOR}; padding: 12px; font-size: 11.5px; line-height: 1.5; border-radius: 0 6px 6px 0;`;

    // Show warnings if they ran it on a site they disabled in the config.
    if (isEpic && !CONFIG.ENABLE_EPIC_EXTRACTION) {
        htmlContent += `<div style="color: #f0ad4e; text-align: center; font-weight: 600; padding: 20px; background: rgba(240, 173, 78, 0.1); border-radius: 8px;">️ Heads up! Epic Games extraction is currently disabled in your CONFIG.</div>`;
    } else if (isAmazon && !CONFIG.ENABLE_AMAZON_EXTRACTION) {
        htmlContent += `<div style="color: #f0ad4e; text-align: center; font-weight: 600; padding: 20px; background: rgba(240, 173, 78, 0.1); border-radius: 8px;">️ Heads up! Amazon Luna extraction is currently disabled in your CONFIG.</div>`;
    } 
    else if (isEpic || isAmazon) {
        const storeName = isEpic ? "Epic Games" : "Amazon Gaming";
        htmlContent += `
                <div style="${infoBoxStyle}; margin-bottom:12px;">
                    <div id="gfn-toggle-info" style="color:var(--theme-color); font-weight:800; margin-bottom:4px; text-transform:uppercase; letter-spacing:0.5px; display:flex; align-items:center; justify-content:space-between; cursor:pointer;" title="Click to Collapse/Expand">
                        <span style="display:flex; align-items:center; gap:6px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:16px;height:16px;"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> Step 1: Extract Your Games</span>
                        <span style="font-size:8px; opacity:0.7;">▼</span>
                    </div>
                    <div id="gfn-info-content">
                        <strong>1.</strong> Click 'Extract' below to grab your ${storeName} games.<br>
                        <strong>2.</strong> Click 'Copy' when it finishes.<br>
                        <strong>3.</strong> Go to <a href="https://play.geforcenow.com" target="_blank" style="color:var(--theme-color); text-decoration:underline;">play.geforcenow.com</a>. Paste this script into the Console again!
                    </div>
                </div>
                
                <div style="display:flex; flex-direction:column; gap:8px; margin-top:12px;">
                    <div style="display:flex; justify-content:space-between; align-items:center; font-size:11px; padding: 0 4px; margin-bottom:4px;">
                        <label style="font-weight:600;" title="Set to 0 to extract everything">Limit Extraction (0 = Grab All):</label>
                        <input type="number" id="cfg-extract-limit" value="${CONFIG.EPIC_EXTRACT_LIMIT || 0}" title="0 = All Games" style="width: 50px; background:#111; color:#fff; border:1px solid #444; border-radius:4px; padding:4px;">
                    </div>
                    <div style="display:flex; gap:10px;">
                        <button id="gfn-action-btn" class="gfn-btn" style="flex:1;">Extract</button>
                        <button id="gfn-pause-btn-epic" class="gfn-btn gfn-btn-secondary gfn-hidden" style="flex:1;">Pause</button>
                        <button id="gfn-stop-btn-epic" class="gfn-btn gfn-btn-secondary gfn-hidden" style="flex:1;">Stop</button>
                        <button id="gfn-retry-btn-epic" class="gfn-btn gfn-hidden" style="flex:1; background:linear-gradient(135deg, #f0ad4e 0%, #d9534f 100%); color:#fff; border:none;">Retry</button>
                    </div>
                    <button id="gfn-copy-btn" class="gfn-btn gfn-btn-secondary" style="width: 100%;">Copy Results To Clipboard</button>
                    <button id="gfn-redirect-btn-epic" class="gfn-btn" style="width: 100%; background:linear-gradient(135deg, #00d2ff 0%, #3a7bd5 100%); color:#fff; border:none; margin-top:4px;"> Open GeForce NOW</button>
                </div>
            </div> <!-- Close Left Pane -->
            
            <div class="gfn-pane-right">
                <div id="gfn-result-container">
                    <div style="display:flex; justify-content:space-between; align-items:center;">
                        <span class="gfn-label" id="gfn-toggle-textarea" style="cursor:pointer; display:flex; align-items:center; gap:4px;" title="Click to Collapse/Expand">Extracted Data <span style="font-size:8px; opacity:0.7;">▼</span></span>
                        <button id="gfn-clear-cache-btn-epic" style="background:none; border:none; color:#f0ad4e; font-size:10px; cursor:pointer; font-weight:bold; text-transform:uppercase;" title="Clear Saved Games">Clear Cache</button>
                    </div>
                    <textarea id="gfn-textarea" style="height:100px; margin-top: 6px;" placeholder="Extracted games will appear here, or you can paste them manually..."></textarea>
                </div>
        `;
    } 
    // Render the Syncer UI for GeForce NOW
    else if (isGFN) {
        htmlContent += `
            <div style="${infoBoxStyle}; margin-bottom:12px;">
                <div id="gfn-toggle-info" style="color:var(--theme-color); font-weight:800; margin-bottom:4px; text-transform:uppercase; letter-spacing:0.5px; display:flex; align-items:center; justify-content:space-between; cursor:pointer;" title="Click to Collapse/Expand">
                    <span style="display:flex; align-items:center; gap:6px;"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="width:16px;height:16px;"><polyline points="20 6 9 17 4 12"></polyline></svg> Step 2: Sync Your Games</span>
                    <span style="font-size:8px; opacity:0.7;">▼</span>
                </div>
                <div id="gfn-info-content">
                    If you already extracted your games from Epic/Amazon, paste them below and click Start.<br><br>
                    <span style="color:#aaa;">Haven't extracted yet? Go to <strong style="color:#fff;">store.epicgames.com</strong>, paste this script in the console, and click Extract first! Or click the <strong>?</strong> icon above for the full guide.</span>
                </div>
            </div>
            <div style="display:flex; justify-content:space-between; align-items:center; margin-top:4px; margin-bottom: 6px;">
                <span class="gfn-label" id="gfn-toggle-textarea" style="cursor:pointer; display:flex; align-items:center; gap:6px; font-weight:700; font-size:12px;" title="Click to Collapse/Expand">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line></svg>
                    Extracted Data <span style="font-size:9px; opacity:0.7;">▼</span>
                </span>
                <div style="display:flex; gap:8px;">
                    <button id="gfn-export-backup-btn" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.12); color:var(--theme-color); font-size:10px; padding:3px 8px; border-radius:5px; cursor:pointer; font-weight:700;" title="Export JSON Backup">EXPORT</button>
                    <button id="gfn-import-backup-btn" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.12); color:#00F0FF; font-size:10px; padding:3px 8px; border-radius:5px; cursor:pointer; font-weight:700;" title="Import JSON Backup">IMPORT</button>
                    <button id="gfn-clear-cache-btn-gfn" style="background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.12); color:#f0ad4e; font-size:10px; padding:3px 8px; border-radius:5px; cursor:pointer; font-weight:700;" title="Clear Saved Games">CLEAR</button>
                </div>
            </div>
            <div style="margin-bottom:8px; position:relative;">
                <input type="text" id="gfn-search-filter" placeholder="🔍 Search / Filter games in list..." style="width:100%; background:rgba(0,0,0,0.5); color:#fff; border:1px solid rgba(255,255,255,0.12); border-radius:8px; padding:6px 10px; font-size:11px; box-sizing:border-box;">
                <span id="gfn-search-filter-count" style="position:absolute; right:10px; top:6px; font-size:10px; color:var(--theme-color); font-weight:700; display:none;"></span>
            </div>
            <div id="gfn-search-results-preview" style="display:none; margin-bottom:6px; font-size:10.5px; padding:4px 8px; background:rgba(255,255,255,0.05); border:1px solid color-mix(in srgb, var(--theme-color) 40%, transparent); border-radius:6px; color:#fff; max-height:60px; overflow-y:auto;"></div>
            <div id="gfn-textarea-container">
                <textarea id="gfn-textarea" placeholder="Paste your extracted array here...\n\nOR manually type games:\nCyberpunk 2077\nThe Witcher 3"></textarea>
            </div>
            
            <div style="display:flex; justify-content:space-between; align-items:center; margin-top:10px; margin-bottom:10px; font-size:11px; padding: 6px 10px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08); border-radius:8px;">
                <label style="font-weight:600; display:flex; align-items:center; gap:4px;" title="Limit total games to sync (0 = Sync All)">
                    <span>Limit Syncing:</span>
                    <span style="font-size:10px; color:#aaa; font-weight:normal;">(0 = All)</span>
                </label>
                <div style="display:flex; gap:6px; align-items:center;">
                    <input type="number" id="cfg-max" value="${CONFIG.MAX_GAMES_TO_SYNC}" title="0 = All Games" style="width: 55px; background:rgba(0,0,0,0.6); color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:6px; padding:4px 6px; font-size:11px; text-align:center;">
                    <select id="cfg-mode" style="background:rgba(0,0,0,0.6); color:#fff; border:1px solid rgba(255,255,255,0.2); border-radius:6px; padding:4px 6px; font-size:11px; cursor:pointer;" title="Select how games are selected when limit is enabled">
                        <option value="first" ${CONFIG.MAX_GAMES_MODE === 'first' ? 'selected' : ''}>from Top</option>
                        <option value="last" ${CONFIG.MAX_GAMES_MODE === 'last' ? 'selected' : ''}>from Bottom</option>
                        <option value="random" ${CONFIG.MAX_GAMES_MODE === 'random' ? 'selected' : ''}>Randomly</option>
                        <option value="alpha" ${CONFIG.MAX_GAMES_MODE === 'alpha' ? 'selected' : ''}>A-Z</option>
                        <option value="revalpha" ${CONFIG.MAX_GAMES_MODE === 'revalpha' ? 'selected' : ''}>Z-A</option>
                        <option value="shortest" ${CONFIG.MAX_GAMES_MODE === 'shortest' ? 'selected' : ''}>Shortest Titles</option>
                        <option value="longest" ${CONFIG.MAX_GAMES_MODE === 'longest' ? 'selected' : ''}>Longest Titles</option>
                    </select>
                </div>
            </div>

            <div style="display:flex; gap:10px; margin-top:8px;">
                <button id="gfn-action-btn" class="gfn-btn" style="flex:1;">
                    <span style="display:flex; align-items:center; justify-content:center; gap:8px;">
                        <svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> START SYNC
                    </span>
                </button>
                <button id="gfn-pause-btn" class="gfn-btn" style="flex:1; background:linear-gradient(135deg, #f0ad4e 0%, #ec971f 100%); display:none;">
                    <span style="display:flex; align-items:center; justify-content:center; gap:8px;">
                        <svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> PAUSE
                    </span>
                </button>
                <button id="gfn-stop-btn" class="gfn-btn" style="flex:0.8; background:linear-gradient(135deg, #d9534f 0%, #c9302c 100%); display:none;">
                    <span style="display:flex; align-items:center; justify-content:center; gap:8px;">
                        <svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><rect x="4" y="4" width="16" height="16"></rect></svg> STOP
                    </span>
                </button>
                <button id="gfn-retry-btn" class="gfn-btn gfn-btn-secondary" style="flex:1; border: 1px solid var(--theme-color); color: var(--theme-color); display:none;">
                    <span style="display:flex; align-items:center; justify-content:center; gap:8px;">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;"><polyline points="1 4 1 10 7 10"></polyline><polyline points="23 20 23 14 17 14"></polyline><path d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"></path></svg> RETRY FAILED
                    </span>
                </button>
            </div>
            </div> <!-- Close Left Pane for GFN -->
            
            <div class="gfn-pane-right">
        `;
    } 
    // Invalid execution context.
    else {
        htmlContent += `
            </div> <!-- Close Left pane -->
            <div class="gfn-pane-right">
            <div style="color: #d9534f; font-weight: 600; text-align: center; padding: 20px; background: rgba(217, 83, 79, 0.1); border-radius: 8px;">
                 Unsupported Website.<br>Please use this script on Epic Games, Amazon Gaming, or GeForce NOW.
            </div>
        `;
    }

    // Inject the terminal and footer UI components.
    htmlContent += `
                <div style="display:flex; justify-content:space-between; align-items:center; margin-top:8px;">
                    <span class="gfn-label" id="gfn-toggle-terminal" style="cursor:pointer; display:flex; align-items:center; gap:4px;" title="Click to Collapse/Expand">Live Activity Terminal <span style="font-size:8px; opacity:0.7;">▼</span></span>
                    <div style="display:flex; gap:6px;">
                        <button id="gfn-clear-terminal-btn" style="background:none; border:none; color:var(--theme-color); font-size:10px; cursor:pointer; font-weight:bold; text-transform:uppercase;" title="Clear Logs">Clear</button>
                        <button id="gfn-export-json-btn" style="background:none; border:none; color:var(--theme-color); font-size:10px; cursor:pointer; font-weight:bold; text-transform:uppercase; display:none;" title="Save stats to JSON">JSON</button>
                        <button id="gfn-export-failed-btn" style="background:none; border:none; color:#f44336; font-size:10px; cursor:pointer; font-weight:bold; text-transform:uppercase; display:none; margin-right:8px;" title="Copy failed games to JSON">Copy Failed</button>
                            <button id="gfn-export-btn" style="background:none; border:none; color:var(--theme-color); font-size:10px; cursor:pointer; font-weight:bold; text-transform:uppercase; display:none; margin-right:8px;" title="Save logs to TXT">TXT Logs</button>
                            <label style="color:#aaa; font-size:10px; cursor:pointer; display:flex; align-items:center; gap:4px;"><input type="checkbox" id="gfn-autoscroll-toggle" checked> Auto-Scroll</label>
                    </div>
                </div>
                <div id="gfn-progress-container" class="gfn-hidden"><div id="gfn-progress-bar"></div></div>
                <div id="gfn-terminal"></div>
            </div> <!-- Close Right Pane -->
        </div> <!-- Close Body -->
        <div id="gfn-footer">
            Created by <a href="https://github.com/arpitdev99/geforce-now-library-sync" target="_blank"><span>AV</span></a>
        </div>
    `;
    root.innerHTML = htmlContent;
    document.body.appendChild(root);

    // Step 3: Wire up the logic and the fake terminal
    const terminal = document.getElementById("gfn-terminal");
    
    // Add Collapsible UI Logic
    const bindToggle = (headerId, contentId) => {
        const header = document.getElementById(headerId);
        const content = document.getElementById(contentId);
        if (header && content) {
            header.addEventListener("click", () => {
                content.classList.toggle("gfn-hidden");
                const arrow = header.querySelector("span:last-child");
                if (arrow && arrow.textContent.trim().match(/[▼▲]/)) arrow.innerText = content.classList.contains("gfn-hidden") ? "▲" : "▼";
            });
        }
    };
    bindToggle("gfn-toggle-info", "gfn-info-content");
    bindToggle("gfn-toggle-textarea", isEpic || isAmazon ? "gfn-result-container" : "gfn-textarea-container");
    bindToggle("gfn-toggle-terminal", "gfn-terminal");
    
    // Helper to print colorful logs straight to our custom UI window.
    const tLog = (msg, type = "info") => {
        const time = new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' });
        const color = type === "error" ? "#f44336" : (type === "success" ? "#4caf50" : (type === "warn" ? "#ff9800" : "#aaa"));
        const icon = type === "error" ? "❌" : (type === "success" ? "🟢" : (type === "warn" ? "⚠️" : "✨"));
        
        terminal.insertAdjacentHTML('beforeend', `<div class="gfn-log-${type}" style="margin-bottom:4px;"><span style="color:#555;">[${time}]</span> ${icon} <span style="color:${color}">${Utils.escapeHTML(msg)}</span></div>`);
        
        // Prune older DOM elements to prevent memory leaks during 1,000+ title sync runs
        const maxLines = CONFIG.MAX_TERMINAL_LOG_LINES || 300;
        while (terminal.childElementCount > maxLines) {
            terminal.removeChild(terminal.firstElementChild);
        }

        // Auto-scroll log container to bottom if enabled.
        const autoScroll = document.getElementById("gfn-autoscroll-toggle");
        if (!autoScroll || autoScroll.checked) {
            terminal.scrollTop = terminal.scrollHeight;
        }
    };

    // Global progress & status updater for dual progress bars (main & minimized)
    const updateProgress = (completed, total, currentTitle = "") => {
        const percent = total ? Math.min(100, Math.round((completed / total) * 100)) : (completed === 0 ? 0 : 100);
        
        const mainBar = document.getElementById("gfn-progress-bar");
        if (mainBar) mainBar.style.width = percent + "%";
        
        const miniBar = document.getElementById("gfn-mini-progress-bar");
        if (miniBar) miniBar.style.width = percent + "%";
        
        const miniPercent = document.getElementById("gfn-mini-percent-text");
        if (miniPercent) miniPercent.textContent = total ? `${completed}/${total} (${percent}%)` : `${percent}%`;
        
        const miniTitle = document.getElementById("gfn-mini-title-text");
        if (miniTitle && currentTitle) {
            miniTitle.textContent = currentTitle;
            miniTitle.title = currentTitle;
        }
    };

    tLog("System initialized. Welcome to Library Sync for GeForce NOW.", "success");

    // Auto-save the text area to localStorage in case you accidentally refresh the page!
    const savedText = Utils.safeStorageGet(CONSTANTS.STORAGE_KEYS.TEXTAREA);
    const ta = document.getElementById("gfn-textarea");
    if (savedText && ta) {
        ta.value = savedText;
        tLog("Loaded previous list from memory.", "info");
    }
    if (ta) {
        ta.addEventListener("input", (e) => {
            const autoSave = document.getElementById('cfg-autosave')?.checked ?? CONFIG.AUTO_SAVE_GAMES;
            if (autoSave) {
                Utils.safeStorageSet(CONSTANTS.STORAGE_KEYS.TEXTAREA, e.target.value);
            }
        });
    }
    
    // Live Title Filter Listener with Highlighted Tag Preview & Textarea Selection
    const searchFilter = document.getElementById("gfn-search-filter");
    const searchFilterCount = document.getElementById("gfn-search-filter-count");
    const searchPreview = document.getElementById("gfn-search-results-preview");
    if (searchFilter && ta) {
        searchFilter.addEventListener("input", (e) => {
            const query = (e.target.value || "").toLowerCase().trim();
            const allGames = Utils.parseGameListInput(ta.value);
            if (!query) {
                if (searchFilterCount) searchFilterCount.style.display = "none";
                if (searchPreview) { searchPreview.style.display = "none"; searchPreview.innerHTML = ""; }
                return;
            }
            const matching = allGames.filter(g => g.toLowerCase().includes(query));
            if (searchFilterCount) {
                searchFilterCount.textContent = `${matching.length}/${allGames.length} match${matching.length === 1 ? '' : 'es'}`;
                searchFilterCount.style.display = "inline";
            }

            // Render live highlighted title badges with quick individual & batch deletion controls
            if (searchPreview) {
                if (matching.length > 0) {
                    searchPreview.style.display = "block";
                    searchPreview.innerHTML = `
                        <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:4px;">
                            <span style="color:var(--theme-color); font-weight:700; font-size:10px;">🎯 Matched (${matching.length}):</span>
                            <button id="gfn-delete-matched-btn" style="background:#dc2626; color:#fff; border:none; padding:2px 6px; border-radius:4px; font-size:9.5px; font-weight:700; cursor:pointer;" title="Remove all matched games from list">🗑️ Delete All ${matching.length} Matched</button>
                        </div>
                        <div style="display:flex; flex-wrap:wrap; gap:4px;">
                            ${matching.slice(0, 8).map(m => `
                                <span class="gfn-tag-pill" style="background:color-mix(in srgb, var(--theme-color) 20%, transparent); color:#fff; border:1px solid color-mix(in srgb, var(--theme-color) 45%, transparent); padding:2px 6px; border-radius:4px; font-weight:600; font-size:10px; display:inline-flex; align-items:center; gap:4px;">
                                    ${Utils.escapeHTML(m)}
                                    <span class="gfn-remove-single-game" data-game="${encodeURIComponent(m)}" style="cursor:pointer; color:#ef4444; font-weight:bold; font-size:11px; margin-left:2px; padding:0 2px;" title="Remove '${Utils.escapeHTML(m)}' from list">✕</span>
                                </span>
                            `).join('')}
                            ${matching.length > 8 ? `<span style="color:#aaa; font-size:9.5px; align-self:center;">+${matching.length - 8} more</span>` : ''}
                        </div>
                    `;
                } else {
                    searchPreview.style.display = "block";
                    searchPreview.innerHTML = `<span style="color:#dc2626; font-weight:700;">⚠️ 0 matches found for "${Utils.escapeHTML(query)}"</span>`;
                }
            }

            // Find matching keyword inside the text area, select (highlight) it, and scroll it into view
            const rawText = ta.value;
            const lowerText = rawText.toLowerCase();
            const matchIndex = lowerText.indexOf(query);
            if (matchIndex !== -1) {
                ta.setSelectionRange(matchIndex, matchIndex + query.length);
                const linesBefore = rawText.substring(0, matchIndex).split("\n").length;
                const approxLineHeight = 18;
                ta.scrollTop = Math.max(0, (linesBefore - 3) * approxLineHeight);
            }
        });
    }

    // Individual & Batch Deletion Handler for Search Preview Badges
    if (searchPreview && ta) {
        searchPreview.addEventListener("click", (e) => {
            const singleRemoveBtn = e.target.closest(".gfn-remove-single-game");
            if (singleRemoveBtn) {
                const rawAttr = singleRemoveBtn.getAttribute("data-game");
                if (rawAttr) {
                    const gameToRemove = decodeURIComponent(rawAttr);
                    const games = Utils.parseGameListInput(ta.value);
                    const updated = games.filter(g => g.trim().toLowerCase() !== gameToRemove.trim().toLowerCase());
                    const isJson = ta.value.trim().startsWith('[') || ta.value.trim().startsWith('{');
                    ta.value = isJson ? JSON.stringify(updated, null, 2) : updated.join('\n');
                    ta.dispatchEvent(new Event("input"));
                    if (searchFilter) searchFilter.dispatchEvent(new Event("input"));
                    tLog(`🗑️ Removed "${gameToRemove}" from list.`, "warn");
                }
                return;
            }

            const batchDeleteBtn = e.target.closest("#gfn-delete-matched-btn");
            if (batchDeleteBtn) {
                const query = (searchFilter?.value || "").toLowerCase().trim();
                if (!query) return;
                const games = Utils.parseGameListInput(ta.value);
                const matching = games.filter(g => g.toLowerCase().includes(query));
                const matchSet = new Set(matching.map(g => g.toLowerCase()));
                const updated = games.filter(g => !matchSet.has(g.toLowerCase()));
                const isJson = ta.value.trim().startsWith('[') || ta.value.trim().startsWith('{');
                ta.value = isJson ? JSON.stringify(updated, null, 2) : updated.join('\n');
                ta.dispatchEvent(new Event("input"));
                if (searchFilter) {
                    searchFilter.value = "";
                    searchFilter.dispatchEvent(new Event("input"));
                }
                tLog(`🗑️ Deleted all ${matching.length} matched games from list.`, "warn");
            }
        });
    }

    // Export & Import Backup Listeners
    const exportBackupBtn = document.getElementById("gfn-export-backup-btn");
    if (exportBackupBtn && ta) {
        exportBackupBtn.addEventListener("click", () => {
            const games = Utils.parseGameListInput(ta.value);
            if (games.length === 0) {
                tLog("No games in list to export!", "warn");
                return;
            }
            Utils.exportBackup(games);
            tLog(`📤 Exported ${games.length} games to GFN_Library_Backup.json`, "success");
        });
    }

    const importBackupBtn = document.getElementById("gfn-import-backup-btn");
    if (importBackupBtn && ta) {
        importBackupBtn.addEventListener("click", async () => {
            const games = await Utils.importBackup();
            if (games && games.length > 0) {
                ta.value = JSON.stringify(games, null, 2);
                ta.dispatchEvent(new Event("input"));
                tLog(`📥 Successfully imported ${games.length} games from backup file!`, "success");
            } else if (games !== null) {
                tLog("No valid game list found in selected file.", "warn");
            }
        });
    }
    


    // Keyboard Shortcuts & Hotkeys
    document.addEventListener("keydown", (e) => {
        // Esc key closes open modal
        if (e.key === "Escape") {
            const modal = document.getElementById("gfn-modal");
            if (modal && !modal.classList.contains("gfn-hidden")) {
                modal.classList.add("gfn-hidden");
                modal.classList.remove("gfn-fade-in");
            }
        }
        // Alt + M toggles minimize
        if (e.altKey && (e.key === "m" || e.key === "M")) {
            e.preventDefault();
            document.getElementById("gfn-min-btn")?.click();
        }
        // Alt + S triggers start/pause button
        if (e.altKey && (e.key === "s" || e.key === "S")) {
            e.preventDefault();
            const actionBtn = document.getElementById("gfn-action-btn");
            const pauseBtn = document.getElementById("gfn-pause-btn");
            if (pauseBtn && pauseBtn.style.display !== "none") pauseBtn.click();
            else if (actionBtn && actionBtn.style.display !== "none") actionBtn.click();
        }
        // Alt + X triggers emergency stop
        if (e.altKey && (e.key === "x" || e.key === "X")) {
            e.preventDefault();
            document.getElementById("gfn-stop-btn")?.click();
        }
    });

    // Clear cache listeners
    const clearEpic = document.getElementById("gfn-clear-cache-btn-epic");
    const clearGfn = document.getElementById("gfn-clear-cache-btn-gfn");
    const handleClear = () => {
        Utils.safeStorageRemove(CONSTANTS.STORAGE_KEYS.TEXTAREA);
        if (ta) ta.value = "";
        tLog("Saved games cache cleared.", "success");
    };
    if (clearEpic) clearEpic.addEventListener("click", handleClear);
    if (clearGfn) clearGfn.addEventListener("click", handleClear);
    
    const redirectEpic = document.getElementById("gfn-redirect-btn-epic");
    if (redirectEpic) {
        redirectEpic.addEventListener("click", () => {
            tLog("Redirecting to GeForce NOW...", "info");
            window.location.href = CONSTANTS.URLS.GFN_MALL;
        });
    }
    
    // Save Configuration globally
    const saveConfig = () => Utils.safeStorageSet(CONSTANTS.STORAGE_KEYS.CONFIG, CONFIG);
    
    // Bind all settings inputs to live update CONFIG, UI, and save to memory
    const bindSetting = (id, configKey, eventType, callback) => {
        const el = document.getElementById(id);
        if (el) {
            el.addEventListener(eventType, (e) => {
                CONFIG[configKey] = el.type === 'checkbox' ? el.checked : el.value;
                if (callback) callback(e.target.value, el.checked);
                saveConfig();
            });
        }
    };

    [
        ["cfg-skip", "SKIP_ALREADY_OWNED"],
        ["cfg-strict-store", "REQUIRE_PRIMARY_STORE_ONLY"],
        ["cfg-retry", "AUTO_RETRY_FAILED"],
        ["cfg-close", "UI_AUTO_CLOSE_ON_FINISH"],
        ["cfg-dry", "DRY_RUN_MODE"],
        ["cfg-verbose", "VERBOSE_LOGGING"],
        ["cfg-headless", "HEADLESS_MODE"],
        ["cfg-autosave", "AUTO_SAVE_GAMES"],
        ["cfg-mode", "MAX_GAMES_MODE"]
    ].forEach(([id, key]) => bindSetting(id, key, "change"));

    // Prevent host page scripts (GFN/Epic) and container drag handlers from hijacking select dropdown clicks
    document.querySelectorAll("#gfn-sync-pro-root select, #gfn-settings-panel select").forEach(sel => {
        ["mousedown", "mouseup", "click", "pointerdown", "pointerup", "touchstart", "touchend"].forEach(evtName => {
            sel.addEventListener(evtName, (e) => e.stopPropagation());
        });
    });

    // cfg-store needs dedicated handling to preserve TARGET_STORE_PRIORITY as an Array
    const storeSelect = document.getElementById("cfg-store");
    if (storeSelect) {
        storeSelect.addEventListener("change", () => {
            const val = storeSelect.value;
            const defaultStores = ["Epic Games", "Steam", "Xbox", "Ubisoft Connect", "GOG"];
            const current = Array.isArray(CONFIG.TARGET_STORE_PRIORITY) ? CONFIG.TARGET_STORE_PRIORITY : defaultStores;
            CONFIG.TARGET_STORE_PRIORITY = [val, ...current.filter(s => s !== val)];
            saveConfig();
        });
    }
    bindSetting("cfg-pos", "UI_POSITION", "change", (val) => {
        root.className = val;
        if (CONFIG.LIGHT_MODE) root.classList.add("light-mode");
    });
    bindSetting("cfg-speed", "API_WAIT_TIME_MS", "input", (val) => { CONFIG.API_WAIT_TIME_MS = parseInt(val) || 3000; });
    bindSetting("cfg-fuzzy", "FUZZY_MATCH_THRESHOLD", "input", (val) => { CONFIG.FUZZY_MATCH_THRESHOLD = parseInt(val) || 4; });
    bindSetting("cfg-color", "UI_THEME_COLOR", "input", (val) => { root.style.setProperty("--theme-color", val); });
    bindSetting("cfg-max", "MAX_GAMES_TO_SYNC", "input", (val) => { CONFIG.MAX_GAMES_TO_SYNC = parseInt(val) || 0; });
    
    document.querySelectorAll(".gfn-preset-theme").forEach(btn => {
        btn.addEventListener("click", (e) => {
            const hex = e.target.getAttribute("data-color");
            if (hex) {
                CONFIG.UI_THEME_COLOR = hex;
                root.style.setProperty("--theme-color", hex);
                const colorPicker = document.getElementById("cfg-color");
                if (colorPicker) colorPicker.value = hex;
                saveConfig();
            }
        });
    });
    bindSetting("cfg-light-mode", "LIGHT_MODE", "change", (val, checked) => { 
        root.classList.toggle("light-mode", checked);
        document.getElementById("gfn-settings-panel")?.classList.toggle("light-mode", checked);
    });
    bindSetting("cfg-aliases", "CUSTOM_NAME_ALIASES", "input", (val) => {
        const obj = {};
        (val || "").split("\n").forEach(line => {
            const parts = line.split("=");
            if (parts.length >= 2) {
                const k = parts[0].trim();
                const v = parts.slice(1).join("=").trim();
                if (k && v) obj[k] = v;
            }
        });
        CONFIG.CUSTOM_NAME_ALIASES = obj;
    });
    bindSetting("cfg-extract-limit", "EPIC_EXTRACT_LIMIT", "input", (val) => { CONFIG.EPIC_EXTRACT_LIMIT = parseInt(val) || 0; });
    
    // Apply initial config to UI logic
    root.style.setProperty("--theme-color", CONFIG.UI_THEME_COLOR);
    if (CONFIG.LIGHT_MODE) {
        root.classList.add("light-mode");
        document.getElementById("gfn-settings-panel")?.classList.add("light-mode");
    }
    
    const resetBtn = document.getElementById("gfn-reset-settings-btn");
    if (resetBtn) {
        resetBtn.addEventListener("click", () => {
            // 1. Reset CONFIG object to factory defaults
            if (typeof DEFAULT_CONFIG !== "undefined") {
                Object.assign(CONFIG, JSON.parse(JSON.stringify(DEFAULT_CONFIG)));
            }
            
            // 2. Remove saved config from local storage
            Utils.safeStorageRemove(CONSTANTS.STORAGE_KEYS.CONFIG);

            // 3. Update all UI elements in Settings Panel
            const get = (id) => document.getElementById(id);
            if (get("cfg-skip")) get("cfg-skip").checked = CONFIG.SKIP_ALREADY_OWNED;
            if (get("cfg-light-mode")) get("cfg-light-mode").checked = CONFIG.LIGHT_MODE;
            if (get("cfg-strict-store")) get("cfg-strict-store").checked = CONFIG.REQUIRE_PRIMARY_STORE_ONLY;
            if (get("cfg-retry")) get("cfg-retry").checked = CONFIG.AUTO_RETRY_FAILED;
            if (get("cfg-close")) get("cfg-close").checked = CONFIG.UI_AUTO_CLOSE_ON_FINISH;
            if (get("cfg-store")) get("cfg-store").value = CONFIG.TARGET_STORE_PRIORITY[0];
            if (get("cfg-pos")) { get("cfg-pos").value = CONFIG.UI_POSITION; root.className = CONFIG.UI_POSITION; }
            if (get("cfg-speed")) get("cfg-speed").value = CONFIG.API_WAIT_TIME_MS;
            if (get("cfg-fuzzy")) get("cfg-fuzzy").value = CONFIG.FUZZY_MATCH_THRESHOLD;
            if (get("cfg-color")) get("cfg-color").value = CONFIG.UI_THEME_COLOR;
            if (get("cfg-dry")) get("cfg-dry").checked = CONFIG.DRY_RUN_MODE;
            if (get("cfg-verbose")) get("cfg-verbose").checked = CONFIG.VERBOSE_LOGGING;
            if (get("cfg-headless")) get("cfg-headless").checked = CONFIG.HEADLESS_MODE;
            if (get("cfg-autosave")) get("cfg-autosave").checked = CONFIG.AUTO_SAVE_GAMES;
            if (get("cfg-max")) get("cfg-max").value = CONFIG.MAX_GAMES_TO_SYNC;
            if (get("cfg-mode")) get("cfg-mode").value = CONFIG.MAX_GAMES_MODE || "first";
            if (get("cfg-extract-limit")) get("cfg-extract-limit").value = "0";
            if (get("cfg-aliases")) get("cfg-aliases").value = "";

            // 4. Update UI themes (color & light mode)
            root.style.setProperty("--theme-color", CONFIG.UI_THEME_COLOR);
            root.classList.remove("light-mode");
            document.getElementById("gfn-settings-panel")?.classList.remove("light-mode");

            tLog("🔄 All settings have been reset to factory defaults.", "warn");
        });
    }
    
    // UI Buttons logic
    document.getElementById("gfn-close-btn").addEventListener("click", () => root?.remove());
    
    // Minimize logic
    const bodyEl = document.getElementById("gfn-body");
    const footerEl = document.getElementById("gfn-footer");
    const minBtn = document.getElementById("gfn-min-btn");
    
    minBtn.addEventListener("click", () => {
        const isMin = bodyEl.classList.toggle("gfn-hidden");
        root.classList.toggle("minimized", isMin);
        if (footerEl) footerEl.classList.toggle("gfn-hidden", isMin);
        minBtn.innerHTML = isMin 
            ? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>` 
            : iconMin;
        minBtn.title = isMin ? "Restore Dashboard" : "Minimize Dashboard";
    });
    
    // Modal Strings
    const MODAL_GUIDE_HTML = `
        <p><strong>Welcome to Library Sync for GeForce NOW!</strong></p>
        <p>Sync your game libraries (Epic Games, Amazon Luna) to GeForce NOW automatically inside your browser tab.</p>
        ${UIComponents.renderModalSection("🎮 Step 1: Extract Your Games", [
            "1. Open a new tab on Epic Games (store.epicgames.com) or Amazon Luna.",
            "2. Open Developer Tools (Press F12 -> Console tab).",
            "3. Paste this script into the console and press Enter.",
            "4. Click Extract Games. When scraping finishes, click Copy List."
        ])}
        ${UIComponents.renderModalSection("⚡ Step 2: Sync to GeForce NOW", [
            "1. Open play.geforcenow.com in your browser and log in.",
            "2. Open Developer Tools (F12), paste the script into Console, and press Enter.",
            "3. Paste your game list into the input box.",
            "4. Click START AUTO-SYNC and let the script run!"
        ])}
        ${UIComponents.renderModalSection("⚙️ Settings Quick Guide", [
            "• Sync Speed / Delay: Adjust wait time between processing each game.",
            "• Match Strictness: Adjusts title search matching strictness.",
            "• Skip Owned: Skips games already in your GFN library to save time.",
            "• Auto-Retry Failed: Retries any failed titles at the end."
        ])}
    `;

    const MODAL_FEATURES_HTML = `
        <p><strong>Feature Highlights</strong></p>
        <p>A quick breakdown of everything included in Library Sync for GeForce NOW:</p>
        ${UIComponents.renderModalSection("✨ Core Features", [
            { h: "🎨 Compact Floating Dashboard", p: "Clean dual-pane UI with settings control, live terminal log output, and minimized mode." },
            { h: "💾 Auto-Saved Progress", p: "Your extracted game list is saved to local storage automatically." }
        ])}
        ${UIComponents.renderModalSection("⚡ Smart Controls", [
            { h: "🎯 Smart Title Matching", p: "Strips edition tags (GOTY, Deluxe) and uses fuzzy matching to find games." },
            { h: "⏯️ Real-Time Pause & Stop", p: "Pause or stop sync at any time without losing your progress." },
            { h: "🔒 Safe & Self-Contained", p: "Runs 100% locally inside your browser tab." }
        ])}
    `;

    // Scalable Modal System
    const modal = document.getElementById("gfn-modal");
    const modalTitle = document.getElementById("gfn-modal-title");
    const modalContent = document.getElementById("gfn-modal-content");
    
    function openModal(title, html) {
        modalTitle.innerHTML = title;
        modalContent.innerHTML = html;
        modal.classList.remove("gfn-hidden");
        modal.classList.add("gfn-fade-in");
    }

    document.getElementById("gfn-guide-btn").addEventListener("click", () => openModal("📖 Quick Setup Guide", MODAL_GUIDE_HTML));
    if (document.getElementById("gfn-features-btn")) {
        document.getElementById("gfn-features-btn").addEventListener("click", () => openModal("✨ Feature Breakdown", MODAL_FEATURES_HTML));
    }
    
    document.getElementById("gfn-modal-close").addEventListener("click", () => {
        modal.classList.add("gfn-hidden");
        modal.classList.remove("gfn-fade-in");
    });
    document.getElementById("gfn-modal-close-top").addEventListener("click", () => {
        modal.classList.add("gfn-hidden");
        modal.classList.remove("gfn-fade-in");
    });
    
    // Export & Clear Logs Logic
    document.getElementById("gfn-clear-terminal-btn").addEventListener("click", () => {
        terminal.innerHTML = "";
        tLog("Terminal cleared.", "success");
    });
    
    const exportBtn = document.getElementById("gfn-export-btn");
    exportBtn.style.display = "block";
    exportBtn.addEventListener("click", () => {
        const text = Array.from(terminal.children).map(div => div.innerText).join("\n");
        Utils.downloadFile(text, CONSTANTS.FILE_NAMES.LOG_TXT);
        tLog("TXT Logs exported successfully!", "success");
    });
    
    // JSON Export button is hooked up inside the SyncEngine's report() method once stats exist.
    const exportJsonBtn = document.getElementById("gfn-export-json-btn");
    // JSON export button stays hidden until sync report() activates it.
    
    const settingsBtn = document.getElementById("gfn-settings-btn");
    const settingsPanel = document.getElementById("gfn-settings-panel");
    if (settingsPanel) {
        const toggleSettings = () => {
            settingsPanel.classList.toggle("open");
            settingsBtn.style.transform = settingsPanel.classList.contains("open") ? "rotate(90deg)" : "rotate(0deg)";
        };
        settingsBtn.addEventListener("click", toggleSettings);
        const settingsCloseBtn = document.getElementById("gfn-settings-close-btn");
        if (settingsCloseBtn) settingsCloseBtn.addEventListener("click", toggleSettings);
        
        }
    
    const advToggle = document.getElementById("gfn-adv-toggle-btn");
    const advOptions = document.getElementById("gfn-adv-options");
    if(advToggle) {
        advToggle.addEventListener("mouseover", () => advToggle.style.color = "var(--theme-color)");
        advToggle.addEventListener("mouseout", () => advToggle.style.color = "#aaa");
        advToggle.addEventListener("click", () => {
            advOptions.classList.toggle("gfn-hidden");
            document.getElementById("gfn-adv-arrow").innerText = advOptions.classList.contains("gfn-hidden") ? "▼" : "▲";
        });
    }

    // Initialize drag-and-drop mechanics for UI containers with 60 FPS rAF batching.
    const makeDraggable = (element, useTransformCenter) => {
        let isDragging = false, startX, startY, initialX, initialY, rafId = null, currentE = null;
        
        element.addEventListener("mousedown", e => {
            // Restrict drag initiation to non-interactive UI elements.
            if (e.target.closest("input, button, textarea, select, option, a, label") || e.target.closest("#gfn-terminal")) return;
            if (e.target.closest("#gfn-close-btn") || e.target.closest("#gfn-settings-close-btn")) return;
            
            isDragging = true;
            startX = e.clientX; startY = e.clientY;
            const rect = element.getBoundingClientRect();
            
            initialX = useTransformCenter ? (rect.left + rect.width / 2) : rect.left;
            initialY = useTransformCenter ? (rect.top + rect.height / 2) : rect.top;
            
            // Remove snap-to-edge classes during free-form drag.
            element.style.bottom = 'auto'; 
            element.style.right = 'auto'; 
            if (!useTransformCenter) element.style.transform = 'none';
            
            element.style.left = initialX + 'px'; 
            element.style.top = initialY + 'px';
            element.style.transition = 'none'; 
            element.style.margin = '0';
        });
        
        document.addEventListener("mousemove", e => {
            if (!isDragging) return;
            e.preventDefault();
            currentE = e;
            if (!rafId) {
                rafId = requestAnimationFrame(() => {
                    if (isDragging && currentE) {
                        element.style.left = (initialX + currentE.clientX - startX) + 'px';
                        element.style.top = (initialY + currentE.clientY - startY) + 'px';
                    }
                    rafId = null;
                });
            }
        });
        
        document.addEventListener("mouseup", () => {
            if (isDragging) {
                isDragging = false;
                if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
                // Restore CSS transitions upon drag release.
                element.style.transition = 'all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275)';
            }
        });
    };
    
    makeDraggable(root, false);
    const settingsPanelObj = document.getElementById("gfn-settings-panel");
    if (settingsPanelObj) makeDraggable(settingsPanelObj, true);

    // Pluggable Extractor Plugin Registry
    const ExtractorRegistry = {
        plugins: {},
        register(name, plugin) { this.plugins[name] = plugin; },
        get(name) { return this.plugins[name]; }
    };

    // Step 4: Execute platform-specific extraction/sync logic.
    const actionBtn = document.getElementById("gfn-action-btn");
    if (!actionBtn) return;
    if (isEpic && CONFIG.ENABLE_EPIC_EXTRACTION) {
        
        const EpicExtractor = {
            gameTitles: new Set(),
            pendingRequests: 0,
            doneLoading: false,
            clickInterval: null,
            finished: false,
            isPaused: false,
            
            init() {
              this.hookFetch();
              this.hookXHR();
              
              const actionBtn = document.getElementById("gfn-action-btn");
              const pauseBtn = document.getElementById("gfn-pause-btn-epic");
              const stopBtn = document.getElementById("gfn-stop-btn-epic");
              const retryBtn = document.getElementById("gfn-retry-btn-epic");
              
              if (!actionBtn) return;
              
              actionBtn.addEventListener("click", () => {
                  actionBtn.classList.add("gfn-hidden");
                  if (pauseBtn) pauseBtn.classList.remove("gfn-hidden");
                  if (stopBtn) stopBtn.classList.remove("gfn-hidden");
                  if (retryBtn) retryBtn.classList.remove("gfn-hidden");
                  
                  tLog("Starting Epic Games extraction protocol...", "info");
                  
                  this.scrapeInitialDOM();
                  this.startAutoClick();
              });

              if (pauseBtn) {
                  pauseBtn.addEventListener("click", () => {
                      if (this.isPaused) {
                          this.isPaused = false;
                          pauseBtn.innerText = "Pause";
                          tLog("Resuming extraction...", "info");
                          this.startAutoClick();
                      } else {
                          this.isPaused = true;
                          pauseBtn.innerText = "Resume";
                          if (this.clickInterval) clearInterval(this.clickInterval);
                          tLog("Extraction paused.", "warn");
                      }
                  });
              }

              if (stopBtn) {
                  stopBtn.addEventListener("click", () => {
                      tLog("Force stopping extraction...", "error");
                      if (this.clickInterval) clearInterval(this.clickInterval);
                      pauseBtn.classList.add("gfn-hidden");
                      stopBtn.classList.add("gfn-hidden");
                      this.doneLoading = true;
                      this.checkDone();
                  });
              }

              if (retryBtn) {
                  retryBtn.addEventListener("click", () => {
                      tLog("To retry properly, the Epic Store UI must be reset.", "warn");
                      tLog(`Reloading page in ${CONSTANTS.DELAYS.EPIC_RETRY_RELOAD / 1000} seconds...`, "warn");
                      setTimeout(() => window.location.reload(), CONSTANTS.DELAYS.EPIC_RETRY_RELOAD);
                  });
              }
            },
            
            // Extract initially visible DOM elements.
            scrapeInitialDOM() {
              const text = document.body.innerText;
              const regex = CONSTANTS.SELECTORS.EPIC.PURCHASED_REGEX;
              let match;
              while ((match = regex.exec(text)) !== null) {
                const title = match[1].trim();
                if (title && !this.gameTitles.has(title)) this.gameTitles.add(title);
              }
            },
            
            // Hijack the global fetch function to secretly intercept Epic background API calls.
            hookFetch() {
              const originalFetch = window.fetch;
              window.fetch = (input, init) => {
                const url = (typeof input === 'string' ? input : input.url) || '';
                const promise = originalFetch(input, init);
                
                if (url.includes(CONSTANTS.URLS.EPIC_API)) {
                  this.pendingRequests++;
                  promise.then(res => res.clone().json()).then(data => { 
                      if (data?.orders) this.onResponse(data.orders); 
                  }).finally(() => { 
                      this.pendingRequests--; 
                      if (!this.isPaused) this.checkDone(); 
                  });
                }
                return promise;
              };
            },
            
            // Also hook XHR just in case some legacy requests try to sneak past us.
            hookXHR() {
              if (XMLHttpRequest.prototype._epicHooked) return;
              XMLHttpRequest.prototype._epicHooked = true;
              
              const originalOpen = XMLHttpRequest.prototype.open;
              const originalSend = XMLHttpRequest.prototype.send;
              
              const self = this;
              
              XMLHttpRequest.prototype.open = function(method, url) { 
                  this._url = url; 
                  originalOpen.apply(this, arguments); 
              };
              
              XMLHttpRequest.prototype.send = function() {
                if (this._url && this._url.includes(CONSTANTS.URLS.EPIC_API)) {
                  self.pendingRequests++;
                  this.addEventListener('loadend', () => {
                    try { 
                        const data = JSON.parse(this.responseText); 
                        if (data?.orders) self.onResponse(data.orders); 
                    } catch (e) {}
                    self.pendingRequests--; 
                    if (!self.isPaused) self.checkDone();
                  });
                }
                return originalSend.apply(this, arguments);
              };
            },
            
            startAutoClick() {
              if (this.clickInterval) clearInterval(this.clickInterval);
              this.clickInterval = setInterval(() => {
                if (this.isPaused) return;

                // Continuous DOM scraping fallback in case the API hooks fail
                this.scrapeInitialDOM();

                const limitEl = document.getElementById("cfg-extract-limit");
                const limit = limitEl ? parseInt(limitEl.value) : 0;
                
                if (limit > 0 && this.gameTitles.size >= limit) {
                    tLog(`Extract limit of ${limit} reached. Stopping pagination.`, "success");
                    clearInterval(this.clickInterval);
                    this.doneLoading = true;
                    this.checkDone();
                    return;
                }
                
                const btn = document.querySelector(CONSTANTS.SELECTORS.EPIC.PAGINATION_BTN);
                if (btn && !btn.disabled) { 
                    btn?.click(); 
                    tLog(`Paginating...`, "info"); 
                } else {
                  this.scrapeInitialDOM(); // Scrape the final page
                  clearInterval(this.clickInterval);
                  this.doneLoading = true;
                  this.checkDone();
                  
                  // Give the network a few seconds to catch up before scraping the final list.
                  setTimeout(() => { 
                      this.pendingRequests = 0; 
                      if (!this.isPaused) this.checkDone(); 
                  }, CONSTANTS.DELAYS.NETWORK_CATCHUP);
                }
              }, CONFIG.EPIC_PAGINATION_DELAY_MS);
            },
            
            onResponse(orders = []) {
              orders.forEach(order => order.items.forEach(item => { 
                  if (item.description.trim()) this.gameTitles.add(item.description.trim()); 
              }));
            },
            
            checkDone() {
              if (this.doneLoading && this.pendingRequests <= 0 && !this.finished && !this.isPaused) {
                this.finished = true;
                let arr = Array.from(this.gameTitles);
                
                const limitEl = document.getElementById("cfg-extract-limit");
                const limit = limitEl ? parseInt(limitEl.value) : 0;
                if (limit > 0 && arr.length > limit) {
                    arr = arr.slice(0, limit);
                }
                
                tLog(`Extraction complete! Saved ${arr.length} unique titles.`, "success");
                
                const ta = document.getElementById("gfn-textarea");
                ta.value = JSON.stringify(arr);
                const autoSave = document.getElementById('cfg-autosave')?.checked ?? CONFIG.AUTO_SAVE_GAMES;
                if (autoSave) Utils.safeStorageSet(CONSTANTS.STORAGE_KEYS.TEXTAREA, ta.value);
                document.getElementById("gfn-result-container").classList.remove("gfn-hidden");
                
                const pauseBtn = document.getElementById("gfn-pause-btn-epic");
                const stopBtn = document.getElementById("gfn-stop-btn-epic");
                if (pauseBtn) pauseBtn.classList.add("gfn-hidden");
                if (stopBtn) stopBtn.classList.add("gfn-hidden");
              }
            }
        };
        
        ExtractorRegistry.register("Epic", EpicExtractor);
        ExtractorRegistry.get("Epic").init();

        // Quick copy button so gamers don't have to manually highlight 500 lines of text.
        document.getElementById("gfn-copy-btn").addEventListener("click", async () => {
            const ok = await Utils.copyToClipboard(document.getElementById("gfn-textarea").value);
            if (ok) tLog("Copied to clipboard! Navigate to GeForce NOW.", "success");
        });

    }        // ========================================== AMAZON LUNA EXTRACTOR ==========================================
    else if (isAmazon && CONFIG.ENABLE_AMAZON_EXTRACTION) {        // Global set maintains state across chunked pagination requests. Allows cumulative extraction across multiple user interactions.
        window._amazonExtracted = window._amazonExtracted || new Set();

        actionBtn.addEventListener("click", async () => {
            actionBtn.disabled = true;
            actionBtn.innerText = "Extracting...";
            tLog("Starting Amazon Luna extraction...", "info");
            
            // Scroll to the absolute bottom of the page to force React to load every single game cover.
            let count = 0;
            while (true) {
                const button = Array.from(document.querySelectorAll(CONSTANTS.SELECTORS.AMAZON.LOAD_MORE_BTN)).find(b => b.innerText.includes(CONSTANTS.STRINGS.BTN_LOAD_MORE));
                if (!button) break; // Reached the bottom!
                
                button.scrollIntoView({ behavior: "smooth", block: "center" });
                await new Promise(r => setTimeout(r, CONFIG.AMAZON_SCROLL_DELAY_MS));
                button?.click();
                count++;
                tLog(`Scrolled to page ${count}...`, "info");
                await new Promise(r => setTimeout(r, CONSTANTS.DELAYS.AMAZON_SCROLL));
            }
            
            // Scrape all visible game titles from the loaded HTML.
            const entries = document.querySelectorAll(CONSTANTS.SELECTORS.AMAZON.ACCORDION_ENTRY);
            let addedThisTime = 0;
            
            entries.forEach((entry) => {
                const titleEl = entry.querySelector(CONSTANTS.SELECTORS.AMAZON.TITLE);
                const innerHTML = entry.innerHTML.toLowerCase();
                if (titleEl) {
                    // Determine store mapping key.
                    const isLuna = innerHTML.includes('luna') && CONFIG.AMAZON_EXTRACT_LUNA_GAMES;
                    const isEpic = innerHTML.includes('epic') && CONFIG.AMAZON_EXTRACT_EPIC_GAMES;
                    const isGOG = innerHTML.includes('gog') && CONFIG.AMAZON_EXTRACT_GOG_GAMES;
                    
                    if (isLuna || isEpic || isGOG) {
                        const title = titleEl.textContent.trim();
                        if (!window._amazonExtracted.has(title)) {
                            window._amazonExtracted.add(title);
                            addedThisTime++;
                        }
                    }
                }
            });
            
            const arr = Array.from(window._amazonExtracted);
            tLog(`Scraped ${addedThisTime} new games.`, "success");
            tLog(`Total accumulated: ${arr.length} games.`, "success");
            tLog(` Change the "Collected in" year dropdown on the page and click Extract again to accumulate more!`, "warn");
            
            const ta = document.getElementById("gfn-textarea");
            ta.value = JSON.stringify(arr);
            const autoSave = document.getElementById('cfg-autosave')?.checked ?? CONFIG.AUTO_SAVE_GAMES;
            if (autoSave) Utils.safeStorageSet(CONSTANTS.STORAGE_KEYS.TEXTAREA, ta.value);
            document.getElementById("gfn-result-container").classList.remove("gfn-hidden");
            
            actionBtn.disabled = false;
            actionBtn.innerText = "Extract Current Year";
        });

        document.getElementById("gfn-copy-btn").addEventListener("click", async () => {
            const ok = await Utils.copyToClipboard(document.getElementById("gfn-textarea").value);
            if (ok) tLog("Copied to clipboard! Navigate to GeForce NOW.", "success");
        });

    }        // ========================================== GEFORCE NOW SYNCHRONIZATION MODULE ==========================================
    else if (isGFN) {        // Hook into GFN XMLHttpRequest to intercept GraphQL API responses. This enables instantaneous state verification for library ownership, bypassing DOM polling latency.
        window.gfnApiResult = null;
        if (!XMLHttpRequest.prototype._hookedForEpicSync) {
            XMLHttpRequest.prototype._hookedForEpicSync = true;
            const origOpen = XMLHttpRequest.prototype.open;
            const origSend = XMLHttpRequest.prototype.send;
            
            XMLHttpRequest.prototype.open = function(method, url) { 
                this._url = url; 
                return origOpen.apply(this, arguments); 
            };
            XMLHttpRequest.prototype.send = function() {
                this.addEventListener("load", () => {
                    // Only care about the GraphQL queries that fetch the 'apps' (games)
                    if (this._url?.includes(CONSTANTS.URLS.GFN_GRAPHQL) && this.responseText.includes(CONSTANTS.STRINGS.API_APPS_PAYLOAD)) {
                        try { window.gfnApiResult = JSON.parse(this.responseText).data.apps.items; } catch {}
                    }
                });
                return origSend.apply(this, arguments);
            };
        }
        
        // Extend hook to fetch API for future-proofing.
        if (!window.fetch._hookedForEpicSync) {
            window.fetch._hookedForEpicSync = true;
            const origFetch = window.fetch;
            window.fetch = async function(...args) {
                const res = await origFetch.apply(this, args);
                const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
                
                if (url.includes(CONSTANTS.URLS.GFN_GRAPHQL)) {
                    res.clone().text().then(text => {
                        if (text.includes(CONSTANTS.STRINGS.API_APPS_PAYLOAD)) { 
                            try { window.gfnApiResult = JSON.parse(text).data.apps.items; } catch {} 
                        }
                    });
                }
                return res;
            };
        }

        // Auto-populate text area from predefined CONFIG.
        const ta = document.getElementById("gfn-textarea");
        if (CONFIG.MANUAL_GAME_LIST && CONFIG.MANUAL_GAME_LIST.length > 0) {
            tLog(`Loaded ${CONFIG.MANUAL_GAME_LIST.length} games directly from your CONFIG file!`, "success");
            ta.value = CONFIG.MANUAL_GAME_LIST.join("\\n");
            
            // Check auto-start configuration flag.
            if (CONFIG.AUTO_START_SYNC_ON_LOAD) {
                tLog(`AUTO_START is enabled. Launching sync automatically!`, "warn");
                setTimeout(() => actionBtn?.click(), CONSTANTS.DELAYS.AUTO_START);
            }
        }

        let stopFlag = false;
        
        // Primary synchronization trigger.
        actionBtn.addEventListener("click", async () => {
            if (actionBtn.innerText.includes("Task Finished")) return;
            
            // Dynamically update CONFIG from UI state.
            CONFIG.SKIP_ALREADY_OWNED = document.getElementById("cfg-skip").checked;
            CONFIG.REQUIRE_PRIMARY_STORE_ONLY = document.getElementById("cfg-strict-store") ? document.getElementById("cfg-strict-store").checked : true;
            CONFIG.AUTO_RETRY_FAILED = document.getElementById("cfg-retry").checked;
            CONFIG.UI_AUTO_CLOSE_ON_FINISH = document.getElementById("cfg-close").checked;
            
            CONFIG.API_WAIT_TIME_MS = parseInt(document.getElementById("cfg-speed").value) || 3000;
            CONFIG.FUZZY_MATCH_THRESHOLD = parseInt(document.getElementById("cfg-fuzzy").value) || 4;
            CONFIG.UI_THEME_COLOR = document.getElementById("cfg-color").value;
            CONFIG.MAX_GAMES_TO_SYNC = parseInt(document.getElementById("cfg-max").value) || 0;
            
            CONFIG.DRY_RUN_MODE = document.getElementById("cfg-dry").checked;
            CONFIG.VERBOSE_LOGGING = document.getElementById("cfg-verbose").checked;
            CONFIG.HEADLESS_MODE = document.getElementById("cfg-headless").checked;
            
            // Parse Custom Aliases.
            const aliasRaw = document.getElementById("cfg-aliases").value;
            if(aliasRaw) {
                aliasRaw.split('\n').forEach(line => {
                    const parts = line.split('=');
                    if(parts.length === 2) {
                        CONFIG.CUSTOM_NAME_ALIASES[parts[0].trim()] = parts[1].trim();
                        if(CONFIG.VERBOSE_LOGGING) tLog(`[DEV] Alias Registered: ${parts[0].trim()} -> ${parts[1].trim()}`, "info");
                    }
                });
            }
            
            // Prioritize selected storefront in matching array.
            const topStore = document.getElementById("cfg-store").value;
            CONFIG.TARGET_STORE_PRIORITY = [topStore, ...CONFIG.TARGET_STORE_PRIORITY.filter(s => s !== topStore)];
            
            let arr;
            try {
                const rawValue = ta.value.trim();
                if (!rawValue) return tLog(" Please paste a games array or list!", "error");
                
                // Handle JSON array input format.
                if (rawValue.startsWith("[") && rawValue.endsWith("]")) {
                    arr = JSON.parse(rawValue);
                } else {
                    // Handle raw text line-by-line parsing.
                    arr = rawValue.split(/[\n,]+/).map(s => s.trim()).filter(s => s.length > 0);
                    tLog(`Parsed ${arr.length} games from your manual list.`, "info");
                }
            } catch(e) {
                return tLog(" Formatting error! Check your list syntax.", "error");
            }
            
            if (!Array.isArray(arr) || arr.length === 0) return tLog(" No games found to sync!", "error");

            // Apply configuration limits and selection modes.
            if (CONFIG.MAX_GAMES_TO_SYNC > 0 && arr.length > CONFIG.MAX_GAMES_TO_SYNC) {
                const mode = document.getElementById("cfg-mode") ? document.getElementById("cfg-mode").value : "first";
                
                if (mode === "random") {
                    tLog(`🎲 Randomly selecting ${CONFIG.MAX_GAMES_TO_SYNC} games from your list.`, "warn");
                    for (let i = arr.length - 1; i > 0; i--) {
                        const j = Math.floor(Math.random() * (i + 1));
                        [arr[i], arr[j]] = [arr[j], arr[i]];
                    }
                    arr = arr.slice(0, CONFIG.MAX_GAMES_TO_SYNC);
                } else if (mode === "alpha") {
                    tLog(`🔤 Sorting Alphabetically (A-Z) and taking top ${CONFIG.MAX_GAMES_TO_SYNC}.`, "info");
                    arr.sort((a, b) => a.localeCompare(b));
                    arr = arr.slice(0, CONFIG.MAX_GAMES_TO_SYNC);
                } else if (mode === "revalpha") {
                    tLog(`🔤 Sorting Reverse Alphabetically (Z-A) and taking top ${CONFIG.MAX_GAMES_TO_SYNC}.`, "info");
                    arr.sort((a, b) => b.localeCompare(a));
                    arr = arr.slice(0, CONFIG.MAX_GAMES_TO_SYNC);
                } else if (mode === "shortest") {
                    tLog(`📏 Sorting by Shortest Title Length and taking top ${CONFIG.MAX_GAMES_TO_SYNC}.`, "info");
                    arr.sort((a, b) => a.length - b.length);
                    arr = arr.slice(0, CONFIG.MAX_GAMES_TO_SYNC);
                } else if (mode === "longest") {
                    tLog(`📏 Sorting by Longest Title Length and taking top ${CONFIG.MAX_GAMES_TO_SYNC}.`, "info");
                    arr.sort((a, b) => b.length - a.length);
                    arr = arr.slice(0, CONFIG.MAX_GAMES_TO_SYNC);
                } else if (mode === "last") {
                    tLog(`⚡ Limited to syncing the LAST ${CONFIG.MAX_GAMES_TO_SYNC} games.`, "warn");
                    arr = arr.slice(-CONFIG.MAX_GAMES_TO_SYNC);
                } else {
                    tLog(`⚡ Limited to syncing the FIRST ${CONFIG.MAX_GAMES_TO_SYNC} games.`, "warn");
                    arr = arr.slice(0, CONFIG.MAX_GAMES_TO_SYNC);
                }
            }

            actionBtn.disabled = true;
            actionBtn.innerText = "Syncing...";
            document.getElementById("gfn-textarea").disabled = true;
            
            tLog(`IGNITION: Starting sync sequence for ${arr.length} titles.`, "success");
            document.getElementById("gfn-progress-container").classList.remove("gfn-hidden");
            const pauseBtn = document.getElementById("gfn-pause-btn");
            const stopBtn = document.getElementById("gfn-stop-btn");
            const retryBtn = document.getElementById("gfn-retry-btn");
            
            // --- V3 EXECUTION STATE MACHINE ---
            window.__GFN_STATE = { isRunning: true, isPaused: false, shouldStop: false };
            
            const gfnPollDelay = async (ms) => {
                let waited = 0;
                const targetMs = Utils.getRandomJitterDelay(ms);
                while (waited < targetMs) {
                    if (window.__GFN_STATE.shouldStop) throw new Error("STOPPED_BY_USER");
                    if (!window.__GFN_STATE.isPaused) {
                        await new Promise(r => setTimeout(r, 100));
                        waited += 100;
                    } else {
                        await new Promise(r => setTimeout(r, 500)); // Sleep while paused
                    }
                }
            };
            
            pauseBtn.onclick = () => {
                window.__GFN_STATE.isPaused = !window.__GFN_STATE.isPaused;
                if (window.__GFN_STATE.isPaused) {
                    pauseBtn.innerHTML = `<span style="display:flex; align-items:center; justify-content:center; gap:8px;"><svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> RESUME</span>`;
                    pauseBtn.style.background = "linear-gradient(135deg, #5cb85c 0%, #4cae4c 100%)";
                    tLog("⏸️ Execution PAUSED by user. Click Resume to continue.", "warn");
                } else {
                    pauseBtn.innerHTML = `<span style="display:flex; align-items:center; justify-content:center; gap:8px;"><svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg> PAUSE</span>`;
                    pauseBtn.style.background = "linear-gradient(135deg, #f0ad4e 0%, #ec971f 100%)";
                    tLog("▶️ Execution RESUMED.", "success");
                }
            };

            const miniStopBtn = document.getElementById("gfn-mini-stop-btn");
            if (miniStopBtn) miniStopBtn.onclick = () => stopBtn.click();

            stopBtn.onclick = () => {
                window.__GFN_STATE.shouldStop = true;
                stopBtn.disabled = true;
                pauseBtn.disabled = true;
                stopBtn.innerText = "Stopping...";
                if (miniStopBtn) {
                    miniStopBtn.disabled = true;
                    miniStopBtn.innerText = "Stopping...";
                }
            };
            
            // State Machine: Navigates GFN DOM structure.
            class SyncEngine {
                constructor(titles) {
                    this.queue = [...titles];
                    this.synced = [];
                    this.skipped = [];
                    this.totalGames = titles.length;
                    this.searchAttempts = 0;
                    this.failStreak = 0;
                    this.startTime = Date.now();
                }
                
                async run() { 
                    actionBtn.style.display = "none";
                    retryBtn.style.display = "none";
                    stopBtn.style.display = "block";
                    pauseBtn.style.display = "block";
                    if (miniStopBtn) {
                        miniStopBtn.classList.remove("gfn-hidden");
                        miniStopBtn.disabled = false;
                        miniStopBtn.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor" style="width:11px;height:11px;"><rect x="4" y="4" width="16" height="16"></rect></svg> STOP`;
                    }
                    
                    stopBtn.disabled = false;
                    pauseBtn.disabled = false;
                    stopBtn.innerHTML = `<span style="display:flex; align-items:center; justify-content:center; gap:8px;"><svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><rect x="4" y="4" width="16" height="16"></rect></svg> STOP</span>`;
                    
                    window.__GFN_STATE.isRunning = true;
                    window.__GFN_STATE.isPaused = false;
                    window.__GFN_STATE.shouldStop = false;
                    
                    // Hook window.open and external link clicks to block accidental store redirections
                    if (!window._origWindowOpen) {
                        window._origWindowOpen = window.open;
                        window.open = function(url, target, features) {
                            if (window.__GFN_STATE && window.__GFN_STATE.isRunning) {
                                tLog(`[INTERCEPTED] Blocked external popup/redirect to: ${url || 'blank'}`, "warn");
                                return null;
                            }
                            return window._origWindowOpen.apply(this, arguments);
                        };
                    }
                    if (!window._gfnClickGuardBound) {
                        window._gfnClickGuardBound = true;
                        document.addEventListener("click", (e) => {
                            if (window.__GFN_STATE && window.__GFN_STATE.isRunning) {
                                const anchor = e.target.closest("a");
                                if (anchor) {
                                    const href = (anchor.getAttribute("href") || "").toLowerCase();
                                    if (href && (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//")) && !href.includes("geforcenow.com")) {
                                        e.preventDefault();
                                        e.stopPropagation();
                                        tLog(`[INTERCEPTED] Blocked external link navigation to: ${href}`, "warn");
                                    }
                                }
                            }
                        }, true);
                    }
                    
                    if (CONFIG.HEADLESS_MODE) {
                        document.querySelector(".gfn-pane-left").classList.add("gfn-hidden");
                    }
                    
                    await this.processNext(); 
                }
                
                async processNext() {
                    const completed = this.synced.length + this.skipped.length;
                    
                    if (window.__GFN_STATE.shouldStop) {
                        tLog("Force stopped by user.", "error");
                        return this.report();
                    }
                    if (this.queue.length === 0) return this.report(); // All done!
                    
                    let rawTitle = this.queue.shift();
                    
                    // Apply custom alias mapping if configured.
                    if (CONFIG.CUSTOM_NAME_ALIASES && CONFIG.CUSTOM_NAME_ALIASES[rawTitle]) {
                        tLog(`Found Alias: Remapping "${rawTitle}" -> "${CONFIG.CUSTOM_NAME_ALIASES[rawTitle]}"`, "info");
                        this.currentTitle = CONFIG.CUSTOM_NAME_ALIASES[rawTitle];
                    } else {
                        this.currentTitle = rawTitle;
                    }

                    updateProgress(completed, this.totalGames, this.currentTitle);
                    this.searchAttempts = 0;
                    tLog(`⏳ Scanning: [${this.currentTitle}]`, "info");
                    
                    try { 
                        await this.trySearch(); 
                        this.failStreak = 0; // Reset streak on success
                    } catch (err) {
                        tLog(` Failed: ${err.message}`, "error");
                        this.skipped.push(this.currentTitle);
                        this.failStreak++;
                        
                        // Attempt graceful exit to preserve subsequent queue items.
                        const closeBtn = document.querySelector(CONSTANTS.SELECTORS.GFN.DIALOG_CLOSE);
                        if (closeBtn) closeBtn?.click();
                        if (window.location.href.includes("game-id")) window.history.back();
                        
                        // Exponential backoff to avoid getting rate limited if GFN is struggling
                        const backoff = Math.min(CONSTANTS.DELAYS.RETRY_BACKOFF_MAX, CONSTANTS.DELAYS.RETRY_BACKOFF_BASE * Math.pow(1.5, this.failStreak - 1));
                        if(CONFIG.VERBOSE_LOGGING) tLog(`[DEV] Applying backoff: ${Math.round(backoff)}ms`, "warn");
                        setTimeout(() => { this.processNext(); }, backoff);
                    }
                }
                
                // Iteratively reduce search term specificity to broaden match potential.
                getSearchTerm() {
                    if (this.searchAttempts === 0) return Utils.cleanGameTitle(this.currentTitle);
                    if (this.searchAttempts === 1) return this.currentTitle;
                    if (this.searchAttempts === 2 && this.currentTitle.includes(":")) return this.currentTitle.split(":")[0].trim();
                    if (this.searchAttempts === 3 && this.currentTitle.includes("-")) return this.currentTitle.split("-")[0].trim();
                    if (this.searchAttempts === 4) return this.currentTitle.replace(/[^\w\s]/g, "").trim();
                    return null;
                }
                
                async trySearch() {
                    const term = this.getSearchTerm();
                    if (!term) throw new Error("Exhausted search variations.");
                    window.gfnApiResult = null; // Clear previous network intercept data.
                    
                    const searchInput = document.querySelector(CONSTANTS.SELECTORS.GFN.SEARCH_INPUT);
                    if (!searchInput) throw new Error("Could not locate GFN search box.");
                    
                    // React inputs are stubborn. We must trigger physical input events or the search bar ignores us.
                    const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
                    setter.call(searchInput, term);
                    searchInput.dispatchEvent(new Event("input", { bubbles: true }));
                    searchInput.dispatchEvent(new Event("change", { bubbles: true }));
                    searchInput?.click();
                    
                    // Use V3 State Polling Delay
                    await gfnPollDelay(CONFIG.API_WAIT_TIME_MS);
                    
                    // Normalize titles for precise matching.
                    const norm = s => s.toLowerCase().replace(/[^\w\s]/g, " ").replace(/\s+/g, " ").trim();
                    const searchTarget = norm(term);
                    
                    // Levenshtein Distance algorithm implementation.
                    const distance = (a, b) => {
                        const matrix = Array.from({ length: b.length + 1 }, (_, i) => [i]);
                        for (let i = 1; i <= a.length; i++) matrix[0][i] = i;
                        for (let i = 1; i <= b.length; i++) {
                            for (let j = 1; j <= a.length; j++) {
                                matrix[i][j] = b.charAt(i-1)===a.charAt(j-1) ? matrix[i-1][j-1] : Math.min(matrix[i-1][j-1]+1, matrix[i][j-1]+1, matrix[i-1][j]+1);
                            }
                        }
                        return matrix[b.length][a.length];
                    };
                    
                    let bestMatchIndex = -1;
                    
                    // Prioritize intercepted API results over DOM extraction for matching.
                    if (window.gfnApiResult?.length > 0) {
                        const match = window.gfnApiResult.find(i => norm(i.title).includes(searchTarget) || searchTarget.includes(norm(i.title)));
                        if (match) {
                            bestMatchIndex = window.gfnApiResult.indexOf(match);
                        } else {
                            // Fallback to fuzzy string comparison if exact match fails.
                            let bestDist = Infinity;
                            window.gfnApiResult.forEach((item, idx) => {
                                const d = distance(norm(item.title), searchTarget);
                                if (d < bestDist && d <= CONFIG.FUZZY_MATCH_THRESHOLD) { 
                                    bestDist = d; bestMatchIndex = idx; 
                                }
                            });
                        }
                        
                        const StoreMatcher = {
                            match(text, storePref) {
                                if (!text || !storePref) return false;
                                const txt = (typeof text === 'string' ? text : (text.innerText || text.textContent || '')).toUpperCase().trim();
                                const p = storePref.toUpperCase().trim();
                                if (p.includes("EPIC")) return txt.includes("EPIC") || txt.includes("EGSTORE") || txt.includes("EGS");
                                if (p.includes("STEAM")) return txt.includes("STEAM");
                                if (p.includes("XBOX")) return txt.includes("XBOX") || txt.includes("MICROSOFT");
                                if (p.includes("UBISOFT")) return txt.includes("UBISOFT") || txt.includes("UPLAY");
                                if (p.includes("GOG")) return txt.includes("GOG");
                                return txt.includes(p);
                            },
                            extractText(v) {
                                return v ? [v.storeName, v.providerName, v.appId, v.storeId].filter(Boolean).join(' ').toUpperCase() : '';
                            },
                            isAvailable(item, storeName) {
                                if (!item || !item.variants || !Array.isArray(item.variants) || item.variants.length === 0) return true;
                                return item.variants.some(v => this.match(this.extractText(v), storeName));
                            },
                            isOwned(item, storeName) {
                                if (!item || !item.variants || !Array.isArray(item.variants) || item.variants.length === 0) return false;
                                const v = item.variants.find(v => this.match(this.extractText(v), storeName));
                                return v && v.gfn && v.gfn.library && v.gfn.library.status !== CONSTANTS.STRINGS.STATUS_NOT_OWNED;
                            }
                        };

                        // Check if the game is already in the user library!
                        if (bestMatchIndex !== -1) {
                            const m = window.gfnApiResult[bestMatchIndex];
                            if(CONFIG.VERBOSE_LOGGING) tLog(`[DEV] Match found in API: "${m.title}"`, "info");
                            
                            const primaryStore = (CONFIG.TARGET_STORE_PRIORITY && CONFIG.TARGET_STORE_PRIORITY[0]) ? CONFIG.TARGET_STORE_PRIORITY[0] : "Epic Games";
                            const requirePrimary = CONFIG.REQUIRE_PRIMARY_STORE_ONLY !== false;

                            // Check if target store (e.g. Epic Games) exists for this game on GFN
                            if (requirePrimary && m.variants && m.variants.length > 0 && !StoreMatcher.isAvailable(m, primaryStore)) {
                                tLog(` Primary store [${primaryStore}] variant not available on GFN for "${m.title}". Skipping.`, "warn");
                                this.skipped.push(this.currentTitle);
                                return this.processNext();
                            }

                            // Check ownership for the target store
                            if (CONFIG.SKIP_ALREADY_OWNED) {
                                const isOwned = requirePrimary ? StoreMatcher.isOwned(m, primaryStore) : m.variants.some(v => v.gfn?.library?.status !== CONSTANTS.STRINGS.STATUS_NOT_OWNED);
                                if (isOwned) {
                                    tLog(` Already in Library (${primaryStore}). Skipping.`, "success");
                                    this.synced.push(this.currentTitle);
                                    return this.processNext();
                                }
                            }
                        } else if(CONFIG.VERBOSE_LOGGING) {
                            tLog(`[DEV] No exact API match found. Falling back to DOM cards.`, "warn");
                        }
                    }
                    
                    // Extract game card elements from DOM.
                    const cards = Array.from(document.querySelectorAll(CONSTANTS.SELECTORS.GFN.GAME_TILE));
                    
                    // Select card matching API index; fallback to generic text search.
                    let targetCard = bestMatchIndex >= 0 ? cards[bestMatchIndex] : cards.find(c => norm(c.textContent || "").includes(searchTarget));
                    
                    if (!targetCard) {
                        this.searchAttempts++;
                        return this.trySearch();
                    }
                    
                    (targetCard.querySelector('img') || targetCard)?.click();
                    await this.handleGamePanel();
                }
                
                async handleGamePanel() {
                    let state = "start";
                    let attempts = 0;
                    const isVisible = (el) => el && (el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0);
                    
                    const isExternalLink = (el) => {
                        if (!el) return false;
                        let curr = el;
                        while (curr && curr !== document.body) {
                            if (curr.tagName === 'A') {
                                const href = (curr.getAttribute('href') || '').toLowerCase();
                                const target = (curr.getAttribute('target') || '').toLowerCase();
                                if (href && (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//'))) {
                                    if (!href.includes('geforcenow.com')) return true;
                                }
                                if (target === '_blank') return true;
                            }
                            const attrHref = (curr.getAttribute('href') || curr.getAttribute('data-href') || '').toLowerCase();
                            if (attrHref && (attrHref.includes('steampowered.com') || attrHref.includes('epicgames.com') || attrHref.includes('gog.com') || attrHref.includes('ubisoft.com') || attrHref.includes('microsoft.com') || attrHref.includes('xbox.com'))) {
                                return true;
                            }
                            curr = curr.parentElement;
                        }
                        const text = (el.innerText || el.textContent || '').toUpperCase().trim();
                        return CONSTANTS.EXTERNAL_LINK_KEYWORDS.some(k => text.includes(k));
                    };
                    
                    while (attempts < CONFIG.PANEL_TIMEOUT_SECONDS) {
                        if (window.__GFN_STATE.shouldStop) return;
                        await gfnPollDelay(CONSTANTS.DELAYS.POLL_INTERVAL);
                        
                        const doneBtn = Array.from(document.querySelectorAll(CONSTANTS.SELECTORS.GFN.BUTTON)).find(b => isVisible(b) && !isExternalLink(b) && b.innerText.trim().toUpperCase() === CONSTANTS.STRINGS.BTN_DONE);
                        if (doneBtn) {
                            tLog("✅ Synced!", "success");
                            doneBtn?.click();
                            this.synced.push(this.currentTitle);
                            await gfnPollDelay(CONSTANTS.DELAYS.DIALOG_WAIT);
                            break;
                        }
                        
                        const confirmBtn = Array.from(document.querySelectorAll(CONSTANTS.SELECTORS.GFN.BUTTON)).find(b => isVisible(b) && !isExternalLink(b) && [CONSTANTS.STRINGS.BTN_YES, CONSTANTS.STRINGS.BTN_CONFIRM, CONSTANTS.STRINGS.BTN_CONTINUE].includes(b.innerText.trim().toUpperCase()));
                        if (confirmBtn) {
                            if (CONFIG.DRY_RUN_MODE) {
                                tLog(`[DRY RUN] Skipped confirming dialog for ${this.currentTitle}`, "warn");
                                this.synced.push(this.currentTitle);
                                break;
                            }
                            tLog("✅ Confirmed dialog.", "success");
                            confirmBtn?.click();
                            this.synced.push(this.currentTitle);
                            await gfnPollDelay(CONSTANTS.DELAYS.DIALOG_WAIT);
                            break;
                        }

                        // Look through the user store priority list and ensure primary store (e.g. Epic Games) is selected
                        const primaryStore = (CONFIG.TARGET_STORE_PRIORITY && CONFIG.TARGET_STORE_PRIORITY[0]) ? CONFIG.TARGET_STORE_PRIORITY[0] : "Epic Games";
                        const requirePrimary = CONFIG.REQUIRE_PRIMARY_STORE_ONLY !== false;

                        const rawStoreChips = Array.from(document.querySelectorAll(CONSTANTS.SELECTORS.GFN.STORE_CHIPS))
                            .filter(b => isVisible(b) && !isExternalLink(b));

                        // Filter chips to only those that explicitly name a store provider
                        const actualStoreChips = rawStoreChips.filter(b => 
                            ["EPIC", "STEAM", "XBOX", "UBISOFT", "GOG"].some(s => (b.innerText || b.textContent || "").toUpperCase().includes(s))
                        );

                        let storeBtn = null;

                        if (actualStoreChips.length > 0) {
                            const primaryChip = actualStoreChips.find(b => StoreMatcher.match(b, primaryStore));
                            if (primaryChip) {
                                storeBtn = primaryChip;
                            } else if (!requirePrimary) {
                                for (const storePref of CONFIG.TARGET_STORE_PRIORITY) {
                                    const fallbackChip = actualStoreChips.find(b => StoreMatcher.match(b, storePref));
                                    if (fallbackChip) {
                                        storeBtn = fallbackChip;
                                        break;
                                    }
                                }
                            } else {
                                const panelText = (document.querySelector('.dialog, .game-details, [role="dialog"], body')?.innerText || '').toUpperCase();
                                if (!StoreMatcher.match(panelText, primaryStore)) {
                                    tLog(`⚠️ ${primaryStore} variant not available on GFN. Skipping.`, "warn");
                                    this.skipped.push(this.currentTitle);
                                    break;
                                }
                            }
                        }
                        
                        if (storeBtn && !state.includes("store_selected")) {
                            tLog(`🔗 Selecting ${storeBtn.innerText.trim()}...`, "info");
                            storeBtn?.click();
                            state += "_store_selected";
                            await gfnPollDelay(CONSTANTS.DELAYS.DIALOG_WAIT);
                            continue; // Go to next loop to let the UI update
                        }
                        
                        // Identify target action button.
                        const addBtns = Array.from(document.querySelectorAll(CONSTANTS.SELECTORS.GFN.BUTTON)).filter(b => isVisible(b) && !isExternalLink(b) && CONSTANTS.STRINGS.ADD_BUTTON_LABELS.some(k => (b.innerText || "").toUpperCase().trim() === k));
                        
                        if (addBtns.length > 0) {
                            const addBtn = addBtns[addBtns.length - 1];        // Add buttons are often layered. The last one in the DOM is usually the active overlay. Check disabled state to determine prior ownership.
                            if (addBtn.disabled || addBtn.getAttribute('aria-disabled') === 'true') {
                                if (state === "start") {
                                    tLog("✅ Already Owned.", "success");
                                    this.synced.push(this.currentTitle);
                                    break;
                                }
                            } else {
                                // Trigger add action.
                                if (!state.includes("adding")) {
                                    if(CONFIG.DRY_RUN_MODE) {
                                        tLog(`[DRY RUN] Skipped adding ${this.currentTitle} to library`, "warn");
                                        this.synced.push(this.currentTitle);
                                        break;
                                    }
                                    if(CONFIG.VERBOSE_LOGGING) tLog(`[DEV] Clicking Primary Add Button`, "info");
                                    addBtn?.click();
                                    state += "_adding";
                                    continue;
                                } else if (addBtns.length > 1 && !state.includes("secondary")) { 
                                    // Sometimes there are two layers of add buttons (e.g., clicking the store, then clicking Add).
                                    if(CONFIG.DRY_RUN_MODE) {
                                        tLog(`[DRY RUN] Skipped secondary add button for ${this.currentTitle}`, "warn");
                                        this.synced.push(this.currentTitle);
                                        break;
                                    }
                                    if(CONFIG.VERBOSE_LOGGING) tLog(`[DEV] Clicking Secondary Add Button`, "info");
                                    addBtn?.click();
                                    state += "_secondary";
                                    continue;
                                }
                            }
                        }
                        
                        attempts++;
                    }
                    
                    if (attempts >= CONFIG.PANEL_TIMEOUT_SECONDS) tLog(`️ Operation Timed Out.`, "warn");
                    
                    // Clean up time: Close any lingering panels or modals before moving to the next game.
                    const closeBtn = document.querySelector(CONSTANTS.SELECTORS.GFN.DIALOG_CLOSE);
                    if (closeBtn) closeBtn?.click();
                    
                    // Revert navigation to primary grid view.
                    if (window.location.href.includes("game-id")) {
                        window.history.back();
                        await gfnPollDelay(CONSTANTS.DELAYS.POLL_INTERVAL);
                    }
                    
                    // Let the DOM settle, then grab the next game in the queue!
                    setTimeout(() => { this.processNext(); }, CONSTANTS.DELAYS.DOM_SETTLE);
                }
                
                report() {
                    window.__GFN_STATE.isRunning = false;
                    updateProgress(this.totalGames, this.totalGames, "Sync Complete!");
                    const miniStopBtn = document.getElementById("gfn-mini-stop-btn");
                    if (miniStopBtn) miniStopBtn.classList.add("gfn-hidden");
                    
                    const durationSec = Math.max(1, Math.round((Date.now() - (this.startTime || Date.now())) / 1000));
                    const avgSpeed = (durationSec / Math.max(1, this.totalGames)).toFixed(1);
                    const matchRate = Math.round((this.synced.length / Math.max(1, this.totalGames)) * 100);

                    // Diff Engine: Calculate new additions vs previous sync history
                    const prevHistoryRaw = Utils.safeStorageGet(CONSTANTS.STORAGE_KEYS.HISTORY);
                    const prevHistory = Utils.safeJsonParse(prevHistoryRaw, []);
                    const newAdditions = this.synced.filter(g => !prevHistory.includes(g));
                    if (newAdditions.length > 0) {
                        tLog(`🆕 ${newAdditions.length} NEW titles added to library history this session!`, "success");
                    }
                    const updatedHistory = [...new Set([...prevHistory, ...this.synced])];
                    Utils.safeStorageSet(CONSTANTS.STORAGE_KEYS.HISTORY, updatedHistory);

                    tLog(`🎯 Sync Complete! Synced: ${this.synced.length}/${this.totalGames} (${matchRate}%) | Duration: ${durationSec}s (${avgSpeed}s/game)`, "success");
                    actionBtn.innerText = "Task Finished";
                    actionBtn.disabled = false;
                    actionBtn.style.display = "block";
                    
                    // When user clicks "Task Finished", reset the entire UI to a fresh state
                    // so they can paste new games and sync again without reloading.
                    actionBtn.onclick = () => {
                        actionBtn.innerHTML = `<span style="display:flex; align-items:center; justify-content:center; gap:8px;">
                            <svg viewBox="0 0 24 24" fill="currentColor" style="width:18px;height:18px;"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> START SYNC
                        </span>`;
                        actionBtn.onclick = null; // Remove this handler so the original click listener takes over
                        
                        // Reset progress bar
                        updateProgress(0, 0, "Ready");
                        
                        // Hide retry button
                        const retryBtn = document.getElementById("gfn-retry-btn");
                        if (retryBtn) retryBtn.style.display = "none";
                        
                        // Hide export buttons
                        const jsonBtn = document.getElementById("gfn-export-json-btn");
                        const failedBtn = document.getElementById("gfn-export-failed-btn");
                        const txtBtn = document.getElementById("gfn-export-btn");
                        if (jsonBtn) jsonBtn.style.display = "none";
                        if (failedBtn) failedBtn.style.display = "none";
                        if (txtBtn) txtBtn.style.display = "none";
                        
                        // Re-enable the textarea so user can paste new games
                        const ta = document.getElementById("gfn-textarea");
                        if (ta) { ta.disabled = false; ta.style.opacity = "1"; }
                        
                        // Show headless pane again if it was hidden
                        const leftPane = document.querySelector(".gfn-pane-left");
                        if (leftPane) leftPane.classList.remove("gfn-hidden");
                        
                        tLog("UI reset. Ready for a new sync session!", "success");
                    };
                    
                    const pauseBtn = document.getElementById("gfn-pause-btn");
                    const stopBtn = document.getElementById("gfn-stop-btn");
                    const retryBtn = document.getElementById("gfn-retry-btn");
                    pauseBtn.style.display = "none";
                    stopBtn.style.display = "none";
                    
                    if (this.skipped.length > 0) {
                        retryBtn.style.display = "block";
                        // Override onclick handler to prevent duplicate listeners.
                        retryBtn.onclick = () => {
                            tLog(` Retrying ${this.skipped.length} failed games...`, "warn");
                            const engine = new SyncEngine(this.skipped);
                            engine.run();
                        };
                    }
                    
                    // Activate Export Buttons
                    const jsonBtn = document.getElementById("gfn-export-json-btn");
                    const failedBtn = document.getElementById("gfn-export-failed-btn");
                    const txtBtn = document.getElementById("gfn-export-btn");
                    
                    if (jsonBtn) {
                        jsonBtn.style.display = "block";
                        jsonBtn.onclick = () => {
                            const data = JSON.stringify({
                                total_games_processed: this.totalGames,
                                successful_syncs: this.synced,
                                failed_or_skipped: this.skipped,
                                timestamp: new Date().toISOString()
                            }, null, 2);
                            Utils.downloadFile(data, CONSTANTS.FILE_NAMES.REPORT_JSON, 'application/json');
                            tLog("JSON Report downloaded successfully!", "success");
                        };
                    }
                    
                    if (this.skipped.length > 0 && failedBtn) {
                        failedBtn.style.display = "block";
                        failedBtn.onclick = async () => {
                            const ok = await Utils.copyToClipboard(JSON.stringify(this.skipped));
                            if (ok) tLog("Failed games copied to clipboard!", "success");
                        };
                    }
                    
                    if (txtBtn) {
                        txtBtn.style.display = "block";
                        txtBtn.onclick = () => {
                            const data = "--- NVIDIA GeForce Library Sync Report ---\n\n" +
                                         "SUCCESSFULLY SYNCED (" + this.synced.length + "):\n" +
                                         this.synced.join("\n") + "\n\n" +
                                         "FAILED/SKIPPED (" + this.skipped.length + "):\n" +
                                         this.skipped.join("\n");
                            Utils.downloadFile(data, CONSTANTS.FILE_NAMES.REPORT_TXT, 'text/plain');
                            tLog("TXT Report downloaded successfully!", "success");
                        };
                    }
                    
                    if (CONFIG.UI_AUTO_CLOSE_ON_FINISH && CONFIG.ENABLE_UI) setTimeout(() => root?.remove(), 3000);
                }
            }
            
            // Initialize and execute SyncEngine
            new SyncEngine(arr).run();
        });
        
        // If UI is disabled, we auto-start the sync using the manual list!
        if (!CONFIG.ENABLE_UI && isGFN) {
            if (CONFIG.MANUAL_GAME_LIST.length > 0) {
                console.log("Auto-Sync Pro: Running in Headless API Mode...");
                document.getElementById("gfn-textarea").value = CONFIG.MANUAL_GAME_LIST.join('\n');
                actionBtn?.click();
            } else {
                console.log("Auto-Sync Pro: ENABLE_UI is false, but MANUAL_GAME_LIST is empty. Nothing to do.");
            }
        }
    }
} catch (error) {
    console.error("==========================================");
    console.error(" AUTO-SYNC PRO EXECUTION ERROR ");
    console.error("==========================================");
    console.error("An unhandled exception occurred during script execution.");
    console.error("Error details:", error.message);
    console.error("Stack trace:", error.stack);
    console.error("If this keeps happening, try refreshing the page and running the script again.");
    console.error("==========================================");
}
})();