☰

Babepedia -> BBCode Bio Extractor

Extracts data from a model's Babepedia profile and generates a ready-to-post BBCode forum entry. Multi-language UI (PL/EN/DE/FR/ES/RU). Personal & Body always included; Rating, Biography and other sections user-selectable via a dropdown menu and saved in localStorage. Emojis stripped from biography.

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         Babepedia -> BBCode Bio Extractor
// @namespace    babepedia-bbcode-extractor
// @version      1.8
// @description  Extracts data from a model's Babepedia profile and generates a ready-to-post BBCode forum entry. Multi-language UI (PL/EN/DE/FR/ES/RU). Personal & Body always included; Rating, Biography and other sections user-selectable via a dropdown menu and saved in localStorage. Emojis stripped from biography.
// @author       Hehek aka PK2
// @match        https://www.babepedia.com/babe/*
// @grant        GM_setClipboard
// @grant        GM_download
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ---------- i18n ----------

    const I18N = {
        pl: {
            menu: 'Menu',
            menuTitle: 'Babepedia Tools',
            menuSub: 'Wybierz akcję',
            genBBCode: 'Generuj BBCode',
            genBBCodeDesc: 'Utwórz gotowy post z danymi modelki',
            download: 'Pobierz 6 zdjęć',
            downloadDesc: 'Zapisz zdjęcia profilowe na dysk',
            settings: 'Ustawienia sekcji',
            settingsDesc: 'Zarządzaj sekcjami i językiem',
            backToMenu: '‹ Wróć do menu',
            saveChoice: '💾 Zapisz wybór',
            savedChoice: 'Zapisano wybór sekcji',
            language: 'Język interfejsu',
            selectSectionsTitle: 'Wybierz sekcje do uwzględnienia',
            selectSectionsSub: 'Sekcje Personal i Body są zawsze dołączane. Pozostałe możesz włączyć lub wyłączyć — wybór zostanie zapamiętany w tej przeglądarce.',
            always: 'zawsze',
            cancel: 'Anuluj',
            saveContinue: 'Zapisz i kontynuuj',
            galleryTitle: 'Wklej BBCode galerii zdjęć',
            gallerySub: 'Wgraj 6 pobranych zdjęć ręcznie (np. na ImageTwist) i wklej poniżej BBCode, który otrzymasz. Zostaw puste, aby wygenerować post bez galerii.',
            galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
            skipNoGallery: 'Pomiń (bez galerii)',
            generateBBCode: 'Generuj BBCode',
            resultTitle: 'Generated BBCode post',
            resultSub: 'Skopiuj poniższy kod i wklej go na forum.',
            close: 'Zamknij',
            copy: '📋 Kopiuj',
            copied: 'Skopiowano do schowka',
            downloading: 'Pobieranie {n} zdjęć...',
            noPhotos: 'Nie znaleziono zdjęć do pobrania na tej stronie.',
            downloadFail: 'Nie udało się pobrać {name}.',
        },
        en: {
            menu: 'Menu',
            menuTitle: 'Babepedia Tools',
            menuSub: 'Choose an action',
            genBBCode: 'Generate BBCode',
            genBBCodeDesc: 'Create a ready-to-post entry from model data',
            download: 'Download 6 photos',
            downloadDesc: 'Save profile photos to disk',
            settings: 'Section settings',
            settingsDesc: 'Manage sections and language',
            backToMenu: '‹ Back to menu',
            saveChoice: '💾 Save selection',
            savedChoice: 'Section selection saved',
            language: 'Interface language',
            selectSectionsTitle: 'Choose sections to include',
            selectSectionsSub: 'Personal and Body are always included. You can toggle the rest — the choice is remembered in this browser.',
            always: 'always',
            cancel: 'Cancel',
            saveContinue: 'Save and continue',
            galleryTitle: 'Paste your photo gallery BBCode',
            gallerySub: 'Upload the 6 downloaded photos yourself (e.g. to ImageTwist) and paste the BBCode it gives you below. Leave empty to generate the post without a gallery.',
            galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
            skipNoGallery: 'Skip (no gallery)',
            generateBBCode: 'Generate BBCode',
            resultTitle: 'Generated BBCode post',
            resultSub: 'Copy the code below and paste it on the forum.',
            close: 'Close',
            copy: '📋 Copy',
            copied: 'Copied to clipboard',
            downloading: 'Downloading {n} photos...',
            noPhotos: 'No downloadable photos found on this page.',
            downloadFail: 'Could not download {name}.',
        },
        de: {
            menu: 'Menü',
            menuTitle: 'Babepedia Tools',
            menuSub: 'Aktion wählen',
            genBBCode: 'BBCode erzeugen',
            genBBCodeDesc: 'Fertigen Beitrag aus Modelldaten erstellen',
            download: '6 Fotos herunterladen',
            downloadDesc: 'Profilfotos auf die Festplatte speichern',
            settings: 'Bereichseinstellungen',
            settingsDesc: 'Bereiche und Sprache verwalten',
            backToMenu: '‹ Zurück zum Menü',
            saveChoice: '💾 Auswahl speichern',
            savedChoice: 'Bereichsauswahl gespeichert',
            language: 'Sprache der Oberfläche',
            selectSectionsTitle: 'Bereiche zum Einfügen wählen',
            selectSectionsSub: 'Personal und Body sind immer enthalten. Die übrigen kannst du ein- oder ausschalten — die Auswahl wird in diesem Browser gespeichert.',
            always: 'immer',
            cancel: 'Abbrechen',
            saveContinue: 'Speichern und fortfahren',
            galleryTitle: 'BBCode deiner Fotogalerie einfügen',
            gallerySub: 'Lade die 6 heruntergeladenen Fotos selbst hoch (z. B. zu ImageTwist) und füge den erhaltenen BBCode unten ein. Leer lassen, um den Beitrag ohne Galerie zu erzeugen.',
            galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
            skipNoGallery: 'Überspringen (ohne Galerie)',
            generateBBCode: 'BBCode erzeugen',
            resultTitle: 'Erzeugter BBCode-Beitrag',
            resultSub: 'Kopiere den Code unten und füge ihn im Forum ein.',
            close: 'Schließen',
            copy: '📋 Kopieren',
            copied: 'In die Zwischenablage kopiert',
            downloading: 'Lade {n} Fotos herunter...',
            noPhotos: 'Keine herunterladbaren Fotos auf dieser Seite gefunden.',
            downloadFail: '{name} konnte nicht heruntergeladen werden.',
        },
        fr: {
            menu: 'Menu',
            menuTitle: 'Babepedia Tools',
            menuSub: 'Choisissez une action',
            genBBCode: 'Générer le BBCode',
            genBBCodeDesc: 'Créer un post prêt à publier à partir des données du modèle',
            download: 'Télécharger 6 photos',
            downloadDesc: 'Enregistrer les photos de profil sur le disque',
            settings: 'Paramètres des sections',
            settingsDesc: 'Gérer les sections et la langue',
            backToMenu: '‹ Retour au menu',
            saveChoice: '💾 Enregistrer la sélection',
            savedChoice: 'Sélection des sections enregistrée',
            language: 'Langue de l\'interface',
            selectSectionsTitle: 'Choisir les sections à inclure',
            selectSectionsSub: 'Personal et Body sont toujours inclus. Vous pouvez activer ou désactiver les autres — le choix est mémorisé dans ce navigateur.',
            always: 'toujours',
            cancel: 'Annuler',
            saveContinue: 'Enregistrer et continuer',
            galleryTitle: 'Collez le BBCode de votre galerie photo',
            gallerySub: 'Téléchargez vous-même les 6 photos (par ex. sur ImageTwist) et collez ci-dessous le BBCode fourni. Laissez vide pour générer le post sans galerie.',
            galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
            skipNoGallery: 'Ignorer (sans galerie)',
            generateBBCode: 'Générer le BBCode',
            resultTitle: 'BBCode généré',
            resultSub: 'Copiez le code ci-dessous et collez-le sur le forum.',
            close: 'Fermer',
            copy: '📋 Copier',
            copied: 'Copié dans le presse-papiers',
            downloading: 'Téléchargement de {n} photos...',
            noPhotos: 'Aucune photo téléchargeable trouvée sur cette page.',
            downloadFail: 'Impossible de télécharger {name}.',
        },
        es: {
            menu: 'Menú',
            menuTitle: 'Babepedia Tools',
            menuSub: 'Elige una acción',
            genBBCode: 'Generar BBCode',
            genBBCodeDesc: 'Crear una publicación lista para el foro con los datos de la modelo',
            download: 'Descargar 6 fotos',
            downloadDesc: 'Guardar las fotos de perfil en el disco',
            settings: 'Ajustes de secciones',
            settingsDesc: 'Gestionar secciones e idioma',
            backToMenu: '‹ Volver al menú',
            saveChoice: '💾 Guardar selección',
            savedChoice: 'Selección de secciones guardada',
            language: 'Idioma de la interfaz',
            selectSectionsTitle: 'Elige las secciones a incluir',
            selectSectionsSub: 'Personal y Body siempre se incluyen. Puedes activar o desactivar las demás — la elección se recuerda en este navegador.',
            always: 'siempre',
            cancel: 'Cancelar',
            saveContinue: 'Guardar y continuar',
            galleryTitle: 'Pega el BBCode de tu galería de fotos',
            gallerySub: 'Sube tú mismo las 6 fotos descargadas (p. ej. a ImageTwist) y pega abajo el BBCode que te dé. Déjalo vacío para generar la publicación sin galería.',
            galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
            skipNoGallery: 'Omitir (sin galería)',
            generateBBCode: 'Generar BBCode',
            resultTitle: 'BBCode generado',
            resultSub: 'Copia el código de abajo y pégalo en el foro.',
            close: 'Cerrar',
            copy: '📋 Copiar',
            copied: 'Copiado al portapapeles',
            downloading: 'Descargando {n} fotos...',
            noPhotos: 'No se encontraron fotos descargables en esta página.',
            downloadFail: 'No se pudo descargar {name}.',
        },
        ru: {
            menu: 'Меню',
            menuTitle: 'Babepedia Tools',
            menuSub: 'Выберите действие',
            genBBCode: 'Сгенерировать BBCode',
            genBBCodeDesc: 'Создать готовый пост с данными модели',
            download: 'Скачать 6 фото',
            downloadDesc: 'Сохранить фотографии профиля на диск',
            settings: 'Настройки разделов',
            settingsDesc: 'Управление разделами и языком',
            backToMenu: '‹ Назад в меню',
            saveChoice: '💾 Сохранить выбор',
            savedChoice: 'Выбор разделов сохранён',
            language: 'Язык интерфейса',
            selectSectionsTitle: 'Выберите разделы для включения',
            selectSectionsSub: 'Разделы Personal и Body включаются всегда. Остальные можно включать и выключать — выбор запоминается в этом браузере.',
            always: 'всегда',
            cancel: 'Отмена',
            saveContinue: 'Сохранить и продолжить',
            galleryTitle: 'Вставьте BBCode галереи фото',
            gallerySub: 'Загрузите 6 скачанных фото самостоятельно (например, на ImageTwist) и вставьте ниже полученный BBCode. Оставьте пустым, чтобы создать пост без галереи.',
            galleryPlaceholder: '[URL=https://imagetwist.com/xxxx/name_1.jpg][IMG]https://img33.imagetwist.com/th/.../xxxx.jpg[/IMG][/URL]...',
            skipNoGallery: 'Пропустить (без галереи)',
            generateBBCode: 'Сгенерировать BBCode',
            resultTitle: 'Сгенерированный BBCode',
            resultSub: 'Скопируйте код ниже и вставьте его на форуме.',
            close: 'Закрыть',
            copy: '📋 Копировать',
            copied: 'Скопировано в буфер обмена',
            downloading: 'Скачивание {n} фото...',
            noPhotos: 'На этой странице не найдено фото для скачивания.',
            downloadFail: 'Не удалось скачать {name}.',
        },
    };

    const LANG_NAMES = {
        pl: 'Polski',
        en: 'English',
        de: 'Deutsch',
        fr: 'Français',
        es: 'Español',
        ru: 'Русский',
    };

    const STORAGE_KEY = 'babepedia_bbcode_sections_v1';
    const MANDATORY_SECTIONS = ['Personal', 'Body'];
    const BIOGRAPHY_KEY = 'Biography';
    const RATING_KEY = 'Rating';
    const DEFAULT_ENABLED = [RATING_KEY, BIOGRAPHY_KEY];

    function detectBrowserLang() {
        const nav = (navigator.language || 'en').slice(0, 2).toLowerCase();
        return I18N[nav] ? nav : 'en';
    }

    function t(key, vars) {
        const lang = currentLang();
        const dict = I18N[lang] || I18N.en;
        let s = dict[key] || I18N.en[key] || key;
        if (vars) {
            Object.keys(vars).forEach(k => {
                s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), vars[k]);
            });
        }
        return s;
    }

    function currentLang() {
        const prefs = loadPrefs();
        if (prefs && prefs.lang && I18N[prefs.lang]) return prefs.lang;
        return detectBrowserLang();
    }

    // ---------- Helpers ----------

    function text(el) {
        return el ? el.textContent.replace(/\s+/g, ' ').trim() : '';
    }

    function slugify(str) {
        return str.replace(/[^\w\-]+/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
    }

    function stripEmojis(str) {
        if (!str) return '';
        return str
            .replace(/[\u{1F000}-\u{1FAFF}]/gu, '')
            .replace(/[\u{2600}-\u{27BF}]/gu, '')
            .replace(/[\u{2B00}-\u{2BFF}]/gu, '')
            .replace(/[\u{FE00}-\u{FE0F}]/gu, '')
            .replace(/[\u{1F1E6}-\u{1F1FF}]/gu, '')
            .replace(/\u200D/g, '')
            .replace(/[\u{2190}-\u{21FF}]/gu, '')
            .replace(/[\u{2300}-\u{23FF}]/gu, '')
            .replace(/[\u{25A0}-\u{25FF}]/gu, '')
            .replace(/[\u{2700}-\u{27BF}]/gu, '')
            .replace(/\s+/g, ' ')
            .trim();
    }

    function getName() {
        return text(document.querySelector('#babename'));
    }

    function getAka() {
        const akaEl = document.querySelector('#name-block #aka');
        if (!akaEl) return '';
        const clone = akaEl.cloneNode(true);
        clone.querySelectorAll('img, #aliasinfo, small').forEach(n => n.remove());
        return stripEmojis(clone.textContent.replace(/\s+/g, ' ').trim());
    }

    function getBioText() {
        const visible = document.querySelector('#visible-text');
        const hidden = document.querySelector('#hidden-text');
        let raw;
        if (visible) {
            raw = (text(visible) + ' ' + text(hidden)).trim();
        } else {
            raw = text(document.querySelector('#biotext'));
        }
        return stripEmojis(raw);
    }

    function getInfoGrid() {
        const grid = document.querySelector('#personal-info-block .info-grid');
        const result = {};
        if (!grid) return result;

        let currentSection = 'Info';
        Array.from(grid.children).forEach(item => {
            if (item.classList.contains('info-title')) {
                const h3 = item.querySelector('h3');
                currentSection = h3 ? text(h3) : currentSection;
                result[currentSection] = result[currentSection] || {};
                return;
            }
            const label = item.querySelector('.label');
            const value = item.querySelector('.value');
            if (label && value) {
                const valClone = value.cloneNode(true);
                valClone.querySelectorAll('small, #cupconversions').forEach(n => n.remove());
                const key = text(label).replace(/:$/, '');
                const val = stripEmojis(valClone.textContent.replace(/\s+/g, ' ').trim());
                result[currentSection] = result[currentSection] || {};
                result[currentSection][key] = val;
            }
        });
        return result;
    }

    function getRating() {
        let score = text(document.querySelector('.rating-global strong'));
        score = stripEmojis(score);
        const votes = text(document.querySelector('.rating-global small'));
        return score ? `${score} (${votes})` : '';
    }

    // ---------- Section preferences ----------

    function loadPrefs() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (!raw) return null;
            const parsed = JSON.parse(raw);
            if (parsed && typeof parsed === 'object' && Array.isArray(parsed.enabled)) {
                return parsed;
            }
        } catch (e) {
            console.warn('[Babepedia] failed to load prefs', e);
        }
        return null;
    }

    function savePrefs(prefs) {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
        } catch (e) {
            console.warn('[Babepedia] failed to save prefs', e);
        }
    }

    function defaultPrefs() {
        return { enabled: [...DEFAULT_ENABLED], lang: detectBrowserLang() };
    }

    function isSectionEnabled(section, prefs) {
        if (MANDATORY_SECTIONS.includes(section)) return true;
        if (section === BIOGRAPHY_KEY || section === RATING_KEY) {
            if (!prefs) return true;
            return prefs.enabled.includes(section);
        }
        if (!prefs) return false;
        return prefs.enabled.includes(section);
    }

    // ---------- Photos ----------

    const IMAGE_EXTENSION_RE = /\.(jpe?g|png|gif|webp)(?:[?#].*)?$/i;

    function getTopImageUrls(limit) {
        limit = limit || 6;
        const urls = [];
        const seen = new Set();

        function addFrom(selector) {
            document.querySelectorAll(selector).forEach(a => {
                if (urls.length >= limit) return;
                const href = a.href;
                if (href && IMAGE_EXTENSION_RE.test(href) && !seen.has(href)) {
                    seen.add(href);
                    urls.push(href);
                }
            });
        }

        addFrom('#profbox2 a.img');
        if (urls.length < limit) addFrom('.useruploads2 a.img');

        return urls.slice(0, limit);
    }

    const DOWNLOAD_DELAY_MS = 400;

    async function downloadOneImageViaFetch(url, filename) {
        const response = await fetch(url, { credentials: 'include' });
        if (!response.ok) throw new Error(`HTTP ${response.status} fetching ${url}`);
        const blob = await response.blob();
        const blobUrl = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = blobUrl;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        a.remove();
        setTimeout(() => URL.revokeObjectURL(blobUrl), 10000);
    }

    function downloadOneImageViaGM(url, filename) {
        return new Promise((resolve, reject) => {
            if (typeof GM_download !== 'function') {
                reject(new Error('GM_download is not available'));
                return;
            }
            GM_download({
                url,
                name: filename,
                saveAs: false,
                onload: resolve,
                onerror: (err) => reject(new Error(`GM_download: ${err && err.error ? err.error : 'unknown error'}`)),
            });
        });
    }

    async function downloadOneImage(url, filename) {
        console.log('[Babepedia downloader] downloading', filename, 'from', url);
        try {
            await downloadOneImageViaFetch(url, filename);
        } catch (fetchErr) {
            console.warn('[Babepedia downloader] fetch failed', filename, fetchErr, '- trying GM_download');
            try {
                await downloadOneImageViaGM(url, filename);
            } catch (gmErr) {
                console.error('[Babepedia downloader] all methods failed', filename, fetchErr, gmErr);
                alert(t('downloadFail', { name: filename }) + `\nFetch: ${fetchErr.message}\nGM_download: ${gmErr.message}\nCheck console (F12).`);
            }
        }
    }

    function downloadImages(urls) {
        const name = slugify(getName() || 'babepedia');
        console.log(`[Babepedia downloader] starting download of ${urls.length} photo(s) for "${name}"`);
        urls.forEach((url, i) => {
            const ext = (url.split('.').pop().split(/[?#]/)[0] || 'jpg');
            const filename = `${name}_${i + 1}.${ext}`;
            setTimeout(() => downloadOneImage(url, filename), i * DOWNLOAD_DELAY_MS);
        });
    }

    // ---------- BBCode builder ----------

    function buildBBCode(galleryBBCode, prefs) {
        const name = getName();
        const aka = getAka();
        const bio = getBioText();
        const info = getInfoGrid();
        const rating = getRating();
        const sourceUrl = window.location.href.split('?')[0];

        let out = '';
        out += `[b][size=150]${name}[/size][/b]\n`;
        if (aka) out += `[i]${aka}[/i]\n`;
        out += `\n`;

        if (galleryBBCode && galleryBBCode.trim()) {
            out += `${galleryBBCode.trim()}\n\n`;
        }

        if (rating && isSectionEnabled(RATING_KEY, prefs)) {
            out += `[b]Rating:[/b] ${rating}\n\n`;
        }

        Object.keys(info).forEach(section => {
            if (!isSectionEnabled(section, prefs)) return;
            const fields = info[section];
            const keys = Object.keys(fields);
            if (!keys.length) return;
            out += `[b]${section}[/b]\n`;
            keys.forEach(k => {
                out += `${k}: ${fields[k]}\n`;
            });
            out += `\n`;
        });

        if (bio && isSectionEnabled(BIOGRAPHY_KEY, prefs)) {
            out += `[b]Biography[/b]\n${bio}\n\n`;
        }

        out += `[i]Source: [URL]${sourceUrl}[/URL][/i]`;

        return out;
    }

    // ---------- Styles ----------

    function injectStyles() {
        if (document.getElementById('bbe-styles')) return;
        const style = document.createElement('style');
        style.id = 'bbe-styles';
        style.textContent = `
            :root {
                --bbe-bg: #1e2130;
                --bbe-bg-darker: #14161f;
                --bbe-bg-footer: #1a1d29;
                --bbe-border: #2c3044;
                --bbe-border-strong: #383d56;
                --bbe-text: #e6e8ef;
                --bbe-text-dim: #9096ad;
                --bbe-accent: #d2452d;
                --bbe-accent-hover: #e0553c;
                --bbe-radius: 10px;
                --bbe-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            }

            /* ===== Floating menu root ===== */
            .bbe-root {
                position: fixed;
                right: 20px;
                bottom: 20px;
                z-index: 99999;
                font-family: var(--bbe-font);
            }

            .bbe-trigger {
                display: flex; align-items: center; gap: 8px;
                padding: 12px 18px;
                background: var(--bbe-accent);
                color: #fff;
                border: none;
                border-radius: 12px;
                font-size: 14px; font-weight: 600;
                font-family: inherit;
                cursor: pointer;
                box-shadow: 0 6px 24px rgba(210, 69, 45, 0.35), 0 2px 8px rgba(0,0,0,0.4);
                transition: transform 0.15s, box-shadow 0.15s, background 0.15s;
            }
            .bbe-trigger:hover {
                background: var(--bbe-accent-hover);
                transform: translateY(-1px);
                box-shadow: 0 8px 28px rgba(210, 69, 45, 0.45), 0 3px 10px rgba(0,0,0,0.45);
            }
            .bbe-trigger:active { transform: translateY(0); }
            .bbe-trigger .bbe-icon {
                display: inline-flex;
                transition: transform 0.2s ease;
                font-size: 16px;
            }
            .bbe-root.open .bbe-trigger .bbe-icon { transform: rotate(90deg); }

            /* ===== Dropdown panel ===== */
            .bbe-panel {
                position: absolute;
                bottom: 62px;
                right: 0;
                width: 300px;
                background: var(--bbe-bg);
                color: var(--bbe-text);
                border: 1px solid var(--bbe-border);
                border-radius: 14px;
                box-shadow: 0 20px 60px rgba(0,0,0,0.55), 0 6px 20px rgba(0,0,0,0.4);
                overflow: hidden;
                opacity: 0;
                transform: translateY(8px) scale(0.97);
                transform-origin: bottom right;
                pointer-events: none;
                transition: opacity 0.16s ease, transform 0.16s ease;
            }
            .bbe-root.open .bbe-panel {
                opacity: 1;
                transform: translateY(0) scale(1);
                pointer-events: auto;
            }

            .bbe-panel-header {
                padding: 14px 16px 10px;
                border-bottom: 1px solid var(--bbe-border);
                background: var(--bbe-bg-footer);
            }
            .bbe-panel-title {
                margin: 0;
                font-size: 13px; font-weight: 600;
                color: #fff;
                letter-spacing: 0.2px;
            }
            .bbe-panel-sub {
                margin: 2px 0 0;
                font-size: 11.5px;
                color: var(--bbe-text-dim);
            }

            .bbe-panel-body {
                padding: 6px;
                max-height: 400px;
                overflow-y: auto;
            }

            .bbe-panel-item {
                display: flex; align-items: center; gap: 12px;
                width: 100%;
                padding: 10px 12px;
                background: transparent;
                border: none;
                border-radius: 8px;
                color: var(--bbe-text);
                font-family: inherit;
                font-size: 13.5px;
                text-align: left;
                cursor: pointer;
                transition: background 0.12s;
            }
            .bbe-panel-item:hover { background: #262a3d; }
            .bbe-panel-item:active { background: #2d3150; }
            .bbe-panel-item .bbe-item-icon {
                flex-shrink: 0;
                width: 32px; height: 32px;
                display: flex; align-items: center; justify-content: center;
                border-radius: 8px;
                background: #262a3d;
                font-size: 15px;
            }
            .bbe-panel-item:hover .bbe-item-icon { background: #333850; }
            .bbe-panel-item .bbe-item-text { flex: 1; min-width: 0; }
            .bbe-panel-item .bbe-item-title {
                font-size: 13.5px;
                font-weight: 500;
                color: #fff;
            }
            .bbe-panel-item .bbe-item-desc {
                font-size: 11.5px;
                color: var(--bbe-text-dim);
                margin-top: 1px;
                line-height: 1.35;
            }
            .bbe-panel-item .bbe-item-chevron {
                color: var(--bbe-text-dim);
                font-size: 12px;
                transition: transform 0.15s;
            }
            .bbe-panel-item:hover .bbe-item-chevron { transform: translateX(2px); color: var(--bbe-text); }

            .bbe-panel-sep {
                height: 1px;
                background: var(--bbe-border);
                margin: 4px 10px;
            }

            /* ===== Settings view inside panel ===== */
            .bbe-settings-view { display: none; }
            .bbe-root.show-settings .bbe-menu-view { display: none; }
            .bbe-root.show-settings .bbe-settings-view { display: block; }

            .bbe-back-btn {
                display: flex; align-items: center; gap: 6px;
                padding: 8px 12px 6px;
                background: transparent;
                border: none;
                color: var(--bbe-text-dim);
                font-family: inherit;
                font-size: 12px;
                cursor: pointer;
                transition: color 0.12s;
            }
            .bbe-back-btn:hover { color: var(--bbe-text); }

            .bbe-section-list {
                display: flex; flex-direction: column; gap: 4px;
                padding: 4px 6px 6px;
            }
            .bbe-section-item {
                display: flex; align-items: center; gap: 10px;
                padding: 9px 10px;
                background: var(--bbe-bg-darker);
                border: 1px solid var(--bbe-border);
                border-radius: 8px;
                cursor: pointer;
                user-select: none;
                transition: background 0.12s, border-color 0.12s;
            }
            .bbe-section-item:hover { background: #181b27; border-color: var(--bbe-border-strong); }
            .bbe-section-item.locked { cursor: not-allowed; opacity: 0.75; }
            .bbe-section-item input[type="checkbox"] {
                width: 16px; height: 16px;
                accent-color: var(--bbe-accent);
                cursor: pointer;
                flex-shrink: 0;
            }
            .bbe-section-item.locked input[type="checkbox"] { accent-color: #6b7089; }
            .bbe-section-name {
                flex: 1;
                font-size: 13px;
                color: var(--bbe-text);
            }
            .bbe-lock-badge {
                font-size: 10.5px;
                padding: 2px 7px;
                border-radius: 10px;
                background: #2a2e40;
                color: var(--bbe-text-dim);
                font-weight: 500;
                letter-spacing: 0.3px;
            }

            .bbe-lang-box {
                padding: 8px 10px 4px;
                border-top: 1px solid var(--bbe-border);
                margin-top: 4px;
            }
            .bbe-lang-label {
                display: block;
                font-size: 11px;
                color: var(--bbe-text-dim);
                margin-bottom: 5px;
                letter-spacing: 0.3px;
                text-transform: uppercase;
            }
            .bbe-lang-select {
                width: 100%;
                background: var(--bbe-bg-darker);
                color: var(--bbe-text);
                border: 1px solid var(--bbe-border);
                border-radius: 8px;
                padding: 8px 10px;
                font-family: inherit;
                font-size: 13px;
                cursor: pointer;
                outline: none;
                transition: border-color 0.12s;
            }
            .bbe-lang-select:hover { border-color: var(--bbe-border-strong); }
            .bbe-lang-select:focus { border-color: var(--bbe-accent); }

            .bbe-save-btn {
                width: calc(100% - 12px);
                margin: 6px 6px 8px;
                padding: 10px 14px;
                background: var(--bbe-accent);
                color: #fff;
                border: none;
                border-radius: 8px;
                font-family: inherit;
                font-size: 13px;
                font-weight: 600;
                cursor: pointer;
                transition: background 0.12s;
            }
            .bbe-save-btn:hover { background: var(--bbe-accent-hover); }

            /* ===== Modals ===== */
            .bbe-overlay {
                position: fixed; inset: 0;
                background: rgba(10, 12, 18, 0.78);
                backdrop-filter: blur(3px);
                z-index: 999999;
                display: flex; align-items: center; justify-content: center;
                padding: 20px;
                animation: bbe-fade 0.15s ease-out;
                font-family: var(--bbe-font);
            }
            @keyframes bbe-fade { from { opacity: 0; } to { opacity: 1; } }
            .bbe-box {
                background: var(--bbe-bg);
                color: var(--bbe-text);
                width: 100%; max-width: 720px; max-height: 85vh;
                border-radius: 14px;
                box-shadow: 0 24px 70px rgba(0,0,0,0.6);
                display: flex; flex-direction: column;
                overflow: hidden;
                border: 1px solid var(--bbe-border);
            }
            .bbe-modal-header {
                padding: 18px 22px 12px;
                border-bottom: 1px solid var(--bbe-border);
            }
            .bbe-modal-title {
                margin: 0 0 4px;
                font-size: 17px; font-weight: 600;
                color: #fff;
            }
            .bbe-modal-sub {
                margin: 0; font-size: 13px; color: var(--bbe-text-dim); line-height: 1.45;
            }
            .bbe-modal-body {
                padding: 16px 22px;
                overflow-y: auto;
                flex: 1 1 auto;
            }
            .bbe-modal-body textarea {
                width: 100%; box-sizing: border-box;
                background: var(--bbe-bg-darker);
                border: 1px solid var(--bbe-border);
                color: #d6d9e3;
                border-radius: 8px;
                padding: 12px;
                font-family: "SF Mono", Consolas, Monaco, monospace;
                font-size: 13px;
                line-height: 1.5;
                resize: vertical;
                outline: none;
                transition: border-color 0.15s;
            }
            .bbe-modal-body textarea:focus { border-color: var(--bbe-accent); }
            .bbe-modal-footer {
                padding: 14px 22px 18px;
                border-top: 1px solid var(--bbe-border);
                display: flex; gap: 8px; justify-content: flex-end;
                background: var(--bbe-bg-footer);
            }
            .bbe-btn {
                border: 1px solid transparent;
                padding: 9px 16px;
                border-radius: 8px;
                font-size: 13.5px;
                font-weight: 500;
                cursor: pointer;
                transition: all 0.15s;
                font-family: inherit;
                color: var(--bbe-text);
                background: #2a2e40;
            }
            .bbe-btn:hover { background: #333850; }
            .bbe-btn.primary { background: var(--bbe-accent); color: #fff; }
            .bbe-btn.primary:hover { background: var(--bbe-accent-hover); }
            .bbe-btn.ghost { background: transparent; border-color: var(--bbe-border); }
            .bbe-btn.ghost:hover { background: #232736; }

            /* ===== Toast ===== */
            .bbe-toast {
                position: fixed;
                bottom: 24px; left: 50%;
                transform: translateX(-50%) translateY(20px);
                background: #2a2e40;
                color: #fff;
                padding: 11px 20px;
                border-radius: 8px;
                font-size: 13.5px;
                font-family: var(--bbe-font);
                box-shadow: 0 8px 30px rgba(0,0,0,0.4);
                opacity: 0;
                transition: opacity 0.18s, transform 0.18s;
                z-index: 1000000;
                pointer-events: none;
                border: 1px solid var(--bbe-border-strong);
            }
            .bbe-toast.show {
                opacity: 1;
                transform: translateX(-50%) translateY(0);
            }
        `;
        document.head.appendChild(style);
    }

    // ---------- Toast ----------

    function toast(msg) {
        let el = document.querySelector('.bbe-toast');
        if (!el) {
            el = document.createElement('div');
            el.className = 'bbe-toast';
            document.body.appendChild(el);
        }
        el.textContent = msg;
        requestAnimationFrame(() => el.classList.add('show'));
        clearTimeout(el._timer);
        el._timer = setTimeout(() => el.classList.remove('show'), 1800);
    }

    // ---------- Modal helpers ----------

    function makeOverlay() {
        const overlay = document.createElement('div');
        overlay.className = 'bbe-overlay';
        document.body.appendChild(overlay);
        overlay.addEventListener('click', e => {
            if (e.target === overlay) overlay.remove();
        });
        return overlay;
    }

    function makeModalBox(title, subtitle) {
        const box = document.createElement('div');
        box.className = 'bbe-box';

        const header = document.createElement('div');
        header.className = 'bbe-modal-header';
        const h = document.createElement('h3');
        h.className = 'bbe-modal-title';
        h.textContent = title;
        header.appendChild(h);
        if (subtitle) {
            const p = document.createElement('p');
            p.className = 'bbe-modal-sub';
            p.textContent = subtitle;
            header.appendChild(p);
        }

        const body = document.createElement('div');
        body.className = 'bbe-modal-body';

        const footer = document.createElement('div');
        footer.className = 'bbe-modal-footer';

        box.appendChild(header);
        box.appendChild(body);
        box.appendChild(footer);
        return { box, body, footer };
    }

    // ---------- Result modal ----------

    function showResult(bbcode) {
        injectStyles();
        const overlay = makeOverlay();
        const { box, body, footer } = makeModalBox(
            t('resultTitle'),
            t('resultSub')
        );

        const textarea = document.createElement('textarea');
        textarea.value = bbcode;
        textarea.style.height = '380px';
        body.appendChild(textarea);

        const closeBtn = document.createElement('button');
        closeBtn.className = 'bbe-btn ghost';
        closeBtn.textContent = t('close');
        closeBtn.onclick = () => overlay.remove();

        const copyBtn = document.createElement('button');
        copyBtn.className = 'bbe-btn primary';
        copyBtn.textContent = t('copy');
        copyBtn.onclick = () => {
            textarea.select();
            if (typeof GM_setClipboard === 'function') {
                GM_setClipboard(bbcode);
            } else {
                document.execCommand('copy');
            }
            toast(t('copied'));
        };

        footer.appendChild(closeBtn);
        footer.appendChild(copyBtn);
        overlay.appendChild(box);

        textarea.focus();
        textarea.select();
    }

    // ---------- Gallery input modal ----------

    function showGalleryInputModal(prefs) {
        injectStyles();
        const overlay = makeOverlay();
        const { box, body, footer } = makeModalBox(
            t('galleryTitle'),
            t('gallerySub')
        );

        const textarea = document.createElement('textarea');
        textarea.placeholder = t('galleryPlaceholder');
        textarea.style.height = '200px';
        body.appendChild(textarea);

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'bbe-btn ghost';
        cancelBtn.textContent = t('cancel');
        cancelBtn.onclick = () => overlay.remove();

        const skipBtn = document.createElement('button');
        skipBtn.className = 'bbe-btn';
        skipBtn.textContent = t('skipNoGallery');
        skipBtn.onclick = () => {
            overlay.remove();
            showResult(buildBBCode('', prefs));
        };

        const genBtn = document.createElement('button');
        genBtn.className = 'bbe-btn primary';
        genBtn.textContent = t('generateBBCode');
        genBtn.onclick = () => {
            overlay.remove();
            showResult(buildBBCode(textarea.value, prefs));
        };

        footer.appendChild(cancelBtn);
        footer.appendChild(skipBtn);
        footer.appendChild(genBtn);
        overlay.appendChild(box);

        textarea.focus();
    }

    // ---------- Sections settings modal (fallback / first-run) ----------

    function showSettingsModal(allSections, prefs, onSave) {
        injectStyles();
        const overlay = makeOverlay();
        const { box, body, footer } = makeModalBox(
            t('selectSectionsTitle'),
            t('selectSectionsSub')
        );

        const checkboxMap = {};
        const list = document.createElement('div');
        list.className = 'bbe-section-list';

        allSections.forEach(section => {
            const mandatory = MANDATORY_SECTIONS.includes(section);
            const item = document.createElement('label');
            item.className = 'bbe-section-item' + (mandatory ? ' locked' : '');

            const cb = document.createElement('input');
            cb.type = 'checkbox';
            cb.checked = mandatory || (prefs && prefs.enabled.includes(section));
            cb.disabled = mandatory;
            checkboxMap[section] = cb;

            const nameSpan = document.createElement('span');
            nameSpan.className = 'bbe-section-name';
            nameSpan.textContent = section;

            item.appendChild(cb);
            item.appendChild(nameSpan);

            if (mandatory) {
                const badge = document.createElement('span');
                badge.className = 'bbe-lock-badge';
                badge.textContent = t('always');
                item.appendChild(badge);
            }
            list.appendChild(item);
        });

        body.appendChild(list);

        // Language selector
        const langBox = document.createElement('div');
        langBox.className = 'bbe-lang-box';
        const langLabel = document.createElement('label');
        langLabel.className = 'bbe-lang-label';
        langLabel.textContent = t('language');
        const langSelect = document.createElement('select');
        langSelect.className = 'bbe-lang-select';
        Object.keys(LANG_NAMES).forEach(code => {
            const opt = document.createElement('option');
            opt.value = code;
            opt.textContent = LANG_NAMES[code];
            if (code === currentLang()) opt.selected = true;
            langSelect.appendChild(opt);
        });
        langBox.appendChild(langLabel);
        langBox.appendChild(langSelect);
        body.appendChild(langBox);

        const cancelBtn = document.createElement('button');
        cancelBtn.className = 'bbe-btn ghost';
        cancelBtn.textContent = t('cancel');
        cancelBtn.onclick = () => overlay.remove();

        const saveBtn = document.createElement('button');
        saveBtn.className = 'bbe-btn primary';
        saveBtn.textContent = t('saveContinue');
        saveBtn.onclick = () => {
            const enabled = Object.keys(checkboxMap)
                .filter(s => checkboxMap[s].checked)
                .filter(s => !MANDATORY_SECTIONS.includes(s));
            const newPrefs = { enabled, lang: langSelect.value };
            savePrefs(newPrefs);
            overlay.remove();
            onSave(newPrefs);
        };

        footer.appendChild(cancelBtn);
        footer.appendChild(saveBtn);
        overlay.appendChild(box);
    }

    // ---------- Main flow ----------

    function collectAllSections() {
        const info = getInfoGrid();
        const sections = Object.keys(info);
        MANDATORY_SECTIONS.forEach(s => {
            if (!sections.includes(s)) sections.unshift(s);
        });
        const rest = sections.filter(s => !MANDATORY_SECTIONS.includes(s));
        const all = [...MANDATORY_SECTIONS, ...rest];

        if (getRating() && !all.includes(RATING_KEY)) all.push(RATING_KEY);
        if (getBioText() && !all.includes(BIOGRAPHY_KEY)) all.push(BIOGRAPHY_KEY);
        return all;
    }

    function startFlow() {
        const prefs = loadPrefs();
        const allSections = collectAllSections();

        if (!prefs) {
            showSettingsModal(allSections, defaultPrefs(), (newPrefs) => {
                showGalleryInputModal(newPrefs);
            });
        } else {
            showGalleryInputModal(prefs);
        }
    }

    // ---------- Floating dropdown menu ----------

    function buildMenu() {
        injectStyles();

        const root = document.createElement('div');
        root.className = 'bbe-root';

        const trigger = document.createElement('button');
        trigger.className = 'bbe-trigger';
        trigger.type = 'button';

        const panel = document.createElement('div');
        panel.className = 'bbe-panel';

        const panelHeader = document.createElement('div');
        panelHeader.className = 'bbe-panel-header';
        const pt = document.createElement('p');
        pt.className = 'bbe-panel-title';
        const ps = document.createElement('p');
        ps.className = 'bbe-panel-sub';
        panelHeader.appendChild(pt);
        panelHeader.appendChild(ps);

        const panelBody = document.createElement('div');
        panelBody.className = 'bbe-panel-body';

        // --- menu view ---
        const menuView = document.createElement('div');
        menuView.className = 'bbe-menu-view';

        function makeMenuItem({ icon, titleKey, descKey, onClick }) {
            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'bbe-panel-item';

            const ic = document.createElement('div');
            ic.className = 'bbe-item-icon';
            ic.textContent = icon;

            const txt = document.createElement('div');
            txt.className = 'bbe-item-text';
            const ttl = document.createElement('div');
            ttl.className = 'bbe-item-title';
            ttl.dataset.i18n = titleKey;
            const d = document.createElement('div');
            d.className = 'bbe-item-desc';
            d.dataset.i18n = descKey;
            txt.appendChild(ttl);
            txt.appendChild(d);

            const chev = document.createElement('span');
            chev.className = 'bbe-item-chevron';
            chev.textContent = '›';

            btn.appendChild(ic);
            btn.appendChild(txt);
            btn.appendChild(chev);
            btn.addEventListener('click', () => {
                closeMenu();
                onClick();
            });
            return btn;
        }

        const bbcodeItem = makeMenuItem({
            icon: '📋',
            titleKey: 'genBBCode',
            descKey: 'genBBCodeDesc',
            onClick: () => startFlow(),
        });

        const downloadItem = makeMenuItem({
            icon: '⬇️',
            titleKey: 'download',
            descKey: 'downloadDesc',
            onClick: () => {
                const urls = getTopImageUrls(6);
                if (!urls.length) {
                    alert(t('noPhotos'));
                    return;
                }
                toast(t('downloading', { n: urls.length }));
                downloadImages(urls);
            },
        });

        const settingsBtn = document.createElement('button');
        settingsBtn.type = 'button';
        settingsBtn.className = 'bbe-panel-item';
        settingsBtn.innerHTML = `
            <div class="bbe-item-icon">⚙️</div>
            <div class="bbe-item-text">
                <div class="bbe-item-title" data-i18n="settings"></div>
                <div class="bbe-item-desc" data-i18n="settingsDesc"></div>
            </div>
            <span class="bbe-item-chevron">›</span>
        `;

        menuView.appendChild(bbcodeItem);
        menuView.appendChild(downloadItem);

        const sep = document.createElement('div');
        sep.className = 'bbe-panel-sep';
        menuView.appendChild(sep);
        menuView.appendChild(settingsBtn);

        // --- settings view ---
        const settingsView = document.createElement('div');
        settingsView.className = 'bbe-settings-view';

        const backBtn = document.createElement('button');
        backBtn.type = 'button';
        backBtn.className = 'bbe-back-btn';
        backBtn.dataset.i18n = 'backToMenu';

        const sectionList = document.createElement('div');
        sectionList.className = 'bbe-section-list';

        const checkboxMap = {};

        function renderSectionList() {
            sectionList.innerHTML = '';
            const prefs = loadPrefs() || defaultPrefs();
            const allSections = collectAllSections();
            Object.keys(checkboxMap).forEach(k => delete checkboxMap[k]);

            allSections.forEach(section => {
                const mandatory = MANDATORY_SECTIONS.includes(section);
                const item = document.createElement('label');
                item.className = 'bbe-section-item' + (mandatory ? ' locked' : '');

                const cb = document.createElement('input');
                cb.type = 'checkbox';
                cb.checked = mandatory || prefs.enabled.includes(section);
                cb.disabled = mandatory;
                checkboxMap[section] = cb;

                const nameSpan = document.createElement('span');
                nameSpan.className = 'bbe-section-name';
                nameSpan.textContent = section;

                item.appendChild(cb);
                item.appendChild(nameSpan);

                if (mandatory) {
                    const badge = document.createElement('span');
                    badge.className = 'bbe-lock-badge';
                    badge.textContent = t('always');
                    item.appendChild(badge);
                }
                sectionList.appendChild(item);
            });
        }

        // Language selector (inline in settings view)
        const langBox = document.createElement('div');
        langBox.className = 'bbe-lang-box';
        const langLabel = document.createElement('label');
        langLabel.className = 'bbe-lang-label';
        langLabel.dataset.i18n = 'language';
        const langSelect = document.createElement('select');
        langSelect.className = 'bbe-lang-select';
        Object.keys(LANG_NAMES).forEach(code => {
            const opt = document.createElement('option');
            opt.value = code;
            opt.textContent = LANG_NAMES[code];
            langSelect.appendChild(opt);
        });
        langBox.appendChild(langLabel);
        langBox.appendChild(langSelect);

        const saveBtn = document.createElement('button');
        saveBtn.type = 'button';
        saveBtn.className = 'bbe-save-btn';
        saveBtn.dataset.i18n = 'saveChoice';
        saveBtn.onclick = () => {
            const enabled = Object.keys(checkboxMap)
                .filter(s => checkboxMap[s].checked)
                .filter(s => !MANDATORY_SECTIONS.includes(s));
            savePrefs({ enabled, lang: langSelect.value });
            applyTranslations();
            toast(t('savedChoice'));
            closeMenu();
        };

        settingsView.appendChild(backBtn);
        settingsView.appendChild(sectionList);
        settingsView.appendChild(langBox);
        settingsView.appendChild(saveBtn);

        panelBody.appendChild(menuView);
        panelBody.appendChild(settingsView);
        panel.appendChild(panelHeader);
        panel.appendChild(panelBody);

        root.appendChild(panel);
        root.appendChild(trigger);
        document.body.appendChild(root);

        function applyTranslations() {
            const lang = currentLang();
            trigger.innerHTML = `<span class="bbe-icon">☰</span><span>${t('menu')}</span>`;
            pt.textContent = t('menuTitle');
            ps.textContent = t('menuSub');
            // Translate all elements with data-i18n
            root.querySelectorAll('[data-i18n]').forEach(el => {
                el.textContent = t(el.dataset.i18n);
            });
            // Select current language
            langSelect.value = lang;
        }

        // --- open/close logic ---
        function openMenu() {
            root.classList.add('open');
            root.classList.remove('show-settings');
            applyTranslations();
            renderSectionList();
        }
        function closeMenu() {
            root.classList.remove('open');
            setTimeout(() => {
                if (!root.classList.contains('open')) {
                    root.classList.remove('show-settings');
                }
            }, 180);
        }
        function toggleMenu() {
            if (root.classList.contains('open')) closeMenu();
            else openMenu();
        }

        trigger.addEventListener('click', (e) => {
            e.stopPropagation();
            toggleMenu();
        });

        backBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            root.classList.remove('show-settings');
        });

        settingsBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            renderSectionList();
            applyTranslations();
            root.classList.add('show-settings');
        });

        // Language change — update immediately in panel, save on saveBtn
        langSelect.addEventListener('change', () => {
            const prefs = loadPrefs() || defaultPrefs();
            prefs.lang = langSelect.value;
            savePrefs(prefs);
            applyTranslations();
        });

        document.addEventListener('click', (e) => {
            if (!root.contains(e.target)) closeMenu();
        });

        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') closeMenu();
        });

        // Initial translate
        applyTranslations();
    }

    // ---------- Init ----------

    buildMenu();
})();