Facebook Login Wall Remover

[Desktop Site | Guest Mode Only] Removes login popups/banners, fixes page jumps, and opens media in a new tab to prevent deadlocks. Disables itself when logged in.

// ==UserScript==
// @name         Facebook Login Wall Remover
// @name:en      Facebook Login Wall Remover
// @name:zh-TW   Facebook 登入牆移除器
// @name:ja      Facebook ログインウォールリムーバー
// @namespace    https://greasyfork.org/en/users/1467948-stonedkhajiit
// @version      0.1.5
// @description  [Desktop Site | Guest Mode Only] Removes login popups/banners, fixes page jumps, and opens media in a new tab to prevent deadlocks. Disables itself when logged in.
// @description:en [Desktop Site | Guest Mode Only] Removes login popups and banners, prevents page jumps, and automatically opens media in a new tab to prevent page deadlocks. Automatically disables itself when a logged-in state is detected.
// @description:zh-TW 【桌面版網頁|未登入專用】移除登入提示與橫幅、解決頁面跳轉,並自動在新分頁開啟媒體以從根源上防止頁面死鎖。偵測到登入狀態時將自動停用。
// @description:ja 【デスクトップサイト|未ログイン専用】ログインポップアップとバナーを削除し、ページのジャンプを修正します。デッドロックを防ぐためにメディアを新しいタブで自動的に開き、ログイン状態では自動的に無効になります。
// @author       StonedKhajiit
// @match        *://*.facebook.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=facebook.com
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const app = {
        // Application configuration
        config: {
            LOG_PREFIX: `[FB Login Wall Remover]`,
            THROTTLE_DELAY: 250,
            PROCESSED_MARKER: 'gmProcessed',
            SCROLL_RESTORER: {
                CORRECTION_DURATION: 250,
                CORRECTION_FREQUENCY: 16,
                WATCHER_FREQUENCY: 150,
                MODAL_GRACE_PERIOD: 300,
            },
            SELECTORS: {
                POST_CONTAINER: 'div[role="article"]',
                MODAL_CONTAINER: 'div.__fb-light-mode',
                DIALOG: '[role="dialog"]',
                LOGIN_FORM: 'form#login_form, form[id="login_popup_cta_form"]',
                MEDIA_LINK: `a[href*="/photo"], a[href*="fbid="], a[href*="/videos/"], a[href*="/watch/"], a[href*="/reel/"]`,
                HIGHLIGHT_CLASS: 'gm-post-highlight',
                CLOSE_BUTTON: [
                    "Close", "關閉", "閉じる", "Cerrar", "Fermer", "Schließen", "Fechar", "Chiudi", "Sluiten", "Закрыть", "Kapat", "Zamknij",
                    "Tutup", "Đóng", "ปิด", "Zatvori", "Zavrieť", "Zavřít", "Bezárás", "Stäng", "Luk", "Lukk", "Sulje", "Κλείσιμο",
                    "Închide", "إغلاق", "סגור"
                ].map(label => `[aria-label="${label}"][role="button"]`).join(', ') + ', div[role="button"]:has(i[data-visualcompletion="css-img"])',
            },
            STRINGS: {
                en: {
                    notificationDeadlock: 'A login prompt was hidden, but the feed can no longer load new content.\n\n[Pro-Tip] To prevent the feed from locking, get used to opening links in a new tab (middle-click). Please reload to continue browsing.',
                    autoOpenMediaInNewTab: 'Auto-open media in new tab (prevents deadlock)',
                    showDeadlockNotification: 'Show deadlock notification',
                    hideUselessElements: 'Hide useless UI elements (for guest)',
                    autoUnmuteEnabled: 'Automatically unmute videos',
                    postNumberingEnabled: 'Display post order numbers on feed',
                    setVolumeLabel: 'Auto-unmute volume',
                    searchPlaceholder: 'Search this page with Google...',
                    searchButton: 'Search',
                    searchScopePosts: 'Posts',
                    searchScopePhotos: 'Photos',
                    searchScopeVideos: 'Videos',
                    searchScopeReels: 'Reels',
                    settingsTitle: 'Settings',
                    saveAndClose: 'Save & Close',
                    menuSettings: '⚙️ Settings',
                    keyboardNavEnabled: 'Enable keyboard navigation',
                    keyNavNextPrimary: 'Next Post (Primary)',
                    keyNavPrevPrimary: 'Previous Post (Primary)',
                    keyNavNextSecondary: 'Next Post (Secondary)',
                    keyNavPrevSecondary: 'Previous Post (Secondary)',
                    floatingNavEnabled: 'Enable floating navigation buttons',
                    floatingNavPrevTooltip: 'Previous Post',
                    floatingNavNextTooltip: 'Next Post',
                    navigationScrollAlignment: 'Scroll alignment',
                    scrollAlignmentCenter: 'Center',
                    scrollAlignmentTop: 'Top',
                    enableSmoothScrolling: 'Enable smooth scrolling',
                    continuousNavInterval: 'Continuous navigation interval',
                    wheelNavEnabled: 'Enable mouse wheel navigation',
                    wheelNavModifier: 'Wheel navigation modifier key',
                    modifierAlt: 'Alt',
                    modifierCtrl: 'Ctrl',
                    modifierShift: 'Shift',
                    modifierNone: 'None (replaces page scroll)',
                    settingsColumnGeneral: 'General',
                    settingsColumnNavigation: 'Navigation',
                },
                'zh-TW': {
                    notificationDeadlock: '登入提示已隱藏,動態消息將無法載入新內容。\n\n【提示】為避免動態消息卡住,請養成用滑鼠中鍵在新分頁開啟連結的習慣。請重新整理頁面以繼續瀏覽。',
                    autoOpenMediaInNewTab: '自動在新分頁開啟媒體 (防鎖定)',
                    showDeadlockNotification: '顯示頁面鎖定通知',
                    hideUselessElements: '隱藏訪客專用介面元素',
                    autoUnmuteEnabled: '自動取消影片靜音',
                    postNumberingEnabled: '在動態消息上顯示貼文順序',
                    setVolumeLabel: '自動音量大小',
                    searchPlaceholder: '使用 Google 搜尋此專頁貼文...',
                    searchButton: '搜尋',
                    searchScopePosts: '貼文',
                    searchScopePhotos: '相片',
                    searchScopeVideos: '影片',
                    searchScopeReels: '連續短片',
                    settingsTitle: '設定',
                    saveAndClose: '儲存並關閉',
                    menuSettings: '⚙️ 設定',
                    keyboardNavEnabled: '啟用鍵盤導覽',
                    keyNavNextPrimary: '下一篇 (主要按鍵)',
                    keyNavPrevPrimary: '上一篇 (主要按鍵)',
                    keyNavNextSecondary: '下一篇 (次要按鍵)',
                    keyNavPrevSecondary: '上一篇 (次要按鍵)',
                    floatingNavEnabled: '啟用浮動導覽按鈕',
                    floatingNavPrevTooltip: '上一篇貼文',
                    floatingNavNextTooltip: '下一篇貼文',
                    navigationScrollAlignment: '導覽滾動對齊',
                    scrollAlignmentCenter: '置中',
                    scrollAlignmentTop: '貼齊頂部',
                    enableSmoothScrolling: '啟用平滑捲動',
                    continuousNavInterval: '連續導覽間隔時間',
                    wheelNavEnabled: '啟用滑鼠滾輪導覽',
                    wheelNavModifier: '滾輪導覽修飾鍵',
                    modifierAlt: 'Alt',
                    modifierCtrl: 'Ctrl',
                    modifierShift: 'Shift',
                    modifierNone: '無 (取代頁面捲動)',
                    settingsColumnGeneral: '一般設定',
                    settingsColumnNavigation: '導覽設定',
                },
                ja: {
                    notificationDeadlock: 'ログインプロンプトが非表示になりましたが、フィードは新しいコンテンツを読み込めなくなりました。\n\n【ヒント】フィードがロックされないように、新しいタブでリンクを開く(中央クリック)習慣を付けてください。閲覧を続けるには、このページをリロードしてください。',
                    autoOpenMediaInNewTab: 'メディアを新しいタブで開く (デッドロック防止)',
                    showDeadlockNotification: 'デッドロック通知を表示',
                    hideUselessElements: '不要なUI要素を非表示にする(ゲスト用)',
                    autoUnmuteEnabled: '動画のミュートを自動解除',
                    postNumberingEnabled: 'フィードに投稿順序番号を表示する',
                    setVolumeLabel: '自動音量',
                    searchPlaceholder: 'Googleでこのページを検索...',
                    searchButton: '検索',
                    searchScopePosts: '投稿',
                    searchScopePhotos: '写真',
                    searchScopeVideos: '動画',
                    searchScopeReels: 'リール',
                    settingsTitle: '設定',
                    saveAndClose: '保存して閉じる',
                    menuSettings: '⚙️ 設定',
                    keyboardNavEnabled: 'キーボードナビゲーションを有効にする',
                    keyNavNextPrimary: '次の投稿 (プライマリ)',
                    keyNavPrevPrimary: '前の投稿 (プライマリ)',
                    keyNavNextSecondary: '次の投稿 (セカンダリ)',
                    keyNavPrevSecondary: '前の投稿 (セカンダリ)',
                    floatingNavEnabled: 'フローティングナビゲーションボタンを有効にする',
                    floatingNavPrevTooltip: '前の投稿',
                    floatingNavNextTooltip: '次の投稿',
                    navigationScrollAlignment: 'スクロール位置',
                    scrollAlignmentCenter: '中央',
                    scrollAlignmentTop: '上部',
                    enableSmoothScrolling: 'スムーズスクロールを有効にする',
                    continuousNavInterval: '連続ナビゲーションの間隔',
                    wheelNavEnabled: 'マウスホイールナビゲーションを有効にする',
                    wheelNavModifier: 'ホイールナビゲーションの修飾キー',
                    modifierAlt: 'Alt',
                    modifierCtrl: 'Ctrl',
                    modifierShift: 'Shift',
                    modifierNone: 'なし (ページのスクロールを置き換える)',
                    settingsColumnGeneral: '一般設定',
                    settingsColumnNavigation: 'ナビゲーション設定',
                },
            },
        },

        // Application state
        state: {
            settings: {},
            T: {}, // For localized strings
        },

        // Utility functions
        utils: {
            isLoggedIn() {
                const banner = document.querySelector('div[role="banner"]');
                if (!banner) return false;
                const loggedOutMarkers = ['form#login_form', 'input[name="pass"]', 'a[href*="/recover/initiate"]'];
                const loggedInMarkers = ['input[type="search"]', 'a[href="/friends/"]', 'svg image[xlink:href*="fbcdn.net"]'];
                if (loggedOutMarkers.some(selector => banner.querySelector(selector))) return false;
                if (loggedInMarkers.some(selector => banner.querySelector(selector))) return true;
                return false;
            },
            throttle(func, delay) {
                let timeoutId = null;
                return (...args) => {
                    if (timeoutId === null) {
                        timeoutId = setTimeout(() => {
                            func.apply(this, args);
                            timeoutId = null;
                        }, delay);
                    }
                };
            },
            createStyledElement(tag, styles = {}, properties = {}) {
                const element = document.createElement(tag);
                Object.assign(element.style, styles);
                Object.assign(element, properties);
                return element;
            },
            isFeedPage() {
                const pathname = window.location.pathname;
                const pathSegments = pathname.split('/').filter(Boolean);

                if (pathSegments.length === 0) {
                    return false; // Homepage is not a target feed
                }
                if (pathSegments[0] === 'groups' && pathSegments.length >= 2) {
                    return true;
                }

                // Exclude known non-feed paths to identify user/page feeds.
                const disallowedFirstSegments = [
                    'watch', 'marketplace', 'gaming', 'events', 'messages',
                    'notifications', 'friends', 'photo', 'videos', 'reel',
                    'posts', 'stories', 'groups'
                ];
                if (!disallowedFirstSegments.includes(pathSegments[0])) {
                    return true;
                }

                return false;
            },
        },

        // Core functional modules
        modules: {
            /**
             * Handles settings persistence and menu command registration.
             */
            settingsManager: {
                app: null,
                registeredCommand: null,
                definitions: (() => {
                    const generalSettings = [
                        { key: 'autoOpenMediaInNewTab', type: 'boolean', defaultValue: true, labelKey: 'autoOpenMediaInNewTab', group: 'general' },
                        { key: 'hideUselessElements', type: 'boolean', defaultValue: true, labelKey: 'hideUselessElements', group: 'general' },
                        { key: 'showDeadlockNotification', type: 'boolean', defaultValue: true, labelKey: 'showDeadlockNotification', group: 'general' },
                        { key: 'autoUnmuteEnabled', type: 'boolean', defaultValue: true, labelKey: 'autoUnmuteEnabled', group: 'general' },
                        { key: 'autoUnmuteVolume', type: 'range', defaultValue: 25, labelKey: 'setVolumeLabel', options: { min: 0, max: 100, step: 5, unit: '%' }, group: 'general' },
                        { key: 'postNumberingEnabled', type: 'boolean', defaultValue: true, labelKey: 'postNumberingEnabled', group: 'general' },
                    ];
                    const navigationSettings = [
                        { key: 'keyboardNavEnabled', type: 'boolean', defaultValue: true, labelKey: 'keyboardNavEnabled', group: 'navigation' },
                        { key: 'keyNavNextPrimary', type: 'text', defaultValue: 'j', labelKey: 'keyNavNextPrimary', group: 'navigation' },
                        { key: 'keyNavPrevPrimary', type: 'text', defaultValue: 'k', labelKey: 'keyNavPrevPrimary', group: 'navigation' },
                        { key: 'keyNavNextSecondary', type: 'text', defaultValue: 'ArrowRight', labelKey: 'keyNavNextSecondary', group: 'navigation' },
                        { key: 'keyNavPrevSecondary', type: 'text', defaultValue: 'ArrowLeft', labelKey: 'keyNavPrevSecondary', group: 'navigation' },
                        { key: 'floatingNavEnabled', type: 'boolean', defaultValue: true, labelKey: 'floatingNavEnabled', group: 'navigation' },
                        { key: 'wheelNavEnabled', type: 'boolean', defaultValue: true, labelKey: 'wheelNavEnabled', group: 'navigation' },
                        { key: 'wheelNavModifier', type: 'select', defaultValue: 'shiftKey', labelKey: 'wheelNavModifier', options: [ { value: 'altKey', labelKey: 'modifierAlt' }, { value: 'ctrlKey', labelKey: 'modifierCtrl' }, { value: 'shiftKey', labelKey: 'modifierShift' }, { value: 'none', labelKey: 'modifierNone' } ], group: 'navigation' },
                        { key: 'navigationScrollAlignment', type: 'select', defaultValue: 'top', labelKey: 'navigationScrollAlignment', options: [ { value: 'center', labelKey: 'scrollAlignmentCenter' }, { value: 'top', labelKey: 'scrollAlignmentTop' } ], group: 'navigation' },
                        { key: 'enableSmoothScrolling', type: 'boolean', defaultValue: true, labelKey: 'enableSmoothScrolling', group: 'navigation' },
                        { key: 'continuousNavInterval', type: 'range', defaultValue: 500, labelKey: 'continuousNavInterval', options: { min: 0, max: 1000, step: 50, unit: 'ms' }, group: 'navigation' },
                    ];
                    return [...generalSettings, ...navigationSettings];
                })(),
                init(app) {
                    this.app = app;
                    this.definitions.forEach(def => {
                        this.app.state.settings[def.key] = GM_getValue(def.key, def.defaultValue);
                    });
                    this.renderMenu();
                },
                renderMenu() {
                    if (this.registeredCommand) {
                        GM_unregisterMenuCommand(this.registeredCommand);
                    }
                    this.registeredCommand = GM_registerMenuCommand(this.app.state.T.menuSettings, () => this.app.modules.settingsModal.open());
                },
                updateSetting(key, value) {
                    GM_setValue(key, value);
                    this.app.state.settings[key] = value;
                },
            },

            /**
             * Renders and manages the settings modal dialog.
             */
            settingsModal: {
                app: null,
                modalContainer: null,
                init(app) {
                    this.app = app;
                },
                open() {
                    if (!this.modalContainer) this._createModal();

                    const T = this.app.state.T;
                    const U = this.app.utils;
                    const body = this.modalContainer.body;
                    body.innerHTML = '';

                    const layoutContainer = U.createStyledElement('div', { display: 'flex', gap: '24px', justifyContent: 'space-between' });
                    const generalColumn = U.createStyledElement('div', { flex: '1', minWidth: '250px' });
                    const navigationColumn = U.createStyledElement('div', { flex: '1', minWidth: '250px' });

                    generalColumn.append(U.createStyledElement('h4', { marginTop: '0', borderBottom: '1px solid #eee', paddingBottom: '8px' }, { textContent: T.settingsColumnGeneral }));
                    navigationColumn.append(U.createStyledElement('h4', { marginTop: '0', borderBottom: '1px solid #eee', paddingBottom: '8px' }, { textContent: T.settingsColumnNavigation }));

                    this.app.modules.settingsManager.definitions.forEach(def => {
                        const wrapper = this._createSettingRow(def);
                        if (def.group === 'navigation') {
                            navigationColumn.append(wrapper);
                        } else {
                            generalColumn.append(wrapper);
                        }
                    });

                    layoutContainer.append(generalColumn, navigationColumn);
                    body.append(layoutContainer);

                    this._setupDependentControls(body);
                    this.modalContainer.backdrop.style.display = 'block';
                    this.modalContainer.modal.style.display = 'block';
                },
                _createSettingRow(def) {
                    const U = this.app.utils;
                    const T = this.app.state.T;
                    const settings = this.app.state.settings;

                    const wrapper = U.createStyledElement('div', { display: 'flex', alignItems: 'center', marginBottom: '12px', transition: 'opacity 0.2s' });
                    const label = U.createStyledElement('label', { marginRight: '8px', flexShrink: '0' }, { htmlFor: `setting-${def.key}`, textContent: T[def.labelKey] });
                    wrapper.append(label);

                    let inputElement;
                    switch(def.type) {
                        case 'boolean':
                            inputElement = U.createStyledElement('input', { marginLeft: 'auto' }, { type: 'checkbox', id: `setting-${def.key}`, checked: settings[def.key] });
                            wrapper.append(inputElement);
                            break;
                        case 'range':
                            const rangeWrapper = U.createStyledElement('div', { display: 'flex', alignItems: 'center', flexGrow: '1' });
                            inputElement = U.createStyledElement('input', { flexGrow: '1' }, { type: 'range', id: `setting-${def.key}`, min: def.options.min, max: def.options.max, step: def.options.step, value: settings[def.key] });
                            const valueSpan = U.createStyledElement('span', { marginLeft: '12px', minWidth: '45px', textAlign: 'right', fontSize: '13px' }, { textContent: `${settings[def.key]}${def.options.unit || ''}` });
                            inputElement.addEventListener('input', () => valueSpan.textContent = `${inputElement.value}${def.options.unit || ''}`);
                            rangeWrapper.append(inputElement, valueSpan);
                            wrapper.append(rangeWrapper);
                            if (def.key === 'autoUnmuteVolume') wrapper.dataset.controls = 'autoUnmuteEnabled';
                            break;
                        case 'text':
                            inputElement = U.createStyledElement('input', { marginLeft: 'auto', border: '1px solid #ccc', borderRadius: '4px', padding: '4px 8px', width: '100px' }, { type: 'text', id: `setting-${def.key}`, value: settings[def.key] });
                            wrapper.append(inputElement);
                            if (def.key.startsWith('keyNav')) wrapper.dataset.controls = 'keyboardNavEnabled';
                            break;
                        case 'select':
                            inputElement = U.createStyledElement('select', { marginLeft: 'auto', border: '1px solid #ccc', borderRadius: '4px', padding: '4px 8px' }, { id: `setting-${def.key}` });
                            def.options.forEach(opt => {
                                const option = U.createStyledElement('option', {}, { value: opt.value, textContent: T[opt.labelKey] });
                                inputElement.appendChild(option);
                            });
                            inputElement.value = settings[def.key];
                            wrapper.append(inputElement);
                            break;
                    }
                    return wrapper;
                },
                _setupDependentControls(container) {
                    const autoUnmuteCheckbox = container.querySelector('#setting-autoUnmuteEnabled');
                    const volumeControl = container.querySelector('[data-controls="autoUnmuteEnabled"]');
                    const keyboardNavCheckbox = container.querySelector('#setting-keyboardNavEnabled');
                    const keyNavControls = container.querySelectorAll('[data-controls="keyboardNavEnabled"]');

                    const toggle = () => {
                        const isUnmuteEnabled = autoUnmuteCheckbox.checked;
                        volumeControl.style.opacity = isUnmuteEnabled ? '1' : '0.5';
                        volumeControl.querySelector('input').disabled = !isUnmuteEnabled;

                        const isNavEnabled = keyboardNavCheckbox.checked;
                        keyNavControls.forEach(control => {
                           control.style.opacity = isNavEnabled ? '1' : '0.5';
                           control.querySelector('input').disabled = !isNavEnabled;
                        });
                    };
                    toggle();
                    autoUnmuteCheckbox.addEventListener('input', toggle);
                    keyboardNavCheckbox.addEventListener('input', toggle);
                },
                _createModal() {
                    const T = this.app.state.T;
                    const U = this.app.utils;

                    const backdrop = U.createStyledElement('div', { position: 'fixed', inset: '0px', backgroundColor: 'rgba(0, 0, 0, 0.5)', zIndex: '99998' });
                    const modal = U.createStyledElement('div', { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: 'white', borderRadius: '8px', boxShadow: '0 4px 12px rgba(0,0,0,0.15)', zIndex: '99999', minWidth: '550px' });
                    const header = U.createStyledElement('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '16px', borderBottom: '1px solid #ddd' });
                    const title = U.createStyledElement('h2', { margin: '0', fontSize: '18px' }, { textContent: T.settingsTitle });
                    const closeButton = U.createStyledElement('button', { background: 'none', border: 'none', fontSize: '24px', cursor: 'pointer', padding: '0 8px' }, { innerHTML: '×' });
                    const body = U.createStyledElement('div', { padding: '16px', maxHeight: '70vh', overflowY: 'auto' });
                    const footer = U.createStyledElement('div', { display: 'flex', justifyContent: 'flex-end', padding: '16px', borderTop: '1px solid #ddd' });
                    const saveButton = U.createStyledElement('button', { padding: '8px 16px', border: '1px solid #1877F2', backgroundColor: '#1877F2', color: 'white', borderRadius: '6px', cursor: 'pointer' }, { textContent: T.saveAndClose });

                    header.append(title, closeButton);
                    footer.append(saveButton);
                    modal.append(header, body, footer);
                    document.body.append(backdrop, modal);

                    this.modalContainer = { backdrop, modal, body };
                    this.close(); // Start hidden

                    closeButton.addEventListener('click', () => this.close());
                    backdrop.addEventListener('click', () => this.close());
                    saveButton.addEventListener('click', () => this.save());
                },
                close() {
                    this.modalContainer.backdrop.style.display = 'none';
                    this.modalContainer.modal.style.display = 'none';
                },
                save() {
                    const SM = this.app.modules.settingsManager;
                    let needsReload = false;

                    SM.definitions.forEach(def => {
                        const input = document.getElementById(`setting-${def.key}`);
                        if (!input) return;

                        let currentValue = this.app.state.settings[def.key];
                        let newValue;

                        if (def.type === 'boolean') newValue = input.checked;
                        else if (def.type === 'range') newValue = parseInt(input.value);
                        else if (def.type === 'text') newValue = input.value.trim();
                        else if (def.type === 'select') newValue = input.value;

                        if (newValue !== currentValue) {
                            SM.updateSetting(def.key, newValue);
                            if (def.key === 'hideUselessElements') needsReload = true;
                        }
                    });

                    if (needsReload) this.app.modules.toastNotifier.show('部分設定已更新,請重新整理頁面以完全生效。');
                    SM.renderMenu();
                    this.close();
                }
            },

            /**
             * Intercepts and modifies native browser/DOM behavior at a low level.
             */
            interceptor: {
                init() {
                    const originalAddEventListener = EventTarget.prototype.addEventListener;
                    Object.defineProperty(EventTarget.prototype, 'addEventListener', {
                        configurable: true,
                        enumerable: true,
                        get: () => function(type, listener, options) {
                            if (type === 'scroll' && this !== window) return;
                            return originalAddEventListener.call(this, type, listener, options);
                        },
                    });
                },
            },
            historyInterceptor: {
                init() {
                    const originalPushState = history.pushState;
                    const originalReplaceState = history.replaceState;
                    const historyChangeEvent = new Event('historyChange');

                    history.pushState = function(...args) {
                        originalPushState.apply(this, args);
                        window.dispatchEvent(historyChangeEvent);
                    };
                    history.replaceState = function(...args) {
                        originalReplaceState.apply(this, args);
                        window.dispatchEvent(historyChangeEvent);
                    };
                }
            },

            /**
             * Displays non-blocking toast notifications.
             */
            toastNotifier: {
                app: null,
                init(app) { this.app = app; },
                show(message, duration = 4000) {
                    const toast = this.app.utils.createStyledElement('div', {
                        position: 'fixed', top: '20px', left: '50%',
                        transform: 'translate(-50%, -100px)', backgroundColor: 'rgba(0, 0, 0, 0.8)',
                        color: 'white', padding: '12px 20px', borderRadius: '8px',
                        zIndex: '99999', fontSize: '14px', textAlign: 'center', opacity: '0',
                        transition: 'transform 0.3s ease, opacity 0.3s ease', whiteSpace: 'pre-wrap',
                    }, { textContent: message });
                    document.body.appendChild(toast);
                    setTimeout(() => {
                        toast.style.opacity = '1';
                        toast.style.transform = 'translate(-50%, 0)';
                    }, 10);
                    setTimeout(() => {
                        toast.style.opacity = '0';
                        toast.style.transform = 'translate(-50%, -100px)';
                        setTimeout(() => toast.remove(), 300);
                    }, duration);
                }
            },

            /**
             * Injects CSS rules into the document head for immediate UI changes.
             */
            styleInjector: {
                app: null,
                init(app) {
                    this.app = app;
                    const hideSelectors = [
                        `div[data-nosnippet]`,
                        `div[role="banner"]:has(a[href*="/reg/"])`
                    ];
                    if (this.app.state.settings.hideUselessElements) {
                        hideSelectors.push(`div[role="banner"]:has(${this.app.config.SELECTORS.LOGIN_FORM})`);
                    }
                    const hideRule = `${hideSelectors.join(',\n')} { display: none !important; }`;
                    const highlightRule = `
                        .${this.app.config.SELECTORS.HIGHLIGHT_CLASS} {
                            outline: 3px solid #1877F2 !important;
                            box-shadow: 0 0 15px rgba(24, 119, 242, 0.5) !important;
                            border-radius: 8px;
                            z-index: 10 !important;
                        }`;
                    const floatingNavRules = `
                        .gm-floating-nav { position: fixed; bottom: 20px; right: 20px; z-index: 9990; display: flex; flex-direction: column; gap: 8px; }
                        .gm-floating-nav button { background-color: rgba(255, 255, 255, 0.9); border: 1px solid #ddd; border-radius: 50%; width: 40px; height: 40px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 5px rgba(0,0,0,0.15); transition: background-color 0.2s, transform 0.1s; }
                        .gm-floating-nav button:hover { background-color: #f0f2f5; }
                        .gm-floating-nav button:active { transform: scale(0.95); }
                        .gm-floating-nav svg { width: 20px; height: 20px; fill: #65676b; }
                    `;
                    const finalCSS = [hideRule, highlightRule, floatingNavRules].join('\n\n');
                    const styleElement = this.app.utils.createStyledElement('style', {}, { textContent: finalCSS });
                    document.head.appendChild(styleElement);
                }
            },

            /**
             * Periodically scans and removes login walls and other annoyances.
             */
            domCleaner: {
                app: null,
                init(app) {
                    this.app = app;
                    const C = this.app.config;
                    const T = this.app.state.T;

                    const FINGERPRINTS = [
                        { selector: `${C.SELECTORS.DIALOG}:has([href*="/policies/cookies/"]) [role="button"][tabindex="0"]`, action: 'click' },
                        { selector: `${C.SELECTORS.DIALOG}:has(${C.SELECTORS.LOGIN_FORM})`, action: 'handle_login_modal' },
                        { selector: `div[style*="top:"][style*="z-index"]:has(div[role="tablist"])`, action: 'make_static', setting: 'hideUselessElements' },
                    ];

                    const hideThreeDotMenuContainer = () => {
                        const selector = `[role="button"][aria-haspopup="menu"]:has(svg circle + circle + circle)`;
                        document.querySelectorAll(selector).forEach(button => {
                            const container = button.parentElement?.parentElement;
                            if (container && container.tagName === 'DIV' && !container.dataset[C.PROCESSED_MARKER]) {
                                container.style.display = 'none';
                                container.dataset[C.PROCESSED_MARKER] = 'true';
                            }
                        });
                    };

                    const hideInteractionToolbars = () => {
                        const PRIMARY = ["Like", "讚", "いいね!", "Me gusta", "J'aime", "Gefällt mir", "Curtir", "Mi piace", "Vind ik leuk", "Нравится", "Beğen", "Lubię to!", "Suka", "Thích", "ถูกใจ", "Sviđa mi se", "Páči sa mi to", "To se mi líbí", "Tetszik", "Îmi place", "أعجبني", "אהבתי", "Gilla", "Liker", "Synes godt om", "Tykkää", "Μου αρέσει!", "Подобається", "लाइक करें"];
                        const SECONDARY = ["Comment", "留言", "コメントする", "Comentar", "Commenter", "Kommentieren", "Commento", "Reageren", "Комментировать", "Yorum Yap", "Skomentuj", "Komentari", "Bình luận", "แสดงความคิดเห็น", "Komentiraj", "Komentovať", "Okmentovat", "Hozzászólás", "Comentează", "تعليق", "הגב", "Kommentera", "Kommenter", "Σχολιάστε", "Коментувати", "टिप्पणी करें"];

                        const containersToProcess = document.querySelectorAll(`${C.SELECTORS.POST_CONTAINER}:not([data-gm-toolbar-processed]), [role="dialog"]:not([data-gm-toolbar-processed])`);
                        containersToProcess.forEach(container => {
                            for (const span of container.querySelectorAll('span')) {
                                if (PRIMARY.includes(span.innerText.trim())) {
                                    let parent = span.parentElement;
                                    for (let i = 0; i < 8 && parent; i++, parent = parent.parentElement) {
                                        const hasSecondary = Array.from(parent.querySelectorAll('span')).some(s => SECONDARY.includes(s.innerText.trim()));
                                        if (hasSecondary && parent.querySelectorAll('div[role="button"]').length > 1) {
                                            parent.parentElement.style.display = 'none';
                                            container.dataset.gmToolbarProcessed = 'true';
                                            return;
                                        }
                                    }
                                }
                            }
                        });
                    };

                    const showDeadlockNotification = () => {
                        if (!this.app.state.settings.showDeadlockNotification) return;
                        this.app.modules.toastNotifier.show(T.notificationDeadlock, 8000);
                    };

                    const runEngine = () => {
                        for (const fp of FINGERPRINTS) {
                            if (fp.setting && !this.app.state.settings[fp.setting]) continue;
                            document.querySelectorAll(fp.selector).forEach(el => {
                                if (el.dataset[C.PROCESSED_MARKER]) return;
                                el.dataset[C.PROCESSED_MARKER] = 'true';
                                switch (fp.action) {
                                    case 'click': el.click(); break;
                                    case 'make_static': el.style.position = 'static'; break;
                                    case 'handle_login_modal':
                                        const closeButton = el.querySelector(C.SELECTORS.CLOSE_BUTTON);
                                        if (closeButton) {
                                            closeButton.click();
                                        } else {
                                            const container = Array.from(document.querySelectorAll(C.SELECTORS.MODAL_CONTAINER)).find(c => c.contains(el));
                                            if (container) {
                                                container.style.display = 'none';
                                                console.warn(`${C.LOG_PREFIX} Non-closable modal hidden. Page may be deadlocked.`);
                                                showDeadlockNotification();
                                            }
                                        }
                                        break;
                                }
                            });
                        }
                        if (this.app.state.settings.hideUselessElements) {
                            hideInteractionToolbars();
                            hideThreeDotMenuContainer();
                        }
                    };

                    const throttledEngine = this.app.utils.throttle(runEngine, C.THROTTLE_DELAY);
                    const observer = new MutationObserver(throttledEngine);
                    observer.observe(document.documentElement, { childList: true, subtree: true });
                    throttledEngine();
                }
            },

            /**
             * Provides the central logic for all post navigation features.
             */
            postNavigatorCore: {
                app: null,
                currentPostIndex: -1,
                isRetrying: false,
                RETRY_INTERVAL: 200,
                MAX_RETRY_DURATION: 3000,
                continuousNavInterval: null,
                init(app) { this.app = app; },
                startContinuousNavigation(direction) {
                    this.stopContinuousNavigation();
                    this.navigateToPost(direction);
                    const interval = this.app.state.settings.continuousNavInterval;
                    this.continuousNavInterval = setInterval(() => {
                        this.navigateToPost(direction);
                    }, interval);
                },
                stopContinuousNavigation() {
                    if (this.continuousNavInterval) {
                        clearInterval(this.continuousNavInterval);
                        this.continuousNavInterval = null;
                    }
                },
                getSortedPosts() {
                    const posts = Array.from(document.querySelectorAll('[role="article"][aria-posinset]'));
                    posts.sort((a, b) => parseInt(a.getAttribute('aria-posinset'), 10) - parseInt(b.getAttribute('aria-posinset'), 10));
                    return posts;
                },
                triggerLoadAndRetry() {
                    if (this.isRetrying) return;
                    this.isRetrying = true;
                    const initialPostCount = this.getSortedPosts().length;
                    window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
                    const startTime = Date.now();
                    const retryInterval = setInterval(() => {
                        const newPostCount = this.getSortedPosts().length;
                        if (newPostCount > initialPostCount) {
                            clearInterval(retryInterval);
                            this.isRetrying = false;
                            this.navigateToPost('next');
                            return;
                        }
                        if (Date.now() - startTime > this.MAX_RETRY_DURATION) {
                            clearInterval(retryInterval);
                            this.isRetrying = false;
                            console.log(`${this.app.config.LOG_PREFIX} Failed to load new posts.`);
                        }
                    }, this.RETRY_INTERVAL);
                },
                navigateToPost(direction) {
                    const posts = this.getSortedPosts();
                    if (posts.length === 0) return;

                    const currentHighlighted = document.querySelector(`.${this.app.config.SELECTORS.HIGHLIGHT_CLASS}`);
                    if (currentHighlighted) {
                        currentHighlighted.classList.remove(this.app.config.SELECTORS.HIGHLIGHT_CLASS);
                        const currentIndex = posts.findIndex(p => p === currentHighlighted);
                        if(currentIndex !== -1) this.currentPostIndex = currentIndex;
                    }

                    if (direction === 'next') this.currentPostIndex++;
                    else this.currentPostIndex--;

                    if (this.currentPostIndex >= posts.length) {
                        if (direction === 'next') {
                            this.currentPostIndex = posts.length - 1;
                            this.triggerLoadAndRetry();
                            return;
                        }
                        this.currentPostIndex = posts.length - 1;
                    }
                    if (this.currentPostIndex < 0) this.currentPostIndex = 0;

                    const targetPost = posts[this.currentPostIndex];
                    if (targetPost) {
                        targetPost.classList.add(this.app.config.SELECTORS.HIGHLIGHT_CLASS);
                        const alignment = this.app.state.settings.navigationScrollAlignment;
                        const useSmoothScroll = this.app.state.settings.enableSmoothScrolling;
                        const scrollBehavior = useSmoothScroll ? 'smooth' : 'auto';

                        if (alignment === 'top') {
                            const searchBarHeight = this.app.modules.searchBar.getOccupiedHeight();
                            const postTop = targetPost.getBoundingClientRect().top + window.scrollY;
                            const targetY = postTop - searchBarHeight - 10;
                            window.scrollTo({ top: targetY, behavior: scrollBehavior });
                        } else {
                            targetPost.scrollIntoView({ behavior: scrollBehavior, block: 'center' });
                        }
                    }
                }
            },

            /**
             * Automatically opens media links in a new tab to prevent deadlocks.
             */
            linkInterceptor: {
                app: null,
                init(app) {
                    this.app = app;
                    const UNSAFE_ANCESTORS = ['[role="tablist"]', '[data-pagelet="ProfileTabs"]'];
                    const unsafeSelector = UNSAFE_ANCESTORS.join(', ');

                    const handleClick = (event) => {
                        if (!this.app.state.settings.autoOpenMediaInNewTab || event.button !== 0) return;
                        const mediaLinkAncestor = event.target.closest(this.app.config.SELECTORS.MEDIA_LINK);
                        if (!mediaLinkAncestor || mediaLinkAncestor.closest(unsafeSelector)) return;

                        let currentElement = event.target;
                        while (currentElement && currentElement !== mediaLinkAncestor) {
                            const tagName = currentElement.tagName.toLowerCase();
                            const role = currentElement.getAttribute('role');
                            if ((tagName === 'a' && currentElement !== mediaLinkAncestor) || role === 'button' || role === 'slider') return;
                            currentElement = currentElement.parentElement;
                        }
                        event.preventDefault();
                        event.stopPropagation();
                        window.open(mediaLinkAncestor.href, '_blank');
                    };
                    document.body.addEventListener('click', handleClick, true);
                }
            },

            /**
             * Fixes page jumps after closing media modals.
             */
            scrollRestorer: {
                app: null,
                init(app) {
                    this.app = app;
                    let restoreY = null;
                    let watcherInterval = null;
                    let correctionInterval = null;
                    const C = this.app.config;

                    const forceScrollCorrection = () => {
                        if (restoreY === null) return;
                        if (correctionInterval) clearInterval(correctionInterval);
                        const { CORRECTION_DURATION, CORRECTION_FREQUENCY } = C.SCROLL_RESTORER;
                        const startTime = Date.now();
                        const initialRestoreY = restoreY;
                        correctionInterval = setInterval(() => {
                            window.scrollTo({ top: initialRestoreY, behavior: 'instant' });
                            if (Date.now() - startTime > CORRECTION_DURATION) {
                                clearInterval(correctionInterval);
                                correctionInterval = null;
                                restoreY = null;
                            }
                        }, CORRECTION_FREQUENCY);
                    };

                    const startWatcher = () => {
                        if (watcherInterval) clearInterval(watcherInterval);
                        let isContentModalDetected = false;
                        let modalFirstSeenTime = null;
                        watcherInterval = setInterval(() => {
                            const modal = document.querySelector(C.SELECTORS.DIALOG);
                            const hasLoginForm = modal && modal.querySelector(C.SELECTORS.LOGIN_FORM);
                            if (!isContentModalDetected && modal && !hasLoginForm) {
                                isContentModalDetected = true;
                                modalFirstSeenTime = Date.now();
                            } else if (isContentModalDetected && !modal) {
                                if (Date.now() - modalFirstSeenTime > C.SCROLL_RESTORER.MODAL_GRACE_PERIOD) {
                                    clearInterval(watcherInterval);
                                    watcherInterval = null;
                                    forceScrollCorrection();
                                }
                            }
                        }, C.SCROLL_RESTORER.WATCHER_FREQUENCY);
                    };

                    const recordClick = e => {
                        if (watcherInterval || correctionInterval) return;
                        if (e.target.closest(C.SELECTORS.POST_CONTAINER) && !e.target.closest('a[target="_blank"]')) {
                            restoreY = window.scrollY;
                            setTimeout(startWatcher, 0);
                        }
                    };

                    const handleCloseClick = e => {
                        if (e.target.closest(C.SELECTORS.CLOSE_BUTTON) && restoreY !== null && watcherInterval) {
                            clearInterval(watcherInterval);
                            watcherInterval = null;
                            forceScrollCorrection();
                        }
                    };

                    document.body.addEventListener('click', recordClick, true);
                    document.body.addEventListener('click', handleCloseClick, true);
                }
            },

            /**
             * Automatically unmutes videos on the page.
             * Adapted from "Facebook Auto Unmute" by areen-c.
             */
            autoUnmuter: {
                app: null,
                init(app) {
                    this.app = app;
                    const nativeVolumeSetter = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'volume').set;
                    const nativeMutedSetter = Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'muted').set;
                    const attemptUnmute = (video) => {
                        if (!this.app.state.settings.autoUnmuteEnabled) return;
                        if (video instanceof HTMLVideoElement && (video.muted || video.volume === 0)) {
                            const targetVolume = this.app.state.settings.autoUnmuteVolume / 100;
                            nativeMutedSetter.call(video, false);
                            nativeVolumeSetter.call(video, targetVolume);
                            if (video.audioTracks?.length > 0) {
                                for (let track of video.audioTracks) track.enabled = true;
                            }
                            video.dispatchEvent(new Event('volumechange', { bubbles: true }));
                        }
                    };
                    document.addEventListener('play', (e) => attemptUnmute(e.target), true);
                    const observer = new MutationObserver(mutations => {
                        mutations.forEach(mutation =>
                            mutation.addedNodes.forEach(node => {
                                if (node.nodeName === 'VIDEO') attemptUnmute(node);
                                else if (node.querySelectorAll) node.querySelectorAll('video').forEach(attemptUnmute);
                            })
                        );
                    });
                    observer.observe(document.body, { childList: true, subtree: true });
                    document.addEventListener('click', (event) => {
                        if (event.target.closest('[aria-label*="mute" i], [aria-label*="sound" i], [role="button"][aria-pressed]')) {
                            setTimeout(() => document.querySelectorAll('video').forEach(attemptUnmute), 150);
                        }
                    }, true);
                    const checkInterval = setInterval(() => document.querySelectorAll('video').forEach(attemptUnmute), 2000);
                    window.addEventListener('beforeunload', () => {
                        clearInterval(checkInterval);
                        observer.disconnect();
                    });
                }
            },

            /**
             * Displays order numbers on feed posts.
             */
            postNumbering: {
                app: null,
                init(app) {
                    this.app = app;
                    if (!this.app.state.settings.postNumberingEnabled) return;

                    const processNewPosts = () => {
                        const posts = document.querySelectorAll('[role="article"][aria-posinset]:not([data-gm-numbered])');
                        posts.forEach(articleElement => {
                            const postNumber = articleElement.getAttribute('aria-posinset');
                            if (!postNumber) return;
                            articleElement.style.position = 'relative';
                            const numberTag = this.app.utils.createStyledElement('span', {
                                position: 'absolute', top: '-10px', left: '-10px', backgroundColor: '#e4e6eb',
                                color: '#65676b', padding: '2px 6px', borderRadius: '4px',
                                fontSize: '12px', fontWeight: 'bold', zIndex: '1',
                            }, { textContent: postNumber });
                            articleElement.appendChild(numberTag);
                            articleElement.dataset.gmNumbered = 'true';
                        });
                    };
                    const throttledProcess = this.app.utils.throttle(processNewPosts, 300);
                    const observer = new MutationObserver(throttledProcess);
                    observer.observe(document.body, { childList: true, subtree: true });
                    throttledProcess();
                }
            },

            /**
             * Adds a persistent, floating search bar for Google site search.
             */
            searchBar: {
                app: null,
                getOccupiedHeight: () => 0,
                init(app) {
                    this.app = app;
                    if (!this.app.state.settings.hideUselessElements) return;

                    const T = this.app.state.T;
                    const C = this.app.config;
                    const U = this.app.utils;

                    const searchInput = U.createStyledElement('input', { flexGrow: '1', border: '1px solid #CED0D4', borderRadius: '20px', backgroundColor: '#F0F2F5', color: '#050505', fontSize: '14px', padding: '8px 32px 8px 12px', width: '100%', boxSizing: 'border-box' }, { type: 'text', placeholder: T.searchPlaceholder });
                    const clearButton = U.createStyledElement('span', { position: 'absolute', right: '12px', top: '50%', transform: 'translateY(-50%)', cursor: 'pointer', color: '#65676B', display: 'none', fontSize: '14px' }, { textContent: '✖' });
                    const searchWrapper = U.createStyledElement('div', { position: 'relative', display: 'flex', alignItems: 'center', flexGrow: '1', maxWidth: '500px' });
                    searchWrapper.append(searchInput, clearButton);

                    const scopeSelector = U.createStyledElement('select', { marginRight: '8px', border: '1px solid #CED0D4', borderRadius: '6px', backgroundColor: '#F0F2F5', color: '#050505', fontSize: '14px', padding: '8px 12px', cursor: 'pointer' });
                    const scopes = { [T.searchScopePosts]: '', [T.searchScopePhotos]: '/photos', [T.searchScopeVideos]: '/videos', [T.searchScopeReels]: '/reels' };
                    for (const [text, value] of Object.entries(scopes)) {
                        scopeSelector.appendChild(U.createStyledElement('option', {}, { value, textContent: text }));
                    }

                    const searchButton = U.createStyledElement('button', { marginLeft: '8px', padding: '8px 16px', border: 'none', borderRadius: '6px', backgroundColor: '#1877F2', color: 'white', fontWeight: 'bold', cursor: 'pointer' }, { textContent: T.searchButton });
                    const pinButton = U.createStyledElement('button', { marginLeft: '8px', padding: '8px 10px', border: '1px solid #CED0D4', borderRadius: '6px', backgroundColor: '#F0F2F5', cursor: 'pointer', fontSize: '14px', transition: 'opacity 0.2s' }, { textContent: '📌' });
                    const settingsButton = U.createStyledElement('button', { marginLeft: '8px', padding: '8px 10px', border: '1px solid #CED0D4', borderRadius: '6px', backgroundColor: '#F0F2F5', cursor: 'pointer', fontSize: '14px' }, { textContent: '⚙️' });
                    const hoverTrigger = U.createStyledElement('div', { position: 'fixed', top: '0', left: '0', width: '100%', height: '10px', zIndex: '9999' });
                    const searchBar = U.createStyledElement('div', { position: 'fixed', top: '0', left: '0', width: '100%', padding: '8px 16px', backgroundColor: '#FFFFFF', zIndex: '9998', display: 'flex', justifyContent: 'center', alignItems: 'center', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', boxSizing: 'border-box', transform: 'translateY(-100%)', transition: 'transform 0.3s ease-in-out' });

                    searchBar.append(scopeSelector, searchWrapper, searchButton, pinButton, settingsButton);
                    document.body.append(searchBar, hoverTrigger);

                    let isPinned = GM_getValue('isSearchBarPinned', true);
                    let isAtTop = null;
                    let showTimeout = null, hideTimeout = null;

                    this.getOccupiedHeight = () => (isPinned && searchBar.style.visibility !== 'hidden') ? searchBar.offsetHeight : 0;

                    const performSearch = () => {
                        const keyword = searchInput.value.trim();
                        if (!keyword) return;
                        let basePath = '';
                        const pathSegments = window.location.pathname.split('/').filter(Boolean);
                        if (pathSegments.length > 0) {
                            if (pathSegments[0] === 'groups' && pathSegments[1]) basePath = `/${pathSegments[0]}/${pathSegments[1]}`;
                            else if (pathSegments[0] === 'people' && pathSegments[2] && !isNaN(pathSegments[2])) basePath = `/${pathSegments[2]}`;
                            else basePath = `/${pathSegments[0]}`;
                        }
                        const siteTarget = `site:facebook.com${basePath}${scopeSelector.value}`;
                        let searchUrl = `https://www.google.com/search?q=${encodeURIComponent(keyword)}+${encodeURIComponent(siteTarget)}`;
                        if (scopeSelector.value === '/photos') searchUrl += '&udm=2';
                        window.open(searchUrl, 'fb_script_google_search');
                    };

                    const showBar = () => searchBar.style.transform = 'translateY(0)';
                    const hideBar = () => searchBar.style.transform = 'translateY(-100%)';
                    const onHoverTriggerEnter = () => { clearTimeout(hideTimeout); showTimeout = setTimeout(showBar, 200); };
                    const onSearchBarEnter = () => clearTimeout(hideTimeout);
                    const onMouseLeave = () => {
                        clearTimeout(showTimeout);
                        hideTimeout = setTimeout(() => { if (!isPinned && !searchBar.contains(document.activeElement)) hideBar(); }, 300);
                    };
                    const enableHoverTrigger = () => {
                        hoverTrigger.addEventListener('mouseenter', onHoverTriggerEnter);
                        searchBar.addEventListener('mouseenter', onSearchBarEnter);
                        hoverTrigger.addEventListener('mouseleave', onMouseLeave);
                        searchBar.addEventListener('mouseleave', onMouseLeave);
                    };
                    const disableHoverTrigger = () => {
                        hoverTrigger.removeEventListener('mouseenter', onHoverTriggerEnter);
                        searchBar.removeEventListener('mouseenter', onSearchBarEnter);
                        hoverTrigger.removeEventListener('mouseleave', onMouseLeave);
                        searchBar.removeEventListener('mouseleave', onMouseLeave);
                    };
                    const handleScroll = (force = false) => {
                        if (isPinned) return showBar();
                        const currentIsAtTop = window.scrollY === 0;
                        if (!force && currentIsAtTop === isAtTop) return;
                        isAtTop = currentIsAtTop;
                        if (isAtTop) {
                            disableHoverTrigger();
                            showBar();
                            if (searchBar.contains(document.activeElement)) document.activeElement.blur();
                        } else {
                            if (!searchBar.contains(document.activeElement)) hideBar();
                            enableHoverTrigger();
                        }
                    };
                    const updateUIVisibility = () => {
                        const currentPath = window.location.pathname;
                        const isDisallowed = ['/photo/', '/videos/', '/reel/', '/posts/', '/watch/'].some(p => currentPath.startsWith(p)) || !!document.querySelector(C.SELECTORS.DIALOG);
                        for (const option of scopeSelector.options) option.hidden = currentPath.startsWith('/groups/') && option.value !== '';
                        if (currentPath.startsWith('/groups/')) scopeSelector.value = '';
                        [searchBar, hoverTrigger].forEach(el => el.style.visibility = isDisallowed ? 'hidden' : 'visible');
                        if (isDisallowed) {
                            window.removeEventListener('scroll', handleScroll, { passive: true });
                            disableHoverTrigger();
                            hideBar();
                        } else {
                            handleScroll();
                            window.addEventListener('scroll', handleScroll, { passive: true });
                        }
                    };
                    const updatePinState = () => {
                        pinButton.style.opacity = isPinned ? '1' : '0.5';
                        pinButton.style.backgroundColor = isPinned ? '#E7F3FF' : '#F0F2F5';
                        if (isPinned) { showBar(); disableHoverTrigger(); }
                        else { handleScroll(true); }
                    };

                    pinButton.addEventListener('click', () => { isPinned = !isPinned; GM_setValue('isSearchBarPinned', isPinned); updatePinState(); });
                    settingsButton.addEventListener('click', () => this.app.modules.settingsModal.open());
                    searchInput.addEventListener('input', () => clearButton.style.display = searchInput.value ? 'block' : 'none');
                    clearButton.addEventListener('click', () => { searchInput.value = ''; searchInput.focus(); clearButton.style.display = 'none'; });
                    clearButton.addEventListener('mousedown', (e) => e.preventDefault());
                    searchButton.addEventListener('click', performSearch);
                    searchInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') performSearch(); });
                    searchBar.addEventListener('focusout', (e) => { if (!isPinned && !searchBar.contains(e.relatedTarget)) setTimeout(hideBar, 100); });
                    window.addEventListener('historyChange', updateUIVisibility);
                    new MutationObserver(U.throttle(updateUIVisibility, 150)).observe(document.body, { childList: true, subtree: true });
                    setTimeout(() => { updateUIVisibility(); updatePinState(); }, 0);
                }
            },

            /**
             * Handles post navigation via keyboard hotkeys.
             */
            keyboardNavigator: {
                app: null,
                activeKey: null,
                init(app) {
                    this.app = app;
                    if (!this.app.state.settings.keyboardNavEnabled) return;
                    document.addEventListener('keydown', this.handleKeyDown.bind(this));
                    document.addEventListener('keyup', this.handleKeyUp.bind(this));
                },
                handleKeyDown(event) {
                    if (event.key === this.activeKey) return;
                    if (!this.app.utils.isFeedPage()) return;
                    const target = event.target;
                    if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return;

                    const settings = this.app.state.settings;
                    let direction = null;
                    switch (event.key) {
                        case settings.keyNavNextPrimary: case settings.keyNavNextSecondary: direction = 'next'; break;
                        case settings.keyNavPrevPrimary: case settings.keyNavPrevSecondary: direction = 'prev'; break;
                    }

                    if (direction) {
                        event.preventDefault();
                        this.activeKey = event.key;
                        this.app.modules.postNavigatorCore.startContinuousNavigation(direction);
                    }
                },
                handleKeyUp(event) {
                    if (event.key === this.activeKey) {
                        this.activeKey = null;
                        this.app.modules.postNavigatorCore.stopContinuousNavigation();
                    }
                }
            },

            /**
             * Renders and manages floating navigation buttons.
             */
            floatingNavigator: {
                app: null,
                container: null,
                init(app) {
                    this.app = app;
                    if (!this.app.state.settings.floatingNavEnabled) return;

                    const T = this.app.state.T;
                    const U = this.app.utils;
                    const Core = this.app.modules.postNavigatorCore;

                    this.container = U.createStyledElement('div', {}, { className: 'gm-floating-nav' });
                    const prevButton = U.createStyledElement('button', {}, { title: T.floatingNavPrevTooltip });
                    prevButton.innerHTML = `<svg viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"></path></svg>`;
                    prevButton.addEventListener('mousedown', () => Core.startContinuousNavigation('prev'));
                    prevButton.addEventListener('mouseleave', () => Core.stopContinuousNavigation());

                    const nextButton = U.createStyledElement('button', {}, { title: T.floatingNavNextTooltip });
                    nextButton.innerHTML = `<svg viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z"></path></svg>`;
                    nextButton.addEventListener('mousedown', () => Core.startContinuousNavigation('next'));
                    nextButton.addEventListener('mouseleave', () => Core.stopContinuousNavigation());

                    document.body.addEventListener('mouseup', () => Core.stopContinuousNavigation());
                    this.container.append(prevButton, nextButton);
                    document.body.appendChild(this.container);

                    this.updateVisibility();
                    window.addEventListener('historyChange', this.updateVisibility.bind(this));
                    new MutationObserver(U.throttle(this.updateVisibility.bind(this), 200)).observe(document.body, { childList: true, subtree: true });
                },
                updateVisibility() {
                    if (!this.container) return;
                    const isVisible = this.app.utils.isFeedPage() && !document.querySelector(this.app.config.SELECTORS.DIALOG);
                    this.container.style.display = isVisible ? 'flex' : 'none';
                }
            },

            /**
             * Handles post navigation via mouse wheel scrolling.
             */
            wheelNavigator: {
                app: null,
                isCoolingDown: false,
                init(app) {
                    this.app = app;
                    if (!this.app.state.settings.wheelNavEnabled) return;
                    document.addEventListener('wheel', this.handleWheel.bind(this), { passive: false });
                },
                handleWheel(event) {
                    if (this.isCoolingDown) return;
                    if (!this.app.utils.isFeedPage() || document.querySelector(this.app.config.SELECTORS.DIALOG)) return;

                    const modifierKey = this.app.state.settings.wheelNavModifier;
                    if (modifierKey !== 'none' && !event[modifierKey]) return;

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

                    const direction = event.deltaY > 0 ? 'next' : 'prev';
                    this.app.modules.postNavigatorCore.navigateToPost(direction);

                    this.isCoolingDown = true;
                    setTimeout(() => { this.isCoolingDown = false; }, this.app.state.settings.continuousNavInterval);
                }
            },

            /**
             * Allows focusing a post by clicking on it, syncing the navigator's state.
             */
            clickToFocusNavigator: {
                app: null,
                init(app) {
                    this.app = app;
                    const settings = this.app.state.settings;
                    if (!settings.keyboardNavEnabled && !settings.floatingNavEnabled && !settings.wheelNavEnabled) return;
                    document.body.addEventListener('click', this.handleClick.bind(this));
                },
                handleClick(event) {
                    const target = event.target;
                    if (target.closest('a, button, [role="button"], input, textarea') || window.getSelection().toString().length > 0) return;
                    const post = target.closest('[role="article"][aria-posinset]');
                    if (!post) return;

                    const Core = this.app.modules.postNavigatorCore;
                    const C = this.app.config;

                    const currentHighlighted = document.querySelector(`.${C.SELECTORS.HIGHLIGHT_CLASS}`);
                    if (currentHighlighted && currentHighlighted !== post) {
                        currentHighlighted.classList.remove(C.SELECTORS.HIGHLIGHT_CLASS);
                    }
                    post.classList.add(C.SELECTORS.HIGHLIGHT_CLASS);

                    const posts = Core.getSortedPosts();
                    const newIndex = posts.findIndex(p => p === post);
                    if (newIndex !== -1) Core.currentPostIndex = newIndex;
                }
            },
        },

        // Application entry point
        init() {
            const lang = navigator.language.toLowerCase();
            if (lang.startsWith('ja')) this.state.T = this.config.STRINGS.ja;
            else if (lang.startsWith('zh')) this.state.T = this.config.STRINGS['zh-TW'];
            else this.state.T = this.config.STRINGS.en;

            // Initialize modules that must run before the DOM is ready.
            this.modules.interceptor.init(this);
            this.modules.historyInterceptor.init(this);

            window.addEventListener('DOMContentLoaded', () => {
                // Initialize all other modules, passing a reference to the app object.
                for (const moduleName in this.modules) {
                    const module = this.modules[moduleName];
                    if (typeof module.init === 'function' && !['interceptor', 'historyInterceptor'].includes(moduleName)) {
                        module.init(this);
                    }
                }
                console.log(`${this.config.LOG_PREFIX} All modules initialized.`);
            });
        },
    };

    if (app.utils.isLoggedIn()) {
        console.log(`${app.config.LOG_PREFIX} Logged-in state detected. Script terminated.`);
        return;
    }
    console.log(`${app.config.LOG_PREFIX} Logged-out state detected. Script active.`);
    app.init();

})();