☰

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 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 Tampermonkey或 Violentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 Tampermonkey或 Userscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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