Gelbooru Suite

Enhances Gelbooru with thumbnail previews, a categorized pop-up search, an immersive viewer, and more.

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name        Gelbooru Suite
// @namespace   GelbooruEnhancer
// @version     2.0
// @description Enhances Gelbooru with thumbnail previews, a categorized pop-up search, an immersive viewer, and more.
// @author      Testador (Refactored by Gemini)
// @match       *://gelbooru.com/*
// @icon        https://gelbooru.com/layout/gelbooru-logo.svg
// @grant       GM_download
// @grant       GM.xmlHttpRequest
// @grant       GM.getValue
// @grant       GM.setValue
// @grant       GM_registerMenuCommand
// @grant       GM_addStyle
// @grant       GM_openInTab
// @license     MIT
// @run-at      document-idle
// ==/UserScript==

/* global navigatePrev, navigateNext */

(function() {
    'use strict';

    // =================================================================================
    // CONFIGURATION AND CONSTANTS MODULE
    // =================================================================================
    const Config = {
        API_URLS: {
            BASE: 'https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1',
            AUTOCOMPLETE: 'https://gelbooru.com/index.php?page=autocomplete2&type=tag_query&limit=10',
        },
        DEFAULT_SETTINGS: {
            DEBUG: true,
            // --- Global Toggles ---
            ENABLE_ADVANCED_SEARCH: true,
            ENABLE_PEEK_PREVIEWS: true,
            ENABLE_ADD_TO_POOL: true,
            ENABLE_DOWNLOADER: true,
            HIDE_PAGE_SCROLLBARS: true,
            BLACKLIST_TAGS: '',

            // --- Downloader ---
            DOWNLOAD_FOLDER: 'gelbooru',

            // --- Previews ---
            PREVIEW_QUALITY: 'high',
            PREVIEW_SCALE_FACTOR: 2.5,
            PREVIEW_VIDEOS_MUTED: true,
            PREVIEW_VIDEOS_LOOP: true,
            SHOW_DELAY: 350,
            HIDE_DELAY: 175,

            // --- Hotkeys ---
            KEY_GALLERY_NEXT_PAGE: 'ArrowRight',
            KEY_GALLERY_PREV_PAGE: 'ArrowLeft',
            KEY_PEEK_VIDEO_PLAY_PAUSE: ' ',
            KEY_PEEK_VIDEO_SEEK_BACK: 'ArrowLeft',
            KEY_PEEK_VIDEO_SEEK_FORWARD: 'ArrowRight',
            KEY_VIEWER_PREV_IMAGE: 'ArrowUp',
            KEY_VIEWER_NEXT_IMAGE: 'ArrowDown',
            KEY_VIEWER_TOGGLE_INFO: 'i',

            // --- API ---
            API_KEY: '',
            USER_ID: '',
        },
        SELECTORS: {
            SEARCH_INPUT: '#tags-search',
            THUMBNAIL_GRID_SELECTOR: '.thumbnail-container, #post-list > div, .mainBodyPadding',
            THUMBNAIL_ANCHOR_SELECTOR: '.thumbnail-preview > a',
            VIDEO_PLAYER_SELECTOR: 'main video#gelcomVideoPlayer',
            IMAGE_SELECTOR: 'main #image',
            PAGINATION_CURRENT_SELECTOR: '.pagination b',
            PREVIEW_CONTAINER_ID: 'enhancer-preview-container',
            SETTINGS_MODAL_ID: 'enhancer-settings-modal',
            ADVANCED_SEARCH_MODAL_ID: 'gbs-advanced-search-modal',
            galleryNavSubmenu: '.navSubmenu',
            postTagListItem: '#tag-list li[class*="tag-type-"]',
            postTagLink: 'a[href*="&tags="]',
            MEDIA_VIEWER_THUMBNAIL_ANCHOR: '.thumbnail-container > span > a, .thumbnail-container > .thumbnail-preview > a',

        },
        STORAGE_KEYS: {
            SUITE_SETTINGS: 'gelbooruSuite_settings',
            POOL_TARGET_ID: 'gbs_target_pool_id'
        },
        COLORS_CONSTANTS: {
            'artist': '#AA0000',
            'character': '#00AA00',
            'copyright': '#AA00AA',
            'metadata': '#FF8800',
            'general': '#337ab7',
            'excluded': '#d55e5e',
        },
    };

    // =================================================================================
    // GLOBAL STATE MODULE
    // =================================================================================
    const GlobalState = {
        searchDebounceTimeout: null,
        previewsTemporarilyDisabled: false, // Used to disable Peek
        pageType: null,
    };

    // =================================================================================
    // UTILITY, API, ZOOM & LOGGER MODULES (CORE)
    // =================================================================================
    const Utils = {
        makeRequest: function(options) {
            let xhr;
            const promise = new Promise((resolve, reject) => {
                xhr = GM.xmlHttpRequest({
                    ...options,
                    onload: (response) => (response.status >= 200 && response.status < 300) ? resolve(response) : reject(new Error(`Request failed: Status ${response.status}`)),
                    onerror: (response) => reject(new Error(`Network error: ${response.statusText}`)),
                    ontimeout: () => reject(new Error('Request timed out.'))
                });
            });
            return { promise, xhr };
        },
        getPostId: (postUrl) => new URL(postUrl).searchParams.get('id'),
        formatHotkeyForStorage: function(key) {
            return key === 'Space' ? ' ' : key.trim();
        },
        formatHotkeyForDisplay: function(key) {
            return key === ' ' ? 'Space' : key;
        },
    };

    const Logger = {
        _log: function(level, ...args) {
            if (!Settings.State.DEBUG) return;
            const prefix = '[Gelbooru Suite]';
            switch (level) {
                case 'log': console.log(prefix, ...args); break;
                case 'warn': console.warn(prefix, ...args); break;
                case 'error': console.error(prefix, ...args); break;
                default: console.log(prefix, ...args); break;
            }
        },
        log: function(...args) { this._log('log', ...args); },
        warn: function(...args) { this._log('warn', ...args); },
        error: function(...args) { this._log('error', ...args); }
    };

    const API = {
        fetchTagCategory: async function(tagName) {
            const encodedTerm = encodeURIComponent(tagName.replace(/ /g, '_'));
            const url = `${Config.API_URLS.AUTOCOMPLETE}&term=${encodedTerm}`;
            try {
                const { promise } = Utils.makeRequest({ method: "GET", url });
                const response = await promise;
                const data = JSON.parse(response.responseText);

                if (data && data.length > 0) {
                    const exactMatch = data.find(tag => tag.value === tagName);
                    if (exactMatch) {
                        return exactMatch.category === 'tag' ? 'general' : exactMatch.category;
                    }
                }

                return 'general';
            } catch (error) {
                Logger.error(`Failed to fetch category for tag "${tagName}":`, error);
                return 'general';
            }
        },
        fetchTagSuggestions: async function(term) {
            if (!term || term.length < 2) return [];
            const encodedTerm = encodeURIComponent(term.replace(/ /g, '_'));
            const url = `${Config.API_URLS.AUTOCOMPLETE}&term=${encodedTerm}`;
            try {
                const { promise } = Utils.makeRequest({ method: "GET", url });
                const response = await promise;
                const data = JSON.parse(response.responseText);
                return data || [];
            } catch (error) {
                Logger.error("Failed to fetch tag suggestions:", error);
                return [];
            }
        },
        fetchMediaDetails: async function(postId) {
            if (!postId) throw new Error("No Post ID provided.");
            let request;
            if (Settings.State.API_KEY && Settings.State.USER_ID) {
                const apiUrl = `${Config.API_URLS.BASE}&id=${postId}&user_id=${Settings.State.USER_ID}&api_key=${Settings.State.API_KEY}`;
                try {
                    request = Utils.makeRequest({ method: "GET", url: apiUrl });
                    Peek.State.pendingPreviewRequest = request.xhr;

                    const response = await request.promise;
                    if (response.status === 401 || response.status === 403) {
                        Settings.UI.openModal("Authentication failed. Your API Key or User ID is incorrect. Please enter valid credentials.");
                        throw new Error("Authentication failed. Please check your credentials.");
                    }
                    let data;
                    try { data = JSON.parse(response.responseText); }
                    catch (e) {
                        Logger.error("Failed to parse API response. It might not be valid JSON.", e);
                        Logger.error("Raw response text:", response.responseText);
                        throw new Error("Failed to parse API response. It might not be valid JSON.");
                    }
                    if (!data?.post?.length) throw new Error("API returned no post data or post not found.");
                    const post = data.post[0];
                    const fileUrl = post.file_url;
                    const isVideo = ['.mp4', '.webm'].some(ext => fileUrl.endsWith(ext));
                    return { url: fileUrl, type: isVideo ? 'video' : 'image' };
                } catch (error) {
                    if (error.message.includes('abort')) {
                        Logger.log(`Request for post ${postId} was aborted.`);
                        throw error;
                    }
                    Logger.warn(`[Gelbooru Suite] API request failed: ${error.message}. Attempting HTML fallback.`);
                } finally {
                    Peek.State.pendingPreviewRequest = null;
                }
            }

            try {
                Logger.log(`[Gelbooru Suite] Using HTML fallback for post ID: ${postId}`);
                const postUrl = `https://gelbooru.com/index.php?page=post&s=view&id=${postId}`;

                request = Utils.makeRequest({ method: "GET", url: postUrl });
                Peek.State.pendingPreviewRequest = request.xhr;

                const mediaData = await this.getPostData(request.promise);
                return { url: mediaData.contentUrl, type: mediaData.type };
            } catch (fallbackError) {
                Logger.error('[Gelbooru Suite] HTML fallback also failed:', fallbackError);
                throw fallbackError;
            } finally {
                Peek.State.pendingPreviewRequest = null;
            }
        },
        fetchMediaDetailsFromHTML: async function(postUrl) {
            if (!postUrl) throw new Error("No Post URL provided.");
            try {
                Logger.log(`[Gelbooru Suite] Using HTML-only fetch for: ${postUrl}`);
                const { promise } = Utils.makeRequest({ method: "GET", url: postUrl });
                const mediaData = await this.getPostData(promise);
                return { url: mediaData.contentUrl, type: mediaData.type };
            } catch (error) {
                Logger.error('[Gelbooru Suite] HTML fetch failed:', error);
                throw error;
            }
        },
        fetchSavedSearches: async function(forceRefresh = false) {
            const cacheKey = 'gbs_saved_searches_cache';
            const cacheDuration = 6 * 60 * 60 * 1000; // 6 hour

            if (!forceRefresh) {
                const cachedData = await GM.getValue(cacheKey, null);
                if (cachedData && (Date.now() - cachedData.timestamp < cacheDuration)) {
                    Logger.log("Using searches saved from the local cache.");
                    return cachedData.searches;
                }
            }

            Logger.log(forceRefresh ? "Forcing cache refresh." : "Cache expired. Fetching from the network...");
            let allSearches = [];
            let nextPageUrl = '/index.php?page=tags&s=saved_search';

            try {
                while (nextPageUrl) {
                    const { promise } = Utils.makeRequest({ method: "GET", url: nextPageUrl });
                    const response = await promise;
                    const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                    const searchNodes = doc.querySelectorAll('span[style*="font-size: 1.5em"] > a:nth-of-type(2)');
                    const searchesOnPage = Array.from(searchNodes).map(node => node.textContent.trim());
                    allSearches.push(...searchesOnPage);
                    const nextPageLink = doc.querySelector('.pagination b + a');
                    nextPageUrl = nextPageLink ? nextPageLink.getAttribute('href') : null;
                }

                await GM.setValue(cacheKey, { searches: allSearches, timestamp: Date.now() });
                Logger.log(`Cache updated with ${allSearches.length} searches.`);
                return allSearches;

            } catch (error) {
                Logger.error("Failed to fetch saved searches:", error);
                const cachedData = await GM.getValue(cacheKey, null);
                if (cachedData) {
                    Logger.warn("Using old cache data as fallback.");
                    return cachedData.searches;
                }
                return [];
            }
        },
        getPostData: async function(requestPromise) {
            const response = await requestPromise;
            const doc = new DOMParser().parseFromString(response.responseText, "text/html");
            const metaTag = doc.querySelector("meta[property='og:image']");
            const videoTag = doc.querySelector("video#gelcomVideoPlayer source");
            let contentUrl, type;
            if (videoTag && videoTag.src) {
                contentUrl = videoTag.src;
                type = 'video';
            } else if (metaTag) {
                contentUrl = metaTag.getAttribute('content');
                type = ['.mp4', '.webm'].some(ext => contentUrl.endsWith(ext)) ? 'video' : 'image';
            }
            if (contentUrl) {
                return { contentUrl, type, tags: this.parseTags(doc) };
            } else {
                throw new Error(`Media not found for post.`);
            }
        },
        parseTags: function(doc) {
            const tags = {};
            doc.querySelectorAll(Config.SELECTORS.postTagListItem).forEach(li => {
                const categoryMatch = li.className.match(/tag-type-([a-z_]+)/);
                const category = categoryMatch ? categoryMatch[1] : 'general';
                const tagLink = li.querySelector(Config.SELECTORS.postTagLink);
                if (tagLink) {
                    if (!tags[category]) { tags[category] = []; }
                    tags[category].push({ name: tagLink.textContent.trim(), url: tagLink.href });
                }
            });
            return tags;
        }
    };

    // =================================================================================
    // SETTINGS MODULE
    // =================================================================================
    const Settings = {
        State: {},
        settingsMap: [
            { id: 'setting-advanced-search', key: 'ENABLE_ADVANCED_SEARCH', type: 'checkbox' },
            { id: 'setting-peek-previews', key: 'ENABLE_PEEK_PREVIEWS', type: 'checkbox' },
            { id: 'setting-add-to-pool', key: 'ENABLE_ADD_TO_POOL', type: 'checkbox' },
            { id: 'setting-downloader', key: 'ENABLE_DOWNLOADER', type: 'checkbox' },
            { id: 'setting-hide-scrollbars', key: 'HIDE_PAGE_SCROLLBARS', type: 'checkbox' },
            { id: 'setting-blacklist-tags', key: 'BLACKLIST_TAGS', type: 'textarea' },
            { id: 'setting-preview-quality', key: 'PREVIEW_QUALITY', type: 'select' },
            { id: 'setting-preview-scale', key: 'PREVIEW_SCALE_FACTOR', type: 'float' },
            { id: 'setting-preview-muted', key: 'PREVIEW_VIDEOS_MUTED', type: 'checkbox' },
            { id: 'setting-preview-loop', key: 'PREVIEW_VIDEOS_LOOP', type: 'checkbox' },
        ],
        load: async function() {
            const savedSettings = await GM.getValue(Config.STORAGE_KEYS.SUITE_SETTINGS, {});
            this.State = { ...Config.DEFAULT_SETTINGS, ...savedSettings };
            Logger.State = this.State; // Pass settings to logger
        },
        save: async function() {
            const getHotkey = (id) => Utils.formatHotkeyForStorage(document.getElementById(id).value);
            const newSettings = {};

            this.settingsMap.forEach(setting => {
                const element = document.getElementById(setting.id);
                if (!element) return;

                switch (setting.type) {
                    case 'checkbox':
                        newSettings[setting.key] = element.checked;
                        break;
                    case 'textarea':
                    case 'text':
                    case 'select':
                        newSettings[setting.key] = element.value.trim();
                        break;
                    case 'float':
                        newSettings[setting.key] = Math.max(1, parseFloat(element.value) || Config.DEFAULT_SETTINGS[setting.key]);
                        break;
                }
            });

            Object.assign(newSettings, {
                KEY_GALLERY_NEXT_PAGE: getHotkey('setting-key-gallery-next') || Config.DEFAULT_SETTINGS.KEY_GALLERY_NEXT_PAGE,
                KEY_GALLERY_PREV_PAGE: getHotkey('setting-key-gallery-prev') || Config.DEFAULT_SETTINGS.KEY_GALLERY_PREV_PAGE,
                KEY_PEEK_VIDEO_PLAY_PAUSE: getHotkey('setting-key-peek-vid-play') || Config.DEFAULT_SETTINGS.KEY_PEEK_VIDEO_PLAY_PAUSE,
                KEY_PEEK_VIDEO_SEEK_FORWARD: getHotkey('setting-key-peek-vid-fwd') || Config.DEFAULT_SETTINGS.KEY_PEEK_VIDEO_SEEK_FORWARD,
                KEY_PEEK_VIDEO_SEEK_BACK: getHotkey('setting-key-peek-vid-back') || Config.DEFAULT_SETTINGS.KEY_PEEK_VIDEO_SEEK_BACK,
                KEY_VIEWER_PREV_IMAGE: getHotkey('setting-key-viewer-prev') || Config.DEFAULT_SETTINGS.KEY_VIEWER_PREV_IMAGE,
                KEY_VIEWER_NEXT_IMAGE: getHotkey('setting-key-viewer-next') || Config.DEFAULT_SETTINGS.KEY_VIEWER_NEXT_IMAGE,
                KEY_VIEWER_TOGGLE_INFO: getHotkey('setting-key-viewer-info') || Config.DEFAULT_SETTINGS.KEY_VIEWER_TOGGLE_INFO,
            });

            Object.assign(newSettings, {
                API_KEY: document.getElementById('setting-api-key').value.trim(),
                USER_ID: document.getElementById('setting-user-id').value.trim(),
                DOWNLOAD_FOLDER: document.getElementById('setting-download-folder').value.trim() || Config.DEFAULT_SETTINGS.DOWNLOAD_FOLDER,
            });

            await GM.setValue(Config.STORAGE_KEYS.SUITE_SETTINGS, newSettings);
            this.State = newSettings;
            this.UI.closeModal();
            window.location.reload();
        },
        clearCredentials: async function() {
            if (confirm("Are you sure you want to clear your API Key and User ID? The page will reload.")) {
                const newSettings = { ...this.State, API_KEY: '', USER_ID: '' };
                await GM.setValue(Config.STORAGE_KEYS.SUITE_SETTINGS, newSettings);
                this.State = newSettings;
                window.location.reload();
            }
        },
        export: function() {
            const jsonString = JSON.stringify(this.State, null, 2);
            const textarea = document.getElementById('enhancer-import-area');
            const originalPlaceholder = textarea.placeholder;

            const resetTextarea = () => {
                textarea.placeholder = originalPlaceholder;
            };

            navigator.clipboard.writeText(jsonString).then(() => {
                textarea.placeholder = 'Settings copied to clipboard!';
                setTimeout(resetTextarea, 3000);
            });
        },
        import: async function() {
            const textarea = document.getElementById('enhancer-import-area');
            const jsonString = textarea.value;
            const originalPlaceholder = textarea.placeholder;

            const showMessage = (message, duration = 3000) => {
                textarea.placeholder = message;
                setTimeout(() => {
                    textarea.placeholder = originalPlaceholder;
                }, duration);
            };

            if (!jsonString.trim()) {
                showMessage('Import field is empty.');
                return;
            }
            try {
                const importData = JSON.parse(jsonString);
                if (importData && typeof importData === 'object') {
                    await GM.setValue(Config.STORAGE_KEYS.SUITE_SETTINGS, importData);
                    showMessage('Settings imported! Page will reload...', 2000);
                    textarea.value = '';
                    setTimeout(() => window.location.reload(), 1500);
                } else {
                    throw new Error("Invalid or incomplete settings format.");
                }
            } catch (error) {
                showMessage(`Import failed: ${error.message}`, 4000);
                Logger.error('Import error:', error);
            }
        },
        testCredentials: async function() {
            const testButton = document.getElementById('enhancer-test-creds');
            const apiKey = document.getElementById('setting-api-key').value.trim();
            const userId = document.getElementById('setting-user-id').value.trim();
            const originalText = 'Test Connection';

            const originalBgColor = testButton.style.backgroundColor;

            const resetButton = (delay) => {
                setTimeout(() => {
                    testButton.textContent = originalText;
                    testButton.style.backgroundColor = originalBgColor;
                    testButton.disabled = false;
                }, delay);
            };

            testButton.disabled = true;

            if (!apiKey || !userId) {
                testButton.textContent = 'Missing Keys';
                testButton.style.backgroundColor = '#EFB700';
                resetButton(3000);
                return;
            }

            testButton.textContent = 'Testing...';
            testButton.style.backgroundColor = '#61afef';

            const testUrl = `${Config.API_URLS.BASE}&limit=1&user_id=${userId}&api_key=${apiKey}`;
            try {
                const { promise } = Utils.makeRequest({ method: "GET", url: testUrl });
                const response = await promise;
                if (response.status === 200) {
                    testButton.textContent = 'Success!';
                    testButton.style.backgroundColor = '#008450';
                } else {
                    throw new Error(`Authentication failed (Status: ${response.status})`);
                }
            } catch (error) {
                Logger.error('API connection test failed:', error);
                testButton.textContent = 'Error';
                testButton.style.backgroundColor = '#B81D13';
            } finally {
                resetButton(3000);
            }
        },
        UI: {
            _getGeneralSettingsHTML: function() {
                return `
                <div class="settings-tab-pane active" data-tab="general">
                    <div class="setting-item"><label for="setting-advanced-search">Enable Tag Editor</label><label class="toggle-switch"><input type="checkbox" id="setting-advanced-search"><span class="toggle-slider"></span></label></div>
                    <div class="setting-item"><label for="setting-peek-previews">Enable Previews</label><label class="toggle-switch"><input type="checkbox" id="setting-peek-previews"><span class="toggle-slider"></span></label></div>
                    <div class="setting-item"><label for="setting-add-to-pool">Enable Add to Pool</label><label class="toggle-switch"><input type="checkbox" id="setting-add-to-pool"><span class="toggle-slider"></span></label></div>
                    <div class="setting-item"><label for="setting-downloader">Enable Downloader</label><label class="toggle-switch"><input type="checkbox" id="setting-downloader"><span class="toggle-slider"></span></label></div>
                    <div class="setting-item"><label for="setting-hide-scrollbars">Hide Page Scrollbars (Global)</label><label class="toggle-switch"><input type="checkbox" id="setting-hide-scrollbars"><span class="toggle-slider"></span></label></div>
                    <hr class="setting-divider">
                    <div class="setting-item-vertical">
                        <label for="setting-blacklist-tags">Blacklisted Tags (space-separated)</label>
                        <p class="setting-note" style="text-align: left; margin: 5px 0 10px 0;">Tags for the 'Toggle Blacklist' button in the Tag Editor modal.</p>
                        <textarea id="setting-blacklist-tags" rows="3" placeholder="Example: muscular red_eyes pov ..."></textarea>
                    </div>
                </div>`;
            },
            _getPeekSettingsHTML: function() {
                return `
                <div class="settings-tab-pane" data-tab="peek">
                    <div class="setting-item"><label for="setting-preview-quality">Preview Quality</label><select id="setting-preview-quality"><option value="high">High</option><option value="low">Low</option></select></div>
                    <div class="setting-item"><label for="setting-preview-scale">Preview Scale Factor</label><input type="number" id="setting-preview-scale" step="0.1" min="1"></div>
                    <hr class="setting-divider">
                    <div class="setting-item"><label for="setting-preview-muted">Mute Preview Videos</label><label class="toggle-switch"><input type="checkbox" id="setting-preview-muted"><span class="toggle-slider"></span></label></div>
                    <div class="setting-item"><label for="setting-preview-loop">Loop Preview Videos</label><label class="toggle-switch"><input type="checkbox" id="setting-preview-loop"><span class="toggle-slider"></span></label></div>
                </div>`;
            },
            _getHotkeysSettingsHTML: function() {
                return `
                <div class="settings-tab-pane" data-tab="hotkeys">
                    <p class="setting-note">Click on a field and press the desired key to set a hotkey.</p>
                    <h4 class="setting-subheader">Gallery Hotkeys</h4>
                    <div class="setting-item"><label for="setting-key-gallery-prev">Prev Page</label><input type="text" id="setting-key-gallery-prev" class="hotkey-input" readonly></div>
                    <div class="setting-item"><label for="setting-key-gallery-next">Next Page</label><input type="text" id="setting-key-gallery-next" class="hotkey-input" readonly></div>
                    <h4 class="setting-subheader">Preview Video Hotkeys</h4>
                    <div class="setting-item"><label for="setting-key-peek-vid-play">Play/Pause</label><input type="text" id="setting-key-peek-vid-play" class="hotkey-input" readonly></div>
                    <div class="setting-item"><label for="setting-key-peek-vid-back">Seek Back</label><input type="text" id="setting-key-peek-vid-back" class="hotkey-input" readonly></div>
                    <div class="setting-item"><label for="setting-key-peek-vid-fwd">Seek Forward</label><input type="text" id="setting-key-peek-vid-fwd" class="hotkey-input" readonly></div>
                    <h4 class="setting-subheader">Media Viewer Hotkeys</h4>
                    <div class="setting-item"><label for="setting-key-viewer-prev">Prev Image</label><input type="text" id="setting-key-viewer-prev" class="hotkey-input" readonly></div>
                    <div class="setting-item"><label for="setting-key-viewer-next">Next Image</label><input type="text" id="setting-key-viewer-next" class="hotkey-input" readonly></div>
                    <div class="setting-item"><label for="setting-key-viewer-info">Toggle Info</label><input type="text" id="setting-key-viewer-info" class="hotkey-input" readonly></div>
                </div>`;
            },
            _getAdvancedSettingsHTML: function() {
                return `
                <div class="settings-tab-pane" data-tab="advanced">
                    <p class="setting-note">API keys can improve script reliability. Find them in Settings > Options.</p>
                    <p class="setting-note"><strong>Security Note: Keys are stored locally and are not encrypted.</strong></p>
                    <div class="setting-item"><label for="setting-api-key">API Key</label><input type="text" id="setting-api-key" placeholder="Your API key"></div>
                    <div class="setting-item"><label for="setting-user-id">User ID</label><input type="text" id="setting-user-id" placeholder="Your user ID"></div>
                    <div class="api-test-container">
                        <button id="enhancer-test-creds">Test Connection</button>
                    </div>
                    <p class="enhancer-error-message" id="enhancer-auth-error" style="display:none; text-align: center;"></p>
                    <hr class="setting-divider">
                    <div class="setting-item">
                        <label for="setting-download-folder">Download Folder</label>
                        <input type="text" id="setting-download-folder" placeholder="e.g., gelbooru_downloads">
                    </div>
                    <hr class="setting-divider">
                    <div class="manage-settings-section">
                        <p class="setting-note">Export your settings for backup, or import them on another browser.</p>
                        <div class="manage-buttons">
                            <button id="enhancer-export-settings">Export</button>
                            <button id="enhancer-import-settings">Import</button>
                        </div>
                        <textarea id="enhancer-import-area" rows="3" placeholder="Paste your exported settings string here and click Import..."></textarea>
                    </div>
                </div>`;
            },
            createModal: function() {
                if (document.getElementById(Config.SELECTORS.SETTINGS_MODAL_ID)) return;
                GM_addStyle(`
                    #${Config.SELECTORS.SETTINGS_MODAL_ID}-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 100000; justify-content: center; align-items: center; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} { box-sizing: border-box !important; background-color: #252525; color: #eee !important; border: 1px solid #666; border-radius: 10px; z-index: 101; padding: 20px 7px 7px 7px; width: 90%; max-width: 450px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} * { color: #eee !important; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} h2 { text-align: center; margin-bottom: 10px; padding-bottom: 10px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tabs { display: flex; margin-bottom: 15px; border-bottom: 1px solid #555; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tab-btn { background: none; border: none; color: #aaa !important; padding: 8px 12px; cursor: pointer; font-size: 1em; border-bottom: 2px solid transparent; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tab-btn.active { color: #fff !important; border-bottom-color: #006FFA; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tab-content { display: grid; align-items: start; padding-top: 10px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tab-pane { grid-row: 1; grid-column: 1; opacity: 0; pointer-events: none; transition: opacity 0.15s ease-in-out; max-height: 65vh; overflow-y: auto; padding: 3px 15px; box-sizing: border-box !important; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tab-pane.active { opacity: 1; pointer-events: auto; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item-vertical { display: flex; flex-direction: column; margin-bottom: 12px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item-vertical label { margin-bottom: 8px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item-vertical textarea, #${Config.SELECTORS.SETTINGS_MODAL_ID} #enhancer-import-area { width: 100%; box-sizing: border-box !important; padding: 5px; background: #333; border: 1px solid #555; color: #fff !important; border-radius: 10px; resize: vertical; height: 70px; margin-top: 10px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item label { color: #eee !important; flex-shrink: 0; font-weight: 300 }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item input[type=number], #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item input[type=text], #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item select { width: 120px; box-sizing: border-box !important; padding: 6px; background: #333; border: 1px solid #555; color: #fff !important; border-radius: 10px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-item input[type=range] { flex-grow: 1; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-note { font-size: 11px; color: #999 !important; text-align: center; margin-top: 5px; margin-bottom: 15px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-note strong { color: #daa520 !important; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-divider { border: 0; height: 1px; background: #444; margin: 20px 0; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .setting-subheader { color: #006FFA !important; border-bottom: 1px solid #444; padding-bottom: 5px; margin-top: 20px; margin-bottom: 15px; font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.5px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-buttons { text-align: right; margin-top: 20px; border-top: 1px solid #555; padding-top: 15px; display: flex; align-items: center; justify-content: flex-end; gap: 10px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-buttons button { padding: 8px 12px; border: none; background-color: #444; color: #fff !important; border-radius: 10px; cursor: pointer; font-weight: bold }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .manage-buttons button { background-color: #444; color: #fff !important; border: 1px solid #666; padding: 8px 12px; border-radius: 10px; cursor: pointer; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .manage-buttons button:hover { background-color: #555; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} #enhancer-save-settings { background-color: #006FFA; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} #enhancer-clear-creds { background-color: #A43535; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} #zoom-sens-value { color: #fff !important; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .api-test-container { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 10px; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} #enhancer-test-creds { padding: 5px 10px; font-size: 0.9em; background-color: #006FFA; border-radius: 3px; font-weight: bold; min-width: 110px; text-align: center; box-sizing: border-box !important; transition: background-color 0.2s ease-in-out; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .hotkey-input { cursor: pointer; text-align: center; }
                    #${Config.SELECTORS.SETTINGS_MODAL_ID} .hotkey-input:focus { background-color: #006FFA; color: #000 !important; font-weight: bold; }
                    .toggle-switch { position: relative; display: inline-block; width: 40px; height: 22px; }
                    .toggle-switch input { opacity: 0; width: 0; height: 0; }
                    .toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #555; transition: .4s; border-radius: 25px; }
                    .toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 3px; bottom: 3px; background-color: white; transition: .4s; border-radius: 50%; }
                    input:checked + .toggle-slider { background-color: #006FFA; }
                    input:checked + .toggle-slider:before { transform: translateX(18px); }
                `);
                const modalHTML = `
                <div id="${Config.SELECTORS.SETTINGS_MODAL_ID}-overlay">
                    <div id="${Config.SELECTORS.SETTINGS_MODAL_ID}">
                        <h2>Gelbooru Suite Settings</h2>
                        <div class="settings-tabs">
                            <button class="settings-tab-btn active" data-tab="general">General</button>
                            <button class="settings-tab-btn" data-tab="peek">Previews</button>
                            <button class="settings-tab-btn" data-tab="hotkeys">Hotkeys</button>
                            <button class="settings-tab-btn" data-tab="advanced">Advanced</button>
                        </div>
                        <div class="settings-tab-content">
                            ${this._getGeneralSettingsHTML()}
                            ${this._getPeekSettingsHTML()}
                            ${this._getHotkeysSettingsHTML()}
                            ${this._getAdvancedSettingsHTML()}
                        </div>
                        <div class="footer-container" style="text-align: right; border-top: 1px solid #555; padding-top: 15px; margin-top: 20px;">
                            <div class="settings-buttons" style="margin-top: 0; border-top: none; padding-top: 0;">
                                <button id="enhancer-clear-creds">Clear Credentials</button>
                                <button id="enhancer-save-settings">Save</button>
                                <button id="enhancer-close-settings">Close</button>
                            </div>
                        </div>
                    </div>
                </div>`;
                document.body.insertAdjacentHTML('beforeend', modalHTML);
                document.getElementById('enhancer-save-settings').addEventListener('click', Settings.save.bind(Settings));
                document.getElementById('enhancer-clear-creds').addEventListener('click', Settings.clearCredentials.bind(Settings));
                document.getElementById('enhancer-close-settings').addEventListener('click', this.closeModal);
                document.getElementById('enhancer-export-settings').addEventListener('click', Settings.export.bind(Settings));
                document.getElementById('enhancer-import-settings').addEventListener('click', Settings.import.bind(Settings));
                document.getElementById('enhancer-test-creds').addEventListener('click', Settings.testCredentials.bind(Settings));
                document.getElementById(`${Config.SELECTORS.SETTINGS_MODAL_ID}-overlay`).addEventListener('click', (e) => {
                    if (e.target.id === `${Config.SELECTORS.SETTINGS_MODAL_ID}-overlay`) this.closeModal();
                });
                const tabsContainer = document.querySelector(`#${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tabs`);
                const panesContainer = document.querySelector(`#${Config.SELECTORS.SETTINGS_MODAL_ID} .settings-tab-content`);
                tabsContainer.addEventListener('click', (e) => {
                    if (e.target.matches('.settings-tab-btn')) {
                        const targetTab = e.target.dataset.tab;
                        if (tabsContainer.querySelector('.active')) { tabsContainer.querySelector('.active').classList.remove('active'); }
                        e.target.classList.add('active');
                        if (panesContainer.querySelector('.active')) { panesContainer.querySelector('.active').classList.remove('active'); }
                        panesContainer.querySelector(`.settings-tab-pane[data-tab="${targetTab}"]`).classList.add('active');
                    }
                });
                document.querySelectorAll('.hotkey-input').forEach(input => {
                    const handleKeyDown = (e) => {
                        e.preventDefault();
                        let key = e.key;
                        if (key === ' ') { key = 'Space'; }
                        input.value = key;
                        input.blur();
                    };
                    const handleFocus = () => {
                        input.value = 'Press a key...';
                        input.addEventListener('keydown', handleKeyDown, { once: true });
                    };
                    const handleBlur = () => {
                        if (input.value === 'Press a key...') {
                            const settingKey = input.id.replace('setting-key-', 'KEY_').toUpperCase().replace(/-/g, '_');
                            let defaultValue = Settings.State[settingKey] || Config.DEFAULT_SETTINGS[settingKey];
                            input.value = Utils.formatHotkeyForDisplay(defaultValue);
                        }
                        input.removeEventListener('keydown', handleKeyDown);
                    };
                    input.addEventListener('focus', handleFocus);
                    input.addEventListener('blur', handleBlur);
                });
            },
            openModal: function(authError = '') {
                if (!document.getElementById(Config.SELECTORS.SETTINGS_MODAL_ID)) { this.createModal(); }

                Settings.settingsMap.forEach(setting => {
                    const element = document.getElementById(setting.id);
                    if (!element) return;

                    if (setting.type === 'checkbox') {
                        element.checked = Settings.State[setting.key];
                    } else {
                        element.value = Settings.State[setting.key];
                    }
                });

                document.getElementById('setting-key-gallery-next').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_GALLERY_NEXT_PAGE);
                document.getElementById('setting-key-gallery-prev').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_GALLERY_PREV_PAGE);
                document.getElementById('setting-key-peek-vid-play').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_PEEK_VIDEO_PLAY_PAUSE);
                document.getElementById('setting-key-peek-vid-fwd').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_PEEK_VIDEO_SEEK_FORWARD);
                document.getElementById('setting-key-peek-vid-back').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_PEEK_VIDEO_SEEK_BACK);
                document.getElementById('setting-key-viewer-prev').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_VIEWER_PREV_IMAGE);
                document.getElementById('setting-key-viewer-next').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_VIEWER_NEXT_IMAGE);
                document.getElementById('setting-key-viewer-info').value = Utils.formatHotkeyForDisplay(Settings.State.KEY_VIEWER_TOGGLE_INFO);

                document.getElementById('setting-api-key').value = Settings.State.API_KEY;
                document.getElementById('setting-user-id').value = Settings.State.USER_ID;
                document.getElementById('setting-download-folder').value = Settings.State.DOWNLOAD_FOLDER;

                const errorMessageElement = document.getElementById('enhancer-auth-error');
                if (authError) {
                    errorMessageElement.textContent = authError;
                    errorMessageElement.style.display = 'block';
                    document.querySelector('.settings-tabs .settings-tab-btn[data-tab="advanced"]').click();
                } else {
                    errorMessageElement.style.display = 'none';
                }
                document.getElementById(`${Config.SELECTORS.SETTINGS_MODAL_ID}-overlay`).style.display = 'flex';
            },
            closeModal: function() {
                const overlay = document.getElementById(`${Config.SELECTORS.SETTINGS_MODAL_ID}-overlay`);
                if (overlay) overlay.style.display = 'none';
            },
        }
    };

    // =================================================================================
    // ADVANCED SEARCH MODULE
    // =================================================================================
    const AdvancedSearch = {
        init: function() {
            const originalInput = document.querySelector(Config.SELECTORS.SEARCH_INPUT);
            if (!originalInput) return;
            this.injectStyles();
            if (originalInput.form) {
                originalInput.form.classList.add('gbs-search-form');
            }
            const { openModal } = this.UI.createModal(originalInput);
            const advButton = document.createElement('button');
            advButton.type = 'button';
            advButton.textContent = 'Tag Editor';
            advButton.id = 'gbs-advanced-search-btn';
            advButton.title = 'Open Advanced Tag Editor';
            const originalSubmitButton = originalInput.form.querySelector('input[name="commit"]');
            if (originalSubmitButton) {
                originalSubmitButton.insertAdjacentElement('afterend', advButton);
            } else {
                originalInput.insertAdjacentElement('afterend', advButton);
            }
            advButton.addEventListener('click', (e) => {
                e.preventDefault();
                openModal();
            });
        },
        injectStyles: function() {
            GM_addStyle(`
                .gbs-search-form { display: flex; align-items: center; gap: 5px; }
                .gbs-search-form > p { display: contents; }
                .gbs-search-form #tags-search { flex-grow: 1; width: auto !important; }
                .gbs-search-form input[type="submit"] { flex-shrink: 0; }
                #${Config.SELECTORS.ADVANCED_SEARCH_MODAL_ID}-overlay { display: flex; opacity: 0; pointer-events: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 100001; justify-content: center; align-items: flex-start; padding-top: 5vh; font-family: sans-serif; }
                #${Config.SELECTORS.ADVANCED_SEARCH_MODAL_ID} { background-color: #252525; border-radius: 10px; box-shadow: 0 5px 25px rgba(0,0,0,0.5); width: 90%; max-width: 550px; padding: 20px 7px 7px 7px; border: 1px solid #666; height: 80vh; display: flex; flex-direction: column; }
                .gbs-search-title { margin-bottom: 10px; padding-bottom: 10px; color: #eee; text-align: center; flex-shrink: 0; font-family: verdana, helvetica; }
                .gbs-input-wrapper { position: relative; flex-shrink: 0; }
                #gbs-tag-input { width: 100%; box-sizing: border-box; background: #333; border: 1px solid #555; padding-left: 40px !important; padding: 10px; border-radius: 10px; font-size: 1em; color: #eee; }
                .gbs-suggestion-container { position: absolute; top: 100%; left: 0; right: 0; background-color: #1F1F1F; border: 1px solid #555; border-top: none; z-index: 100002; max-height: 200px; overflow-y: auto; display: none; box-shadow: 0 4px 6px rgba(0,0,0,0.2); }
                .gbs-suggestion-item { font-size: 1.08em; padding: 3px 10px; cursor: pointer; display: flex; justify-content: space-between; align-items: center; color: #ddd; }
                .gbs-suggestion-item:hover { background-color: #E6E6FA; }
                .gbs-suggestion-label { display: flex; align-items: center; gap: 8px; }
                .gbs-suggestion-category { font-size: 0.8em; color: #999; text-transform: capitalize; }
                .gbs-suggestion-count { font-size: 0.9em; }
                #gbs-category-sections-wrapper { overflow-y: auto; margin-top: 15px; padding-right: 15px; flex-grow: 1; min-height: 80px;  }
                .gbs-category-section { margin-bottom: 10px; }
                .gbs-category-title { color: #eee; margin: 0 0 8px 0; padding-bottom: 4px; border-bottom: 2px solid; font-size: 1em; text-transform: capitalize; }
                .gbs-pill-container { display: flex; flex-wrap: wrap; gap: 6px; padding: 10px 0; min-height: 20px; }
                .gbs-tag-pill { display: inline-flex; align-items: center; color: white; padding: 5px 10px; border-radius: 10px; font-size: 0.9em; font-weight: bold; text-shadow: 1px 1px 3px rgba(0,0,0,0.9); }
                .gbs-remove-tag-btn { margin-left: 8px; cursor: pointer; font-style: normal; font-weight: bold; line-height: 1; padding: 2px; font-size: 1.2em; opacity: 0.7; }
                .gbs-remove-tag-btn:hover { opacity: 1; }
                .gbs-modal-actions { display: inline-flex; justify-content: flex-end; gap: 10px; margin-top: 15px; border-top: 1px solid #555; padding-top: 15px; flex-shrink: 0; }
                .gbs-modal-button, .gbs-modal-button-primary { padding: 8px 12px; border:none; border-radius: 10px; cursor: pointer; font-weight: bold; }
                #gbs-blacklist-toggle { min-width: 145px; text-align: center; }
                .gbs-modal-button { background-color: #444; color: #fff;  }
                .gbs-modal-button-primary { background-color: #006FFA; color: white; }
                #gbs-advanced-search-btn { margin-left: 0px; padding: 7px 15px; vertical-align: top; cursor: pointer; background: #333333; color: #EEEEEE; border: 1px solid #555555; font-weight: bold; }
                #gbs-advanced-search-btn:hover { background: #555; }
                .gbs-modifiers-section { margin-top: 15px; padding: 10px; background-color: rgba(0,0,0,0.2); border-radius: 10px; border: 1px solid #555; }
                .gbs-modifiers-title { margin: 0 0 12px 0; font-size: 1em; color: #ccc; border-bottom: 1px solid #555; padding-bottom: 5px; }
                .gbs-modifier-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
                .gbs-modifier-row label { font-weight: bold; color: #ddd; flex-basis: 30%; }
                .gbs-modifier-row select, .gbs-modifier-row input { flex-grow: 1; background: #333; border: 1px solid #555; padding: 5px; border-radius: 10px; color: #eee; width: 110px; }
                .gbs-score-group { display: flex; gap: 5px; flex-grow: 1; }
                .gbs-score-group select { width: 60px; flex-grow: 0; }
                #gbs-saved-searches-toggle-btn { position: absolute; left: 5px; top: 50%; transform: translateY(-50%); cursor: pointer; transition: color 0.2s; font-size: 1.8em }
                #gbs-saved-searches-toggle-btn:hover,
                #gbs-saved-searches-toggle-btn.active { color: #daa520 !important; }
                #gbs-modifiers-toggle-btn { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); cursor: pointer; transition: color 0.2s; font-size: 1.8em; }
                #gbs-modifiers-toggle-btn:hover,
                #gbs-modifiers-toggle-btn.active { color: #006FFA !important; }
                #gbs-modifiers-panel.hidden { display: none; }
                #gbs-saved-searches-panel { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; max-height: 20vh; overflow-y: auto; }
                #gbs-saved-searches-panel.hidden { display: none; }
                .gbs-modal-saved-search { background-color: #4a4a4a; color: #ddd; padding: 4px 8px; border-radius: 10px; font-size: 1em; cursor: pointer; transition: background-color .2s, border-color .2s; user-select: none; text-shadow: 1px 1px 3px rgba(0,0,0,0.9); }
                .gbs-modal-saved-search:hover { background-color: #daa520; border-color: #daa520; color: white; }
                @media (max-width: 850px) {
                .gbs-search-form { flex-wrap: wrap; }
                #gbs-advanced-search-btn { flex-grow: 1; width: auto !important; background: #666 !important;}
                .searchList { width: 70px; }
                }
            `);
        },
        UI: {
            createModal: function(originalInput) {
                if (document.getElementById(Config.SELECTORS.ADVANCED_SEARCH_MODAL_ID)) return;
                const modalOverlay = document.createElement('div');
                modalOverlay.id = `${Config.SELECTORS.ADVANCED_SEARCH_MODAL_ID}-overlay`;

                const modalPanel = document.createElement('div');
                modalPanel.id = Config.SELECTORS.ADVANCED_SEARCH_MODAL_ID;
                let categorySectionsHTML = `
                    <div class="gbs-category-section" id="gbs-section-main">
                        <h4 class="gbs-category-title" style="border-color: #006FFA">Tags</h4>
                        <div class="gbs-pill-container" id="gbs-pill-container-main"></div>
                    </div>
                    <div class="gbs-category-section" id="gbs-section-excluded">
                        <h4 class="gbs-category-title" style="border-color: ${Config.COLORS_CONSTANTS.excluded}">Excluded</h4>
                        <div class="gbs-pill-container" id="gbs-pill-container-excluded"></div>
                    </div>
                `;
                modalPanel.innerHTML = `
                    <h2 class="gbs-search-title">Advanced Tag Editor</h2>
                    <div class="gbs-input-wrapper">
                        <input type="text" id="gbs-tag-input" placeholder="Use '-' to exclude. Press space for '_'.">
                        <i class="fas fa-star" id="gbs-saved-searches-toggle-btn" title="Show Saved Searches"></i>
                        <i class="fas fa-cog" id="gbs-modifiers-toggle-btn" title="Show Search Modifiers"></i>
                        <div class="gbs-suggestion-container" id="gbs-main-suggestion-container"></div>
                    </div>
                    <div id="gbs-saved-searches-panel" class="hidden"></div>

                    <div id="gbs-modifiers-panel" class="gbs-modifiers-section hidden">
                        <h4 class="gbs-modifiers-title">Search Modifiers</h4>
                        <div class="gbs-modifier-row">
                            <label for="gbs-sort-select">Sort By</label>
                            <select id="gbs-sort-select">
                                <option value="">Default (ID)</option>
                                <option value="random">Random</option>
                                <option value="score">Score (High to Low)</option>
                                <option value="score:asc">Score (Low to High)</option>
                                <option value="updated:desc">Updated Date</option>
                                <option value="id:asc">ID (Ascending)</option>
                            </select>
                        </div>
                        <div class="gbs-modifier-row">
                            <label for="gbs-rating-select">Rating</label>
                            <select id="gbs-rating-select">
                                <option value="">Any</option>
                                <option value="explicit">Explicit</option>
                                <option value="questionable">Questionable</option>
                                <option value="sensitive">Sensitive</option>
                                <option value="general">General</option>
                            </select>
                        </div>
                        <div class="gbs-modifier-row">
                             <label for="gbs-score-value">Score</label>
                             <div class="gbs-score-group">
                                <select id="gbs-score-operator">
                                    <option value=">=">≥</option>
                                    <option value="<=">≤</option>
                                    <option value=">">&gt;</option>
                                    <option value="<">&lt;</option>
                                    <option value="">=</option>
                                </select>
                                <input type="number" id="gbs-score-value" placeholder="e.g., 50">
                             </div>
                        </div>
                    </div>

                    <div id="gbs-category-sections-wrapper">${categorySectionsHTML}</div>
                    <div class="gbs-modal-actions">
                        <button id="gbs-blacklist-toggle" class="gbs-modal-button" style="margin-right: auto;">Toggle Blacklist</button>
                        <button id="gbs-search-apply" class="gbs-modal-button-primary">Search</button>
                        <button id="gbs-search-close" class="gbs-modal-button">Close</button>
                    </div>
                `;
                modalOverlay.appendChild(modalPanel);
                document.body.appendChild(modalOverlay);

                const tagInput = modalPanel.querySelector('#gbs-tag-input');
                const suggestionBox = modalPanel.querySelector('#gbs-main-suggestion-container');
                const applyBtn = modalPanel.querySelector('#gbs-search-apply');
                const closeBtn = modalPanel.querySelector('#gbs-search-close');
                const blacklistToggleBtn = modalPanel.querySelector('#gbs-blacklist-toggle');

                const sortSelect = modalPanel.querySelector('#gbs-sort-select');
                const ratingSelect = modalPanel.querySelector('#gbs-rating-select');
                const scoreOp = modalPanel.querySelector('#gbs-score-operator');
                const scoreVal = modalPanel.querySelector('#gbs-score-value');

                let syncToOriginalInput = () => {
                    const regularPills = modalPanel.querySelectorAll('.gbs-pill-container .gbs-tag-pill');
                    const regularTags = Array.from(regularPills).map(pill => pill.dataset.value);

                    const modifiers = [];
                    if (sortSelect.value) {
                        modifiers.push(`sort:${sortSelect.value}`);
                    }
                    if (ratingSelect.value) {
                        modifiers.push(`rating:${ratingSelect.value}`);
                    }
                    if (scoreVal.value) {
                        modifiers.push(`score:${scoreOp.value}${scoreVal.value}`);
                    }

                    const allTags = [...modifiers, ...regularTags];
                    originalInput.value = allTags.join(' ');
                };

                sortSelect.addEventListener('change', syncToOriginalInput);
                ratingSelect.addEventListener('change', syncToOriginalInput);
                scoreOp.addEventListener('change', syncToOriginalInput);
                scoreVal.addEventListener('input', syncToOriginalInput);

                const parseAndSetModifiers = (tagsArray) => {
                    const remainingTags = [];
                    tagsArray.forEach(tag => {
                        if (tag.startsWith('sort:')) {
                            sortSelect.value = tag.substring(5);
                        } else if (tag.startsWith('rating:')) {
                            ratingSelect.value = tag.substring(7);
                        } else if (tag.startsWith('score:')) {
                            const match = tag.match(/score:([><=]*)(\d+)/);
                            if (match) {
                                const [, op, val] = match;
                                scoreOp.value = op || '';
                                scoreVal.value = val;
                            }
                        } else {
                            remainingTags.push(tag);
                        }
                    });
                    return remainingTags;
                };

                const updateBlacklistButton = () => {
                    const blacklistTags = (Settings.State.BLACKLIST_TAGS || '').trim().split(/\s+/).filter(Boolean);
                    if (blacklistTags.length === 0) {
                        blacklistToggleBtn.style.display = 'none';
                        return;
                    }
                    blacklistToggleBtn.style.display = '';
                    const tagsAsPills = Array.from(modalPanel.querySelectorAll('.gbs-tag-pill')).map(pill => pill.dataset.value);
                    const negativeBlacklistTags = blacklistTags.map(t => `-${t}`);
                    const areTagsActive = negativeBlacklistTags.every(negTag => tagsAsPills.includes(negTag));
                    if (areTagsActive) {
                        blacklistToggleBtn.textContent = 'Remove Blacklist';
                    } else {
                        blacklistToggleBtn.textContent = 'Add Blacklist';
                    }
                };
                const _determineTagInfo = async (rawTag) => {
                    const isNegative = rawTag.startsWith('-');
                    let processedTag = (isNegative ? rawTag.substring(1) : rawTag).trim();
                    let category = 'general';
                    let finalTagName = processedTag;

                    const knownMetadataTags = new Set([
                        'commentary'
                    ]);

                    const parts = processedTag.split(':');
                    if (parts.length > 1 && Object.keys(Config.COLORS_CONSTANTS).includes(parts[0])) {
                        category = parts[0];
                        finalTagName = parts.slice(1).join(':');
                    } else {
                        category = await API.fetchTagCategory(processedTag);
                    }

                    if (isNegative) category = 'excluded';
                    return {
                        fullValue: (isNegative ? '-' : '') + finalTagName,
                        tagName: finalTagName,
                        category: category
                    };
                };

                const _createPillElement = (tagInfo) => {
                    const pill = document.createElement('span');
                    pill.className = 'gbs-tag-pill';
                    pill.textContent = tagInfo.tagName.replace(/_/g, ' ');
                    pill.dataset.value = tagInfo.fullValue;
                    pill.dataset.category = tagInfo.category;
                    pill.style.backgroundColor = Config.COLORS_CONSTANTS[tagInfo.category] || '#777';

                    const removeBtn = document.createElement('i');
                    removeBtn.className = 'gbs-remove-tag-btn';
                    removeBtn.textContent = '×';
                    removeBtn.onclick = () => {
                        pill.remove();
                        syncToOriginalInput();
                        updateBlacklistButton();
                    };

                    pill.appendChild(removeBtn);
                    return pill;
                };

                const _sortPills = (container) => {
                    const order = ['artist', 'character', 'copyright', 'metadata', 'general'];
                    const pills = Array.from(container.querySelectorAll('.gbs-tag-pill'));

                    pills.sort((a, b) => {
                        const catA = a.dataset.category;
                        const catB = b.dataset.category;
                        const indexA = order.indexOf(catA);
                        const indexB = order.indexOf(catB);
                        return indexA - indexB;
                    });

                    container.innerHTML = '';
                    pills.forEach(pill => container.appendChild(pill));
                };

                const addPill = async (rawTag) => {
                    if (!rawTag || rawTag.trim() === '') return;
                    const tagInfo = await _determineTagInfo(rawTag.trim());
                    if (modalPanel.querySelector(`.gbs-tag-pill[data-value="${tagInfo.fullValue}"]`)) {
                        return;
                    }

                    const mainPillContainer = modalPanel.querySelector('#gbs-pill-container-main');
                    const excludedPillContainer = modalPanel.querySelector('#gbs-pill-container-excluded');

                    const pillElement = _createPillElement(tagInfo);

                    if (tagInfo.category === 'excluded') {
                        excludedPillContainer.appendChild(pillElement);
                    } else {
                        mainPillContainer.appendChild(pillElement);
                        _sortPills(mainPillContainer);
                    }

                    syncToOriginalInput();
                    updateBlacklistButton();
                };

                const toggleBlacklistTags = () => {
                    const blacklistTags = (Settings.State.BLACKLIST_TAGS || '').trim().split(/\s+/).filter(Boolean);
                    if (blacklistTags.length === 0) {
                        alert('Your blacklist is empty. Please add tags in the Suite Settings.');
                        return;
                    }
                    const tagsAsPills = Array.from(modalPanel.querySelectorAll('.gbs-tag-pill'));
                    const pillValues = tagsAsPills.map(pill => pill.dataset.value);
                    const negativeBlacklistTags = blacklistTags.map(t => `-${t}`);
                    const areTagsActive = negativeBlacklistTags.every(negTag => pillValues.includes(negTag));
                    if (areTagsActive) {
                        tagsAsPills.forEach(pill => {
                            if (negativeBlacklistTags.includes(pill.dataset.value)) {
                                pill.remove();
                            }
                        });
                    } else {
                        negativeBlacklistTags.forEach(negTag => {
                            if (!pillValues.includes(negTag)) {
                                addPill(negTag);
                            }
                        });
                    }
                    syncToOriginalInput();
                    updateBlacklistButton();
                };
                blacklistToggleBtn.addEventListener('click', toggleBlacklistTags);

                const quickTagsBtn = modalPanel.querySelector('#gbs-saved-searches-toggle-btn');
                const quickTagsPanel = modalPanel.querySelector('#gbs-saved-searches-panel');

                const _populateQuickTagsPanel = async (forceRefresh = false) => {
                    quickTagsPanel.innerHTML = '<span style="color: #ccc; font-style: italic;">Loading saved searches...</span>';

                    const renderTags = (searches) => {
                        quickTagsPanel.innerHTML = '';
                        if (searches && searches.length > 0) {
                            const fragment = document.createDocumentFragment();
                            searches.forEach(tag => {
                                const tagEl = document.createElement('span');
                                tagEl.className = 'gbs-modal-saved-search';
                                tagEl.textContent = tag.replace(/_/g, ' ');
                                tagEl.dataset.tag = tag;
                                tagEl.addEventListener('click', () => {
                                    tag.split(/\s+/).filter(Boolean).forEach(singleTag => addPill(singleTag));
                                });
                                fragment.appendChild(tagEl);
                            });
                            quickTagsPanel.appendChild(fragment);
                        } else {
                            quickTagsPanel.innerHTML = '<span style="color: #A43535;">No saved searches found.</span>';
                        }
                        const footer = document.createElement('div');
                        footer.style.cssText = 'margin-top: 5px; text-align: right;';
                        const refreshBtn = document.createElement('button');
                        refreshBtn.innerHTML = 'Update <i class=" fas fa-sync-alt" style="font-size: 0.8em;"></i>';
                        refreshBtn.style.cssText = 'background: none; border: none; color: #ccc; padding: 0px 10px; cursor: pointer;';
                        refreshBtn.title = 'Force the list to update. Avoid repeated clicks to prevent overloading the site and triggering temporary blocks.';
                        refreshBtn.onclick = () => _populateQuickTagsPanel(true);
                        footer.appendChild(refreshBtn);
                        quickTagsPanel.appendChild(footer);
                    };

                    if (!forceRefresh) {
                        const cachedSearches = await API.fetchSavedSearches();
                        renderTags(cachedSearches);
                    } else {
                        const freshSearches = await API.fetchSavedSearches(true);
                        renderTags(freshSearches);
                    }
                };

                quickTagsBtn.addEventListener('click', () => {
                    const isOpen = !quickTagsPanel.classList.contains('hidden');
                    if (isOpen) {
                        quickTagsPanel.classList.add('hidden');
                        quickTagsBtn.classList.remove('active');
                    } else {
                        quickTagsPanel.classList.remove('hidden');
                        quickTagsBtn.classList.add('active');
                        if (!quickTagsPanel.dataset.loaded) {
                            _populateQuickTagsPanel(false);
                            quickTagsPanel.dataset.loaded = 'true';
                        }
                    }
                });

                const modifiersToggleBtn = modalPanel.querySelector('#gbs-modifiers-toggle-btn');
                const modifiersPanel = modalPanel.querySelector('#gbs-modifiers-panel');

                modifiersToggleBtn.addEventListener('click', () => {
                    modifiersPanel.classList.toggle('hidden');
                    modifiersToggleBtn.classList.toggle('active', !modifiersPanel.classList.contains('hidden'));
                });


                const openModal = () => {
                    modalPanel.querySelector('#gbs-pill-container-main').innerHTML = '';
                    modalPanel.querySelector('#gbs-pill-container-excluded').innerHTML = '';

                    sortSelect.value = '';
                    ratingSelect.value = '';
                    scoreOp.value = '>=';
                    scoreVal.value = '';

                    const allTags = originalInput.value.trim().split(/\s+/).filter(Boolean);
                    const regularTags = parseAndSetModifiers(allTags);
                    regularTags.forEach(tag => addPill(tag));

                    _sortPills(modalPanel.querySelector('#gbs-pill-container-main'));

                    modalOverlay.style.opacity = '1';
                    modalOverlay.style.pointerEvents = 'auto';
                    tagInput.focus();
                    updateBlacklistButton();
                };
                const closeModal = () => { modalOverlay.style.opacity = '0'; modalOverlay.style.pointerEvents = 'none'; };
                tagInput.addEventListener('keydown', e => {
                    if (e.key === ' ') {
                        e.preventDefault();
                        tagInput.value += '_';
                    }
                    if (e.key === 'Enter') {
                        e.preventDefault();
                        const finalTag = tagInput.value.trim().replace(/ /g, '_');
                        addPill(finalTag);
                        tagInput.value = '';
                        suggestionBox.style.display = 'none';
                    }
                });
                tagInput.addEventListener('input', () => {
                    clearTimeout(GlobalState.searchDebounceTimeout);
                    const term = tagInput.value.trim();
                    if (term.length < 2) {
                        suggestionBox.style.display = 'none';
                        return;
                    }
                    GlobalState.searchDebounceTimeout = setTimeout(async () => {
                        const suggestions = await API.fetchTagSuggestions(term);
                        suggestionBox.innerHTML = '';
                        if (suggestions.length > 0) {
                            suggestionBox.style.display = 'block';

                            const fragment = document.createDocumentFragment();
                            suggestions.forEach(sugg => {
                                const item = document.createElement('div');
                                item.className = 'gbs-suggestion-item';

                                const labelSpan = document.createElement('span');
                                labelSpan.className = 'gbs-suggestion-label';

                                const nameSpan = document.createElement('span');
                                let category = sugg.category === 'tag' ? 'general' : sugg.category;
                                const color = Config.COLORS_CONSTANTS[category] || Config.COLORS_CONSTANTS.general;
                                nameSpan.style.color = color;
                                nameSpan.textContent = sugg.label.replace(/_/g, ' ');
                                const categorySpan = document.createElement('span');
                                categorySpan.className = 'gbs-suggestion-category';
                                categorySpan.textContent = `[${sugg.category}]`;

                                labelSpan.append(nameSpan, categorySpan);

                                const countSpan = document.createElement('span');
                                countSpan.className = 'gbs-suggestion-count';
                                countSpan.style.color = color;
                                countSpan.textContent = parseInt(sugg.post_count).toLocaleString();

                                item.append(labelSpan, countSpan);

                                item.onmousedown = (e) => {
                                    e.preventDefault();
                                    e.stopPropagation();
                                    let tagToAdd = sugg.value;
                                    if (tagInput.value.trim().startsWith('-')) {
                                        tagToAdd = '-' + tagToAdd;
                                    }
                                    addPill(tagToAdd);
                                    tagInput.value = '';
                                    suggestionBox.style.display = 'none';
                                    tagInput.focus();
                                };
                                fragment.appendChild(item);
                            });
                            suggestionBox.appendChild(fragment);
                        } else {
                            suggestionBox.style.display = 'none';
                        }
                    }, 250);
                });

                closeBtn.addEventListener('click', closeModal);
                modalOverlay.addEventListener('click', e => { if (e.target === modalOverlay) closeModal(); });
                applyBtn.addEventListener('click', () => {
                    syncToOriginalInput();
                    closeModal();
                    originalInput.form.submit();
                });
                return { openModal };
            }
        }
    };

    // =================================================================================
    // PEEK: PREVIEWS MODULE
    // =================================================================================
    const Peek = {
        State: {
            currentThumb: null,
            hideTimeout: null,
            showTimeout: null,
            dynamicSeekTime: 5,
            previewElements: null,
            lastHoveredThumb: null,
            pendingPreviewRequest: null,
            thumbnailListenerCleanups: new Map()
        },
        init: function() {
            this.injectStyles();
            document.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(this.initializeThumbnailFeatures.bind(this));
            const observer = new MutationObserver((mutations) => {
                if (GlobalState.previewsTemporarilyDisabled) return;

                mutations.forEach(mutation => {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === 1) {
                            if (node.matches(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR)) this.initializeThumbnailFeatures(node);
                            node.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(this.initializeThumbnailFeatures.bind(this));
                        }
                    });
                    mutation.removedNodes.forEach(node => {
                        if (node.nodeType === 1) {
                            if (node.matches(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR)) this.cleanupThumbnailFeatures(node);
                            node.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(this.cleanupThumbnailFeatures.bind(this));
                        }
                    });
                });
            });
            observer.observe(document.body, { childList: true, subtree: true });

            window.addEventListener('pagehide', this.cleanupAllFeatures.bind(this));
        },
        injectStyles: function() {
            GM_addStyle(`
                .thumbnail-preview img { border-radius: 10px; }
                .thumbnail-preview img.gbs-animated-img { box-shadow: 0 0 0 2.5px #C2185B !important; }
                #${Config.SELECTORS.PREVIEW_CONTAINER_ID} { position: fixed !important; z-index: 99999 !important; opacity: 0; transform: translate(-50%, -50%) scale(1); pointer-events: none; background-color: #000; border-radius: 10px; box-shadow: 0 0 4px 4px rgba(0,0,0,0.6); overflow: hidden; transition: transform 0.35s ease-out, opacity 0s linear 0.35s; }
                #${Config.SELECTORS.PREVIEW_CONTAINER_ID}:focus { outline: none; }
                #${Config.SELECTORS.PREVIEW_CONTAINER_ID}.show { opacity: 1; transform: translate(-50%, -50%) scale(${Settings.State.PREVIEW_SCALE_FACTOR}); pointer-events: auto; cursor: pointer; transition: transform 0.35s ease-out, opacity 0.35s ease-out; }
                .enhancer-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; object-fit: contain; transition: opacity 0.2s ease-in-out; background-color: transparent; border: none; outline: none; }
                .enhancer-seekbar-container { position: absolute; bottom: 0; left: 0; width: 100%; height: 20px; display: none; justify-content: center; align-items: center; }
                #${Config.SELECTORS.PREVIEW_CONTAINER_ID}.video-active .enhancer-seekbar-container { display: flex; }
                .enhancer-seekbar { opacity: 0; transition: opacity 0.2s ease-out; pointer-events: none; width: 95%; margin: 0; height: 5.5px; -webkit-appearance: none; appearance: none; background: rgba(255,255,255,0.15); outline: none; border-radius: 10px; border: none; }
                .enhancer-seekbar-container:hover .enhancer-seekbar { opacity: 1; pointer-events: auto; }
                .enhancer-seekbar::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 8px; height: 8px; background: #fff; cursor: pointer; border-radius: 50%; }
                .enhancer-seekbar::-moz-range-thumb { width: 9.5px; height: 9.5px; background: #fff; cursor: pointer; border-radius: 50%; border: none; }
            `);
        },
        initializeThumbnailFeatures: function(grid) {
            if (grid.dataset.enhancerInitialized) return;
            grid.dataset.enhancerInitialized = 'true';

            Logger.log('Initializing Peek features for grid:', grid);

            if (!this.State.previewElements) {
                this.UI.createPreviewElement();
            }

            const thumbnailClickHandler = function(event) {
                if (Downloader.State.isSelectionModeActive) {
                    return;
                }

                event.preventDefault();
                event.stopPropagation();
                GM_openInTab(this.href, { active: false, setParent: true });
            };

            // Decorate thumbnails (e.g., for animated gifs)
            grid.querySelectorAll(Config.SELECTORS.THUMBNAIL_ANCHOR_SELECTOR).forEach(thumbLink => {
                const img = thumbLink.querySelector('img');
                if (img) {
                    const title = img.getAttribute('title') || '';
                    const isAnimated = title.includes('animated_gif') || title.includes('animated_png');
                    const isVideo = img.classList.contains('webm');
                    if (isAnimated && !isVideo) img.classList.add('gbs-animated-img');
                }
                thumbLink.addEventListener('click', thumbnailClickHandler, { capture: true });
            });

            // Strip titles to prevent native tooltips from conflicting with Peek
            grid.querySelectorAll('[title]').forEach(el => el.removeAttribute('title'));
            const stripTitleHandler = e => {
                const el = e.target.closest('[title]');
                if (el) el.removeAttribute('title');
            };
            grid.addEventListener('mouseover', stripTitleHandler, true);

            const observer = new MutationObserver(mutations => {
                for (const mutation of mutations) {
                    if (mutation.type === 'attributes' && mutation.attributeName === 'title') {
                        mutation.target.removeAttribute('title');
                    } else if (mutation.type === 'childList') {
                        mutation.addedNodes.forEach(node => {
                            if (node.nodeType === 1) {
                                if (node.hasAttribute?.('title')) node.removeAttribute('title');
                                node.querySelectorAll?.('[title]').forEach(el => el.removeAttribute('title'));
                            }
                        });
                    }
                }
            });
            observer.observe(grid, { subtree: true, childList: true, attributes: true, attributeFilter: ['title'] });

            grid.gbsThumbnailClickHandler = thumbnailClickHandler;

            const cleanup = this.UI.setupThumbnailEventListeners(grid);
            this.State.thumbnailListenerCleanups.set(grid, {
                cleanupFunc: () => cleanup(stripTitleHandler),
                observer
            });
        },
        cleanupThumbnailFeatures: function(grid) {
            if (!grid.dataset.enhancerInitialized) return;
            Logger.log('Cleaning up Peek features for grid:', grid);

            if (grid.gbsThumbnailClickHandler) {
                grid.querySelectorAll(Config.SELECTORS.THUMBNAIL_ANCHOR_SELECTOR).forEach(thumbLink => {
                    thumbLink.removeEventListener('click', grid.gbsThumbnailClickHandler, { capture: true });
                });
                delete grid.gbsThumbnailClickHandler;
            }

            const cleanupData = this.State.thumbnailListenerCleanups.get(grid);
            if (cleanupData) {
                cleanupData.cleanupFunc();
                cleanupData.observer.disconnect();
                this.State.thumbnailListenerCleanups.delete(grid);
            }

            if (this.State.thumbnailListenerCleanups.size === 0) {
                this.UI.destroyPreviewElement();
            }
            delete grid.dataset.enhancerInitialized;
        },
        cleanupAllFeatures: function() {
            Logger.log('Page is hiding. Cleaning up all residual Peek features.');
            for (const grid of this.State.thumbnailListenerCleanups.keys()) {
                this.cleanupThumbnailFeatures(grid);
            }
        },
        UI: {
            createPreviewElement: function() {
                const previewContainer = document.createElement('div');
                previewContainer.id = Config.SELECTORS.PREVIEW_CONTAINER_ID;
                previewContainer.setAttribute('tabindex', '-1');
                previewContainer.innerHTML = `<img class="enhancer-layer low-res-img"><img class="enhancer-layer high-res-img"><video class="enhancer-layer video-layer" playsinline></video><div class="enhancer-error-message"></div><div class="enhancer-seekbar-container"><input type="range" class="enhancer-seekbar" value="0" step="0.1"></div>`;
                document.body.appendChild(previewContainer);

                const elements = {
                    previewContainer,
                    lowResImg: previewContainer.querySelector('.low-res-img'),
                    highResImg: previewContainer.querySelector('.high-res-img'),
                    videoLayer: previewContainer.querySelector('.video-layer'),
                    errorMessage: previewContainer.querySelector('.enhancer-error-message'),
                    seekBarContainer: previewContainer.querySelector('.enhancer-seekbar-container'),
                    seekBar: previewContainer.querySelector('.enhancer-seekbar'),
                    handlers: {}
                };
                Peek.State.previewElements = elements;
                return elements;
            },
            destroyPreviewElement: function() {
                if (!Peek.State.previewElements) return;
                Logger.log('Destroying Peek Preview element and its listeners.');

                if (Peek.State.pendingPreviewRequest) {
                    Peek.State.pendingPreviewRequest.abort();
                    Peek.State.pendingPreviewRequest = null;
                }

                const { previewContainer, handlers } = Peek.State.previewElements;
                previewContainer.removeEventListener('mouseenter', handlers.stopHideTimer);
                previewContainer.removeEventListener('mouseleave', handlers.startHideTimer);
                previewContainer.removeEventListener('click', handlers.handlePreviewClick);
                previewContainer.removeEventListener('keydown', handlers.handlePreviewKeyDown);
                Peek.State.previewElements.videoLayer.removeEventListener('timeupdate', handlers.handleVideoTimeUpdate);
                Peek.State.previewElements.seekBar.removeEventListener('input', handlers.handleSeekBarInput);
                Peek.State.previewElements.seekBarContainer.removeEventListener('click', handlers.handleSeekBarContainerClick);
                window.removeEventListener('scroll', handlers.handleInstantHideOnScroll);
                if (previewContainer) {
                    previewContainer.remove();
                }

                Peek.State.previewElements = null;
            },
            hidePreview: function() {
                if (!Peek.State.previewElements) return;
                if (Peek.State.pendingPreviewRequest) {
                    Peek.State.pendingPreviewRequest.abort();
                    Peek.State.pendingPreviewRequest = null;
                }

                const { previewContainer, videoLayer } = Peek.State.previewElements;
                videoLayer.pause();
                videoLayer.removeAttribute('src');
                videoLayer.load();
                previewContainer.classList.remove('show', 'video-active', 'error', 'loading');
                previewContainer.blur();
                Peek.State.currentThumb = null;
            },
            updatePreviewPositionAndSize: function(thumb, previewContainer) {
                const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
                const thumbImg = thumb.querySelector('img');
                if (!thumbImg) return;
                const thumbRect = thumb.getBoundingClientRect();
                const initialWidth = thumbRect.width;
                const initialHeight = thumbRect.height;
                const finalWidth = initialWidth * Settings.State.PREVIEW_SCALE_FACTOR;
                const finalHeight = initialHeight * Settings.State.PREVIEW_SCALE_FACTOR;
                const margin = 10;
                const rect = thumb.getBoundingClientRect();
                let idealTop = rect.top + (rect.height / 2);
                let idealLeft = rect.left + (rect.width / 2);
                idealLeft = clamp(idealLeft, finalWidth / 2 + margin, window.innerWidth - finalWidth / 2 - margin);
                idealTop = clamp(idealTop, finalHeight / 2 + margin, window.innerHeight - finalHeight / 2 - margin);
                Object.assign(previewContainer.style, {
                    width: `${initialWidth}px`,
                    height: `${initialHeight}px`,
                    top: `${idealTop}px`,
                    left: `${idealLeft}px`
                });
            },
            showPreview: async function(thumb) {
                if (!Peek.State.previewElements) return;
                if (Peek.State.pendingPreviewRequest) {
                    Peek.State.pendingPreviewRequest.abort();
                    Peek.State.pendingPreviewRequest = null;
                }

                Peek.State.currentThumb = thumb;
                const { previewContainer, lowResImg, highResImg, videoLayer, errorMessage } = Peek.State.previewElements;
                const thumbImg = thumb.querySelector('img');
                if (!thumbImg) return;
                previewContainer.className = '';
                errorMessage.textContent = '';

                requestAnimationFrame(() => {
                    this.updatePreviewPositionAndSize(thumb, previewContainer);
                });
                lowResImg.src = thumbImg.src;
                lowResImg.style.opacity = '1';
                highResImg.src = "";
                highResImg.style.display = 'none';
                highResImg.style.opacity = '0';
                videoLayer.style.opacity = '0';
                videoLayer.pause();
                previewContainer.classList.add('show');
                if (Settings.State.PREVIEW_QUALITY === 'low') return;

                previewContainer.classList.add('loading');
                try {
                    const postId = Utils.getPostId(thumb.href);
                    const media = await API.fetchMediaDetails(postId);

                    if (Peek.State.currentThumb !== thumb) return;

                    previewContainer.classList.remove('loading');
                    if (media.type === 'video') {
                        videoLayer.src = media.url;
                        videoLayer.loop = Settings.State.PREVIEW_VIDEOS_LOOP;
                        videoLayer.muted = Settings.State.PREVIEW_VIDEOS_MUTED;
                        videoLayer.play().catch(() => {});
                        videoLayer.onloadedmetadata = () => {
                            if (Peek.State.currentThumb === thumb) {
                                const seekStep = videoLayer.duration * 0.05;
                                Peek.State.dynamicSeekTime = Math.max(1, Math.min(seekStep, 10));
                                videoLayer.style.opacity = '1';
                                previewContainer.classList.add('video-active');
                                previewContainer.focus();
                            }
                        };
                    } else {
                        highResImg.src = media.url;
                        highResImg.onload = () => {
                            if (Peek.State.currentThumb === thumb) {
                                highResImg.style.display = 'block';
                                highResImg.style.opacity = '1';
                            }
                        };
                    }
                } catch (error) {
                    if (error.message.includes('abort')) return;
                    if (Peek.State.currentThumb === thumb) {
                        previewContainer.classList.remove('loading');
                        previewContainer.classList.add('error');
                        errorMessage.textContent = error.message;
                    }
                }
            },
            setupThumbnailEventListeners: function(thumbnailGrid) {
                if (!Peek.State.previewElements) return () => {};
                const self = Peek; // Reference to the Peek module
                const { handlers, previewContainer, videoLayer, seekBarContainer, seekBar } = self.State.previewElements;
                handlers.handleInstantHideOnScroll = () => {
                    if (!self.State.previewElements?.previewContainer || !self.State.currentThumb) { return; }
                    const { previewContainer } = self.State.previewElements;
                    previewContainer.style.transition = 'none';
                    self.UI.hidePreview();
                    setTimeout(() => {
                        if (previewContainer) { previewContainer.style.transition = ''; }
                    }, 50);
                };
                handlers.startHideTimer = () => {
                    clearTimeout(self.State.showTimeout);
                    clearTimeout(self.State.hideTimeout);
                    self.State.hideTimeout = setTimeout(() => self.UI.hidePreview(), Settings.State.HIDE_DELAY);
                };
                handlers.stopHideTimer = () => { clearTimeout(self.State.hideTimeout); };
                handlers.handleGridMouseOver = (e) => {
                    const thumb = e.target.closest(Config.SELECTORS.THUMBNAIL_ANCHOR_SELECTOR);
                    if (thumb) {
                        handlers.stopHideTimer();
                        if (self.State.lastHoveredThumb && self.State.lastHoveredThumb !== thumb) {
                            const oldTitle = self.State.lastHoveredThumb.dataset.originalTitle;
                            if (oldTitle) self.State.lastHoveredThumb.setAttribute('title', oldTitle);
                        }
                        if (thumb.hasAttribute('title')) {
                            thumb.dataset.originalTitle = thumb.getAttribute('title');
                            thumb.removeAttribute('title');
                        }
                        self.State.lastHoveredThumb = thumb;
                        if (self.State.currentThumb !== thumb) {
                            if (self.State.currentThumb) self.UI.hidePreview();
                            self.State.showTimeout = setTimeout(() => self.UI.showPreview(thumb), Settings.State.SHOW_DELAY);
                        }
                    } else if (!previewContainer.matches(':hover')) {
                        handlers.startHideTimer();
                    }
                };
                handlers.handleGridMouseLeave = () => {
                    if (self.State.lastHoveredThumb) {
                        const oldTitle = self.State.lastHoveredThumb.dataset.originalTitle;
                        if (oldTitle) self.State.lastHoveredThumb.setAttribute('title', oldTitle);
                        self.State.lastHoveredThumb = null;
                    }
                    handlers.startHideTimer();
                };
                handlers.handlePreviewClick = () => { if (self.State.currentThumb?.href) GM_openInTab(self.State.currentThumb.href, { active: false, setParent: true }); };
                handlers.handlePreviewKeyDown = e => {
                    if (self.State.currentThumb && videoLayer.style.opacity === '1') {
                        const hotkeys = [Settings.State.KEY_PEEK_VIDEO_SEEK_BACK, Settings.State.KEY_PEEK_VIDEO_SEEK_FORWARD, Settings.State.KEY_PEEK_VIDEO_PLAY_PAUSE];
                        if (hotkeys.includes(e.key)) e.preventDefault();
                        if (e.key === Settings.State.KEY_PEEK_VIDEO_SEEK_BACK) videoLayer.currentTime -= self.State.dynamicSeekTime;
                        else if (e.key === Settings.State.KEY_PEEK_VIDEO_SEEK_FORWARD) videoLayer.currentTime += self.State.dynamicSeekTime;
                        else if (e.key === Settings.State.KEY_PEEK_VIDEO_PLAY_PAUSE) videoLayer.paused ? videoLayer.play() : videoLayer.pause();
                    }
                };
                handlers.handleVideoTimeUpdate = () => {
                    if (videoLayer.duration) {
                        if (seekBar.max != videoLayer.duration) seekBar.max = videoLayer.duration;
                        seekBar.value = videoLayer.currentTime;
                    }
                };
                handlers.handleSeekBarInput = e => { e.stopPropagation(); videoLayer.currentTime = seekBar.value; };
                handlers.handleSeekBarContainerClick = e => e.stopPropagation();
                thumbnailGrid.addEventListener('mouseover', handlers.handleGridMouseOver);
                thumbnailGrid.addEventListener('mouseleave', handlers.handleGridMouseLeave);
                previewContainer.addEventListener('mouseenter', handlers.stopHideTimer);
                previewContainer.addEventListener('mouseleave', handlers.startHideTimer);
                previewContainer.addEventListener('click', handlers.handlePreviewClick);
                previewContainer.addEventListener('keydown', handlers.handlePreviewKeyDown);
                videoLayer.addEventListener('timeupdate', handlers.handleVideoTimeUpdate);
                seekBar.addEventListener('input', handlers.handleSeekBarInput);
                seekBarContainer.addEventListener('click', handlers.handleSeekBarContainerClick);
                window.addEventListener('scroll', handlers.handleInstantHideOnScroll, { passive: true });
                return (stripTitleHandler) => {
                    Logger.log(`Cleaning up event listeners for grid:`, thumbnailGrid);
                    thumbnailGrid.removeEventListener('mouseover', handlers.handleGridMouseOver);
                    thumbnailGrid.removeEventListener('mouseleave', handlers.handleGridMouseLeave);
                    thumbnailGrid.removeEventListener('mouseover', stripTitleHandler, true);
                };
            },
        }
    };

    // =================================================================================
    // DOWNLOADER MODULE
    // =================================================================================
    const Downloader = {
        State: {
            isSelectionModeActive: false,
            isDownloading: false,
            isCancelled: false,
            downloadQueue: new Set(),
            processingQueue: [],
            failedDownloads: new Set(),
            downloadStatus: new Map()
        },

        // --- Session Management ---
        resetState: function() {
            this.State.isDownloading = false;
            this.State.isCancelled = false;
            this.State.processingQueue = [];
            this.State.failedDownloads.clear();
            this.State.downloadStatus.clear();
        },

        // --- Core Download Logic ---
        downloadSinglePost: async function(thumbAnchor) {
            const pId = Utils.getPostId(thumbAnchor.href);
            if (this.State.downloadQueue.has(pId)) return;
            this.State.downloadQueue.add(pId);
            this.State.downloadStatus.set(pId, 'downloading');

            const { progressOverlay, circleFG, circumference } = this.UI.createProgressCircle();
            thumbAnchor.appendChild(progressOverlay);

            try {
                this.UI.updateThumbnailFeedback(thumbAnchor, 'downloading');
                await new Promise(r => setTimeout(r, 150));
                if (this.State.isCancelled) throw new Error('Cancelled before start');

                const media = await API.fetchMediaDetails(pId);
                const ext = new URL(media.url).pathname.split('.').pop() || 'jpg';
                const filename = `${Settings.State.DOWNLOAD_FOLDER}/post_${pId}.${ext}`;

                const { promise } = Utils.makeRequest({
                    method: "HEAD",
                    timeout: 15000,
                    url: media.url,
                });
                const headResponse = await promise;
                const headers = headResponse.responseHeaders;

                const totalSizeMatch = headers.match(/content-length:\s*(\d+)/i);
                const totalSize = totalSizeMatch ? parseInt(totalSizeMatch[1], 10) : 0;
                if (totalSize === 0) throw new Error('Could not determine file size.');

                await new Promise((resolve, reject) => {
                    GM_download({
                        url: media.url,
                        name: filename,
                        onprogress: (progress) => {
                            if (this.State.isCancelled) {
                                return reject(new Error('Cancelled during download'));
                            }
                            const percentComplete = progress.loaded / totalSize;
                            const offset = circumference * (1 - percentComplete);
                            circleFG.style.strokeDashoffset = offset;
                        },
                        onload: () => {
                            if (this.State.isCancelled) return reject(new Error('Cancelled on complete'));
                            this.UI.updateThumbnailFeedback(thumbAnchor, 'success');
                            this.State.downloadStatus.set(pId, 'success');
                            resolve();
                        },
                        onerror: (err) => reject(err),
                        ontimeout: () => reject(new Error('Timeout'))
                    });
                });
            } catch (er) {
                if (er.message && er.message.toLowerCase().includes('cancelled')) {
                    this.UI.updateThumbnailFeedback(thumbAnchor, null);
                    this.State.downloadStatus.set(pId, 'cancelled');
                    Logger.log(`Download for post ${pId} cancelled.`);
                } else {
                    this.UI.updateThumbnailFeedback(thumbAnchor, 'error');
                    this.State.downloadStatus.set(pId, 'error');
                    Logger.error(`Download failed for post ID ${pId}:`, er);
                    this.State.failedDownloads.add(pId);
                }
                throw er;
            } finally {
                progressOverlay.remove();
                this.State.downloadQueue.delete(pId);
            }
        },
        downloadWithRetries: async function(thumb, maxAttempts = 3) {
            let lastError = null;
            for (let attempt = 1; attempt <= maxAttempts; attempt++) {
                try {
                    await this.downloadSinglePost(thumb);
                    return;
                } catch (err) {
                    lastError = err;
                    if (err.message && err.message.toLowerCase().includes('cancelled')) {
                        throw err;
                    }
                    Logger.warn(`Download attempt ${attempt}/${maxAttempts} failed for post ${Utils.getPostId(thumb.href)}.`);
                    if (attempt < maxAttempts) {
                        await new Promise(r => setTimeout(r, 2000 * attempt));
                    }
                }
            }
            throw lastError;
        },
        startDownloadAllProcess: async function() {
            if (this.State.isDownloading) {
                this.State.isCancelled = true;
                const btn = document.getElementById('gbs-fab-download-all');
                if (btn) {
                    btn.querySelector('.gbs-fab-text').textContent = 'Cancelling...';
                    btn.disabled = true;
                }
                return;
            }

            const allThumbs = Array.from(document.querySelectorAll(Config.SELECTORS.MEDIA_VIEWER_THUMBNAIL_ANCHOR));
            if (allThumbs.length === 0) {
                Logger.warn('No items on the page to download.');
                return;
            }

            const blocker = document.createElement('div');
            blocker.id = 'gbs-page-blocker';
            document.body.appendChild(blocker);
            setTimeout(() => { blocker.style.opacity = '1'; }, 10);

            this.resetState();

            this.State.isDownloading = true;
            this.State.isCancelled = false;
            document.getElementById('gbs-fab-select').disabled = true;
            this.UI.updateDownloadAllButton(true);
            this.UI.toggleMenuTriggerCursor(true);
            this.UI.showProgressBar(true);
            this.UI.updateProgressBar(0, 0, allThumbs.length);
            this.UI.resetThumbnailsFeedback();

            this.State.processingQueue = [...allThumbs];
            Logger.log(`Starting download for ${allThumbs.length} posts.`);

            let s = 0, e = 0;
            const t = allThumbs.length;
            const MAX_CONCURRENCY = 6;
            this.UI.updateProgressBar(s, e, t);

            const processQueue = async () => {
                while (this.State.processingQueue.length > 0 && !this.State.isCancelled) {
                    const thumb = this.State.processingQueue.shift();
                    try {
                        await this.downloadWithRetries(thumb);
                        s++;
                    } catch(err) {
                        if (!err.message?.toLowerCase().includes('cancelled')) {
                            e++;
                        }
                    } finally {
                        this.UI.updateProgressBar(s, e, t);
                        await new Promise(r => setTimeout(r, 300 + Math.random() * 500));
                    }
                }
            };

            const workers = Array.from({ length: Math.min(MAX_CONCURRENCY, this.State.processingQueue.length) }, processQueue);
            await Promise.allSettled(workers);
            this.finishDownloadProcess(s, e, t);
        },
        finishDownloadProcess: function(s, e, t) {
            const blocker = document.getElementById('gbs-page-blocker');
            if (blocker) blocker.remove();

            if (this.State.isCancelled) {
                Logger.log('Download process cancelled by user.');
            } else {
                Logger.log(`Download process completed. Success: ${s}, Errors: ${e}, Total: ${t}`);
            }

            this.UI.showCompletionModal(Array.from(this.State.failedDownloads));
            this.UI.toggleActionButtons(true);
            document.getElementById('gbs-fab-download-all').disabled = false;
            this.UI.updateDownloadAllButton(false);
            this.UI.toggleMenuTriggerCursor(false);
            this.UI.showProgressBar(false);
            this.resetState();
        },
        UI: {
            toggleActionButtons: (enable) => {
                document.getElementById('gbs-fab-select').disabled = !enable;
                document.getElementById('gbs-fab-download-all').disabled = !enable;
            },
            showProgressBar: (show) => {
                const el = document.getElementById('gbs-progress-bar-container');
                if (el) {
                    el.style.display = show ? 'flex' : 'none';
                    if (show) Downloader.UI.updateProgressBar(0, 0, 1);
                }
            },
            updateProgressBar: (s, e, t) => {
                const p = t > 0 ? ((s + e) / t) * 100 : 0;
                const fill = document.querySelector('#gbs-progress-bar-container .gbs-progress-bar-fill');
                const text = document.querySelector('#gbs-progress-bar-container .gbs-progress-bar-text');
                if (!fill || !text) return;
                fill.style.width = `${p}%`;
                if (e > 0) {
                    fill.style.backgroundColor = '#A43535';
                    text.textContent = `Downloading... (${s}/${t-e}) (Errors: ${e})`;
                } else {
                    fill.style.backgroundColor = '#008450';
                    text.textContent = `Downloading... (${s}/${t})`;
                }
            },
            updateDownloadAllButton: (isDownloading) => {
                const btn = document.getElementById('gbs-fab-download-all');
                if (!btn) return;
                const btnText = btn.querySelector('.gbs-fab-text');
                if (isDownloading) {
                    btnText.textContent = 'Cancel';
                    btn.classList.add('gbs-btn-cancel');
                } else {
                    btnText.textContent = 'Download All';
                    btn.classList.remove('gbs-btn-cancel');
                }
            },
            updateThumbnailFeedback: (thumb, status) => {
                if (thumb) {
                    thumb.classList.remove('gbs-thumb-selected', 'gbs-thumb-success', 'gbs-thumb-error', 'gbs-thumb-downloading');
                    if (status) thumb.classList.add(`gbs-thumb-${status}`);
                }
            },
            resetThumbnailsFeedback: () => {
                document.querySelectorAll('.gbs-thumb-success, .gbs-thumb-error, .gbs-thumb-selected, .gbs-thumb-downloading').forEach(el => {
                    el.classList.remove('gbs-thumb-success', 'gbs-thumb-error', 'gbs-thumb-selected', 'gbs-thumb-downloading');
                });
            },
            toggleMenuTriggerCursor: (isBlocked) => {
                document.getElementById('gbs-downloader-trigger')?.classList.toggle('is-blocked', isBlocked);
            },
            createProgressCircle: function() {
                const svgNS = "http://www.w3.org/2000/svg";
                const progressOverlay = document.createElement('div');
                progressOverlay.className = 'gbs-progress-overlay';
                const svg = document.createElementNS(svgNS, 'svg');
                svg.setAttribute('viewBox', '0 0 50 50');
                svg.style.width = '60%'; svg.style.height = '60%';
                const circleBG = document.createElementNS(svgNS, 'circle');
                circleBG.setAttribute('cx', '25'); circleBG.setAttribute('cy', '25'); circleBG.setAttribute('r', '20'); circleBG.setAttribute('fill', 'transparent'); circleBG.setAttribute('stroke-width', '5');
                circleBG.setAttribute('class', 'gbs-progress-circle-bg');
                const circleFG = circleBG.cloneNode();
                circleFG.setAttribute('class', 'gbs-progress-circle-fg');
                const circumference = 2 * Math.PI * 20;
                Object.assign(circleFG.style, { strokeDasharray: circumference, strokeDashoffset: circumference });
                svg.append(circleBG, circleFG);
                progressOverlay.appendChild(svg);
                return { progressOverlay, circleFG, circumference };
            },
            showCompletionModal: function(failedIds) {
                if (failedIds.length === 0) {
                    alert('All downloads on this page completed successfully!');
                    return;
                }
                if (document.getElementById('gbs-completion-modal')) return;
                const modalHTML = `
                <div id="gbs-completion-modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.7); z-index: 100000; display: flex; align-items: center; justify-content: center; font-family: sans-serif;">
                    <div id="gbs-completion-modal" style="background-color: #252525; color: #eee; padding: 20px; border-radius: 10px; width: 90%; max-width: 500px; text-align: center;">
                        <h3 style="margin-top: 0; border-bottom: 1px solid #555; padding-bottom: 10px;">Download Process Finished</h3>
                        <p>The following posts could not be downloaded. You can copy the IDs for a manual check:</p>
                        <textarea readonly style="width: 95%; height: 150px; background: #333; color: #fff; border: 1px solid #555; resize: none; margin-top: 10px;">${failedIds.join(' ')}</textarea>
                        <button id="gbs-completion-close" style="margin-top: 15px; padding: 8px 16px; background-color: #007BFF; color: white; border: none; border-radius: 10px; cursor: pointer;">Close</button>
                    </div>
                </div>`;
                document.body.insertAdjacentHTML('beforeend', modalHTML);
                document.getElementById('gbs-completion-close').addEventListener('click', () => {
                    document.getElementById('gbs-completion-modal-overlay').remove();
                });
            },
        },
        toggleSelectionMode: function() {
            if (this.State.isDownloading) return;
            this.State.isSelectionModeActive = !this.State.isSelectionModeActive;
            document.body.classList.toggle('gbs-selection-active', this.State.isSelectionModeActive);
            this.UI.toggleMenuTriggerCursor(this.State.isSelectionModeActive);

            const selectButton = document.getElementById('gbs-fab-select');
            selectButton.querySelector('.gbs-fab-text').textContent = this.State.isSelectionModeActive ? 'Cancel' : 'Download (Select)';
            selectButton.classList.toggle('active', this.State.isSelectionModeActive);

            GlobalState.previewsTemporarilyDisabled = this.State.isSelectionModeActive;
            const grids = document.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR);
            if (this.State.isSelectionModeActive) {
                Logger.log("Selection mode activated. Disabling Peek Previews.");
                grids.forEach(grid => Peek.cleanupThumbnailFeatures(grid));
            } else {
                Logger.log("Selection mode deactivated. Re-enabling Peek Previews.");
                grids.forEach(grid => Peek.initializeThumbnailFeatures(grid));
                this.UI.resetThumbnailsFeedback();
            }
        },
        handleThumbnailClick: function(event) {
            if (!this.State.isSelectionModeActive) return;
            const thumbAnchor = event.target.closest(Config.SELECTORS.MEDIA_VIEWER_THUMBNAIL_ANCHOR);
            if (!thumbAnchor) return;
            event.preventDefault();
            event.stopPropagation();
            this.downloadSinglePost(thumbAnchor);
        },
        injectUI: function() {
            GM_addStyle(`
            #gbs-downloader-wrapper { position: fixed; top: 50%; left: 0; transform: translateY(-50%); z-index: 9998; }
            #gbs-downloader-trigger { width: 18px; height: 50px; background-color: #252525; border-radius: 0 10px 10px 0; cursor: pointer; border: 2px solid #333; border-left: none; transition: background-color 0.2s ease; }
            #gbs-downloader-wrapper:hover #gbs-downloader-trigger { background-color: #007BFF; }
            #gbs-downloader-wrapper.menu-open #gbs-downloader-trigger { background-color: #A43535; }
            #gbs-downloader-trigger.is-blocked { cursor: not-allowed; }
            #gbs-action-list { position: absolute; top: 50%; left: 100%; transform: translateY(-50%) scale(0.95); margin-left: 15px; display: flex; flex-direction: column; align-items: flex-start; gap: 5px; opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; }
            #gbs-downloader-wrapper.menu-open #gbs-action-list { opacity: 1; transform: translateY(-50%) scale(1); pointer-events: auto; }
            .gbs-selection-active body, .gbs-selection-active .thumbnail-preview a, .gbs-selection-active .thumbnail-container > span > a { cursor: crosshair !important; }
            .thumbnail-preview > a, .thumbnail-container > span > a { display:inline-block; line-height:0; position:relative; transition:transform 0.2s, box-shadow 0.2s; }
            .gbs-thumb-downloading { transform:scale(0.95); border-radius: 10px !important; overflow:hidden; outline: 4px solid #EFB700 !important; }
            .gbs-thumb-success { border-radius: 10px !important; overflow:hidden; outline: 4px solid #008450 !important; }
            .gbs-thumb-error { border-radius: 10px !important; overflow:hidden; outline: 4px solid #B81D13 !important; }
            .gbs-thumb-success::after, .gbs-thumb-error::after { content:''; position:absolute; top:0; left:0; width:100%; height:100%; display:flex; align-items:center; justify-content:center; font-size:50px; color:white; text-shadow:0 0 5px black; }
            .gbs-thumb-success::after { background-color:rgba(40, 167, 69, 0.7); content:'✔'; }
            .gbs-thumb-error::after { background-color:rgba(220, 53, 69, 0.7); content:'✖'; }
            #gbs-progress-bar-container { position:fixed; bottom:0; left:0; width:100%; height:25px; background-color:#333; z-index:99999; display:none; align-items:center; border-top:1px solid #555; }
            .gbs-progress-bar-fill { background-color:#008450; height:100%; width:0%; transition:width 0.3s ease-in-out; }
            .gbs-progress-bar-text { position:absolute; width:100%; text-align:center; color:white; font-weight:bold; text-shadow:1px 1px 1px #000; z-index:10; }
            .gbs-progress-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; pointer-events: none; background-color: rgba(0,0,0,0.5); opacity: 0; transition: opacity 0.2s ease-in-out; }
            .gbs-thumb-downloading .gbs-progress-overlay { opacity: 1; }
            .gbs-progress-circle-bg { stroke: rgba(255,255,255,0.2); }
            .gbs-progress-circle-fg { stroke: #EFB700; transform: rotate(-90deg); transform-origin: 50% 50%; transition: stroke-dashoffset 0.1s linear; }
            #gbs-page-blocker { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); cursor: progress; z-index: 9997; opacity: 0; transition: opacity 0.3s ease-in-out; }
        `);

            document.body.insertAdjacentHTML('beforeend', `
            <div id="gbs-downloader-wrapper">
                <div id="gbs-downloader-trigger"></div>
                <div id="gbs-action-list">
                    <button id="gbs-fab-download-all" class="gbs-fab-action-btn"><span class="gbs-fab-text">Download All</span></button>
                    <button id="gbs-fab-select" class="gbs-fab-action-btn"><span class="gbs-fab-text">Download (Select)</span></button>
                </div>
            </div>
            <div id="gbs-progress-bar-container" style="display: none;"><div class="gbs-progress-bar-text"></div><div class="gbs-progress-bar-fill"></div></div>
        `);
        },
        setupEventListeners: function() {
            const wrapper = document.getElementById('gbs-downloader-wrapper');
            const menuTrigger = document.getElementById('gbs-downloader-trigger');

            menuTrigger.addEventListener('click', (event) => {
                if (this.State.isDownloading || this.State.isSelectionModeActive) return;
                event.stopPropagation();
                wrapper.classList.toggle('menu-open');
                if (!wrapper.classList.contains('menu-open')) {
                    this.UI.showProgressBar(false);
                    this.UI.resetThumbnailsFeedback();
                }
            });

            document.getElementById('gbs-fab-download-all').addEventListener('click', () => this.startDownloadAllProcess());
            document.getElementById('gbs-fab-select').addEventListener('click', () => this.toggleSelectionMode());
            document.body.addEventListener('click', (e) => this.handleThumbnailClick(e), true);
        },
        toggleVisibility: function(visible) {
            const wrapper = document.getElementById('gbs-downloader-wrapper');
            if (wrapper) {
                wrapper.style.display = visible ? 'block' : 'none';
                Logger.log(`Downloader UI visibility set to: ${visible}`);
            }
        },
        init: function() {
            const isPoolPage = window.location.search.includes('page=pool');
            const isGalleryPage = window.location.search.includes('page=post&s=list');
            if (!isPoolPage && !isGalleryPage) return;

            this.injectUI();
            this.setupEventListeners();
            Logger.log('Downloader: Side-drawer UI initialized.');
        }
    };

    // =================================================================================
    // ADD TO POOL MODULE
    // =================================================================================
    const AddToPool = {
        State: {
            isSelectionModeActive: false,
            targetPoolId: null,
        },
        elements: {},
        async init() {
            const isPoolPage = window.location.search.includes('page=pool');
            const isGalleryPage = window.location.search.includes('page=post&s=list');
            if (!isPoolPage && !isGalleryPage) return;

            this.injectUI();
            this.setupEventListeners();
            await this.loadSavedPoolId();
        },
        async loadSavedPoolId() {
            const savedId = await GM.getValue(Config.STORAGE_KEYS.POOL_TARGET_ID, null);
            if (savedId) {
                this.State.targetPoolId = savedId;
                if (this.elements.poolIdInput) {
                    this.elements.poolIdInput.value = savedId;
                }
                Logger.log(`[AddToPool] Loaded saved Target Pool ID: ${savedId}`);
            }
        },
        async savePoolId(poolId) {
            if (poolId && poolId.match(/^\d+$/)) {
                await GM.setValue(Config.STORAGE_KEYS.POOL_TARGET_ID, poolId);
                this.State.targetPoolId = poolId;
                alert(`Target Pool ID set to: ${poolId}`);
            } else {
                await GM.setValue(Config.STORAGE_KEYS.POOL_TARGET_ID, null);
                this.State.targetPoolId = null;
                alert('Invalid or empty Pool ID. Target has been cleared.');
            }
        },
        injectUI() {
            GM_addStyle(`
            #gbs-pool-wrapper { position: fixed; top: 30%; left: 0; transform: translateY(-50%); z-index: 9998; }
            #gbs-pool-trigger { width: 18px; height: 50px; background-color: #252525; border-radius: 0 10px 10px 0; cursor: pointer; border: 2px solid #333; border-left: none; transition: background-color 0.2s ease; }
            #gbs-pool-wrapper:hover #gbs-pool-trigger { background-color: #007BFF; }
            #gbs-pool-wrapper.menu-open #gbs-pool-trigger { background-color: #A43535; }
            #gbs-pool-trigger.is-blocked { cursor: not-allowed; }
            #gbs-pool-action-list { position: absolute; top: 50%; left: 100%; transform: translateY(-50%) scale(0.95); margin-left: 15px; gap: 5px; display: flex; flex-direction: column; align-items: flex-start; opacity: 0; transition: opacity 0.2s ease, transform 0.2s ease; pointer-events: none; }
            #gbs-pool-wrapper.menu-open #gbs-pool-action-list { opacity: 1; transform: translateY(-50%) scale(1); pointer-events: auto; }

            #gbs-pool-input-container { display: flex; gap: 5px; width: 100%; }
            #gbs-pool-target-input { flex-grow: 1; width: 0; padding: 8px; background: #333; color: #fff; border-radius: 10px; }
            #gbs-pool-target-set-btn { padding: 8px 12px; background-color: #343a40; color: #fff; border: none; border-radius: 10px; font-weight: bold; cursor: pointer; }
            #gbs-pool-target-set-btn:hover { background-color: #555; }

            body.gbs-pool-select-mode-active .thumbnail-preview img,
            body.gbs-pool-select-mode-active .thumbnail-container > span > a { cursor: crosshair !important; }
            .thumbnail-container > span > a { display: inline-block; line-height: 0; }
            .gbs-pool-add-notification { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 132, 80, 0.8); color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; z-index: 10; border-radius: 10px; pointer-events: none; opacity: 0; transition: opacity 0.3s; }
        `);

            const wrapper = document.createElement('div');
            wrapper.id = 'gbs-pool-wrapper';
            wrapper.innerHTML = `
            <div id="gbs-pool-trigger"></div>
            <div id="gbs-pool-action-list">
                <div id="gbs-pool-input-container">
                    <input type="text" id="gbs-pool-target-input" placeholder="Pool ID" />
                    <button id="gbs-pool-target-set-btn">Set</button>
                </div>
                <button id="gbs-pool-select-btn" class="gbs-fab-action-btn">
                    <span class="gbs-fab-text">Add to Pool (Select)</span>
                </button>
            </div>
        `;
            document.body.appendChild(wrapper);

            this.elements = {
                wrapper,
                trigger: wrapper.querySelector('#gbs-pool-trigger'),
                selectButton: wrapper.querySelector('#gbs-pool-select-btn'),
                poolIdInput: wrapper.querySelector('#gbs-pool-target-input'),
                poolIdSetBtn: wrapper.querySelector('#gbs-pool-target-set-btn'),
            };
        },
        setupEventListeners() {
            this.elements.trigger.addEventListener('click', (event) => {
                if (this.State.isSelectionModeActive) return;
                event.stopPropagation();
                this.elements.wrapper.classList.toggle('menu-open');
            });

            this.elements.poolIdSetBtn.addEventListener('click', () => {
                const poolId = this.elements.poolIdInput.value.trim();
                this.savePoolId(poolId);
            });

            this.elements.selectButton.addEventListener('click', (e) => {
                e.preventDefault();
                this.toggleSelectionMode();
            });
        },
        toggleSelectionMode() {
            this.State.isSelectionModeActive = !this.State.isSelectionModeActive;
            const buttonText = this.elements.selectButton.querySelector('.gbs-fab-text');

            this.elements.selectButton.classList.toggle('active', this.State.isSelectionModeActive);
            this.elements.trigger.classList.toggle('is-blocked', this.State.isSelectionModeActive);
            document.body.classList.toggle('gbs-pool-select-mode-active', this.State.isSelectionModeActive);

            if (this.State.isSelectionModeActive) {
                buttonText.textContent = 'Cancel';
                document.body.addEventListener('click', this.handleThumbnailClick, true);

                GlobalState.previewsTemporarilyDisabled = true;
                document.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(grid => Peek.cleanupThumbnailFeatures(grid));
                Logger.log("[AddToPool] Selection mode activated. Disabling Peek Previews.");
            } else {
                buttonText.textContent = 'Add to Pool (Select)';
                document.body.removeEventListener('click', this.handleThumbnailClick, true);

                GlobalState.previewsTemporarilyDisabled = false;
                document.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(grid => Peek.initializeThumbnailFeatures(grid));
                Logger.log("[AddToPool] Selection mode deactivated. Re-enabling Peek Previews.");
            }
        },
        handleThumbnailClick: (event) => {
            const self = AddToPool;
            if (!self.State.isSelectionModeActive) return;

            const thumbAnchor = event.target.closest(Config.SELECTORS.MEDIA_VIEWER_THUMBNAIL_ANCHOR);
            if (!thumbAnchor) return;

            event.preventDefault();
            event.stopPropagation();

            if (!self.State.targetPoolId) {
                alert('Please set a target Pool ID first.');
                self.elements.wrapper.classList.add('menu-open');
                self.elements.poolIdInput.focus();
                return;
            }

            const postId = Utils.getPostId(thumbAnchor.href);
            if (!postId) return;

            // If the site's addToPool function is changed or stops using window.prompt, this feature will break.
            const script = document.createElement('script');
            script.textContent = `(() => {
            const originalPrompt = window.prompt;
            window.prompt = () => '${self.State.targetPoolId}';
            if (typeof addToPoolID === 'function') {
                addToPoolID(${postId});
            }
            window.prompt = originalPrompt;
            })();`;
            document.body.appendChild(script).remove();

            const notif = document.createElement('div');
            notif.className = 'gbs-pool-add-notification';
            notif.textContent = 'Added!';
            thumbAnchor.style.position = 'relative';
            thumbAnchor.appendChild(notif);
            setTimeout(() => { notif.style.opacity = '1'; }, 10);
            setTimeout(() => {
                notif.style.opacity = '0';
                setTimeout(() => notif.remove(), 300);
            }, 1500);
        },
        toggleVisibility: function(visible) {
            const wrapper = document.getElementById('gbs-pool-wrapper');
            if (wrapper) {
                wrapper.style.display = visible ? 'block' : 'none';
                Logger.log(`AddToPool UI visibility set to: ${visible}`);
            }
        },
    };

    // =================================================================================
    // MEDIA VIEWER MODULE
    // =================================================================================
    const MediaViewer = {
        _boundKeyDownHandler: null,
        _isNavigating: false,
        elements: {},
        State: {
            isLargeViewActive: false,
            currentImageIndex: -1,
            largeMediaElements: [],
            thumbnailAnchors: [],
            inactivityTimer: null,
            _boundResetInactivityTimer: null,
            _scrollHandler: null,
            _isThrottled: false,
        },
        init() {
            const isPoolPage = window.location.search.includes('page=pool');
            const isGalleryPage = window.location.search.includes('page=post&s=list');
            if (!isPoolPage && !isGalleryPage) return;
            this.State.thumbnailAnchors = Array.from(document.querySelectorAll(Config.SELECTORS.MEDIA_VIEWER_THUMBNAIL_ANCHOR));
            this.injectUI();
            this.setupEventListeners();
        },
        injectUI() {
            GM_addStyle(`
            body.gbs-viewer-mode-active #container { display: block !important; }
            body.gbs-viewer-mode-active section.aside { display: none !important; }
            body.gbs-hide-viewer-cursor, body.gbs-hide-viewer-cursor * { cursor: none !important; }
            .gbs-large-view-active.thumbnail-container > div[style*="text-align: center"] { display: none !important; }

            .gbs-large-view-active.thumbnail-container { display: block !important; }
            .gbs-large-view-active.thumbnail-container > .thumbnail-preview { width: 100vw !important; max-width: none !important; height: 100vh !important; display: flex !important; justify-content: center !important; align-items: center !important; padding: 0 !important; margin: 0 !important; }
            .gbs-large-view-active.thumbnail-container > span { height: 100vh; display: flex; justify-content: center; align-items: center; }
            .gbs-large-view-active.thumbnail-container a { pointer-events: none; }
            .gbs-large-view-media { max-width: 93vw !important; max-height: 98vh !important; object-fit: contain; pointer-events: auto; border-radius: 0px !important; }

            .gbs-viewer-nav-item { color: #fff; background-color: rgba(37, 37, 37, 0.8); padding: 5px; border-radius: 10px; border: 2px solid rgba(51, 51, 51, 0.5); width: 45px; height: 40px; display: flex; align-items: center; justify-content: center; box-sizing: border-box !important; transition: background-color 0.2s, border-color 0.2s ease; }
            .gbs-viewer-nav-btn { font-size: 24px; cursor: pointer; }
            .gbs-viewer-nav-btn:hover { background-color: #333; border-color: #333; }
            #gbs-viewer-nav-counter { font-size: 14px; font-weight: bold; user-select: none; height: 30px;}
            #gbs-viewer-nav-container { position: fixed; top: 80%; left: 4px; z-index: 99999; display: none; flex-direction: column; gap: 5px; }
            #gbs-viewer-nav-container.visible { display: flex; }

            #gbs-viewer-top-controls { position: fixed; top: 4%; left: 4px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; }
            #gbs-viewer-btn, #gbs-viewer-show-info-btn, #gbs-viewer-open-post-btn { color: #fff !important; position: static; transform: none; width: 45px; height: 45px; padding: 5px; background-color: rgba(37, 37, 37, 0.8); backdrop-filter: blur(5px); border: 2px solid rgba(51, 51, 51, 0.5); border-radius: 10px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; }
            #gbs-viewer-show-info-btn, #gbs-viewer-open-post-btn { display: none; }
            #gbs-viewer-btn:hover, #gbs-viewer-show-info-btn:hover, #gbs-viewer-open-post-btn:hover { background-color: #006FFA; border-color: #006FFA; }
            #gbs-viewer-btn.active { background-color: rgba(37, 37, 37, 0.8); border-color: rgba(51, 51, 51, 0.5); }
            #gbs-viewer-btn.active:hover { background-color: #A43535; border-color: #A43535; }
            #gbs-viewer-show-info-btn.active { color: #006FFA !important; border-color: #006FFA; }
            #gbs-viewer-show-info-btn:hover { color: white; }
            #gbs-viewer-top-controls, #gbs-viewer-nav-container { transition: opacity 0.3s ease-in-out; }

            #gbs-viewer-info-sidebar { position: fixed !important; top: 0; right: -280px; width: 250px; height: 100vh; background-color: #1f1f1f; border-left: 2px solid #333; z-index: 99999; padding: 2px; box-sizing: border-box; overflow-y: auto; transition: right 0.3s ease-in-out; }
            #gbs-viewer-info-sidebar.visible { right: 0; }
            #gbs-viewer-info-sidebar .tag-list { position: absolute !important; width: 90%; box-sizing: border-box; word-wrap: break-word; padding: 0px; border: 0 !important; }
            #gbs-viewer-info-sidebar .tag-type-artist a { color: #AA0000 !important; }
            #gbs-viewer-info-sidebar .tag-type-character a { color: #00AA00 !important; }
            #gbs-viewer-info-sidebar .tag-type-copyright a { color: #AA00AA !important; }
            #gbs-viewer-info-sidebar .tag-type-metadata a { color: #FF8800 !important; }
            #gbs-viewer-info-sidebar .tag-type-general a { color: white !important; }
            #gbs-viewer-info-sidebar #tag-list li { padding: 0 !important; margin: 0 !important; width: 100% !important; line-height: 23px !important; }
            .gbs-ui-hidden { opacity: 0; pointer-events: none; }
            .gbs-custom-action-btn { display: block;background-color: rgba(0, 111, 250, 0.5); color: white !important; padding: 8px; margin-bottom: 10px; margin-top: 10px; border-radius: 10px; text-align: center; font-weight: bold; font-size: 22px; transition: background-color 0.2s ease; }
            .gbs-custom-action-btn:hover { background-color: #006FFA; color: white !important; }
            .gbs-action-buttons-container { display: grid !important; grid-template-columns: 1fr 3fr; gap: 10px; }

            .gbs-thumb-placeholder { display: inline-flex; align-items: center; justify-content: center; }
            .gbs-thumb-placeholder .gbs-thumb-loader { color: white; font-size: 2em; animation: gbs-spin 1.2s linear infinite; }
            @keyframes gbs-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
            #gbs-loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.9); z-index: 100000; opacity: 1; transition: opacity 0.2s ease-out; pointer-events: none; }

            @media (max-width: 850px) {
                #gbs-viewer-top-controls { top: auto; bottom: 20px; left: 15px; flex-direction: row; gap: 2px; }
                #gbs-viewer-nav-container {top: auto; bottom: 20px; left: auto; right: 15px; flex-direction: row; gap: 2px; }
                #gbs-viewer-nav-counter { height: 45px; }
                .gbs-viewer-nav-item { height: 45px; }
                #gbs-viewer-nav-up i { transform: rotate(-90deg); }
                #gbs-viewer-nav-down i { transform: rotate(-90deg); }
                body.gbs-viewer-mode-active .thumbnail-container { column-count: 1 !important; }
                #gbs-viewer-info-sidebar { width: 215px; }
            }
            `);

            const topControlsContainer = document.createElement('div');
            topControlsContainer.id = 'gbs-viewer-top-controls';
            const viewerButton = document.createElement('button');
            viewerButton.id = 'gbs-viewer-btn';
            viewerButton.title = 'Open/Close Media Viewer';
            viewerButton.innerHTML = '<i class="fas fa-images"></i>';
            const showInfoBtn = document.createElement('button');
            showInfoBtn.id = 'gbs-viewer-show-info-btn';
            showInfoBtn.title = 'Show Post Info';
            showInfoBtn.innerHTML = '<i class="fas fa-info"></i>';
            const openPostBtn = document.createElement('button');
            openPostBtn.id = 'gbs-viewer-open-post-btn';
            openPostBtn.title = 'Open Post in New Tab';
            openPostBtn.innerHTML = '<i class="fas fa-external-link-alt"></i>';
            topControlsContainer.append(viewerButton, showInfoBtn, openPostBtn);
            document.body.appendChild(topControlsContainer);

            const navContainer = document.createElement('div');
            navContainer.id = 'gbs-viewer-nav-container';
            navContainer.innerHTML = `
            <button class="gbs-viewer-nav-item gbs-viewer-nav-btn" id="gbs-viewer-nav-up" title="Previous Image"><i class="fas fa-angle-up"></i></button>
                <div class="gbs-viewer-nav-item" id="gbs-viewer-nav-counter">0</div>
                <button class="gbs-viewer-nav-item gbs-viewer-nav-btn" id="gbs-viewer-nav-down" title="Next Image"><i class="fas fa-angle-down"></i></button>
                `;
            document.body.appendChild(navContainer);

            const infoSidebar = document.createElement('div');
            infoSidebar.id = 'gbs-viewer-info-sidebar';
            document.body.appendChild(infoSidebar);

            this.elements = {
                viewerButton,
                topControlsContainer,
                navContainer,
                navUpButton: navContainer.querySelector('#gbs-viewer-nav-up'),
                navCounter: navContainer.querySelector('#gbs-viewer-nav-counter'),
                navDownButton: navContainer.querySelector('#gbs-viewer-nav-down'),
                showInfoButton: showInfoBtn,
                openPostButton: openPostBtn,
                infoSidebar,
            };
        },
        setupEventListeners() {
            this.elements.viewerButton.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                this.toggleLargeThumbnails();
            });
            this.elements.navUpButton.addEventListener('click', () => this.navigateToImage(-1));
            this.elements.navDownButton.addEventListener('click', () => this.navigateToImage(1));
            this.elements.showInfoButton.addEventListener('click', () => this.toggleInfoSidebar());
            this.elements.openPostButton.addEventListener('click', () => {
                if (this.State.currentImageIndex >= 0 && this.State.largeMediaElements.length > 0) {
                    const currentMedia = this.State.largeMediaElements[this.State.currentImageIndex];
                    const anchor = currentMedia.closest('a');
                    if (anchor && anchor.href) {
                        GM_openInTab(anchor.href, { active: false });
                    }
                }
            });
        },
        async toggleLargeThumbnails() {
            this.State.isLargeViewActive = !this.State.isLargeViewActive;
            this.elements.viewerButton.classList.toggle('active', this.State.isLargeViewActive);
            const container = document.querySelector('.thumbnail-container');
            if (container) container.classList.toggle('gbs-large-view-active', this.State.isLargeViewActive);
            if (this.State.isLargeViewActive) {
                await this.activateLargeView();
            } else {
                this.deactivateLargeView();
            }
        },
        async activateLargeView() {
            const loadingOverlay = document.createElement('div');
            loadingOverlay.id = 'gbs-loading-overlay';
            document.body.appendChild(loadingOverlay);

            try {
                GlobalState.previewsTemporarilyDisabled = true;
                if (typeof Peek !== 'undefined' && Settings.State.ENABLE_PEEK_PREVIEWS) {
                    Logger.log("[MediaViewer] Activated. Disabling Peek Previews to prevent conflicts.");
                    document.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(grid => {
                        if (grid.dataset.enhancerInitialized) Peek.cleanupThumbnailFeatures(grid);
                    });
                }
                document.body.classList.add('gbs-viewer-mode-active');
                if (typeof Downloader !== 'undefined') Downloader.toggleVisibility(false);
                if (typeof AddToPool !== 'undefined') AddToPool.toggleVisibility(false);

                this.elements.viewerButton.innerHTML = '<i class="fas fa-times"></i>';
                document.body.style.overflow = 'hidden';

                const firstThumbImg = this.State.thumbnailAnchors.length > 0 ? this.State.thumbnailAnchors[0].querySelector('img') : null;
                const placeholderDims = firstThumbImg ? { w: firstThumbImg.getBoundingClientRect().width, h: firstThumbImg.getBoundingClientRect().height } : { w: 150, h: 150 };

                this.State.thumbnailAnchors.forEach(anchor => {
                    const originalThumb = anchor.querySelector('img');
                    if (originalThumb) originalThumb.style.display = 'none';

                    const placeholder = document.createElement('div');
                    placeholder.className = 'gbs-thumb-placeholder';
                    placeholder.style.width = `${placeholderDims.w}px`;
                    placeholder.style.height = `${placeholderDims.h}px`;
                    placeholder.innerHTML = `<i class="fas fa-spinner gbs-thumb-loader"></i>`;
                    placeholder.dataset.loaded = 'false';
                    anchor.appendChild(placeholder);
                });

                if (this.State.thumbnailAnchors.length > 0) {
                    const firstAnchor = this.State.thumbnailAnchors[0];
                    const placeholder = firstAnchor.querySelector('.gbs-thumb-placeholder');
                    if (placeholder) {
                        placeholder.dataset.loaded = 'loading';
                        await this.loadAndReplaceMedia(firstAnchor, placeholderDims);
                    }
                }

                if (!this._boundKeyDownHandler) {
                    this._boundKeyDownHandler = this.handleNavKeyDown.bind(this);
                }
                document.addEventListener('keydown', this._boundKeyDownHandler);
                this.State.currentImageIndex = -1;
                this.elements.navContainer.classList.add('visible');
                this.setupInactivityListeners();

                this.navigateToImage(1);

                this.State._scrollHandler = this._lazyLoadCheck.bind(this);
                window.addEventListener('scroll', this.State._scrollHandler);
                window.addEventListener('resize', this.State._scrollHandler);

                this._lazyLoadCheck();
            } finally {
                loadingOverlay.style.opacity = '0';
                loadingOverlay.addEventListener('transitionend', () => {
                    loadingOverlay.remove();
                }, { once: true });
            }
        },
        deactivateLargeView() {
            document.getElementById('gbs-loading-overlay')?.remove();

            GlobalState.previewsTemporarilyDisabled = false;
            if (typeof Peek !== 'undefined' && Settings.State.ENABLE_PEEK_PREVIEWS) {
                Logger.log("[MediaViewer] Deactivated. Re-enabling Peek Previews.");
                document.querySelectorAll(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR).forEach(grid => {
                    Peek.initializeThumbnailFeatures(grid);
                });
            }
            document.body.classList.remove('gbs-viewer-mode-active');
            this.cleanupInactivityListeners();
            if (typeof Downloader !== 'undefined') Downloader.toggleVisibility(true);
            if (typeof AddToPool !== 'undefined') AddToPool.toggleVisibility(true);

            if (this.State._scrollHandler) {
                window.removeEventListener('scroll', this.State._scrollHandler);
                window.removeEventListener('resize', this.State._scrollHandler);
                this.State._scrollHandler = null;
            }

            this.elements.viewerButton.innerHTML = '<i class="fas fa-images"></i>';
            this.elements.showInfoButton.style.display = 'none';
            this.elements.openPostButton.style.display = 'none';
            this.elements.infoSidebar.classList.remove('visible');
            this.elements.showInfoButton.classList.remove('active');
            document.body.style.overflow = '';
            const thumbnailContainer = document.querySelector('.thumbnail-container');
            if (thumbnailContainer) thumbnailContainer.classList.remove('gbs-large-view-active');
            if (this._boundKeyDownHandler) document.removeEventListener('keydown', this._boundKeyDownHandler);
            this.elements.navContainer.classList.remove('visible');

            this.State.thumbnailAnchors.forEach(anchor => {
                anchor.querySelector('.gbs-large-view-media, .gbs-thumb-placeholder')?.remove();
                const originalThumb = anchor.querySelector('img');
                if (originalThumb) originalThumb.style.display = '';
            });
            this.State.largeMediaElements = [];
        },
        resetInactivityTimer() {
            if (!this.State.isLargeViewActive) return;
            this.elements.topControlsContainer?.classList.remove('gbs-ui-hidden');
            this.elements.navContainer?.classList.remove('gbs-ui-hidden');
            document.body.classList.remove('gbs-hide-viewer-cursor');
            clearTimeout(this.State.inactivityTimer);
            this.State.inactivityTimer = setTimeout(() => {
                const isHoveringControls = this.elements.topControlsContainer?.matches(':hover') || this.elements.navContainer?.matches(':hover');
                if (!isHoveringControls) {
                    this.elements.topControlsContainer?.classList.add('gbs-ui-hidden');
                    this.elements.navContainer?.classList.add('gbs-ui-hidden');
                    document.body.classList.add('gbs-hide-viewer-cursor');
                }
            }, 3000);
        },
        setupInactivityListeners() {
            this.State._boundResetInactivityTimer = this.resetInactivityTimer.bind(this);
            document.addEventListener('mousemove', this.State._boundResetInactivityTimer);
            this.resetInactivityTimer();
        },
        cleanupInactivityListeners() {
            if (this.State._boundResetInactivityTimer) {
                document.removeEventListener('mousemove', this.State._boundResetInactivityTimer);
                this.State._boundResetInactivityTimer = null;
            }
            clearTimeout(this.State.inactivityTimer);
            this.State.inactivityTimer = null;
            this.elements.topControlsContainer?.classList.remove('gbs-ui-hidden');
            this.elements.navContainer?.classList.remove('gbs-ui-hidden');
            document.body.classList.remove('gbs-hide-viewer-cursor');
        },
        _lazyLoadCheck() {
            if (this.State._isThrottled) return;
            this.State._isThrottled = true;

            setTimeout(() => {
                const placeholders = document.querySelectorAll('.gbs-thumb-placeholder[data-loaded="false"]');
                const viewportHeight = window.innerHeight;

                placeholders.forEach(placeholder => {
                    const rect = placeholder.getBoundingClientRect();
                    if (rect.top < viewportHeight * 6.5) {
                        placeholder.dataset.loaded = 'loading';
                        const anchor = placeholder.closest('a');
                        if (anchor) {
                            const dims = { w: rect.width, h: rect.height };
                            this.loadAndReplaceMedia(anchor, dims);
                        }
                    }
                });
                this.State._isThrottled = false;
            }, 200);
        },
        loadAndReplaceMedia(anchor, dims) {
            return new Promise(async (resolve) => {
                const placeholder = anchor.querySelector('.gbs-thumb-placeholder');
                if (!placeholder) return resolve();

                const postId = Utils.getPostId(anchor.href);
                if (!postId) {
                    placeholder.remove();
                    return resolve();
                }

                try {
                    const media = await API.fetchMediaDetailsFromHTML(anchor.href);
                    const mediaElement = media.type === 'video' ? document.createElement('video') : document.createElement('img');
                    mediaElement.addEventListener('click', (e) => e.preventDefault());
                    mediaElement.src = media.url;
                    mediaElement.className = 'gbs-large-view-media';
                    if (media.type === 'video') {
                        mediaElement.controls = true;
                        mediaElement.loop = true;
                        mediaElement.muted = false;
                    }
                    const loadEvent = media.type === 'video' ? 'loadeddata' : 'load';
                    mediaElement.addEventListener(loadEvent, async () => {
                        if (media.type === 'image') {
                            try {
                                await mediaElement.decode();
                            } catch (e) {
                                Logger.warn('Image decode failed, but proceeding:', e);
                            }
                        }
                        placeholder.replaceWith(mediaElement);
                        this.State.largeMediaElements = Array.from(document.querySelectorAll('.gbs-large-view-media'));
                        resolve();
                    }, { once: true });
                    mediaElement.addEventListener('error', () => {
                        placeholder.remove();
                        resolve();
                    }, { once: true });
                } catch (error) {
                    Logger.error(`Failed to load media for post ${postId}:`, error);
                    placeholder.remove();
                    resolve();
                }
            });
        },
        navigateToImage(direction) {
            if (this._isNavigating) return;
            this._isNavigating = true;

            if (this.State.currentImageIndex === this.State.thumbnailAnchors.length - 1 && direction > 0) {
                window.scrollBy({ top: 400, behavior: 'smooth' });
                this._isNavigating = false;
                return;
            }

            if (this.State.currentImageIndex >= 0) {
                const prevAnchor = this.State.thumbnailAnchors[this.State.currentImageIndex];
                const prevMedia = prevAnchor?.querySelector('video.gbs-large-view-media');
                if (prevMedia) prevMedia.pause();
            }

            if (this.State.thumbnailAnchors.length === 0) {
                this._isNavigating = false;
                return;
            }

            const newIndex = Math.max(0, Math.min(this.State.thumbnailAnchors.length - 1, this.State.currentImageIndex + direction));

            if (newIndex !== this.State.currentImageIndex) {
                this.State.currentImageIndex = newIndex;
                const targetAnchor = this.State.thumbnailAnchors[this.State.currentImageIndex];
                if (targetAnchor) {
                    targetAnchor.scrollIntoView({ behavior: 'auto', block: 'center' });
                }
                if (this.elements.infoSidebar.classList.contains('visible')) {
                    this.fetchAndDisplayInfo();
                }
            }
            this.updateNavCounter();
            this._lazyLoadCheck();
            setTimeout(() => { this._isNavigating = false; }, 50);
        },
        handleNavKeyDown(event) {
            if (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA') return;

            const viewerHotkeys = [
                Settings.State.KEY_VIEWER_PREV_IMAGE,
                Settings.State.KEY_VIEWER_NEXT_IMAGE,
                Settings.State.KEY_VIEWER_TOGGLE_INFO
            ];

            if (!viewerHotkeys.includes(event.key)) return;

            event.preventDefault();
            event.stopPropagation();

            if (event.key === Settings.State.KEY_VIEWER_PREV_IMAGE) {
                this.navigateToImage(-1);
            } else if (event.key === Settings.State.KEY_VIEWER_NEXT_IMAGE) {
                this.navigateToImage(1);
            } else if (event.key === Settings.State.KEY_VIEWER_TOGGLE_INFO) {
                this.toggleInfoSidebar();
            }
        },
        updateNavCounter() {
            if (!this.elements.navCounter) return;
            const currentImageNum = Math.max(0, this.State.currentImageIndex + 1);
            this.elements.navCounter.textContent = currentImageNum;
            const shouldShow = this.State.isLargeViewActive && currentImageNum > 0;
            this.elements.showInfoButton.style.display = shouldShow ? 'flex' : 'none';
            this.elements.openPostButton.style.display = shouldShow ? 'flex' : 'none';
        },
        toggleInfoSidebar() {
            const isVisible = this.elements.infoSidebar.classList.toggle('visible');
            this.elements.showInfoButton.classList.toggle('active', isVisible);
            if (isVisible) this.fetchAndDisplayInfo();
        },
        async fetchAndDisplayInfo() {
            const sidebar = this.elements.infoSidebar;
            if (this.State.currentImageIndex < 0 || this.State.currentImageIndex >= this.State.thumbnailAnchors.length) return;

            const anchor = this.State.thumbnailAnchors[this.State.currentImageIndex];

            if (!anchor || !anchor.href) return;
            const postId = Utils.getPostId(anchor.href);
            if (sidebar.dataset.currentPostId === postId) return;
            sidebar.innerHTML = '<p>&nbsp;&nbsp;&nbsp;Loading info...</p>';
            try {
                const { promise } = Utils.makeRequest({ method: "GET", url: anchor.href });
                const response = await promise;
                const doc = new DOMParser().parseFromString(response.responseText, "text/html");
                const tagListElement = doc.querySelector('#tag-list');
                if (tagListElement) {

                    const actionButtonsContainer = document.createElement('li');
                    actionButtonsContainer.className = 'gbs-action-buttons-container';

                    const favoritesLi = Array.from(tagListElement.querySelectorAll('li a')).find(a => a.textContent.includes('Add to favorites'))?.closest('li');
                    if (favoritesLi) {
                        const favoritesLink = favoritesLi.querySelector('a');
                        favoritesLink.innerHTML = '<i class="fas fa-star"></i>';
                        favoritesLink.className = 'gbs-custom-action-btn';
                        favoritesLink.addEventListener('click', function() {
                            this.style.backgroundColor = '#daa520';
                            this.style.pointerEvents = 'none';
                        }, { once: true });

                        actionButtonsContainer.appendChild(favoritesLink);
                        favoritesLi.remove();
                    }

                    const poolLi = Array.from(tagListElement.querySelectorAll('li a')).find(a => a.textContent.includes('Add to Pool'))?.closest('li');
                    if (poolLi) {
                        const poolLink = poolLi.querySelector('a');

                        poolLink.removeAttribute('onclick');

                        poolLink.innerHTML = 'Add to Pool';
                        poolLink.className = 'gbs-custom-action-btn';
                        poolLink.style.fontSize = '15px';
                        poolLink.addEventListener('click', function(e) {
                            e.preventDefault();
                            e.stopPropagation();

                            if (!AddToPool.State.targetPoolId) {
                                alert('Target Pool ID not set. Please configure it on the main page.');
                                return;
                            }

                            const script = document.createElement('script');
                            script.textContent = `(() => {
                                const originalPrompt = window.prompt;
                                window.prompt = () => '${AddToPool.State.targetPoolId}';
                                if (typeof addToPoolID === 'function') {
                                    addToPoolID(${postId});
                                }
                                window.prompt = originalPrompt;
                            })();`;
                            document.body.appendChild(script).remove();

                            this.textContent = 'Added to Pool';
                            this.style.backgroundColor = '#daa520';
                            this.style.pointerEvents = 'none';
                        }, { once: true });

                        actionButtonsContainer.appendChild(poolLink);
                        poolLi.remove();
                    }

                    if (actionButtonsContainer.hasChildNodes()) {
                        tagListElement.prepend(actionButtonsContainer);
                    }

                    tagListElement.querySelectorAll('a').forEach(a => {
                        if (!a.classList.contains('gbs-custom-action-btn')) {
                            a.href = new URL(a.getAttribute('href'), 'https://gelbooru.com/').href;
                            a.target = '_blank';
                            a.rel = 'noopener noreferrer';
                        }
                    });

                    sidebar.innerHTML = '';
                    sidebar.appendChild(tagListElement);
                    sidebar.dataset.currentPostId = postId;
                } else {
                    throw new Error("Could not find '#tag-list' in the post page.");
                }
            } catch (error) {
                Logger.error("Failed to fetch or parse post info:", error);
                sidebar.innerHTML = `<p style="color: #A43535;">&nbsp;&nbsp;&nbsp;Error loading info.</p>`;
            }
        },
    };

    // =================================================================================
    // MAIN APPLICATION ORCHESTRATOR
    // =================================================================================
    const App = {
        addGlobalStyles: function() {
            let customCss = '';
            if (window.location.href.includes('page=favorites')) {
                customCss += `html, body { background-color: #1F1F1F !important; }`;
            }
            if (Settings.State.HIDE_PAGE_SCROLLBARS) {
                customCss += `html, body { scrollbar-width: none !important;} html::-webkit-scrollbar, body::-webkit-scrollbar { display: none !important; }`;
            }

            if (GlobalState.pageType === 'post') {
                GM_addStyle(`
                    main #image, main video#gelcomVideoPlayer { width: 100vw !important; margin: auto !important; object-fit: contain !important; }
                    @media (max-width: 850px) {
                        #scrollebox { flex-direction: column-reverse; align-items: center; gap: 50px; }
                    }
                `);
            }

            if (!document.querySelector('link[href*="font-awesome"]')) {
                const faLink = document.createElement('link');
                faLink.rel = 'stylesheet';
                faLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css';
                document.head.appendChild(faLink);
            }
            if (customCss.trim() !== '') {
                GM_addStyle(customCss);
            }
            GM_addStyle(`
                .gbs-fab-action-btn {min-width: 168px; justify-content: center; background-color: #343a40; color: white; font-weight: bold; border: none; border-radius: 10px; padding: 10px 15px; cursor: pointer; display: flex; align-items: center; box-shadow: 0 2px 5px rgba(0,0,0,0.2); white-space: nowrap; transition: background-color 0.2s; }
                .gbs-fab-action-btn:hover { background-color: #007BFF; }
                .gbs-fab-action-btn:disabled { opacity: 0.6; cursor: not-allowed; background-color: #343a40; }
                .gbs-fab-action-btn.active { background-color: #A43535; color: white !important; }
                .gbs-fab-action-btn.active:hover { background-color: #e74c3c; }
            `);
        },
        collapseStatsByDefault: function() {
            const toggleButton = document.querySelector('.profileToggleStats a');

            if (toggleButton) {
                toggleButton.click();
            }
        },
        movePostActions: function() {
            const scrollbox = document.querySelector('#scrollebox');
            const mediaElement = document.querySelector('#image, #gelcomVideoPlayer');

            if (scrollbox && mediaElement) {
                mediaElement.after(scrollbox);
                scrollbox.style.marginTop = '10px';
                scrollbox.style.paddingLeft = '10px';
                scrollbox.style.paddingRight = '10px';
                scrollbox.style.fontSize = '12px';
                scrollbox.style.display = 'flex';
                scrollbox.style.justifyContent = 'space-between';
                scrollbox.style.alignItems = 'center';
            }
            const allH2s = document.querySelectorAll('h2');
            const commentsHeader = Array.from(allH2s).find(h2 => h2.textContent.trim() === 'User Comments:');

            const adLinkAnchor = document.querySelector('div > a[rel="nofollow"][target="_blank"]');
            const adContainer = adLinkAnchor ? adLinkAnchor.parentElement : null;

            if (adContainer && commentsHeader) {
                commentsHeader.parentElement.insertBefore(adContainer, commentsHeader);
            }

            const mobileAdInnerDiv = document.querySelector('div[data-cl-spot]');
            const mobileAdContainer = mobileAdInnerDiv ? mobileAdInnerDiv.closest('center') : null;

            if (mobileAdContainer && commentsHeader) {
                commentsHeader.parentElement.insertBefore(mobileAdContainer, commentsHeader);
            }

            const originalContentNodes = Array.from(scrollbox.childNodes);
            scrollbox.innerHTML = '';
            const leftContainer = document.createElement('span');
            const rightContainer = document.createElement('span');
            scrollbox.appendChild(leftContainer);
            scrollbox.appendChild(rightContainer);
            const prevLink = document.querySelector('a[onclick="navigatePrev();"]');
            const nextLink = document.querySelector('a[onclick="navigateNext();"]');
            if (prevLink && nextLink) {
                const navContainer = prevLink.closest('div.alert');
                nextLink.removeAttribute('style');
                leftContainer.append('(', prevLink, ' / ', nextLink, ')');
                if (navContainer) navContainer.remove();
            }
            originalContentNodes.forEach(node => {
                rightContainer.appendChild(node);
            });
            const allLinksInList = document.querySelectorAll('#tag-list li a');
            const addToPoolLink = Array.from(allLinksInList).find(link => link.textContent.trim() === 'Add to Pool');
            if (addToPoolLink) {
                const parentLi = addToPoolLink.closest('li');
                rightContainer.append(' | ', addToPoolLink);
                if (parentLi) parentLi.remove();
            }
            const resizeLinkContainer = document.querySelector('#resize-link');
            if (resizeLinkContainer) {
                resizeLinkContainer.remove();
            }
        },
        scrollToActionBar: function() {
            const scrollbox = document.querySelector('#scrollebox');

            if (scrollbox) {
                setTimeout(() => {
                    scrollbox.style.scrollMarginBottom = '10px';

                    scrollbox.scrollIntoView({
                        behavior: 'smooth',
                        block: 'end'
                    });
                }, 100);
            }
        },
        setupScrollTrigger: function() {
            if (document.visibilityState === 'visible') {
                this.scrollToActionBar();
            } else {
                document.addEventListener('visibilitychange', () => {
                    if (document.visibilityState === 'visible') {
                        this.scrollToActionBar();
                    }
                }, { once: true });
            }
        },
        adjustMediaHeight: function() {
            const mediaElement = document.querySelector('#image, #gelcomVideoPlayer');
            const scrollbox = document.querySelector('#scrollebox');
            const topBar = document.querySelector('.searchArea');

            if (mediaElement && scrollbox && topBar) {
                const topBarHeight = topBar.offsetHeight;
                const scrollboxHeight = scrollbox.offsetHeight;
                const reservedSpace = topBarHeight + scrollboxHeight;

                mediaElement.style.maxHeight = `calc(100vh - ${reservedSpace}px)`;
            }
        },
        setupGalleryHotkeys: function() {
            document.addEventListener('keydown', e => {
                const activeEl = document.activeElement;
                if (activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' || activeEl === document.getElementById(Config.SELECTORS.PREVIEW_CONTAINER_ID) || activeEl.closest(`#${Config.SELECTORS.ADVANCED_SEARCH_MODAL_ID}`)) return;

                const currentPageElement = document.querySelector(Config.SELECTORS.PAGINATION_CURRENT_SELECTOR);
                if (!currentPageElement) return;
                let targetLink = null;
                if (e.key === Settings.State.KEY_GALLERY_NEXT_PAGE) {
                    targetLink = currentPageElement.nextElementSibling;
                } else if (e.key === Settings.State.KEY_GALLERY_PREV_PAGE) {
                    targetLink = currentPageElement.previousElementSibling;
                }
                if (targetLink?.tagName === 'A') {
                    window.location.href = targetLink.href;
                }
            });
        },
        async init() {
            await Settings.load();

            const isGalleryPage = !!document.querySelector(Config.SELECTORS.THUMBNAIL_GRID_SELECTOR);
            const contentElement = document.querySelector(Config.SELECTORS.IMAGE_SELECTOR) || document.querySelector(Config.SELECTORS.VIDEO_PLAYER_SELECTOR);
            const isPostPage = contentElement && !contentElement.closest('.thumbnail-preview');
            const isAccountPage = window.location.search.includes('page=account');

            if (isPostPage) {
                GlobalState.pageType = 'post';
            } else if (isGalleryPage) {
                GlobalState.pageType = 'gallery';
            } else if (isAccountPage) {
                GlobalState.pageType = 'account';
            }

            this.addGlobalStyles();
            GM_registerMenuCommand('Suite Settings', () => Settings.UI.openModal());

            if (GlobalState.pageType === 'account') {
                this.collapseStatsByDefault();
            }

            if (GlobalState.pageType === 'gallery') {
                this.setupGalleryHotkeys();

                if (Settings.State.ENABLE_ADVANCED_SEARCH) {
                    AdvancedSearch.init();
                }
                if (Settings.State.ENABLE_PEEK_PREVIEWS) {
                    Peek.init();
                }
                if (Settings.State.ENABLE_ADD_TO_POOL) {
                    await AddToPool.init();
                }
                if (Settings.State.ENABLE_DOWNLOADER) {
                    Downloader.init();
                }
            }

            if (GlobalState.pageType === 'post') {
                this.movePostActions();
                this.adjustMediaHeight();
                this.setupScrollTrigger();
            }

            MediaViewer.init();
            Logger.log("Gelbooru Suite initialized.");
        }
    };

    // =================================================================================
    // SCRIPT ENTRY POINT
    // =================================================================================
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', App.init.bind(App));
    } else {
        App.init();
    }
})();