/**
 * LoadingScreenOverlay — внутриигровой экран загрузки (задача 12).
 *
 * Программный mid-game transition: чёрный фон (fadeIn/Out), картинка-превью
 * (cover) по центру, прогресс-бар (жёлтый по серому) + процент, спиннер
 * «ЗАГРУЗКА» справа-снизу (CSS keyframes), кнопка «ПРОПУСТИТЬ» по центру-снизу
 * (появляется через 0.5с — анти-accidental), логотип игры слева-снизу.
 *
 * Вызывается из скрипта через game.loading.show(opts) / game.loading.transition(opts).
 * Покрывает и кейс задачи 05 (начальный экран при входе).
 *
 * Реализация — лёгкий DOM-оверлей поверх canvas (как ShopInventoryUi), а не
 * Babylon-GUI: фиксированный layout с прогресс-баром/спиннером/кнопкой на HTML
 * делается быстрее и доступнее. Класс самодостаточен: хранит state, рисует DOM,
 * имеет tick(dt) для fade-фаз и авто-duration (в отличие от ShopInventoryUi,
 * которому tick не нужен).
 *
 * Один активный экран одновременно: повторный show() мгновенно закрывает
 * предыдущий (как ModalManager) — нет утечки overlay'ев при нескольких
 * transition подряд.
 *
 * Фича-парность: идентичный модуль в rublox-player/src/engine/.
 */

const EASE_OUT = (t) => 1 - Math.pow(1 - t, 3);

// CSS спиннера вставляем один раз в <head> (keyframes нельзя инлайнить в style).
let _spinCssInjected = false;
function injectSpinnerCss() {
    if (_spinCssInjected) return;
    _spinCssInjected = true;
    try {
        const style = document.createElement('style');
        style.id = 'kbn-loading-spin-css';
        style.textContent =
            '@keyframes kbn-ls-spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}' +
            '.kbn-ls-spinner{animation:kbn-ls-spin 0.9s linear infinite}' +
            // Ken Burns — медленный pan+zoom фона (задача 05).
            '@keyframes kbn-ls-kenburns{' +
            '0%{transform:scale(1.0) translate3d(0,0,0)}' +
            '50%{transform:scale(1.10) translate3d(-3%,-2%,0)}' +
            '100%{transform:scale(1.0) translate3d(-6%,0,0)}}' +
            '.kbn-ls-kenburns{animation:kbn-ls-kenburns 24s ease-in-out infinite}' +
            // particles — медленно всплывающие искры.
            '@keyframes kbn-ls-rise{' +
            '0%{transform:translateY(0) scale(1);opacity:0}' +
            '10%{opacity:0.9}' +
            '90%{opacity:0.7}' +
            '100%{transform:translateY(-110vh) scale(1.4);opacity:0}}' +
            '.kbn-ls-particle{animation:kbn-ls-rise linear infinite}' +
            // лёгкий «дыхательный» glow карточки-превью.
            '@keyframes kbn-ls-cardglow{' +
            '0%,100%{box-shadow:0 18px 60px rgba(0,0,0,0.6),0 0 0 rgba(120,160,255,0)}' +
            '50%{box-shadow:0 18px 60px rgba(0,0,0,0.6),0 0 40px rgba(120,160,255,0.35)}}' +
            '.kbn-ls-cardglow{animation:kbn-ls-cardglow 4s ease-in-out infinite}' +
            '@media (prefers-reduced-motion:reduce){.kbn-ls-spinner,.kbn-ls-kenburns,.kbn-ls-particle,.kbn-ls-cardglow{animation:none}}';
        document.head.appendChild(style);
    } catch { /* ignore */ }
}

export class LoadingScreenOverlay {
    constructor(scene3d) {
        this.s = scene3d;
        this.root = null;
        this._st = null;             // state активного экрана или null
        this._idSeq = 0;
        // Мост наружу (GameRuntime подписывает) — id-based колбэки.
        this._onSkipCb = null;       // (id) => void
        this._onCompleteCb = null;   // (id) => void
        this._onHideCb = null;       // () => void — задача 05 (game.loading.onHide)
        this._parallaxHandler = null;
        // DOM-ссылки активного экрана:
        this._els = null;
    }

    /** Колбэки-мост к GameRuntime (он рассылает globalEvent в sandbox'ы). */
    setBridge(onSkip, onComplete, onHide) {
        this._onSkipCb = onSkip;
        this._onCompleteCb = onComplete;
        if (onHide) this._onHideCb = onHide;
    }

    /** Конфиг проекта (логотип/акцент по умолчанию) из BabylonScene._loadingConfig. */
    _cfg() {
        return (this.s && this.s._loadingConfig) || {};
    }

    /**
     * Показать экран загрузки. Возвращает числовой id (для матчинга команд).
     * opts — см. 12_ingame_loading.md §2.2.
     */
    show(opts) {
        injectSpinnerCss();
        opts = opts && typeof opts === 'object' ? opts : {};
        // Один активный — мгновенно убрать предыдущий.
        if (this._st) this._instantClose();

        const cfg = this._cfg();
        const accent = opts.progressColor || cfg.accentColor || '#ffc020';
        const st = {
            id: ++this._idSeq,
            // Фон
            bgColor: opts.bgColor || '#000',
            bgOpacity: opts.bgOpacity != null ? Number(opts.bgOpacity) : 1,
            fadeIn: opts.fadeIn != null ? Number(opts.fadeIn) : 0.3,
            fadeOut: opts.fadeOut != null ? Number(opts.fadeOut) : 0.3,
            // Прогресс
            progressBar: opts.progressBar !== false,
            progressColor: accent,
            progressBgColor: opts.progressBgColor || '#444',
            percentText: opts.percentText !== false,
            progress: Math.max(0, Math.min(1, Number(opts.initialProgress) || 0)),
            duration: Number.isFinite(opts.duration) && opts.duration > 0 ? Number(opts.duration) : null,
            manualProgress: false,
            // Спиннер
            spinner: opts.spinner != null ? !!opts.spinner : (cfg.defaultSpinner !== false),
            spinnerText: opts.spinnerText != null ? String(opts.spinnerText) : 'ЗАГРУЗКА',
            // Кнопка Пропустить
            skipButton: opts.skipButton != null ? !!opts.skipButton : !!cfg.defaultSkipButton,
            skipButtonText: opts.skipButtonText != null ? String(opts.skipButtonText) : 'ПРОПУСТИТЬ',
            skipButtonColor: opts.skipButtonColor || accent,
            skipShown: false,
            // Логотип
            logo: opts.logo || cfg.logo || (this.s && this.s._projectThumbnail) || null,
            logoCornerRadius: opts.logoCornerRadius != null ? Number(opts.logoCornerRadius) : 12,
            // Текст под картинкой
            text: opts.text != null ? String(opts.text) : '',
            // --- Задача 05: Ken-Burns фон + карточка места ---
            // style: 'ken-burns' | 'static' | 'parallax' | 'particles'
            style: opts.style || cfg.style || 'ken-burns',
            // фоновое размытое изображение (на весь экран); резолвится в _resolveCover.
            background: opts.background != null ? opts.background : (cfg.background || null),
            // карточка-витрина по центру (название места + автор), как в Roblox.
            placeName: opts.placeName != null ? String(opts.placeName) : (cfg.placeName || ''),
            studioName: opts.studioName != null ? String(opts.studioName) : (cfg.studioName || ''),
            verified: opts.verified != null ? !!opts.verified : !!cfg.verified,
            // Поведение
            blockInput: opts.blockInput !== false,
            pauseSimulation: opts.pauseSimulation !== false,
            // Жизненный цикл
            phase: 'in',         // 'in' | 'visible' | 'out'
            alpha: 0,
            elapsed: 0,          // время с момента полного появления (для duration/skip)
            fadeT: 0,
            completed: false,    // onComplete уже вызывался
        };
        this._st = st;
        this._build(st, opts.cover);

        // Блок ввода + пауза симуляции.
        if (st.blockInput) { try { this.s.player?.setInputBlocked?.(true); } catch { /* ignore */ } }
        if (st.pauseSimulation && this.s.gameRuntime) { try { this.s.gameRuntime.paused = true; } catch { /* ignore */ } }

        return st.id;
    }

    /** Резолв cover в URL/dataURL. */
    _resolveCover(cover) {
        if (!cover) return null;
        if (typeof cover === 'string') {
            // asset:xxx → пробуем через AssetManager, иначе как прямой URL.
            try {
                const r = this.s.assetManager?.resolveUrl?.(cover);
                if (r) return r;
            } catch { /* ignore */ }
            return cover;
        }
        if (typeof cover === 'object') {
            if (cover.sceneSnapshot) {
                try {
                    const canvas = this.s.engine?.getRenderingCanvas?.();
                    if (canvas) return canvas.toDataURL('image/jpeg', 0.72);
                } catch { /* ignore */ }
                return null;
            }
            if (cover.url) return cover.url;
        }
        return null;
    }

    _build(st, cover) {
        const parent = (this.s.canvas && this.s.canvas.parentElement) || document.body;
        try { if (getComputedStyle(parent).position === 'static') parent.style.position = 'relative'; } catch { /* ignore */ }

        const root = document.createElement('div');
        root.className = 'kbn-loading';
        root.style.cssText =
            'position:absolute;inset:0;z-index:60;overflow:hidden;' +
            'display:flex;align-items:center;justify-content:center;' +
            'opacity:0;transition:none;font-family:system-ui,"Segoe UI",sans-serif;' +
            `background:${st.bgColor};`;
        // фон с настраиваемой непрозрачностью — отдельный слой, чтобы контент был непрозрачным
        // (используем opacity всего root для fade, а bgOpacity — через rgba фон):
        root.style.background = this._bgRgba(st.bgColor, st.bgOpacity);

        // --- Фоновый слой (Ken Burns / parallax / static) ---
        // Размытое изображение игры на весь экран. Отдельный div под контентом,
        // чтобы blur/анимация не трогали карточку и текст.
        const bgUrl = this._resolveCover(st.background);
        const bgLayer = document.createElement('div');
        let bgClass = '';
        if (bgUrl) {
            if (st.style === 'ken-burns') bgClass = 'kbn-ls-kenburns';
            bgLayer.className = bgClass;
            bgLayer.style.cssText =
                'position:absolute;inset:-8%;z-index:0;background-size:cover;background-position:center;' +
                'filter:blur(8px) brightness(0.55);will-change:transform;' +
                `background-image:url("${bgUrl}");`;
            // parallax — лёгкий сдвиг по мыши (2 слоя имитируем одним + transform).
            if (st.style === 'parallax') {
                bgLayer.style.transition = 'transform 0.25s ease-out';
                this._parallaxHandler = (e) => {
                    const cx = (e.clientX / window.innerWidth - 0.5) * 28;
                    const cy = (e.clientY / window.innerHeight - 0.5) * 18;
                    bgLayer.style.transform = `translate3d(${-cx}px,${-cy}px,0) scale(1.06)`;
                };
                window.addEventListener('mousemove', this._parallaxHandler);
            }
            root.appendChild(bgLayer);
        }

        // --- particles слой (медленные искры) ---
        if (st.style === 'particles') {
            const pLayer = document.createElement('div');
            pLayer.style.cssText = 'position:absolute;inset:0;z-index:1;overflow:hidden;pointer-events:none;';
            for (let i = 0; i < 26; i++) {
                const sp = document.createElement('span');
                sp.className = 'kbn-ls-particle';
                const size = 2 + Math.round(Math.random() * 4);
                const dur = 7 + Math.random() * 10;
                sp.style.cssText =
                    `position:absolute;bottom:-10px;left:${(Math.random() * 100).toFixed(1)}%;` +
                    `width:${size}px;height:${size}px;border-radius:50%;` +
                    `background:rgba(${180 + Math.round(Math.random() * 70)},${190 + Math.round(Math.random() * 60)},255,0.85);` +
                    `box-shadow:0 0 ${size * 2}px rgba(140,170,255,0.7);` +
                    `animation-duration:${dur.toFixed(1)}s;animation-delay:${(-Math.random() * dur).toFixed(1)}s;`;
                pLayer.appendChild(sp);
            }
            root.appendChild(pLayer);
        }

        // Обёртка контента (над фоном).
        const content = document.createElement('div');
        content.style.cssText =
            'position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;height:100%;';

        // --- Cover (картинка-карточка по центру) ---
        const coverUrl = this._resolveCover(cover);
        // Режим карточки места (задача 05): квадрат + название + автор под ней.
        const hasPlaceCard = !!(st.placeName || st.studioName);
        const coverImg = document.createElement('div');
        if (hasPlaceCard) {
            coverImg.className = 'kbn-ls-cardglow';
            coverImg.style.cssText =
                'width:min(42vw,400px);aspect-ratio:1/1;border-radius:18px;' +
                'background-size:cover;background-position:center;background-color:#1a1f2b;' +
                'border:2px solid rgba(255,255,255,0.12);';
        } else {
            coverImg.style.cssText =
                'max-width:min(62vw,860px);width:62vw;aspect-ratio:16/9;border-radius:16px;' +
                'box-shadow:0 18px 60px rgba(0,0,0,0.6);background-size:cover;background-position:center;' +
                'background-color:#1a1f2b;margin-bottom:140px;';
        }
        if (coverUrl) coverImg.style.backgroundImage = `url("${coverUrl}")`;
        else if (bgUrl && hasPlaceCard) coverImg.style.backgroundImage = `url("${bgUrl}")`;

        // --- Название места (крупный белый, под карточкой) ---
        const placeEl = document.createElement('div');
        placeEl.style.cssText =
            'margin-top:22px;color:#fff;font-size:38px;font-weight:800;letter-spacing:0.5px;' +
            'text-align:center;text-shadow:0 3px 14px rgba(0,0,0,0.7);' +
            (st.placeName ? '' : 'display:none;');
        placeEl.textContent = st.placeName || '';

        // --- Автор + verified-галочка ---
        const studioRow = document.createElement('div');
        studioRow.style.cssText =
            'margin-top:8px;display:flex;align-items:center;gap:7px;' +
            'color:#cdd6e6;font-size:16px;font-weight:600;text-shadow:0 1px 4px rgba(0,0,0,0.6);' +
            (st.studioName ? '' : 'display:none;');
        const studioTxt = document.createElement('span');
        studioTxt.textContent = st.studioName || '';
        studioRow.appendChild(studioTxt);
        if (st.verified) studioRow.appendChild(this._buildVerifiedBadge());

        // --- Текст под картинкой (для не-карточного режима / mid-game) ---
        const textEl = document.createElement('div');
        if (hasPlaceCard) {
            textEl.style.cssText =
                'margin-top:14px;color:#e8edf5;font-size:16px;font-weight:500;text-align:center;' +
                'text-shadow:0 1px 3px rgba(0,0,0,0.6);' + (st.text ? '' : 'display:none;');
        } else {
            textEl.style.cssText =
                'position:absolute;left:50%;transform:translateX(-50%);bottom:170px;' +
                'color:#e8edf5;font-size:18px;font-weight:500;text-align:center;text-shadow:0 1px 3px rgba(0,0,0,0.6);';
        }
        textEl.textContent = st.text || '';

        // --- Прогресс-бар ---
        const barWrap = document.createElement('div');
        barWrap.style.cssText =
            'position:absolute;left:50%;transform:translateX(-50%);bottom:120px;' +
            `width:min(74vw,1180px);height:14px;border-radius:8px;background:${st.progressBgColor};` +
            'overflow:hidden;box-shadow:inset 0 1px 3px rgba(0,0,0,0.5);' +
            (st.progressBar ? '' : 'display:none;');
        const bar = document.createElement('div');
        bar.style.cssText =
            `height:100%;width:${(st.progress * 100).toFixed(1)}%;border-radius:8px;` +
            `background:linear-gradient(90deg,${st.progressColor},${this._lighten(st.progressColor)});` +
            'transition:width 0.12s linear;box-shadow:0 0 8px rgba(255,200,40,0.4);';
        barWrap.appendChild(bar);

        // --- Процент ---
        const percent = document.createElement('div');
        percent.style.cssText =
            'position:absolute;left:50%;transform:translateX(-50%);bottom:74px;' +
            `color:${st.progressColor};font-size:30px;font-weight:800;text-shadow:0 2px 4px rgba(0,0,0,0.5);` +
            (st.percentText ? '' : 'display:none;');
        percent.textContent = `${Math.round(st.progress * 100)}%`;

        // --- Кнопка Пропустить ---
        const skipBtn = document.createElement('button');
        skipBtn.type = 'button';
        skipBtn.textContent = st.skipButtonText;
        skipBtn.style.cssText =
            'position:absolute;left:50%;transform:translateX(-50%);bottom:18px;' +
            'min-width:260px;padding:14px 36px;border:none;border-radius:12px;cursor:pointer;' +
            `background:linear-gradient(180deg,${this._lighten(st.skipButtonColor)},${st.skipButtonColor});` +
            'color:#3a2a00;font-size:18px;font-weight:800;letter-spacing:0.5px;' +
            'box-shadow:0 6px 16px rgba(0,0,0,0.4),inset 0 1px 0 rgba(255,255,255,0.4);' +
            'opacity:0;transition:opacity 0.25s,transform 0.1s;pointer-events:none;' +
            (st.skipButton ? '' : 'display:none;');
        skipBtn.onmouseenter = () => { skipBtn.style.transform = 'translateX(-50%) translateY(-2px)'; };
        skipBtn.onmouseleave = () => { skipBtn.style.transform = 'translateX(-50%)'; };
        skipBtn.onclick = () => {
            if (skipBtn.style.pointerEvents === 'none') return;
            this._fireSkip();
        };

        // --- Логотип (слева снизу) ---
        const logo = document.createElement('div');
        logo.style.cssText =
            'position:absolute;left:28px;bottom:24px;max-width:200px;max-height:110px;' +
            `border-radius:${st.logoCornerRadius}px;background-size:contain;background-repeat:no-repeat;` +
            'background-position:left bottom;width:200px;height:90px;';
        if (st.logo) logo.style.backgroundImage = `url("${st.logo}")`;
        else logo.style.display = 'none';

        // --- Спиннер + «ЗАГРУЗКА» (справа снизу) ---
        const spinWrap = document.createElement('div');
        spinWrap.style.cssText =
            'position:absolute;right:32px;bottom:32px;display:flex;align-items:center;gap:14px;' +
            'color:#fff;font-size:20px;font-weight:700;letter-spacing:1px;' +
            (st.spinner ? '' : 'display:none;');
        const spinTxt = document.createElement('span');
        spinTxt.textContent = st.spinnerText;
        const spinCircle = document.createElement('span');
        spinCircle.className = 'kbn-ls-spinner';
        spinCircle.style.cssText =
            `display:inline-block;width:28px;height:28px;border:3px solid rgba(255,255,255,0.25);` +
            `border-top-color:${st.progressColor};border-radius:50%;`;
        spinWrap.appendChild(spinTxt);
        spinWrap.appendChild(spinCircle);

        // Центральная композиция (карточка + название + автор + текст) — в content.
        content.appendChild(coverImg);
        content.appendChild(placeEl);
        content.appendChild(studioRow);
        content.appendChild(textEl);
        root.appendChild(content);
        // Нижние/угловые элементы — абсолютно на root (поверх фона, рядом с content).
        root.appendChild(barWrap);
        root.appendChild(percent);
        root.appendChild(skipBtn);
        root.appendChild(logo);
        root.appendChild(spinWrap);
        parent.appendChild(root);

        this.root = root;
        this._els = { root, content, coverImg, placeEl, studioRow, textEl, barWrap, bar, percent, skipBtn, logo, spinWrap };
    }

    /** Verified-галочка (синий круг + белый чек) — inline SVG, без emoji-шрифта. */
    _buildVerifiedBadge() {
        const wrap = document.createElement('span');
        wrap.style.cssText = 'display:inline-flex;align-items:center;';
        wrap.innerHTML =
            '<svg width="18" height="18" viewBox="0 0 24 24" aria-label="verified">' +
            '<circle cx="12" cy="12" r="11" fill="#3897f0"/>' +
            '<path d="M7 12.5l3 3 7-7" fill="none" stroke="#fff" stroke-width="2.4" ' +
            'stroke-linecap="round" stroke-linejoin="round"/></svg>';
        return wrap;
    }

    /** tick — fade-фазы, авто-duration, появление кнопки Пропустить. */
    tick(dt) {
        const st = this._st;
        if (!st || !this._els) return;
        dt = Number(dt) || 0;

        if (st.phase === 'in') {
            st.fadeT += dt;
            const d = st.fadeIn > 0 ? st.fadeIn : 0.0001;
            st.alpha = Math.min(1, EASE_OUT(st.fadeT / d));
            this._els.root.style.opacity = String(st.alpha);
            if (st.fadeT >= d) { st.phase = 'visible'; st.alpha = 1; st.fadeT = 0; }
        } else if (st.phase === 'visible') {
            st.elapsed += dt;
            // Кнопка Пропустить — появляется через 0.5с.
            if (!st.skipShown && st.skipButton && st.elapsed >= 0.5) {
                st.skipShown = true;
                this._els.skipBtn.style.opacity = '1';
                this._els.skipBtn.style.pointerEvents = 'auto';
            }
            // Авто-duration (если не было ручного setProgress).
            if (st.duration && !st.manualProgress) {
                st.progress = Math.min(1, st.elapsed / st.duration);
                this._applyProgress(st);
                if (st.progress >= 1 && !st.completed) {
                    st.completed = true;
                    this._fireComplete();
                    this.close();
                }
            }
        } else if (st.phase === 'out') {
            st.fadeT += dt;
            const d = st.fadeOut > 0 ? st.fadeOut : 0.0001;
            st.alpha = Math.max(0, 1 - EASE_OUT(st.fadeT / d));
            this._els.root.style.opacity = String(st.alpha);
            if (st.fadeT >= d) this._teardown();
        }
    }

    _applyProgress(st) {
        if (!this._els) return;
        this._els.bar.style.width = `${(st.progress * 100).toFixed(1)}%`;
        this._els.percent.textContent = `${Math.round(st.progress * 100)}%`;
    }

    setProgress(value) {
        const st = this._st;
        if (!st) return;
        st.manualProgress = true;
        st.progress = Math.max(0, Math.min(1, Number(value) || 0));
        this._applyProgress(st);
        if (st.progress >= 1 && !st.completed) {
            st.completed = true;
            this._fireComplete();
            this.close();
        }
    }

    setText(text) {
        const st = this._st;
        if (!st || !this._els) return;
        st.text = String(text == null ? '' : text);
        this._els.textEl.textContent = st.text;
    }

    setCover(cover) {
        if (!this._st || !this._els) return;
        const url = this._resolveCover(cover);
        if (url) this._els.coverImg.style.backgroundImage = `url("${url}")`;
    }

    /** Задача 05: сменить фоновое (Ken-Burns) изображение на лету. */
    setBackground(bg) {
        if (!this._st || !this._els) return;
        const url = this._resolveCover(bg);
        if (!url) return;
        this._st.background = bg;
        // фоновый слой — первый ребёнок root с background-image; найдём его.
        const layer = this._els.root.querySelector('.kbn-ls-kenburns')
            || this._els.root.firstElementChild;
        if (layer && layer !== this._els.content) layer.style.backgroundImage = `url("${url}")`;
    }

    /** Задача 05: виден ли экран сейчас. */
    isVisible() {
        return !!(this._st && this._st.phase !== 'out');
    }

    /** Закрыть программно (с fadeOut). */
    close() {
        const st = this._st;
        if (!st) return;
        if (st.phase !== 'out') { st.phase = 'out'; st.fadeT = 0; }
    }

    _fireSkip() {
        const st = this._st;
        if (!st) return;
        if (this._onSkipCb) { try { this._onSkipCb(st.id); } catch { /* ignore */ } }
        this.close();
    }

    _fireComplete() {
        const st = this._st;
        if (!st) return;
        if (this._onCompleteCb) { try { this._onCompleteCb(st.id); } catch { /* ignore */ } }
    }

    /** Мгновенно убрать без fade (повторный show / выход из Play). */
    _instantClose() {
        this._teardown();
    }

    _teardown() {
        // Снять блок ввода / паузу.
        const st = this._st;
        if (st) {
            if (st.blockInput) { try { this.s.player?.setInputBlocked?.(false); } catch { /* ignore */ } }
            if (st.pauseSimulation && this.s.gameRuntime) { try { this.s.gameRuntime.paused = false; } catch { /* ignore */ } }
        }
        // Снять parallax-listener (задача 05).
        if (this._parallaxHandler) {
            try { window.removeEventListener('mousemove', this._parallaxHandler); } catch { /* ignore */ }
            this._parallaxHandler = null;
        }
        // onHide-мост (задача 05) — сообщаем скриптам что экран скрылся.
        if (this._onHideCb) { try { this._onHideCb(); } catch { /* ignore */ } }
        if (this.root) { try { this.root.remove(); } catch { /* ignore */ } }
        this.root = null;
        this._els = null;
        this._st = null;
    }

    dispose() {
        this._instantClose();
        this._onSkipCb = null;
        this._onCompleteCb = null;
    }

    // --- утилиты цвета ---
    _lighten(hex) {
        try {
            const h = String(hex).replace('#', '');
            if (h.length !== 6) return hex;
            const r = Math.min(255, parseInt(h.slice(0, 2), 16) + 40);
            const g = Math.min(255, parseInt(h.slice(2, 4), 16) + 40);
            const b = Math.min(255, parseInt(h.slice(4, 6), 16) + 40);
            return `rgb(${r},${g},${b})`;
        } catch { return hex; }
    }

    _bgRgba(hex, opacity) {
        try {
            const h = String(hex).replace('#', '');
            if (h.length !== 6) return hex;
            const r = parseInt(h.slice(0, 2), 16);
            const g = parseInt(h.slice(2, 4), 16);
            const b = parseInt(h.slice(4, 6), 16);
            const a = opacity != null ? Math.max(0, Math.min(1, opacity)) : 1;
            return `rgba(${r},${g},${b},${a})`;
        } catch { return hex; }
    }
}
