import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { jwtDecode } from 'jwt-decode';
import { Client } from 'colyseus.js';
import * as Kubikon3DApi from '../api/Kubikon3DService';
import { BabylonScene } from '../editor/engine/BabylonScene';
import { attachConsoleHook, devlogReset } from '../editor/engine/devlog';
import { MultiplayerSync } from '../editor/engine/MultiplayerSync';
import { REALTIME_WS } from '../api/API';
import GameHud from '../editor/GameHud';
import GuiOverlay from '../editor/GuiOverlay';
import KubikonLeaderboard, { formatTimeMs } from '../components/KubikonLeaderboard/KubikonLeaderboard';
import KubikonPerfOverlay from '../components/KubikonPerfOverlay/KubikonPerfOverlay';
import Hotbar from '../editor/Hotbar';
import PlayerHud from '../editor/PlayerHud';
import ModalOverlay from '../editor/ModalOverlay';
import SkinShopOverlay from '../editor/SkinShopOverlay';
import KubikonBugReportButton from '../components/KubikonBugReport/KubikonBugReportButton';
import KubikonChatPanel from './KubikonChatPanel';
import { useAuth, redirectToLogin } from '../auth/AuthContext.jsx';
import RublocsLogo from '../components/RublocsLogo/RublocsLogo';
import useDeviceType from '../hooks/useDeviceType';
import KubikonMobileControls from './KubikonMobileControls';

/**
 * Палитра тёмного игрового HUD Рублокса.
 * Подобрана так, чтобы UI хорошо читался поверх любой 3D-сцены, но
 * визуально совпадал с синим акцентом Рублокса (#3357ff).
 */
const HUD = {
    bg:          'rgba(10, 14, 26, 0.92)',
    bgGlass:     'rgba(15, 19, 38, 0.78)',
    bgPanel:     'rgba(20, 24, 45, 0.94)',
    bgInput:     'rgba(15, 19, 38, 0.85)',
    border:      'rgba(255, 255, 255, 0.10)',
    borderHover: 'rgba(255, 255, 255, 0.18)',
    borderAcc:   'rgba(51, 87, 255, 0.55)',
    text:        '#f1f5fb',
    textMuted:   'rgba(241, 245, 251, 0.62)',
    textDim:     'rgba(241, 245, 251, 0.42)',
    accent:      '#3357ff',
    accentBg:    'rgba(51, 87, 255, 0.20)',
    accentSoft:  'rgba(51, 87, 255, 0.12)',
    success:     '#22d97a',
    successBg:   'rgba(34, 217, 122, 0.20)',
    danger:      '#ff6f7a',
    dangerBg:    'rgba(255, 111, 122, 0.18)',
    gradientBrand: 'linear-gradient(135deg, #3357ff 0%, #1e2da5 100%)',
    gradientHot:   'linear-gradient(135deg, #ec4899 0%, #ef4444 50%, #f59e0b 100%)',
    font:        '"Roboto Condensed", system-ui, -apple-system, sans-serif',
};

const HUD_KEYFRAMES = `
@keyframes hudFadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }
@keyframes hudFadeInScale { from { opacity: 0; transform: scale(0.94); } to { opacity: 1; transform: scale(1); } }
@keyframes hudPulseRing {
    0%   { box-shadow: 0 0 0 0 rgba(51, 87, 255, 0.55); }
    70%  { box-shadow: 0 0 0 12px rgba(51, 87, 255, 0); }
    100% { box-shadow: 0 0 0 0 rgba(51, 87, 255, 0); }
}
@keyframes hudSpin { to { transform: rotate(360deg); } }
@keyframes hudFloat {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-8px); }
}
`;

/**
 * KubikonPlayer — полноэкранная страница для прохождения опубликованной игры.
 * URL: /play/:id
 *
 * Не редактор. Только Babylon-canvas + UI игры (HUD, hot-bar, GUI-оверлей).
 * Снизу — компактная панель кнопок: Назад / Лайк / Жалоба / Поделиться.
 */
const KubikonPlayer = () => {
    const { id } = useParams();
    const navigate = useNavigate();
    const projectId = Number(id);
    const { isAuthenticated, isLoading: authLoading } = useAuth();
    const device = useDeviceType();
    // На телефонах И планшетах — тач-управление. На десктопе обычное.
    const isTouch = device.isPhone || device.isTablet;
    // Ориентация — для портрет-promp'та на телефонах
    const [isPortrait, setIsPortrait] = useState(
        typeof window !== 'undefined' && window.innerHeight > window.innerWidth,
    );
    useEffect(() => {
        const onResize = () => {
            setIsPortrait(window.innerHeight > window.innerWidth);
        };
        window.addEventListener('resize', onResize);
        window.addEventListener('orientationchange', onResize);
        return () => {
            window.removeEventListener('resize', onResize);
            window.removeEventListener('orientationchange', onResize);
        };
    }, []);

    // Только зарегистрированные. Если гость / без токена — на вход минки (SSO)
    // с возвратом на текущий URL (своей страницы /login в студии нет).
    useEffect(() => {
        if (authLoading) return;
        if (!isAuthenticated) {
            redirectToLogin();
        }
    }, [isAuthenticated, authLoading]);

    const canvasRef = useRef(null);
    const sceneRef = useRef(null);
    const viewportRef = useRef(null);
    const hudRef = useRef(null);
    /** Colyseus Room (только если игра мультиплеерная). */
    const roomRef = useRef(null);
    /** MultiplayerSync (мост между room и Babylon-сценой). */
    const mpSyncRef = useRef(null);
    /** Выбранный R15-скин текущего игрока (из rublox_equipped_skin).
     *  Грузится при старте, уходит в мультиплеер как modelType. */
    const skinFolderRef = useRef('skin_bacon-hair');

    const [meta, setMeta] = useState(null);          // { title, description, user_id, ... }
    const [forbidden, setForbidden] = useState(false);
    const [error, setError] = useState(null);
    const [loading, setLoading] = useState(true);
    // Раньше была стартовая заглушка «тапни чтобы начать» — убрали по
    // фидбэку, она бесила. Теперь fullscreen опционально через кнопку
    // в углу. Этот state остался для совместимости с handleMobileStart.
    const [mobileStartTapped, setMobileStartTapped] = useState(true);
    const [hp, setHp] = useState({ hp: 100, maxHp: 100 });
    // Скрипт через game.hud.setVisible(false) полностью скрывает стандартный HUD.
    const [stdHudVisible, setStdHudVisible] = useState(true);
    // Задача 03: отдельный контроль хотбара/HP — для игр без инвентаря/жизней.
    const [hotbarVisible, setHotbarVisible] = useState(true);
    const [hpVisible, setHpVisible] = useState(true);
    const [ammo, setAmmo] = useState(null);
    const [hurtFlash, setHurtFlash] = useState(0);
    const [inventoryState, setInventoryState] = useState({ slots: [], activeIndex: 0 });
    const [guiList, setGuiList] = useState([]);
    const [isPlaying, setIsPlaying] = useState(false);
    const [liked, setLiked] = useState(false);
    const [likesCount, setLikesCount] = useState(0);
    const [reportOpen, setReportOpen] = useState(false);
    const [infoModal, setInfoModal] = useState(null);   // { title, text, icon } | null
    const [needAuthModal, setNeedAuthModal] = useState(null); // строка-описание действия | null
    // Чат свёрнут по умолчанию — открывается кнопкой ☰ в верхнем баре.
    const [chatOpen, setChatOpen] = useState(false);
    // Roblox-style выпадашка из верхнего бара (☰)
    const [topMenuOpen, setTopMenuOpen] = useState(false);
    // Голос пользователя: 'like' | 'dislike' | null
    const [vote, setVote] = useState(null);
    const [dislikesCount, setDislikesCount] = useState(0);

    // === Лидерборд / таймер прохождения ===
    const [leaderboardVisible, setLeaderboardVisible] = useState(true);
    const [timerMs, setTimerMs] = useState(0);
    const [timerRunning, setTimerRunning] = useState(false);
    const [leaderboardRefreshKey, setLeaderboardRefreshKey] = useState(0);
    // Есть ли в игре таймер прохождения / рекорды (на не-ранерах нет)
    const [leaderboardEnabled, setLeaderboardEnabled] = useState(false);
    // Тост «+200 рейтинга за 1 место в таблице рекордов».
    // Появляется после submitLeaderboard если бэк выдал rating_award текущему юзеру.
    // null | { place: 1|2|3, amount: number }
    const [ratingToast, setRatingToast] = useState(null);
    const timerRafRef = useRef(null);
    /** Кэш загруженного project_data для soft-restart игры. */
    const initialStateRef = useRef(null);

    // rAF-цикл обновления таймера в HUD (только когда таймер запущен)
    useEffect(() => {
        if (!timerRunning) return;
        let cancelled = false;
        const tick = () => {
            if (cancelled) return;
            const s = sceneRef.current;
            if (s?.getTimerMs) {
                setTimerMs(s.getTimerMs());
            }
            timerRafRef.current = requestAnimationFrame(tick);
        };
        timerRafRef.current = requestAnimationFrame(tick);
        return () => {
            cancelled = true;
            if (timerRafRef.current) cancelAnimationFrame(timerRafRef.current);
        };
    }, [timerRunning]);

    // Tab — переключение видимости таблицы лидеров
    useEffect(() => {
        const onKey = (e) => {
            if (e.code === 'Tab' && !e.ctrlKey && !e.altKey) {
                const tag = (e.target && e.target.tagName) || '';
                if (tag === 'INPUT' || tag === 'TEXTAREA') return;
                e.preventDefault();
                setLeaderboardVisible(v => !v);
            }
        };
        window.addEventListener('keydown', onKey);
        return () => window.removeEventListener('keydown', onKey);
    }, []);

    // Проверяем при монтировании: есть ли в проекте рекорды? Если да —
    // игра-ранер (поддерживает таймер), показываем виджет лидерборда.
    // Иначе — игра без таймера (зомби, песочница и т.п.) — скрываем.
    useEffect(() => {
        if (!projectId) return;
        let active = true;
        Kubikon3DApi.getLeaderboard(projectId, 1)
            .then(r => {
                if (!active) return;
                const list = r.data?.records || [];
                if (list.length > 0) setLeaderboardEnabled(true);
            })
            .catch(() => {});
        return () => { active = false; };
    }, [projectId]);

    // Перехват Ctrl-комбинаций которые ломают игру (Ctrl+W = закрыть вкладку,
    // Ctrl+R = reload, Ctrl+T/N — мешают). Большинство браузеров блокирует
    // отмену системных шорткатов, но beforeunload даёт пользователю шанс
    // подтвердить выход. Также превентим preventDefault на keydown для
    // случаев когда фокус НЕ на window-уровне (Chrome иногда позволяет).
    useEffect(() => {
        const onKey = (e) => {
            if (!e.ctrlKey && !e.metaKey) return;
            // Список «опасных» в игре сочетаний — превентим
            const dangerousCodes = ['KeyW', 'KeyR', 'KeyT', 'KeyN'];
            if (dangerousCodes.includes(e.code)) {
                e.preventDefault();
                e.stopPropagation();
            }
        };
        const onBeforeUnload = (e) => {
            // Показываем подтверждение выхода — браузер не даст случайно
            // закрыть вкладку Ctrl+W во время игры.
            e.preventDefault();
            e.returnValue = '';
            return '';
        };
        window.addEventListener('keydown', onKey, { capture: true });
        window.addEventListener('beforeunload', onBeforeUnload);
        return () => {
            window.removeEventListener('keydown', onKey, { capture: true });
            window.removeEventListener('beforeunload', onBeforeUnload);
        };
    }, []);

    /**
     * Soft-restart игры: выходим из Play, перезагружаем оригинальный
     * project_data (восстанавливая удалённые/скрытые скриптом примитивы),
     * заходим в Play заново. Полностью сбрасывает игровое состояние —
     * полезно после прохождения если игрок хочет улучшить рекорд.
     */
    const handleRestartGame = useCallback(async () => {
        const scene = sceneRef.current;
        const initial = initialStateRef.current;
        if (!scene || !initial) return;
        try {
            if (scene.isPlaying?.()) {
                scene.exitPlayMode?.();
            }
            // Перезагрузка состояния — сбрасывает все примитивы/блоки/модели
            await scene.loadFromState(initial);
            // Сбросим UI-состояние таймера
            setTimerMs(0);
            setTimerRunning(false);
            // Ждём кадр и заходим в Play
            await new Promise((r) => setTimeout(r, 50));
            scene.enterPlayMode?.();
        } catch (e) {
            console.warn('[KubikonPlayer] restart failed', e);
        }
    }, []);

    // === Мультиплеер ===
    /** 'idle' | 'connecting' | 'connected' | 'failed'. */
    const [mpStatus, setMpStatus] = useState('idle');
    /** Сообщение об ошибке (если 'failed'). */
    const [mpError, setMpError] = useState(null);
    /** Кол-во удалённых игроков в комнате (для UI индикатора). */
    const [mpRemotePlayers, setMpRemotePlayers] = useState(0);
    /** Код комнаты для UI «🔑 ABC123». */
    const [mpRoomCode, setMpRoomCode] = useState('');

    const userId = (() => {
        try {
            const t = localStorage.getItem('Authorization');
            if (!t) return null;
            const p = jwtDecode(t);
            return p?.id || p?.user_id || null;
        } catch (e) { return null; }
    })();

    // === Инициализация сцены и загрузка проекта ===
    useEffect(() => {
        if (!canvasRef.current) return;
        // Dev-logging: на localhost console.* шлётся в devlog.txt
        // для разработки. На проде запросы тихо игнорируются.
        try { devlogReset(); attachConsoleHook(); } catch (e) {}
        const scene = new BabylonScene(canvasRef.current);
        scene.init();
        // Тач-режим включаем ДО enterPlayMode — чтобы PlayerController
        // создался без pointer-lock. На десктопе оставляем false.
        try { scene.setTouchMode?.(isTouch); } catch (e) {}
        sceneRef.current = scene;

        // В плеере пол редактора не нужен — пользовательская сцена
        // принесёт свой пол / блоки; сразу скрываем editorGround.
        try { scene.setFloorEnabled(false); } catch (e) {}

        // В плеере мы не редактируем — отключаем все инструменты редактора:
        // блоки, модели, эраза. Иначе случайный клик мог поставить блок в мир.
        scene._activeTool = 'select';
        scene._activeBlockType = null;
        scene._activeModelType = null;
        // Флаг для нашего кода — на случай если потребуется проверка позже
        scene._isPlayerOnly = true;

        // === User-models API: критично для рендера воксельных моделей ===
        // Без этих двух вызовов UserModelManager._api остаётся null и при
        // загрузке проекта _loadModelData() тихо возвращает null для каждой
        // воксельной модели в scene.userModels — игрок видит сцену БЕЗ
        // воксельных моделей (кристаллы, животные, артефакты и т.д.).
        // В редакторе это делается в KubikonEditor.jsx сразу после init().
        scene.setUserModelsApi(Kubikon3DApi);
        scene.setCurrentUserId(userId);
        // projectId нужен для game.save.* (универсальные сейвы).
        if (projectId) scene.setCurrentProjectId(projectId);
        // game.hud.setVisible(false) скроет HP-бар/hotbar для своего меню
        scene.setOnStdHudVisibilityChange?.((v) => setStdHudVisible(v));
        // Задача 03: отдельные подписки
        scene.setOnHotbarVisibilityChange?.((v) => setHotbarVisible(v));
        scene.setOnHpVisibilityChange?.((v) => setHpVisible(v));

        // Колбэки HUD
        scene.setOnPlayerHpChange?.((h) => {
            setHp({ hp: h.hp, maxHp: h.maxHp });
            if (h.damaged) setHurtFlash(Date.now());
        });
        scene.setOnPlayerDeath?.(() => {
            setHp({ hp: 0, maxHp: 100 });
            setTimeout(() => {
                const s = sceneRef.current;
                if (!s?.player) return;
                s.player.healFull?.();
                if (s.player._pos && s._spawnPoint) {
                    const sp = s._spawnPoint;
                    const halfH = s.player.HALF_H ?? 0.9;
                    s.player._pos.set(sp.x, sp.y + halfH + 0.2, sp.z);
                    s.player._vy = 0;
                }
            }, 2000);
        });
        scene.setOnAmmoChange?.((a) => setAmmo(a));
        scene.setOnInventoryChange?.(() =>
            setInventoryState(scene.getInventoryState?.() || { slots: [], activeIndex: 0 })
        );
        scene.setOnGuiChange?.(() => setGuiList(scene.getGuiElements?.() || []));
        scene.setOnScriptHud?.((event) => hudRef.current?.handle?.(event));
        // === Таймер прохождения для лидерборда ===
        scene.setOnTimer?.(({ state, timeMs }) => {
            // Если скрипт зовёт timer — игра поддерживает таблицу лидеров
            if (state === 'start' || state === 'submit' || state === 'stop') {
                setLeaderboardEnabled(true);
            }
            if (state === 'start') {
                setTimerMs(0);
                setTimerRunning(true);
            } else if (state === 'stop') {
                setTimerMs(timeMs);
                setTimerRunning(false);
            } else if (state === 'submit') {
                setTimerMs(timeMs);
                setTimerRunning(false);
                // Шлём результат на сервер. В ответе бэк возвращает
                // rating_awards = [{user_id, place, rating_amount}, ...] —
                // если попал в топ-3 (и раньше за это место рейтинг не получал),
                // показываем тост и обновляем глобальный счётчик профиля.
                if (userId && projectId && timeMs > 0) {
                    Kubikon3DApi.submitLeaderboard(projectId, userId, timeMs)
                        .then((res) => {
                            setLeaderboardRefreshKey(k => k + 1);
                            const awards = res?.data?.rating_awards;
                            if (Array.isArray(awards)) {
                                const mine = awards.find(
                                    (a) => Number(a.user_id) === Number(userId)
                                );
                                if (mine && mine.rating_amount > 0) {
                                    setRatingToast({
                                        place: mine.place,
                                        amount: mine.rating_amount,
                                    });
                                    // авто-скрытие через 6 сек
                                    setTimeout(() => setRatingToast(null), 6000);
                                }
                            }
                        })
                        .catch((e) => console.warn('[leaderboard] submit failed', e));
                }
            }
        });
        scene.setOnPlayChange?.((playing) => {
            setIsPlaying(playing);
            // ESC обрабатывается через pointerlockchange-перехват в плеере
            // (см. отдельный useEffect ниже). Сюда мы попадаем только если
            // exitPlayMode вызвался по другой причине — тогда просто открываем
            // меню, чтобы пользователь мог выйти/вернуться, и пересоздаём Play
            // в UI-cursor режиме.
            if (!playing) {
                setTimeout(() => {
                    const s = sceneRef.current;
                    if (!s) return;
                    s.enterPlayMode?.();
                    s.player?.setUiCursorMode?.(true);
                }, 30);
                setChatOpen(false);
                setTopMenuOpen(true);
            }
        });

        // Загружаем проект
        (async () => {
            try {
                const res = await Kubikon3DApi.getProjectForPlay(projectId, userId);
                const data = res.data;
                setMeta(data);
                setLikesCount(data.likes_count || 0);
                setDislikesCount(data.dislikes_count || 0);

                if (data.project_data) {
                    const parsed = JSON.parse(data.project_data);
                    initialStateRef.current = parsed;
                    await scene.loadFromState(parsed);
                }

                // Ждём пока Babylon реально загрузит и скомпилит все
                // материалы/текстуры/GLB. Без этого при первом кадре
                // половина мешей рисуется без текстур (chess-pattern fallback)
                // и постепенно подтягивается — игрок видит «пустых зомби».
                await new Promise((resolve) => {
                    if (!scene.scene) { resolve(); return; }
                    let done = false;
                    const finish = () => { if (!done) { done = true; resolve(); } };
                    scene.scene.executeWhenReady(finish);
                    // Safety-таймаут: 3с максимум, чтобы не залип в loading навсегда
                    // если что-то не загрузилось.
                    setTimeout(finish, 3000);
                });

                // === Персональный скин игрока ===
                // Грузим выбранный скин из БД (rublox_equipped_skin) и
                // применяем его к локальному игроку ДО enterPlayMode —
                // тогда player.setModelType подхватит правильный скин.
                // Этот же skinFolder уйдёт в мультиплеер как modelType,
                // чтобы соперники видели наш реальный скин.
                let mySkin = 'skin_bacon-hair';
                if (userId) {
                    try {
                        const skinRes = await Kubikon3DApi.getEquippedSkin(userId);
                        const sf = skinRes?.data?.skin_folder;
                        if (sf && typeof sf === 'string') mySkin = sf;
                    } catch (e) {
                        // Сеть/ошибка — играем с дефолтным скином, не блокируем.
                        console.warn('[KubikonPlayer] equipped-skin load failed', e);
                    }
                }
                skinFolderRef.current = mySkin;
                try { scene.setPlayerModelType?.(mySkin); } catch (e) {}

                setLoading(false);
                // Засчитываем плей. Передаём user_id (если залогинен) —
                // активирует self-cooldown и user-cooldown на бэке.
                Kubikon3DApi.incrementPlay(projectId, userId).catch(() => {});
                // Запускаем игру сразу
                setTimeout(() => {
                    scene.enterPlayMode?.();
                    // Если игра мультиплеерная — подключаемся к realtime-серверу.
                    // Делаем после enterPlayMode, чтобы scene.player уже был создан.
                    if (data.multiplayer) {
                        setTimeout(() => connectMultiplayer(data), 300);
                    }
                }, 200);
            } catch (e) {
                if (e?.response?.status === 403) {
                    setForbidden(true);
                } else {
                    setError(e?.message || 'Ошибка загрузки');
                }
                setLoading(false);
            }
        })();

        // Проверим голос пользователя (like/dislike)
        if (userId) {
            Kubikon3DApi.getLikeStatus(projectId, userId)
                .then(r => {
                    setVote(r.data?.vote || null);
                    setLiked(r.data?.vote === 'like');
                })
                .catch(() => {});
        }

        return () => {
            // Сначала рвём мультиплеер — иначе sync будет тыкаться в disposed scene
            try { scene.setMultiplayerSync?.(null); } catch (e) {}
            if (mpSyncRef.current) {
                try { mpSyncRef.current.dispose(); } catch (e) {}
                if (mpSyncRef.current._countTimer) {
                    clearInterval(mpSyncRef.current._countTimer);
                }
                mpSyncRef.current = null;
            }
            if (roomRef.current) {
                try { roomRef.current.leave(true); } catch (e) {}
                roomRef.current = null;
            }
            try { scene.dispose(); } catch (e) {}
            sceneRef.current = null;
        };
    }, [projectId, userId, navigate]);

    // session_id живёт в течение одной игровой сессии; шлём его в heartbeat
    const sessionId = useMemo(() => {
        const a = new Uint32Array(4);
        (window.crypto || window.msCrypto)?.getRandomValues?.(a);
        return Array.from(a).map(x => x.toString(16).padStart(8, '0')).join('');
    }, []);

    // Heartbeat каждые 30 секунд для онлайн-метрики админки.
    useEffect(() => {
        if (!projectId || loading) return;
        let alive = true;
        const beat = () => {
            if (!alive) return;
            Kubikon3DApi.playHeartbeat(sessionId, projectId, userId).catch(() => {});
        };
        beat();   // первый сразу после загрузки
        const t = setInterval(beat, 30_000);
        return () => { alive = false; clearInterval(t); };
    }, [projectId, userId, sessionId, loading]);

    // Хоткеи 1-5 для слотов инвентаря.
    // Babylon ловит ввод на canvas — слушаем в capture-phase на window
    // и не привязываемся к isPlaying (state-флаг может быть ещё false на старте).
    useEffect(() => {
        const onKey = (e) => {
            if (e.repeat) return;
            const code = e.code || '';
            if (code.startsWith('Digit')) {
                const n = parseInt(code.slice(5), 10);
                if (n >= 1 && n <= 5) sceneRef.current?.setActiveInventorySlot?.(n - 1);
            } else if (code.startsWith('Numpad') && code.length === 7) {
                const n = parseInt(code.slice(6), 10);
                if (n >= 1 && n <= 5) sceneRef.current?.setActiveInventorySlot?.(n - 1);
            }
        };
        window.addEventListener('keydown', onKey, true);
        return () => window.removeEventListener('keydown', onKey, true);
    }, []);

    // ESC во время игры — открыть меню (без выхода из Play).
    //
    // Тонкость: в большинстве браузеров ESC, нажатый при активном
    // pointer-lock'е, обрабатывается нативно — снимает lock БЕЗ keydown
    // (или с keydown позже pointerlockchange). Поэтому keydown-listener
    // не успевает выставить _uiCursorMode перед pointerlockchange.
    //
    // Решение: слушаем сам pointerlockchange. Когда lock теряется во время
    // активной игры — это значит игрок нажал ESC, и нам нужно:
    //   1) пометить player как UI-cursor mode (чтобы Babylon не сделал
    //      exitPlayMode → respawn),
    //   2) открыть наше меню.
    // Этот listener должен быть в capture-фазе, чтобы сработать раньше
    // listener'а PlayerController.
    useEffect(() => {
        const onLockChange = () => {
            const s = sceneRef.current;
            if (!s || !s._isPlaying) return;
            const locked = !!document.pointerLockElement;
            // Lock потерян, мы НЕ в UI-cursor mode → пользователь нажал ESC
            if (!locked && s.player && !s.player._uiCursorMode) {
                // Синхронно ставим флаг — listener PlayerController сработает
                // следующим и увидит true, не вызовет _onExitRequest.
                s.player._uiCursorMode = true;
                // Открываем меню в следующий тик (state-update React)
                setChatOpen(false);
                setTopMenuOpen(true);
            }
        };
        // capture-фаза, чтобы успеть раньше PlayerController
        document.addEventListener('pointerlockchange', onLockChange, true);
        return () => document.removeEventListener('pointerlockchange', onLockChange, true);
    }, []);

    // Повторный ESC (когда меню уже открыто) — закрыть меню и вернуть
    // мышь в игру.
    useEffect(() => {
        if (!topMenuOpen) return;
        const onEsc = (e) => {
            if (e.key !== 'Escape') return;
            const s = sceneRef.current;
            if (!s || !s._isPlaying) return;
            setTopMenuOpen(false);
            s.player?.setUiCursorMode?.(false);
        };
        window.addEventListener('keydown', onEsc, true);
        return () => window.removeEventListener('keydown', onEsc, true);
    }, [topMenuOpen]);

    /** Переродиться — телепорт на spawn point с восстановлением hp. */
    /**
     * Подключение к мультиплеерной комнате этой игры.
     * Если уже подключены — ничего не делаем.
     * При успехе создаём MultiplayerSync, который двусторонне синхронизирует
     * локального игрока (PlayerController) с сервером и рисует remote-игроков.
     *
     * Защита от race-conditions: если sceneRef ещё не готов или scene/player
     * пропали (HMR во время загрузки), пробуем ещё несколько раз с интервалом
     * 200мс, потом сдаёмся.
     */
    const connectMultiplayer = useCallback(async (projectMeta, retries = 8) => {
        if (mpSyncRef.current || roomRef.current) return;

        const sceneObj = sceneRef.current;
        const babylonScene = sceneObj?.scene;
        const player = sceneObj?.player;

        // Если что-то ещё не готово — повторяем попытку.
        if (!sceneObj || !babylonScene || !player) {
            if (retries > 0) {
                setTimeout(() => connectMultiplayer(projectMeta, retries - 1), 200);
            } else {
                console.warn('[mp] connect aborted — scene/player not ready');
                setMpStatus('failed');
                setMpError('Сцена не готова');
            }
            return;
        }

        const tokenRaw = localStorage.getItem('Authorization') || '';
        if (!tokenRaw) return;

        setMpStatus('connecting');
        setMpError(null);
        try {
            const client = new Client(REALTIME_WS);
            // modelType — ПЕРСОНАЛЬНЫЙ скин этого игрока (из rublox_equipped_skin,
            // загружен при старте в skinFolderRef). Сервер всё равно перепроверит
            // скин по userId из JWT и при расхождении возьмёт значение из БД —
            // так каждый игрок виден соперникам в своём реальном скине.
            const modelType = skinFolderRef.current || 'skin_bacon-hair';
            const room = await client.joinOrCreate('battle', {
                projectId: projectMeta?.id || projectId,
                token: tokenRaw,
                modelType,
            });
            roomRef.current = room;
            setMpRoomCode((room.roomId || '').slice(0, 6).toUpperCase());

            const sync = new MultiplayerSync(
                babylonScene,   // Babylon Scene (взяли в начале функции)
                room,
                () => {
                    // Колбэк-источник позиции локального игрока
                    const p = sceneRef.current?.player?._pos;
                    const yaw = sceneRef.current?.player?._yaw;
                    if (!p) return null;
                    // p.y — это центр игрока (HALF_H выше пола под ногами).
                    // Передаём как есть, на клиенте при рендере других уберём HALF_H.
                    return { x: p.x, y: p.y || 0, z: p.z, yaw: yaw || 0 };
                },
                {
                    onLocalHit: (damage, hpVal, maxHpVal) => {
                        // Применяем урон в локальный PlayerController.
                        // Сервер — авторитет, синхронизируем hp напрямую,
                        // не используя takeDamage (он для локальных зомби,
                        // имеет i-frames и считает свой hp — может разойтись
                        // с сервером и пропустить смерть).
                        const p = sceneRef.current?.player;
                        if (p) {
                            const wasDead = p.hp <= 0;
                            p.hp = hpVal;
                            p.maxHp = maxHpVal;
                            // Звук «ой»
                            try { p._playHurtSound?.(); } catch (e) {}
                            // Если только что умерли — debris + спрятать модель
                            if (!wasDead && hpVal <= 0) {
                                try { p._spawnDeathDebris?.(); } catch (e) {}
                                if (p._modelRoot) p._modelRoot.setEnabled(false);
                                if (p._onDeath) {
                                    try { p._onDeath(); } catch (e) {}
                                }
                            }
                        }
                        setHurtFlash(Date.now());
                        setHp({ hp: hpVal, maxHp: maxHpVal });
                    },
                    onKilled: (killerName) => {
                        console.log(`[mp] killed by ${killerName}`);
                    },
                    onRespawn: () => {
                        const s = sceneRef.current;
                        if (s?.player && s._spawnPoint) {
                            s.player.healFull?.();
                            const sp = s._spawnPoint;
                            const halfH = s.player.HALF_H ?? 0.9;
                            s.player._pos?.set?.(sp.x, sp.y + halfH + 0.2, sp.z);
                            if (s.player._vy != null) s.player._vy = 0;
                        }
                    },
                    onLog: (level, ...parts) => {
                        // eslint-disable-next-line no-console
                        console.log(`[mp:${level}]`, ...parts);
                    },
                },
            );
            sync.start();
            mpSyncRef.current = sync;
            // Прокидываем sync в сцену, чтобы _handlePlayClick шёл и в мультиплеер тоже.
            sceneRef.current?.setMultiplayerSync?.(sync);

            // === Локальная смерть → сервер ===
            // Когда наш PlayerController сам поставил hp=0 (от куклы Squid Game,
            // ловушки, скриптовой смерти и т.п.) — серверу надо сообщить, чтобы
            // он бродкастил 'kill' всем остальным. Иначе другие клиенты не
            // увидят рассыпание нашего скина на кубики.
            //
            // Сервер защищён от повторных вызовов (isDead-guard), поэтому если
            // умерли от выстрела — повторное 'die' проигнорируется.
            try {
                const p = sceneRef.current?.player;
                if (p && typeof p.setOnDeath === 'function') {
                    p.setOnDeath(() => {
                        try { room.send('die', {}); } catch (e) { /* room закрыта */ }
                    });
                }
            } catch (e) {
                console.warn('[mp] setOnDeath failed:', e);
            }

            // Обновляем счётчик remote-игроков для UI
            const updateCount = () => {
                setMpRemotePlayers(sync.remotePlayers.size);
            };
            // Простой таймер — раз в секунду
            const countTimer = setInterval(updateCount, 1000);
            sync._countTimer = countTimer;

            room.onLeave(() => {
                if (sync._countTimer) clearInterval(sync._countTimer);
                setMpStatus('idle');
                setMpRemotePlayers(0);
                setMpRoomCode('');
            });

            setMpStatus('connected');
        } catch (err) {
            console.error('[mp] connect error:', err);
            setMpStatus('failed');
            setMpError(err?.message || String(err));
        }
    }, [projectId]);

    /** Запросить fullscreen. Вызывается ИЗ user gesture (клик по кнопке),
     *  иначе браузер запретит. Orientation lock НЕ ставим — играть можно
     *  и в портрете, и в ландшафте. */
    const handleMobileStart = useCallback(async () => {
        const root = document.documentElement;
        const req = root.requestFullscreen
            || root.webkitRequestFullscreen
            || root.mozRequestFullScreen
            || root.msRequestFullscreen;
        if (req) {
            try { await req.call(root); } catch (e) { /* отменено */ }
        }
        setMobileStartTapped(true);
    }, []);

    // При выходе со страницы — снимаем fullscreen / orientation lock,
    // чтобы возврат в школу не остался залочен в landscape.
    useEffect(() => {
        return () => {
            try {
                if (document.fullscreenElement) document.exitFullscreen?.();
                else if (document.webkitFullscreenElement) document.webkitExitFullscreen?.();
            } catch (e) {}
            try { window.screen?.orientation?.unlock?.(); } catch (e) {}
        };
    }, []);

    const respawnPlayer = useCallback(() => {
        const s = sceneRef.current;
        if (s?.player && s._spawnPoint) {
            s.player.healFull?.();
            const sp = s._spawnPoint;
            const halfH = s.player.HALF_H ?? 0.9;
            s.player._pos?.set?.(sp.x, sp.y + halfH + 0.2, sp.z);
            if (s.player._vy != null) s.player._vy = 0;
        }
        // Возвращаем мышь в игру
        if (s) {
            setTimeout(() => {
                if (!s._isPlaying) s.enterPlayMode?.();
                s.player?.setUiCursorMode?.(false);
            }, 30);
        }
    }, []);

    const handleVote = useCallback(async (kind /* 'like' | 'dislike' */) => {
        if (!userId) {
            setNeedAuthModal(kind === 'dislike' ? 'оценивать игры' : 'лайкать игры');
            return;
        }
        try {
            const res = await Kubikon3DApi.toggleLike(projectId, userId, kind);
            setVote(res.data?.vote || null);
            setLiked(res.data?.vote === 'like');
            setLikesCount(res.data?.likes_count || 0);
            setDislikesCount(res.data?.dislikes_count || 0);
        } catch (e) { /* ignore */ }
    }, [projectId, userId]);

    const handleShare = useCallback(() => {
        const url = window.location.href;
        try { navigator.clipboard?.writeText(url); } catch (e) {}
        setInfoModal({
            icon: '🔗',
            title: 'Ссылка скопирована',
            text: url,
        });
    }, []);

    const handleReportClick = useCallback(() => {
        if (!userId) {
            setNeedAuthModal('оставлять жалобы');
            return;
        }
        setReportOpen(true);
    }, [userId]);

    if (forbidden) {
        return <CenteredCard
            icon="🔒"
            title="Игра недоступна"
            text="Эта игра ещё не одобрена модерацией или скрыта автором."
            onBack={() => navigate('/')}
        />;
    }
    if (error) {
        return <CenteredCard
            icon="⚠️"
            title="Ошибка"
            text={error}
            onBack={() => navigate('/')}
        />;
    }

    return (
        <div style={{
            position: 'fixed', inset: 0,
            background: '#070a14',
            zIndex: 1000,
            display: 'flex', flexDirection: 'column',
            fontFamily: HUD.font,
        }}>
            <style>{HUD_KEYFRAMES}</style>
            <div ref={viewportRef} style={{
                flex: 1, position: 'relative', minHeight: 0,
                background: '#070a14',
            }}>
                <canvas
                    ref={canvasRef}
                    style={{
                        width: '100%', height: '100%',
                        display: 'block',
                        outline: 'none',
                    }}
                />
                {/* Loading-оверлей */}
                {loading && (
                    <div style={{
                        position: 'absolute', inset: 0,
                        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
                        background:
                            'radial-gradient(ellipse at center, rgba(51,87,255,0.22) 0%, rgba(7,10,20,0.96) 60%)',
                        gap: 18, color: HUD.text,
                    }}>
                        <div style={{
                            position: 'relative',
                            animation: 'hudFloat 3s ease-in-out infinite',
                        }}>
                            <div style={{
                                position: 'absolute', inset: -10,
                                borderRadius: 20,
                                animation: 'hudPulseRing 1.6s ease-out infinite',
                            }} />
                            <RublocsLogo size={72} />
                        </div>
                        <div style={{
                            display: 'flex', alignItems: 'center', gap: 10,
                            fontSize: 15, fontWeight: 700, letterSpacing: 0.3,
                        }}>
                            <div style={{
                                width: 14, height: 14,
                                border: `2.5px solid ${HUD.accentBg}`,
                                borderTopColor: HUD.accent,
                                borderRadius: '50%',
                                animation: 'hudSpin 0.8s linear infinite',
                            }} />
                            Загрузка игры…
                        </div>
                        <div style={{
                            fontSize: 11, color: HUD.textDim,
                            textTransform: 'uppercase', letterSpacing: 1.4, fontWeight: 700,
                        }}>
                            Рублокс • 3D
                        </div>
                    </div>
                )}

                {/* Игровой UI (HUD, GUI, hotbar, прицел в первом лице) */}
                {!loading && (
                    <>
                        {/* Задача 04: модал-overlay (затемнение + spotlight mask). */}
                        <ModalOverlay scene={sceneRef.current} />
                        {/* Задача 07: встроенный магазин скинов (B / API). */}
                        <SkinShopOverlay scene={sceneRef.current} />
                        {/* HUD: на мобиле уменьшаем и сдвигаем компактно. */}
                        {isTouch ? (
                            <>
                                {/* HP-бар: вверху слева, scale 0.75 */}
                                <div data-mobile-hud="hp" style={{
                                    position: 'absolute', top: 0, left: 0,
                                    transform: 'scale(0.75)', transformOrigin: 'top left',
                                    pointerEvents: 'none', zIndex: 30,
                                }}>
                                    <PlayerHud
                                        visible={stdHudVisible && hpVisible}
                                        hp={hp.hp}
                                        maxHp={hp.maxHp}
                                        ammo={null}
                                        damaged={Date.now() - hurtFlash < 350}
                                    />
                                </div>
                                {/* Mini ammo block: правый верх. Большие цифры,
                                    кнопка R для перезарядки внизу. */}
                                {ammo && (
                                    <MobileAmmoBlock
                                        ammo={ammo}
                                        onReload={() => sceneRef.current?.weapons?.reload?.()}
                                    />
                                )}
                                {/* Hotbar — только если в инвентаре есть хоть
                                    один предмет. Пустой инвентарь не показываем. */}
                                {stdHudVisible && hotbarVisible && (inventoryState.slots || []).some(s => s) && (
                                    <Hotbar
                                        visible={stdHudVisible && hotbarVisible}
                                        mobileMode
                                        slots={inventoryState.slots}
                                        activeIndex={inventoryState.activeIndex}
                                        onSelect={(i) => sceneRef.current?.setActiveInventorySlot?.(i)}
                                    />
                                )}
                            </>
                        ) : (
                            <>
                                <PlayerHud
                                    visible={stdHudVisible && hpVisible}
                                    hp={hp.hp}
                                    maxHp={hp.maxHp}
                                    ammo={ammo}
                                    damaged={Date.now() - hurtFlash < 350}
                                />
                                {stdHudVisible && hotbarVisible && (inventoryState.slots || []).some(s => s) && (
                                    <Hotbar
                                        visible={stdHudVisible && hotbarVisible}
                                        slots={inventoryState.slots}
                                        activeIndex={inventoryState.activeIndex}
                                        onSelect={(i) => sceneRef.current?.setActiveInventorySlot?.(i)}
                                    />
                                )}
                            </>
                        )}
                        <GameHud visible={true} hudRef={hudRef} />
                        {/* Performance overlay — клавиша F или клик по бейджу.
                            Также фоном шлёт метрики на бэк раз в 5 сек. */}
                        <KubikonPerfOverlay
                            getScene={() => sceneRef.current}
                            projectId={projectId}
                            userId={userId}
                        />
                        {/* Тост-награда «+200 рейтинга за 1 место в таблице рекордов».
                            Появляется когда submitLeaderboard вернул rating_award
                            для текущего юзера (попадание в топ-3 впервые).
                            Авто-закрытие через 6 сек или клик. */}
                        {ratingToast && (
                            <RatingAwardToast
                                place={ratingToast.place}
                                amount={ratingToast.amount}
                                onClose={() => setRatingToast(null)}
                            />
                        )}

                        {/* Лидерборд (правый верхний угол) — Tab скрыть/показать.
                            Сама панель НЕ перехватывает клики/мышь (pointer-events: none),
                            чтобы не разблокировать курсор в Play-режиме.
                            Только кнопки внутри (✕ закрыть, 🔄 заново) кликабельны. */}
                        {leaderboardVisible && leaderboardEnabled && (
                            <div style={{
                                position: 'absolute',
                                top: 64, right: 12,
                                zIndex: 35,
                                pointerEvents: 'none',
                                animation: 'hudFadeIn 0.3s ease-out',
                                display: 'flex', flexDirection: 'column', gap: 8,
                                alignItems: 'flex-end',
                            }}>
                                <div style={{ pointerEvents: 'auto' }}>
                                    <KubikonLeaderboard
                                        projectId={projectId}
                                        limit={5}
                                        currentUserId={userId}
                                        currentTimeMs={timerRunning ? timerMs : null}
                                        onClose={() => setLeaderboardVisible(false)}
                                        compact={true}
                                        refreshKey={leaderboardRefreshKey}
                                        clickable={false}
                                        onLoaded={(records) => {
                                            // Если в проекте уже есть рекорды — игра-ранер,
                                            // включаем виджет даже если timer ещё не стартовал.
                                            if (records && records.length > 0) {
                                                setLeaderboardEnabled(true);
                                            }
                                        }}
                                    />
                                </div>
                                {/* Кнопка «Заново» — для попытки улучшить рекорд */}
                                <button
                                    onClick={handleRestartGame}
                                    title="Начать игру заново (сбросить состояние)"
                                    style={{
                                        background: 'linear-gradient(135deg, rgba(34,217,122,0.22), rgba(20,24,45,0.92))',
                                        border: '1px solid rgba(34, 217, 122, 0.55)',
                                        borderRadius: 10,
                                        color: '#22d97a',
                                        padding: '8px 14px',
                                        fontFamily: '"Roboto Condensed", system-ui, sans-serif',
                                        fontWeight: 700, fontSize: 14,
                                        cursor: 'pointer',
                                        backdropFilter: 'blur(6px)',
                                        boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
                                        transition: 'transform 0.12s ease',
                                        pointerEvents: 'auto',
                                    }}
                                    onMouseEnter={(e) => {
                                        e.currentTarget.style.transform = 'translateY(-2px)';
                                    }}
                                    onMouseLeave={(e) => {
                                        e.currentTarget.style.transform = 'translateY(0)';
                                    }}
                                >🔄 Заново</button>
                            </div>
                        )}
                        {/* Кнопки «Топ» и «Заново» когда лидерборд скрыт.
                            Размещены ниже компактного таймера в правом верхнем углу.
                            Кнопка «Топ» показывается только если игра поддерживает таймер. */}
                        {!leaderboardVisible && leaderboardEnabled && (
                            <div style={{
                                position: 'absolute',
                                top: timerRunning ? 60 : 18, right: 12,
                                zIndex: 35,
                                display: 'flex', flexDirection: 'column', gap: 8,
                                alignItems: 'flex-end',
                            }}>
                                <button
                                    onClick={() => setLeaderboardVisible(true)}
                                    title="Показать таблицу лидеров (Tab)"
                                    style={{
                                        background: 'rgba(20, 24, 45, 0.85)',
                                        border: '1px solid rgba(255, 215, 0, 0.55)',
                                        borderRadius: 10,
                                        color: '#ffd700',
                                        padding: '8px 14px',
                                        fontFamily: '"Roboto Condensed", system-ui, sans-serif',
                                        fontWeight: 700, fontSize: 14,
                                        cursor: 'pointer',
                                        backdropFilter: 'blur(6px)',
                                    }}
                                    onMouseEnter={e => { e.currentTarget.style.background = 'rgba(40, 44, 65, 0.95)'; }}
                                    onMouseLeave={e => { e.currentTarget.style.background = 'rgba(20, 24, 45, 0.85)'; }}
                                >
                                    🏆 Топ
                                </button>
                                <button
                                    onClick={handleRestartGame}
                                    title="Начать игру заново"
                                    style={{
                                        background: 'rgba(20, 24, 45, 0.85)',
                                        border: '1px solid rgba(34, 217, 122, 0.55)',
                                        borderRadius: 10,
                                        color: '#22d97a',
                                        padding: '8px 14px',
                                        fontFamily: '"Roboto Condensed", system-ui, sans-serif',
                                        fontWeight: 700, fontSize: 14,
                                        cursor: 'pointer',
                                        backdropFilter: 'blur(6px)',
                                    }}
                                    onMouseEnter={e => { e.currentTarget.style.background = 'rgba(40, 44, 65, 0.95)'; }}
                                    onMouseLeave={e => { e.currentTarget.style.background = 'rgba(20, 24, 45, 0.85)'; }}
                                >
                                    🔄 Заново
                                </button>
                            </div>
                        )}
                        {/* Компактный таймер справа сверху над кнопками — когда
                            таблица лидеров скрыта (иначе видно в самой таблице). */}
                        {timerRunning && !leaderboardVisible && (
                            <div style={{
                                position: 'absolute',
                                top: 18, right: 12,
                                zIndex: 35,
                                background: 'rgba(20, 24, 45, 0.92)',
                                border: '1px solid rgba(34, 217, 122, 0.55)',
                                borderRadius: 10,
                                color: '#22d97a',
                                padding: '6px 12px',
                                fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace',
                                fontWeight: 800, fontSize: 16,
                                pointerEvents: 'none',
                                backdropFilter: 'blur(6px)',
                            }}>
                                ⏱ {formatTimeMs(timerMs)}
                            </div>
                        )}
                        <GuiOverlay
                            elements={guiList}
                            isPlaying={true}
                            containerRef={viewportRef}
                            // 2026-05-27: без resolveAsset картинки GUI с
                            // imageAsset (вставленные через AssetManager) не
                            // показывались в плеере — el.imageAsset резолвился
                            // в null. Берём dataURL из assetManager сцены.
                            resolveAsset={(id) =>
                                sceneRef.current?.assetManager?.getDataUrl?.(id) || null}
                            onPlayClick={(gid) => {
                                const rt = sceneRef.current?.gameRuntime;
                                if (!rt) return;
                                rt.routeEvent({ kind: 'gui', id: gid }, 'click', {});
                                rt.routeGlobalEvent('guiClick', { id: gid });
                            }}
                        />
                        {/* Мобильное управление — на любых тач-устройствах,
                            и в портрете и в ландшафте (ранее был блок portrait,
                            убрали по фидбэку — играть можно как угодно). */}
                        {isTouch && (
                            <KubikonMobileControls getScene={() => sceneRef.current} />
                        )}
                    </>
                )}
                {/* Кнопка «полный экран» — маленькая, в правом верхнем углу,
                    только на тач-устройствах. Браузеры требуют user gesture
                    для requestFullscreen() — поэтому без кнопки никак.
                    Кнопка автоматически скрывается после входа в fullscreen. */}
                {isTouch && !loading && !document.fullscreenElement && (
                    <button
                        data-mobile-hud="fullscreen"
                        onClick={handleMobileStart}
                        style={{
                            position: 'absolute',
                            top: 8, right: 8,
                            width: 36, height: 36,
                            borderRadius: 8,
                            background: 'rgba(15, 19, 38, 0.78)',
                            border: '1px solid rgba(255, 255, 255, 0.18)',
                            color: '#fff',
                            cursor: 'pointer',
                            zIndex: 1100,
                            display: 'flex', alignItems: 'center', justifyContent: 'center',
                            padding: 0,
                            WebkitTapHighlightColor: 'transparent',
                        }}
                        title="Полноэкранный режим"
                    >
                        <svg width="20" height="20" viewBox="0 0 24 24" fill="none">
                            <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"
                                stroke="#fff" strokeWidth="2.2"
                                strokeLinecap="round" strokeLinejoin="round" />
                        </svg>
                    </button>
                )}
            </div>

            {/* === Floating top bar (слева вверху, под HP-баром) === */}
            {!loading && (
                <div data-mobile-hud="topbar" style={{
                    position: 'absolute',
                    // На мобиле HP-бар сжат scale(0.75) занимает примерно 60px
                    // высоты в верху. Кладём top-bar НИЖЕ него, чтобы не перекрывал.
                    top: isTouch ? 60 : 96,
                    left: isTouch ? 8 : 14,
                    zIndex: 1100,
                    display: 'flex', gap: isTouch ? 6 : 8, alignItems: 'flex-start',
                    animation: 'hudFadeIn 320ms ease',
                    transform: isTouch ? 'scale(0.85)' : 'none',
                    transformOrigin: 'top left',
                }}>
                    {/* Бургер-меню — выпадашка с действиями */}
                    <div style={{ position: 'relative' }}>
                        <button
                            data-kubikon-top-menu-btn
                            onClick={() => setTopMenuOpen(v => {
                                if (!v) setChatOpen(false);
                                return !v;
                            })}
                            style={{
                                ...topbarBtn,
                                background: topMenuOpen
                                    ? HUD.gradientBrand
                                    : HUD.bgGlass,
                                color: HUD.text,
                                borderColor: topMenuOpen ? 'transparent' : HUD.border,
                                boxShadow: topMenuOpen
                                    ? '0 8px 24px rgba(51,87,255,0.45)'
                                    : '0 2px 8px rgba(0,0,0,0.4)',
                            }}
                            title="Меню"
                        >
                            <span style={{ fontSize: 16, marginRight: 6 }}>☰</span>
                            Меню
                        </button>
                        {topMenuOpen && (
                            <TopDropdown
                                meta={meta}
                                vote={vote}
                                likes={likesCount}
                                dislikes={dislikesCount}
                                isTouch={isTouch}
                                onClose={() => setTopMenuOpen(false)}
                                onLike={() => { handleVote('like'); }}
                                onDislike={() => { handleVote('dislike'); }}
                                onShare={() => { setTopMenuOpen(false); handleShare(); }}
                                onReport={() => { setTopMenuOpen(false); handleReportClick(); }}
                                onBugReport={() => {
                                    setTopMenuOpen(false);
                                    document.querySelector('[data-kubikon-bug-btn]')?.click();
                                }}
                                onRespawn={() => { setTopMenuOpen(false); respawnPlayer(); }}
                                onExit={() => navigate('/')}
                            />
                        )}
                    </div>

                    {/* Toggle чата */}
                    <button
                        onClick={() => setChatOpen(v => {
                            if (!v) setTopMenuOpen(false);
                            return !v;
                        })}
                        style={{
                            ...topbarBtn,
                            padding: '8px 12px',
                            background: chatOpen
                                ? 'linear-gradient(135deg, #22d97a 0%, #0f9d56 100%)'
                                : HUD.bgGlass,
                            color: HUD.text,
                            borderColor: chatOpen ? 'transparent' : HUD.border,
                            boxShadow: chatOpen
                                ? '0 8px 24px rgba(34,217,122,0.40)'
                                : '0 2px 8px rgba(0,0,0,0.4)',
                        }}
                        title="Показать/скрыть чат"
                    >
                        <span style={{ fontSize: 16 }}>💬</span>
                    </button>

                    {/* Мультиплеер-индикатор (только если игра multiplayer=true) */}
                    {meta?.multiplayer && (
                        <div style={{
                            ...topbarBtn,
                            cursor: 'default',
                            background: mpStatus === 'connected'
                                ? 'linear-gradient(135deg, #22d97a 0%, #0f9d56 100%)'
                                : mpStatus === 'failed'
                                    ? 'linear-gradient(135deg, #ff6f7a 0%, #c0303f 100%)'
                                    : HUD.bgGlass,
                            color: HUD.text,
                            borderColor: mpStatus === 'connected' || mpStatus === 'failed'
                                ? 'transparent'
                                : HUD.border,
                            display: 'inline-flex', alignItems: 'center', gap: 6,
                        }}
                        title={
                            mpStatus === 'connected'
                                ? `Мультиплеер активен · комната ${mpRoomCode}`
                                : mpStatus === 'connecting'
                                    ? 'Подключаемся к мультиплееру…'
                                    : mpStatus === 'failed'
                                        ? `Мультиплеер не подключён: ${mpError || ''}`
                                        : 'Мультиплеер'
                        }>
                            <span style={{ fontSize: 14 }}>🌐</span>
                            <span>
                                {mpStatus === 'connected' && (
                                    <>
                                        <span style={{ fontWeight: 800 }}>{mpRemotePlayers + 1}</span>
                                        {mpRoomCode && (
                                            <span style={{
                                                marginLeft: 6, fontSize: 10,
                                                background: 'rgba(0,0,0,0.20)',
                                                padding: '1px 6px',
                                                borderRadius: 999,
                                                fontFamily: 'monospace',
                                                letterSpacing: 1,
                                            }}>{mpRoomCode}</span>
                                        )}
                                    </>
                                )}
                                {mpStatus === 'connecting' && '…'}
                                {mpStatus === 'failed' && 'offline'}
                                {mpStatus === 'idle' && '—'}
                            </span>
                        </div>
                    )}
                </div>
            )}

            {/* === Компактный чат слева-сверху (под top bar) ===
                Держим компонент СМОНТИРОВАННЫМ всегда (после loading) и
                скрываем через CSS — иначе при unmount теряется история
                сообщений и WS-подписка на 'chat' слетает с realtime-room.
                Когда chatOpen=false — visibility:hidden, pointer-events:none. */}
            {!loading && (
                <div style={{
                    visibility: chatOpen ? 'visible' : 'hidden',
                    pointerEvents: chatOpen ? 'auto' : 'none',
                }}>
                    <KubikonChatPanel
                        projectId={projectId}
                        compact
                        mobileMode={isTouch}
                        onClose={() => setChatOpen(false)}
                        onRequestAuth={(action) => {
                            setChatOpen(false);
                            setNeedAuthModal(action);
                        }}
                        /* Если игра мультиплеерная и мы подключены — чат идёт
                           через realtime-room (видят только игроки в этой комнате).
                           Иначе fallback к storys-чату игры. */
                        realtimeRoom={mpStatus === 'connected' ? roomRef.current : null}
                    />
                </div>
            )}

            {reportOpen && (
                <ReportModal
                    onClose={() => setReportOpen(false)}
                    onSubmit={async ({ category, text }) => {
                        if (!userId) {
                            setReportOpen(false);
                            setNeedAuthModal('оставлять жалобы');
                            return;
                        }
                        await Kubikon3DApi.createReport({
                            reporter_user_id: userId,
                            target_type: 'project',
                            target_id: projectId,
                            category,
                            text,
                        });
                        setReportOpen(false);
                        setInfoModal({
                            icon: '✅',
                            title: 'Жалоба отправлена',
                            text: 'Модераторы рассмотрят её в ближайшее время. Спасибо за внимательность!',
                        });
                    }}
                />
            )}

            {infoModal && (
                <InfoModal
                    icon={infoModal.icon}
                    title={infoModal.title}
                    text={infoModal.text}
                    onClose={() => setInfoModal(null)}
                />
            )}

            {needAuthModal && (
                <NeedAuthModal
                    action={needAuthModal}
                    onClose={() => setNeedAuthModal(null)}
                    onLogin={() => navigate('/auth')}
                />
            )}


            {/* Плавающая кнопка баг-репорта (открывается из ESC-меню).
                Сама кнопка скрыта — данные нужны только её модалке. */}
            {!loading && (
                <KubikonBugReportButton
                    bugType="player"
                    projectId={projectId}
                    bottomOffset={70}
                    rightOffset={16}
                    hidden
                />
            )}
        </div>
    );
};

const btn = {
    padding: '8px 16px',
    background: HUD.bgInput,
    border: `1px solid ${HUD.border}`,
    borderRadius: 8,
    color: HUD.text,
    cursor: 'pointer',
    fontSize: 13,
    fontWeight: 600,
    fontFamily: HUD.font,
    whiteSpace: 'nowrap',
    transition: 'all 150ms ease',
};

const btnPrimary = {
    ...btn,
    background: HUD.gradientBrand,
    border: '1px solid transparent',
    color: '#fff',
    fontWeight: 700,
    boxShadow: '0 6px 16px rgba(51,87,255,0.32)',
};

const btnDanger = {
    ...btn,
    background: 'linear-gradient(135deg, #ff6f7a 0%, #c0303f 100%)',
    border: '1px solid transparent',
    color: '#fff',
    fontWeight: 700,
    boxShadow: '0 6px 16px rgba(255,111,122,0.30)',
};

/** Кнопка в верхней плашке плеера. */
const topbarBtn = {
    padding: '8px 14px',
    background: HUD.bgGlass,
    border: `1px solid ${HUD.border}`,
    borderRadius: 999,
    color: HUD.text,
    cursor: 'pointer',
    fontSize: 13, fontWeight: 700,
    fontFamily: HUD.font,
    whiteSpace: 'nowrap',
    boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
    backdropFilter: 'blur(20px)',
    WebkitBackdropFilter: 'blur(20px)',
    display: 'inline-flex', alignItems: 'center',
    transition: 'all 200ms ease',
    letterSpacing: 0.2,
};

// === Тост «+N рейтинга за место в таблице рекордов» ===
//
// Показывается когда submitLeaderboard вернул rating_award для текущего
// пользователя (попадание в топ-3 впервые на этом проекте).
// Бэкенд PLACE_RATING = {1: 200, 2: 150, 3: 100} в Kubikon3D.py.
const RatingAwardToast = ({ place, amount, onClose }) => {
    const meta = {
        1: { medal: '🥇', label: '1 МЕСТО', accent: '#ffd84a', glow: 'rgba(255, 216, 74, 0.55)' },
        2: { medal: '🥈', label: '2 МЕСТО', accent: '#cfd8dc', glow: 'rgba(207, 216, 220, 0.55)' },
        3: { medal: '🥉', label: '3 МЕСТО', accent: '#d29066', glow: 'rgba(210, 144, 102, 0.55)' },
    }[place] || { medal: '🏅', label: 'ТОП-3', accent: '#ffd84a', glow: 'rgba(255, 216, 74, 0.55)' };

    return (
        <div
            onClick={onClose}
            style={{
                position: 'absolute',
                top: 80,
                left: '50%',
                transform: 'translateX(-50%)',
                zIndex: 60,
                pointerEvents: 'auto',
                cursor: 'pointer',
                background: 'linear-gradient(135deg, rgba(15,22,40,0.96) 0%, rgba(28,40,72,0.96) 100%)',
                border: `2px solid ${meta.accent}`,
                borderRadius: 12,
                padding: '14px 22px',
                display: 'flex', alignItems: 'center', gap: 14,
                boxShadow: `0 12px 40px ${meta.glow}, 0 0 0 1px rgba(255,255,255,0.05) inset`,
                animation: 'hudFadeIn 0.45s ease-out',
                fontFamily: HUD.font,
                color: '#fff',
                minWidth: 280,
                maxWidth: 'calc(100vw - 40px)',
            }}
            title="Кликни чтобы закрыть"
        >
            <div style={{
                fontSize: 42, lineHeight: 1, flexShrink: 0,
                filter: `drop-shadow(0 0 8px ${meta.glow})`,
            }}>
                {meta.medal}
            </div>
            <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{
                    fontSize: 11, fontWeight: 800,
                    color: meta.accent, letterSpacing: 1.5,
                    textTransform: 'uppercase',
                }}>
                    {meta.label} В ТАБЛИЦЕ РЕКОРДОВ
                </div>
                <div style={{
                    fontSize: 22, fontWeight: 900,
                    marginTop: 3,
                    background: `linear-gradient(90deg, ${meta.accent}, #fff)`,
                    WebkitBackgroundClip: 'text',
                    WebkitTextFillColor: 'transparent',
                    backgroundClip: 'text',
                }}>
                    +{amount} рейтинга
                </div>
                <div style={{
                    fontSize: 11, opacity: 0.7, marginTop: 2,
                }}>
                    зачислено в твой профиль
                </div>
            </div>
        </div>
    );
};


const CenteredCard = ({ icon, title, text, onBack }) => (
    <div style={{
        position: 'fixed', inset: 0,
        background:
            'radial-gradient(ellipse at center, rgba(51,87,255,0.18) 0%, #070a14 60%)',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        flexDirection: 'column', gap: 18,
        color: HUD.text,
        fontFamily: HUD.font,
        padding: 24,
    }}>
        <style>{HUD_KEYFRAMES}</style>
        <div style={{
            fontSize: 76,
            animation: 'hudFloat 3.6s ease-in-out infinite',
            filter: 'drop-shadow(0 8px 24px rgba(0,0,0,0.4))',
        }}>{icon}</div>
        <div style={{
            fontSize: 26, fontWeight: 800, letterSpacing: -0.3,
        }}>{title}</div>
        <div style={{
            fontSize: 14, color: HUD.textMuted, maxWidth: 440,
            textAlign: 'center', lineHeight: 1.55,
        }}>{text}</div>
        <button
            onClick={onBack}
            style={btnPrimary}
            onMouseEnter={e => {
                e.currentTarget.style.transform = 'translateY(-2px)';
                e.currentTarget.style.boxShadow = '0 12px 28px rgba(51,87,255,0.45)';
            }}
            onMouseLeave={e => {
                e.currentTarget.style.transform = 'translateY(0)';
                e.currentTarget.style.boxShadow = '0 6px 16px rgba(51,87,255,0.32)';
            }}
        >
            ← В ленту Рублокса
        </button>
    </div>
);

const REPORT_CATEGORIES = [
    { id: 'profanity', label: 'Мат / нецензурная лексика' },
    { id: 'inappropriate', label: 'Неподходящий контент / возраст' },
    { id: 'ad', label: 'Реклама / спам' },
    { id: 'rights', label: 'Нарушение авторских прав' },
    { id: 'broken', label: 'Игра сломана / не работает' },
    { id: 'other', label: 'Другое' },
];

const ReportModal = ({ onClose, onSubmit }) => {
    const [category, setCategory] = useState('inappropriate');
    const [text, setText] = useState('');
    const [submitting, setSubmitting] = useState(false);

    const [submitError, setSubmitError] = useState(null);

    const submit = async () => {
        setSubmitting(true);
        setSubmitError(null);
        try {
            await onSubmit({ category, text });
        } catch (e) {
            setSubmitError(e?.message || 'Неизвестная ошибка');
            setSubmitting(false);
        }
    };

    return (
        <div onClick={onClose} style={modalBackdrop}>
            <style>{HUD_KEYFRAMES}</style>
            <div onClick={(e) => e.stopPropagation()} style={{
                ...modalCard, maxWidth: 480,
            }}>
                {/* Градиентный заголовок */}
                <div style={{
                    background: HUD.gradientBrand,
                    padding: '20px 24px',
                    display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                    gap: 12,
                }}>
                    <div style={{
                        display: 'inline-flex', alignItems: 'center', gap: 12,
                    }}>
                        <div style={{
                            width: 40, height: 40, borderRadius: 12,
                            background: 'rgba(255,255,255,0.18)',
                            display: 'flex', alignItems: 'center', justifyContent: 'center',
                            fontSize: 22,
                        }}>🚩</div>
                        <h2 style={{
                            margin: 0, fontSize: 18, fontWeight: 800, color: '#fff',
                            letterSpacing: -0.3,
                        }}>Пожаловаться на игру</h2>
                    </div>
                    <button onClick={onClose} style={iconCloseBtn}>×</button>
                </div>

                <div style={{ padding: 22 }}>
                    <div style={{ marginBottom: 14 }}>
                        <div style={fieldLabel}>Категория</div>
                        <select
                            value={category}
                            onChange={(e) => setCategory(e.target.value)}
                            style={fieldInput}
                        >
                            {REPORT_CATEGORIES.map(c =>
                                <option key={c.id} value={c.id} style={{ background: '#0f1326' }}>
                                    {c.label}
                                </option>
                            )}
                        </select>
                    </div>
                    <div style={{ marginBottom: 14 }}>
                        <div style={fieldLabel}>Подробности (необязательно)</div>
                        <textarea
                            value={text}
                            onChange={(e) => setText(e.target.value)}
                            placeholder="Опиши, что не так…"
                            rows={4}
                            style={{
                                ...fieldInput,
                                resize: 'vertical',
                                lineHeight: 1.5,
                            }}
                        />
                    </div>
                    {submitError && (
                        <div style={{
                            background: HUD.dangerBg,
                            border: `1px solid ${HUD.danger}`,
                            borderRadius: 10,
                            padding: '10px 14px',
                            marginBottom: 14,
                            color: HUD.danger, fontSize: 13, fontWeight: 600,
                        }}>
                            ⚠️ {submitError}
                        </div>
                    )}
                    <div style={{ display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
                        <button onClick={onClose} style={btn} disabled={submitting}>
                            Отмена
                        </button>
                        <button
                            onClick={submit}
                            disabled={submitting}
                            style={btnDanger}
                        >
                            {submitting ? 'Отправка…' : '🚩 Отправить'}
                        </button>
                    </div>
                </div>
            </div>
        </div>
    );
};

const InfoModal = ({ icon, title, text, onClose }) => (
    <div onClick={onClose} style={modalBackdrop}>
        <style>{HUD_KEYFRAMES}</style>
        <div onClick={(e) => e.stopPropagation()} style={{
            ...modalCard, maxWidth: 420, textAlign: 'center', padding: '32px 28px',
        }}>
            <div style={{
                fontSize: 56, marginBottom: 14,
                animation: 'hudFloat 3s ease-in-out infinite',
            }}>{icon}</div>
            <div style={{
                fontSize: 20, fontWeight: 800, color: HUD.text, marginBottom: 10,
                letterSpacing: -0.3,
            }}>
                {title}
            </div>
            {text && (
                <div style={{
                    fontSize: 13, color: HUD.textMuted, marginBottom: 22,
                    wordBreak: 'break-all', lineHeight: 1.5,
                    background: HUD.bgInput,
                    border: `1px solid ${HUD.border}`,
                    borderRadius: 10,
                    padding: '10px 14px',
                }}>
                    {text}
                </div>
            )}
            <button onClick={onClose} style={{ ...btnPrimary, minWidth: 140 }}>
                Закрыть
            </button>
        </div>
    </div>
);

const NeedAuthModal = ({ action, onClose, onLogin }) => (
    <div onClick={onClose} style={modalBackdrop}>
        <style>{HUD_KEYFRAMES}</style>
        <div onClick={(e) => e.stopPropagation()} style={{
            ...modalCard, maxWidth: 420, textAlign: 'center', padding: '32px 28px',
        }}>
            <div style={{
                fontSize: 56, marginBottom: 14,
                animation: 'hudFloat 3s ease-in-out infinite',
            }}>🔒</div>
            <div style={{
                fontSize: 20, fontWeight: 800, color: HUD.text, marginBottom: 10,
                letterSpacing: -0.3,
            }}>
                Нужна авторизация
            </div>
            <div style={{
                fontSize: 14, color: HUD.textMuted, marginBottom: 22, lineHeight: 1.5,
            }}>
                Чтобы {action}, войдите в аккаунт Рублокса.
            </div>
            <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
                <button onClick={onClose} style={btn}>Отмена</button>
                <button onClick={onLogin} style={btnPrimary}>
                    🚀 Войти
                </button>
            </div>
        </div>
    </div>
);

const modalBackdrop = {
    position: 'fixed', inset: 0, zIndex: 2100,
    background: 'rgba(7,10,20,0.78)',
    backdropFilter: 'blur(8px)',
    WebkitBackdropFilter: 'blur(8px)',
    display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
    fontFamily: HUD.font,
    animation: 'hudFadeIn 200ms ease',
};

const modalCard = {
    background: HUD.bgPanel,
    border: `1px solid ${HUD.border}`,
    borderRadius: 18,
    width: '100%',
    color: HUD.text,
    overflow: 'hidden',
    boxShadow: '0 24px 60px rgba(0,0,0,0.55), 0 0 0 1px rgba(51,87,255,0.08)',
    animation: 'hudFadeInScale 240ms cubic-bezier(0.34, 1.56, 0.64, 1)',
};

const iconCloseBtn = {
    width: 32, height: 32,
    border: '1px solid rgba(255,255,255,0.25)',
    background: 'rgba(255,255,255,0.10)',
    color: '#fff', borderRadius: 8,
    fontSize: 20, fontWeight: 700, lineHeight: 1,
    cursor: 'pointer', fontFamily: HUD.font,
    display: 'flex', alignItems: 'center', justifyContent: 'center',
    transition: 'all 150ms ease',
};

const fieldLabel = {
    fontSize: 11, color: HUD.textMuted, marginBottom: 6,
    textTransform: 'uppercase', letterSpacing: 0.8, fontWeight: 700,
};

const fieldInput = {
    width: '100%', boxSizing: 'border-box',
    background: HUD.bgInput,
    color: HUD.text,
    border: `1px solid ${HUD.border}`,
    borderRadius: 10,
    padding: '10px 12px',
    fontSize: 14, fontFamily: HUD.font,
    outline: 'none',
    transition: 'border-color 150ms ease',
};

/**
 * MobileAmmoBlock — компактный счётчик патронов в правом верхнем углу
 * на мобиле + кнопка R перезарядки. ammo = { magazine, magazineMax,
 * reserve, reloading, reloadProgress }.
 */
const MobileAmmoBlock = ({ ammo, onReload }) => {
    if (!ammo) return null;
    const reloading = ammo.reloading;
    const progress = Math.max(0, Math.min(1, ammo.reloadProgress || 0));
    return (
        <div data-mobile-hud="ammo" style={{
            position: 'absolute',
            top: 8, right: 56,    // оставляем 48px справа под fullscreen-кнопку
            display: 'flex', alignItems: 'center', gap: 8,
            padding: '6px 10px',
            background: 'rgba(15, 19, 38, 0.78)',
            border: '1px solid rgba(255, 255, 255, 0.12)',
            borderRadius: 12,
            backdropFilter: 'blur(20px)',
            WebkitBackdropFilter: 'blur(20px)',
            zIndex: 30,
            color: '#f1f5fb',
            fontFamily: '"Roboto Condensed", system-ui, sans-serif',
            pointerEvents: 'auto',
        }}>
            <div style={{ fontSize: 14, opacity: 0.7 }}>🔫</div>
            <div style={{
                fontSize: 18, fontWeight: 800, lineHeight: 1,
                letterSpacing: 0.3, minWidth: 44, textAlign: 'center',
            }}>
                {reloading
                    ? <span style={{ opacity: 0.7 }}>{Math.round(progress * 100)}%</span>
                    : <><span>{ammo.magazine}</span>
                        <span style={{ fontSize: 12, opacity: 0.55, fontWeight: 700 }}>
                            {' /' + ammo.magazineMax}
                        </span>
                    </>}
            </div>
            <button
                onClick={onReload}
                onTouchStart={(e) => { e.stopPropagation(); onReload(); }}
                style={{
                    width: 30, height: 30,
                    borderRadius: 8,
                    background: reloading
                        ? 'rgba(255, 200, 87, 0.30)'
                        : 'rgba(51, 87, 255, 0.30)',
                    border: '1px solid rgba(255, 255, 255, 0.20)',
                    color: '#fff',
                    fontSize: 13, fontWeight: 800,
                    cursor: 'pointer',
                    display: 'flex', alignItems: 'center', justifyContent: 'center',
                    padding: 0, fontFamily: 'inherit',
                    WebkitTapHighlightColor: 'transparent',
                }}
                title="Перезарядка"
            >
                R
            </button>
        </div>
    );
};

/**
 * TopDropdown — выпадающее меню из верхнего бара (Roblox-style).
 * Не модалка, не паузит игру. Закрывается по клику вне или по любому
 * выбранному пункту.
 */
const TopDropdown = ({
    meta, vote, likes, dislikes,
    onClose, onLike, onDislike,
    onShare, onReport, onBugReport, onRespawn, onExit,
    isTouch = false,
}) => {
    const totalVotes = (likes || 0) + (dislikes || 0);
    const ratingPct = totalVotes > 0 ? Math.round(100 * likes / totalVotes) : null;

    // Закрытие по клику вне (event capture на документе)
    useEffect(() => {
        const onDocClick = (e) => {
            if (!e.target.closest?.('[data-kubikon-top-dropdown]')
                && !e.target.closest?.('[data-kubikon-top-menu-btn]')) {
                onClose();
            }
        };
        // Регистрируем на следующий тик чтобы клик-открытие не закрыл сразу
        const t = setTimeout(() => {
            document.addEventListener('mousedown', onDocClick);
        }, 0);
        return () => {
            clearTimeout(t);
            document.removeEventListener('mousedown', onDocClick);
        };
    }, [onClose]);

    return (
        <div
            data-kubikon-top-dropdown
            style={{
                position: 'absolute',
                top: 'calc(100% + 8px)',
                left: 0,
                width: 280,
                background: HUD.bgPanel,
                backdropFilter: 'blur(24px)',
                WebkitBackdropFilter: 'blur(24px)',
                border: `1px solid ${HUD.border}`,
                borderRadius: 16,
                padding: 8,
                zIndex: 1200,
                boxShadow: '0 16px 40px rgba(0,0,0,0.55), 0 0 0 1px rgba(51,87,255,0.10)',
                fontFamily: HUD.font,
                color: HUD.text,
                animation: 'hudFadeInScale 200ms cubic-bezier(0.34, 1.56, 0.64, 1)',
                transformOrigin: 'top left',
                maxHeight: 'calc(100vh - 120px)',
                overflowY: 'auto',
            }}
        >
            {/* Заголовок: thumbnail + имя игры + рейтинг */}
            {meta && (
                <div style={{
                    padding: '4px 4px 12px',
                    borderBottom: `1px solid ${HUD.border}`,
                    marginBottom: 6,
                    display: 'flex', gap: 10, alignItems: 'center',
                }}>
                    <div style={{
                        width: 48, height: 48, borderRadius: 10,
                        flexShrink: 0,
                        background: meta.thumbnail
                            ? `url(${meta.thumbnail}) center/cover`
                            : HUD.gradientBrand,
                        display: 'flex', alignItems: 'center', justifyContent: 'center',
                        fontSize: 22,
                        boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
                    }}>
                        {!meta.thumbnail && '🎮'}
                    </div>
                    <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{
                            fontSize: 14, fontWeight: 800, color: HUD.text,
                            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                            letterSpacing: -0.2,
                        }}>{meta.title}</div>
                        {ratingPct != null ? (
                            <div style={{
                                fontSize: 11, color: HUD.textMuted, marginTop: 3,
                                display: 'inline-flex', alignItems: 'center', gap: 8,
                            }}>
                                <span style={{
                                    color: ratingPct >= 70 ? HUD.success
                                        : ratingPct >= 40 ? '#ffc857' : HUD.danger,
                                    fontWeight: 800,
                                }}>⭐ {ratingPct}%</span>
                                <span style={{ color: HUD.textDim }}>
                                    {(likes || 0) + (dislikes || 0)} {(likes || 0) + (dislikes || 0) === 1 ? 'голос' : 'голосов'}
                                </span>
                            </div>
                        ) : (
                            <div style={{ fontSize: 11, color: HUD.textDim, marginTop: 3 }}>
                                Ещё нет оценок
                            </div>
                        )}
                    </div>
                </div>
            )}

            {/* Голосование */}
            <div style={{ display: 'flex', gap: 6, padding: '4px 4px 6px' }}>
                <button onClick={onLike} style={voteBtnStyle(vote === 'like', 'like')}>
                    <span style={{ fontSize: 16 }}>👍</span>
                    <span>{likes || 0}</span>
                </button>
                <button onClick={onDislike} style={voteBtnStyle(vote === 'dislike', 'dislike')}>
                    <span style={{ fontSize: 16 }}>👎</span>
                    <span>{dislikes || 0}</span>
                </button>
            </div>

            <DropdownDivider />

            {/* Действия */}
            <DropdownItem icon="🔄" label="Переродиться" onClick={onRespawn} accent />
            <DropdownItem icon="🔗" label="Поделиться"  onClick={onShare} />
            <DropdownItem icon="🚩" label="Пожаловаться" onClick={onReport} />
            <DropdownItem icon="🐛" label="Сообщить об ошибке" onClick={onBugReport} />

            {/* Горячие клавиши — только на десктопе. На мобиле/планшете
                клавиатуры нет, эта секция бесполезна и занимает место. */}
            {!isTouch && (
                <>
                    <DropdownDivider />
                    <div style={{
                        padding: '8px 12px 6px',
                        fontSize: 10, color: HUD.textDim,
                        textTransform: 'uppercase', letterSpacing: 1.2, fontWeight: 800,
                    }}>
                        ⌨️ Горячие клавиши
                    </div>
                    <div style={{
                        padding: '0 12px 8px',
                        display: 'flex', flexDirection: 'column', gap: 6,
                        fontSize: 11.5, color: HUD.textMuted,
                    }}>
                        <HotkeyRow keys={['W', 'A', 'S', 'D']} label="Движение" />
                        <HotkeyRow keys={['Space']} label="Прыжок" />
                        <HotkeyRow keys={['Shift']} label="Бег" />
                        <HotkeyRow keys={['Мышь']} label="Камера" />
                        <HotkeyRow keys={['ЛКМ']} label="Атака / стрельба" />
                        <HotkeyRow keys={['R']} label="Перезарядка" />
                        <HotkeyRow keys={['1', '…', '5']} label="Слот инвентаря" />
                        <HotkeyRow keys={['Q']} label="Бросить предмет" />
                        <HotkeyRow keys={['C']} label="1-е / 3-е лицо" />
                        <HotkeyRow keys={['Tab']} label="Курсор для GUI" />
                        <HotkeyRow keys={['Esc']} label="Меню / отдать мышь" />
                    </div>
                </>
            )}

            <DropdownDivider />
            <DropdownItem icon="🚪" label="Выйти из игры" onClick={onExit} danger />
        </div>
    );
};

const DropdownDivider = () => (
    <div style={{
        height: 1,
        background: `linear-gradient(90deg, transparent 0%, ${HUD.border} 50%, transparent 100%)`,
        margin: '6px 4px',
    }} />
);

const HotkeyRow = ({ keys, label }) => (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <span style={{ flex: 1, color: HUD.textMuted, fontWeight: 600 }}>{label}</span>
        <span style={{ display: 'flex', gap: 3 }}>
            {keys.map((k, i) => (
                <kbd key={i} style={{
                    background: 'rgba(255,255,255,0.06)',
                    border: `1px solid ${HUD.border}`,
                    borderRadius: 4,
                    padding: '2px 7px',
                    fontSize: 10, fontFamily: HUD.font, fontWeight: 700,
                    color: HUD.text,
                    minWidth: 18, textAlign: 'center',
                    boxShadow: '0 1px 0 rgba(255,255,255,0.04) inset, 0 1px 2px rgba(0,0,0,0.4)',
                }}>{k}</kbd>
            ))}
        </span>
    </div>
);

const DropdownItem = ({ icon, label, onClick, danger, accent }) => {
    const [hovered, setHovered] = useState(false);
    const hoverBg = danger
        ? HUD.dangerBg
        : accent
            ? HUD.accentBg
            : 'rgba(255,255,255,0.06)';
    const color = danger ? HUD.danger
        : accent ? '#fff'
        : HUD.text;
    return (
        <button
            onClick={onClick}
            onMouseEnter={() => setHovered(true)}
            onMouseLeave={() => setHovered(false)}
            style={{
                width: '100%', padding: '10px 12px',
                background: hovered ? hoverBg : 'transparent',
                border: 'none',
                borderRadius: 10,
                color,
                fontSize: 13, fontWeight: 700,
                cursor: 'pointer', fontFamily: HUD.font,
                textAlign: 'left',
                display: 'flex', alignItems: 'center', gap: 12,
                transition: 'all 150ms ease',
                letterSpacing: 0.1,
            }}
        >
            <span style={{
                fontSize: 16, width: 24, textAlign: 'center',
                filter: hovered ? 'none' : 'grayscale(0.15)',
            }}>{icon}</span>
            <span style={{ flex: 1 }}>{label}</span>
            {hovered && (
                <span style={{ fontSize: 12, color: HUD.textDim }}>→</span>
            )}
        </button>
    );
};

const voteBtnStyle = (active, kind) => {
    const activeBg = kind === 'like'
        ? 'linear-gradient(135deg, #22d97a 0%, #0f9d56 100%)'
        : 'linear-gradient(135deg, #ff6f7a 0%, #c0303f 100%)';
    return {
        flex: 1, padding: '10px 8px',
        background: active ? activeBg : 'rgba(255,255,255,0.04)',
        border: `1px solid ${active ? 'transparent' : HUD.border}`,
        borderRadius: 10,
        fontSize: 14, fontWeight: 800,
        color: active ? '#fff' : HUD.text,
        cursor: 'pointer', fontFamily: HUD.font,
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
        transition: 'all 200ms ease',
        boxShadow: active
            ? (kind === 'like'
                ? '0 6px 16px rgba(34,217,122,0.40)'
                : '0 6px 16px rgba(255,111,122,0.40)')
            : 'none',
    };
};

export default KubikonPlayer;
