// DramaScope — auth, i18n, ratings, reactions
// Exposes: useAuth, useI18n, LANGUAGES, AuthModal, AuthContext, I18nContext
//          LanguageMenu, AuthButtons, AvatarMenu, GuestGate
//          DramaRatingPanel, DramaReactionPanel
//          ActorReactionPanel, ActorTraitsPanel

const { useState, useEffect, useRef, useCallback, createContext, useContext } = React;

// ── I18N ──────────────────────────────────────────────────────────────────────
const LANGUAGES = [
{ code: 'en', short: 'EN', label: 'English' },
{ code: 'ru', short: 'RU', label: 'Русский' }];
// Скрытые языки — переводы в I18N сохранены на будущее, можно вернуть позже:
// { code: 'es', short: 'ES', label: 'Español' },
// { code: 'de', short: 'DE', label: 'Deutsch' },
// { code: 'uk', short: 'UK', label: 'Українська' }


// Translation dictionary. Missing keys fall back to English (the key itself).
const I18N = {
  en: {
    // Mega-menu Explore — русские ключи NAV_ITEMS переводим на английский для en-режима.
    'Разделы': 'Sections',
    'Расширенный поиск': 'Advanced search',
    'Инструкция к Мао': 'Mao guide',
    'Новости': 'News',
    'Календарь': 'Calendar',
    'Дорамы': 'Dramas',
    'Актёры': 'Actors',
    'Режиссёры': 'Directors',
    'Жанры': 'Genres',
    'Все дорамы / поиск': 'All dramas / search',
    'Все актёры / поиск': 'All actors / search',
    'Все режиссёры / поиск': 'All directors / search',
    'Игры': 'Games',
    'Кто твой xianxia-герой?': 'Who\'s your xianxia hero?',
    // Страны в меню (Дорамы / Актёры → 🇨🇳/🇰🇷/🇹🇭)
    'Китай': 'China',
    'Южная Корея': 'South Korea',
    'Тайланд': 'Thailand',
  },
  es: {
    'Home': 'Inicio', 'Chinese Dramas': 'C-Dramas', 'Chinese Actors': 'Actores Chinos',
    'Mao Picks': 'Mao Picks', 'Rankings': 'Rankings', 'News': 'Noticias',
    'Community': 'Comunidad', 'Watchlist': 'Lista', 'Profile': 'Perfil',
    'Sign In': 'Iniciar sesión', 'Join Free': 'Regístrate', 'Sign Up': 'Registrarse',
    'Log Out': 'Cerrar sesión', 'My Profile': 'Mi perfil', 'My Library': 'Mi biblioteca',
    'My Ratings': 'Mis valoraciones', 'My Reactions': 'Mis reacciones',
    'My Lists': 'Mis listas', 'Account Settings': 'Configuración', 'Language': 'Idioma',
    'Upgrade to Premium': 'Actualizar a Premium',
    'Discover Chinese Dramas & Actors': 'Descubre C-Dramas y actores chinos',
    'Explore C-Dramas': 'Explorar C-Dramas', 'Browse Chinese Actors': 'Ver actores chinos',
    'Get AI Recommendations': 'Recomendaciones IA',
    'Sign in to rate': 'Inicia sesión para valorar',
    'Sign in to react': 'Inicia sesión para reaccionar',
    'Sign in to rate this drama': 'Inicia sesión para valorar este drama',
    'Sign in to react to this actor': 'Inicia sesión para reaccionar a este actor',
    'Create an account to save this drama': 'Crea una cuenta para guardar este drama',
    'Create an account to save to your watchlist': 'Crea una cuenta para guardar en tu lista',
    'Join free to follow actors': 'Regístrate gratis para seguir actores',
    'Join free to unlock your C-Drama library': 'Únete gratis para desbloquear tu biblioteca de C-Dramas',
    'Want to see more? Create a free account.': '¿Quieres ver más? Crea una cuenta gratis.',
    'Sign up to unlock more details.': 'Regístrate para más detalles.',
    'Create an account to access full actor profiles.': 'Crea una cuenta para perfiles completos.',
    'Register to see full drama details, ratings, reactions, and recommendations.': 'Regístrate para ver detalles completos, valoraciones y recomendaciones.',
    'Join free to save dramas, follow actors, and get personalized AI recommendations.': 'Únete gratis para guardar dramas, seguir actores y obtener recomendaciones personalizadas.',
    'Sign in to rate, react, comment, and build your C-Drama library.': 'Inicia sesión para valorar, reaccionar y comentar.',
    'Create your free account to unlock fandom rankings, watchlists, reviews, and AI picks.': 'Crea tu cuenta gratis para desbloquear rankings, listas, reseñas y selección IA.',
    'Sign Up to See More': 'Regístrate para ver más', 'Create Free Account': 'Crear cuenta gratis',
    'Unlock Full Profile': 'Desbloquear perfil', 'Unlock More Details': 'Más detalles',
    'Sign In to Continue': 'Inicia sesión para continuar', 'Register to Save': 'Regístrate para guardar',
    'Register to Rate': 'Regístrate para valorar', 'Register to React': 'Regístrate para reaccionar',
    'Your Ratings': 'Tus valoraciones', 'Your Reactions': 'Tus reacciones',
    'Overall Rating': 'Valoración general', 'OST Rating': 'Banda sonora',
    'Side Characters Rating': 'Personajes secundarios', 'Chemistry Score': 'Química',
    'Recommendation Score': 'Recomendación', 'Rewatch Score': 'Volver a ver',
    'Rate this drama': 'Valorar este drama', 'React to this drama': 'Reacciona a este drama',
    'React to this actor': 'Reacciona a este actor', 'Vote on traits': 'Vota rasgos',
    'Agree': 'De acuerdo', 'Disagree': 'En desacuerdo',
    'Welcome back': 'Bienvenido', 'Create your account': 'Crea tu cuenta',
    'Email': 'Correo', 'Password': 'Contraseña', 'Display name': 'Nombre',
    'Already have an account?': '¿Ya tienes cuenta?', 'Need an account?': '¿Necesitas cuenta?',
    'Continue': 'Continuar', 'Cancel': 'Cancelar',
    'Follow': 'Seguir', 'Following': 'Siguiendo'
  },
  ru: {
    'Home': 'Главная', 'Dramas': 'Дорамы', 'Actors': 'Актёры', 'Menu': 'Меню',
    'Chinese Dramas': 'Дорамы Китая', 'Chinese Actors': 'Актёры',
    'Mao Picks': 'Привет, я Mao!', 'Rankings': 'Рейтинги', 'News': 'Новости',
    'Explore': 'Каталог', 'Calendar': 'Календарь', 'All genres': 'Все жанры',
    // Avatar menu
    'My Mao': 'Мой Mao', 'Messages': 'Сообщения', 'Notifications': 'Уведомления',
    'Settings': 'Настройки', 'Premium': 'Премиум', 'Sign Out': 'Выйти',
    'Online': 'В сети', 'last seen': 'был(а)',
    'ADMIN': 'АДМИН', 'MODERATOR': 'МОДЕРАТОР', 'EXPERT': 'ЭКСПЕРТ', 'VIEWER': 'ЗРИТЕЛЬ',
    // Drama statuses (badges + carousels)
    'Upcoming': 'Ожидается', 'On hold': 'Приостановлено',
    'Upcoming releases': 'Ожидается релиз', 'Release on hold': 'Релиз приостановлен',
    'Premieres': 'Премьеры',
    // Release notify button
    'Notify me about the release': 'Сообщить о релизе',
    "We'll notify you about the release": 'Сообщим о релизе',
    'Notify me': 'Сообщить', 'Subscribed': 'Сообщим',
    'Sign in to get a release alert': 'Войди чтобы получить уведомление',
    // Cast sections
    'Main cast': 'Главные роли', 'Supporting cast': 'Другие актёры', 'Crew': 'Режиссёрский состав',
    'Directors': 'Режиссёры', 'directors': 'режиссёров',
    'Browse profiles and filmographies of Asian drama directors.': 'Профили и фильмографии режиссёров азиатских дорам.',
    'Search director by name…': 'Поиск режиссёра по имени…',
    'Find directors by drama…': 'Найти режиссёров по дораме…',
    'No directors linked to this drama yet.': 'У этой дорамы пока не указаны режиссёры.',
    'No directors match this filter.': 'Никто из режиссёров не подходит под фильтр.',
    'No directors found for': 'Не найдено режиссёров по запросу',
    'No directors yet — add them via drama admin → crew.': 'Пока нет режиссёров — добавь их через админку дорамы → crew.',
    'Все режиссёры / поиск': 'Все режиссёры / поиск', 'Режиссёры': 'Режиссёры',
    // Videos
    'Videos': 'Видео',
    // Dramas hero search
    'Search drama by name…': 'Поиск дорамы по названию…',
    "I'll find your next drama 🐾": 'Я найду тебе дораму 🐾',
    "Tell me your mood, favorite category, or a drama you loved — and I'll suggest 5 dramas you'll love.": 'Расскажи про настроение, любимый жанр или дораму которую любишь — я подберу 5 дорам которые тебе зайдут.',
    'What do you want tonight?': 'Чего хочется сегодня?',
    "What's your mood?": 'Какое настроение?',
    'e.g., I want something epic and emotional': 'например: хочу что-то эпичное и эмоциональное',
    'Favorite category': 'Любимый жанр',
    'Favorite C-drama (optional)': 'Любимая дорама (необязательно)',
    'e.g., Nirvana in Fire': 'например: Список архива Ланъя',
    'Any': 'Любой',
    'Generate Recommendations': 'Получить подборку',
    'Thinking...': 'Думаю...',
    "Mao's Recommendations": 'Подборка Mao',
    'Community': 'Сообщество', 'Watchlist': 'Избранное', 'Profile': 'Профиль',
    'Sign In': 'Войти', 'Join Free': 'Регистрация', 'Sign Up': 'Создать аккаунт',
    'Log Out': 'Выйти', 'My Profile': 'Мой профиль', 'My Library': 'Моя библиотека',
    'My Ratings': 'Мои оценки', 'My Reactions': 'Мои реакции',
    'My Lists': 'Мои списки', 'Account Settings': 'Настройки', 'Language': 'Язык',
    'Upgrade to Premium': 'Перейти на Премиум',
    'Advanced Search': 'Расширенный поиск',
    'Explore Dramas': 'Все дорамы',
    'Browse Actors': 'Все актёры',
    'Ask Mao for Recommendations': 'Спросить Mao',
    'Ask Mao in plain language': 'Спроси Mao своими словами',
    'Ask Mao': 'Спросить Mao',
    'Ask Mao anything…': 'Спроси Mao о чём угодно…',
    'Send': 'Отправить',
    'Mao Recommendations': 'Подборка Mao',
    'Find your next C-drama': 'Найди свою следующую дораму',
    "Tell me your mood or favorite drama — I'll suggest the perfect one for you.": 'Расскажи про настроение или любимую дораму — я подберу идеальную.',
    'Try Mao Picks →': 'Попробовать →',
    "Describe what you want — I'll pick 5 dramas for you.": 'Опиши что хочешь посмотреть — я подберу 5 дорам.',
    "Pick a vibe — I'll pick 5 dramas matching it instantly.": 'Выбери настроение — я подберу 5 дорам.',

    // Carousels
    "What's New on MaoDrama": 'Новое на MaoDrama',
    'New in 2026': 'Новинки 2026 года',
    'Trending Dramas': 'В тренде',
    'Popular Dramas': 'Популярные',
    'Airing Now': 'Сейчас выходят',
    'Movie Selection': 'Фильмы',
    'Variety Selection': 'Шоу',
    'Romance & Modern Romance': 'Романтика',
    'Top Rated': 'Топ рейтинга',
    'Your Choice': 'Твой выбор',
    'View all →': 'Все →',
    'Popular Celebrities': 'Популярные актёры',

    // Page heros
    'Latest Asian Entertainment News': 'Новости',
    'Explore the full catalog of Asian dramas — South Korea, China, Japan, Thailand.': 'Полный каталог азиатских дорам — Южная Корея, Китай, Япония, Таиланд.',
    'Browse profiles, biographies, and filmographies of popular Asian actors and actresses.': 'Профили, биографии и фильмографии популярных азиатских актёров.',
    'Definitive C-drama rankings based on ratings, popularity, and fandom buzz.': 'Рейтинги дорам на основе оценок, популярности и обсуждений.',
    'Casting announcements, releases, ratings, and industry news from the Asian drama world.': 'Касты, релизы, рейтинги и индустриальные новости мира азиатских дорам.',
    'Discussions, theories, and recommendations from the C-drama fandom.': 'Обсуждения, теории и рекомендации от фанатов дорам.',
    'Filter dramas by genre, year, rating, country, network, cast, keywords and more.': 'Фильтруй дорамы по жанру, году, рейтингу, стране, площадке, актёрам, ключевым словам.',

    // Footer
    'About MaoDrama': 'О MaoDrama',
    'About us': 'О нас',
    'TOP 10': 'Топ 10',
    'Products and Services': 'Продукты',
    'Ways to Watch': 'Где смотреть',
    'Press Center': 'Пресс-центр',
    'Cooperation': 'Сотрудничество',
    'Advertise': 'Реклама',
    'Corporate relations': 'Корпоративные связи',
    'Preinstall Cooperation': 'Партнёрство',
    'Help and support': 'Поддержка',
    'Feedback': 'Обратная связь',
    'Security Response Center': 'Центр безопасности',
    'FAQ': 'FAQ',
    'Terms of Service': 'Условия',
    'Privacy Policy': 'Политика конфиденциальности',
    'Cookie settings': 'Настройки cookie',

    // Search
    'Search actor by name…': 'Найти актёра по имени…',
    'Search actor by name.': 'Найти актёра по имени.',
    'Find actors by drama…': 'Найти актёра по дораме…',
    'Type actor name…': 'Введи имя актёра…',
    'Year': 'Год', 'Title': 'Название', 'of': 'из',
    'Search results for': 'Результаты поиска по запросу',
    'results': 'результатов', 'No results found for': 'Ничего не найдено по запросу',
    'Loading…': 'Загрузка…',
    'Showing first': 'Показано первых',
    'news': 'новостей',
    'incl. filmography': 'включая фильмографию',
    // Advanced Search page
    'Filter dramas by genre, year, rating, country, network, cast, keywords and more.': 'Фильтруй дорамы по жанру, году, рейтингу, стране, площадке, актёрам, ключевым словам и т.д.',
    'e.g. Cozy slow-burn romance with chef heroine and small-town vibes…': 'например: уютный slow-burn с героиней-шефом и атмосферой провинциального городка…',
    'Free text (title)': 'Свободный поиск по названию',
    'e.g. Nirvana in Fire': 'например: Список архива Ланъя',
    'If set, other filters except sort are bypassed.': 'Если задано — другие фильтры игнорируются, кроме сортировки.',
    'Genres': 'Жанры',
    'Tags': 'Теги',
    'Keywords': 'Ключевые слова',
    'Country of origin': 'Страна производства',
    'Network / streaming platform': 'Канал / стриминговая платформа',
    'Year from': 'Год с',
    'Year to': 'Год по',
    'Popularity': 'Популярность',
    'Status': 'Статус',
    'Sort by': 'Сортировка',
    'Min episodes': 'Серий от',
    'Max episodes': 'Серий до',
    'any': 'любое',
    'Cast (actors)': 'Каст (актёры)',
    'Type actor name...': 'Введи имя актёра…',
    'Keywords / tags': 'Ключевые слова / теги',
    'e.g. time travel, palace...': 'например: путешествие во времени, дворец…',
    'Apply Filters': 'Применить фильтры',
    'Reset': 'Сбросить',
    'No matches. Try relaxing filters.': 'Совпадений нет. Попробуй ослабить фильтры.',
    'First': 'Первая', 'Last': 'Последняя',
    // Status options
    'Any': 'Любой',
    'Returning Series': 'Выходит сейчас',
    'In Production': 'В производстве',
    'Pilot': 'Пилот',
    'Planned': 'Запланировано',
    'Canceled': 'Отменено',
    // Sort options
    'Most Popular': 'По популярности',
    'Least Popular': 'Сначала менее популярные',
    'Highest Rated': 'По рейтингу',
    'Lowest Rated': 'Сначала с низким рейтингом',
    'Newest': 'Сначала новые',
    'Oldest': 'Сначала старые',
    'Most Votes': 'По числу оценок',
    'Newest First': 'Сначала новые',
    'Oldest First': 'Сначала старые',
    'A → Z': 'А → Я',
    'Ended': 'Завершено', 'Cancelled': 'Отменено',
    // Country chip names
    'China': 'Китай', 'Hong Kong': 'Гонконг', 'Taiwan': 'Тайвань',
    'South Korea': 'Южная Корея', 'Japan': 'Япония', 'Thailand': 'Таиланд',
    // Genres (TMDB-style) — частично уже есть
    'Action & Adventure': 'Боевик и приключения', 'Reality': 'Реалити',
    'Soap': 'Сериал', 'Animation': 'Анимация', 'Talk': 'Ток-шоу',
    'War & Politics': 'Война и политика', 'Western': 'Вестерн', 'Kids': 'Детское',
    'Info': 'Информация',
    'Follow actor for updates': 'Подписаться на обновления об актёре',
    'Following — updates on': 'Подписан — обновления включены',
    'Rate this actor': 'Оценить актёра',
    'Your average': 'Ваша оценка',
    'Reset ratings': 'Сбросить оценки',
    'Reset all ratings for this actor?': 'Сбросить все оценки этого актёра?',
    'Acting Performance': 'Актёрская игра',
    'Role Versatility': 'Разноплановость ролей',
    'On-screen Chemistry': 'Химия с партнёрами',
    'Screen Charisma': 'Экранная харизма',
    'Difficult Scenes': 'Работа в сложных сценах',
    'Attractiveness': 'Привлекательность',
    'Find Asian Dramas, Movies...': 'Поиск',

    // Mood picker on home
    "What's your mood tonight?": 'Какое у тебя настроение?',
    'Cozy & Heartwarming': 'Уютное',
    'Edge-of-Seat Thriller': 'Триллер',
    'Romantic & Sweet': 'Романтика',
    'Epic & Historical': 'Эпос и история',
    'Mind-Bending': 'Закрученное',
    'Light & Funny': 'Лёгкое и весёлое',

    // Mood chips on AI Picks page
    'Cry': 'Слёзы',
    'Fall in love': 'Влюбиться',
    'Chemistry': 'Химия',
    'Comfort': 'Уют',
    'Epic wuxia': 'Эпичное уся',
    'Toxic romance': 'Токсичная любовь',
    'Happy ending': 'Хэппи-энд',
    'Green flag male lead': 'Зелёные флаги',
    'Emotional OST': 'Эмоциональный OST',
    'Strong female lead': 'Сильная героиня',

    // Categories (dramas)
    'All': 'Все',
    'Romance': 'Романтика',
    'Historical': 'Историческая',
    'Wuxia': 'Уся',
    'Xianxia': 'Сянься',
    'Modern Romance': 'Современная романтика',
    'Fantasy': 'Фэнтези',
    'Youth': 'Молодёжная',
    'Family': 'Семейная',
    'Mystery': 'Мистика',
    'Thriller': 'Триллер',
    'Comedy': 'Комедия',
    'Melodrama': 'Мелодрама',
    'Republican Era': 'Республиканская эпоха',
    'Costume Drama': 'Костюмная',
    'Urban Romance': 'Городская романтика',
    'Drama': 'Драма',
    'Crime': 'Криминал',
    'Documentary': 'Документальная',
    'Reality': 'Реалити',
    'Soap': 'Сериал',
    'Action & Adventure': 'Боевик и приключения',
    'Sci-Fi & Fantasy': 'Sci-Fi и Фэнтези',
    'Animation': 'Анимация',

    // News categories
    'Casting': 'Кастинг',
    'Release': 'Релиз',
    'Ratings': 'Рейтинги',
    'Announcement': 'Анонс',
    'Awards': 'Награды',
    'Production': 'Производство',

    // Rankings tabs
    '⭐ Top Rated': '⭐ По рейтингу',
    '🔥 Most Popular': '🔥 Популярное',
    '📈 Trending': '📈 В тренде',
    'Top Rated Dramas': 'Дорамы по рейтингу',
    'Most Popular': 'Популярное',
    'Trending': 'В тренде',
    'Highest-rated Asian dramas of all time, by user votes.': 'Дорамы с самым высоким рейтингом по голосам пользователей.',

    // Common UI
    'NEWS': 'НОВОСТИ',
    'HOT': 'HOT',
    'TOP': 'ТОП',
    'NEW': 'NEW',
    'Confirmed': 'Подтверждено',
    'UPCOMING': 'СКОРО',
    '+ New Topic': '+ Новая тема',
    'PINNED': 'ЗАКРЕП',
    'ALL TOPICS': 'ВСЕ ТЕМЫ',
    'Discussion': 'Обсуждение',
    'Theory': 'Теория',
    'topics': 'тем',
    'replies': 'ответов',
    'members': 'участников',
    'daily': 'ежедневно',
    'active': 'активно',

    // Drama detail page sidebar/blocks
    'Find dramas like': 'Найди дорамы как',
    "I'll suggest similar shows based on your taste and this drama's vibe.": 'Я подберу похожие сериалы под твой вкус и вайб этой дорамы.',
    'Try My Picks →': 'Моя подборка →',
    'Recommend this drama': 'Рекомендуй дораму',
    'Recommended by': 'Рекомендуют',
    'of users': 'пользователей',
    'I recommend this drama': 'Я рекомендую',
    'You recommend this drama': 'Ты рекомендуешь',

    // Actor detail page labels
    'Biography': 'Биография',
    'Filmography': 'Фильмография',
    'Back to Actors': 'К списку актёров',
    'Born': 'Дата рождения',
    'Zodiac': 'Знак зодиака',
    'Chinese Zodiac': 'Китайский гороскоп',

    // Western zodiac signs
    'Aries': 'Овен', 'Taurus': 'Телец', 'Gemini': 'Близнецы', 'Cancer': 'Рак',
    'Leo': 'Лев', 'Virgo': 'Дева', 'Libra': 'Весы', 'Scorpio': 'Скорпион',
    'Sagittarius': 'Стрелец', 'Capricorn': 'Козерог', 'Aquarius': 'Водолей', 'Pisces': 'Рыбы',

    // Chinese zodiac animals
    'Rat': 'Крыса', 'Ox': 'Бык', 'Tiger': 'Тигр', 'Rabbit': 'Кролик',
    'Dragon': 'Дракон', 'Snake': 'Змея', 'Horse': 'Лошадь', 'Goat': 'Коза',
    'Monkey': 'Обезьяна', 'Rooster': 'Петух', 'Dog': 'Собака', 'Pig': 'Свинья',

    // Occupations
    'actor': 'актёр', 'actress': 'актриса', 'singer': 'певец', 'model': 'модель',
    'film actor': 'киноактёр', 'television actor': 'актёр сериалов',
    'film director': 'режиссёр', 'film producer': 'продюсер',
    'screenwriter': 'сценарист', 'dancer': 'танцор', 'voice actor': 'актёр озвучки',
    'television presenter': 'телеведущий', 'composer': 'композитор',
    'choreographer': 'хореограф', 'businessperson': 'бизнесмен',

    // Countries (when used in citizenship)
    'China': 'Китай', 'Taiwan': 'Тайвань', 'Hong Kong': 'Гонконг', 'Macau': 'Макао',
    'South Korea': 'Южная Корея', 'North Korea': 'Северная Корея',
    'Japan': 'Япония', 'Thailand': 'Таиланд', 'Malaysia': 'Малайзия',
    'Indonesia': 'Индонезия', 'Philippines': 'Филиппины', 'Vietnam': 'Вьетнам',
    'Singapore': 'Сингапур', 'India': 'Индия',
    'Citizenship': 'Гражданство',
    'Occupation': 'Профессия',
    'Height': 'Рост',
    'Weight': 'Вес',
    'Education': 'Образование',
    'Place of Birth': 'Место рождения',
    'Popularity': 'Популярность',
    'Read full bio →': 'Читать всё →',
    'Read more →': 'Читать дальше →',
    'Read more': 'Читать дальше',
    'Show less': 'Свернуть',
    'Mao Character Analysis': 'Анализ актёра от Mao',
    "Let me dive deep into {name}'s acting style, signature roles, and emotional range.": 'Я разберу актёрский стиль {name}, ключевые роли и эмоциональный диапазон.',
    'Generate analysis': 'Сгенерировать',

    // Library statuses
    'Add to Library': 'В библиотеку',
    'Add to Watchlist': 'В избранное',
    'Watching': 'Смотрю',
    'Plan to Watch': 'В планах',
    'Completed': 'Просмотрено', 'Finished airing': 'Завершено',
    'Favorites': 'Любимое',
    'Rewatch': 'Пересматриваю',
    'Dropped': 'Брошено',
    'Remove from Library': 'Убрать из библиотеки',

    // Share
    'Share': 'Поделиться',
    'Copied!': 'Скопировано!',

    // Profile actions
    'Save Changes': 'Сохранить',
    'Saving...': 'Сохраняем…',

    // Reviews
    'Reviews': 'Отзывы',
    'Cancel': 'Отмена',
    '+ Write Review': '+ Написать отзыв',
    'Sign in to write a review': 'Войди, чтобы написать отзыв',
    'Share your thoughts on this drama...': 'Поделись впечатлениями об этой дораме...',
    'Contains spoilers': 'Содержит спойлеры',
    'Post Review': 'Опубликовать',
    'No reviews yet. Be the first to share your thoughts!': 'Отзывов пока нет. Будь первым!',

    // Recommend reasons
    'Why do you recommend it? (pick all that apply)': 'Почему рекомендуешь? (выбери всё подходящее)',
    'Emotional story': 'Эмоциональная история',
    'Green flag male lead': 'Зелёный флаг (главный герой)',
    'Amazing OST': 'Шикарный саундтрек',
    'Beautiful cinematography': 'Красивая съёмка',
    'Addictive': 'Затягивает',
    'Comfort drama': 'Уютная',
    'Strong acting': 'Сильная игра',
    'Beautiful romance': 'Красивая романтика',
    'Great ending': 'Отличная концовка',
    'Public summary': 'Общая сводка',
    'Most recommended for:': 'Чаще всего рекомендуют за:',
    'Emotional impact': 'Эмоциональный эффект',
    'Visuals': 'Картинка',
    'OST': 'Саундтрек',
    'Comfort': 'Уют',

    // Drama reactions
    'React to this drama': 'Реакция на дораму',
    'Emotional Damage': 'Эмоциональная травма',
    'Comfort Drama': 'Уютная',
    'Insane Chemistry': 'Бешеная химия',
    'Toxic Romance': 'Токсичная любовь',
    'Masterpiece': 'Шедевр',

    // Actor reactions
    'React to this actor': 'Реакция на актёра',
    'Love': 'Любовь',
    'Hot': 'Огонь',
    'Soft Boy': 'Мягкий',
    'Worship': 'Преклоняюсь',
    'Emotional Damage': 'Эмоциональная травма',
    'Red Flag': 'Красный флаг',

    // Actor traits
    'Vote on traits': 'Голосовать за черты',
    'Chemistry': 'Химия',
    'Acting Skills': 'Актёрские навыки',
    'Smile': 'Улыбка',
    'Emotional Acting': 'Эмоциональная игра',
    'Charisma': 'Харизма',
    'Overhyped': 'Переоценён',
    'Repetitive Roles': 'Однотипные роли',
    'Agree': 'Согласен',
    'Disagree': 'Не согласен',

    // DramaAIButtons actions
    'Similar Dramas': 'Похожие дорамы',
    'Cast': 'В ролях',
    'Network:': 'Канал:',
    'ep.': 'эп.',
    'First Air': 'Премьера',
    'Episodes': 'Серий',
    'Duration': 'Продолжительность',
    'Rating': 'Рейтинг',
    'Votes': 'Голосов',
    'Origin': 'Страна', 'Country': 'Страна',
    'Ongoing': 'Выходит',
    'In Production': 'В производстве',
    'Pilot': 'Пилот',
    'Planned': 'Запланировано',
    'Canceled': 'Отменено',
    'Same Vibe': 'Тот же вайб',
    'More Like This': 'Ещё в таком стиле',
    'Better Chemistry': 'Лучше химия',
    'Same Emotional Damage': 'Такая же боль',
    'More Comfort Dramas': 'Уютные дорамы',
    'More Green Flag Romance': 'Зелёные флаги в романтике',

    // Sidebar blocks
    'Latest News': 'Новости',
    'all': 'все',
    'Upcoming Releases': 'Скоро в эфире',
    'Active Topics': 'Активные обсуждения',
    'forum': 'форум',
    'replies': 'ответов',
    'Sponsored': 'Реклама',
    'Bookmarks': 'Закладки',
    'Show bookmarks': 'Показать закладки',
    'Hide bookmarks': 'Скрыть закладки',
    'Hide': 'Скрыть',
    "Hi! I'm Mao 🐱 Ask me anything about Asian dramas — recommendations, plots, actors, vibes.": 'Привет! Я Mao 🐱 Спрашивай меня про азиатские дорамы — рекомендации, сюжеты, актёров, настроение.',
    'Discover Chinese Dramas & Actors': 'Открой китайские дорамы и актёров',
    'Explore C-Dramas': 'Смотреть дорамы', 'Browse Chinese Actors': 'Актёры Китая',
    'Get AI Recommendations': 'AI рекомендации',
    'Sign in to rate': 'Войди, чтобы оценить',
    'Sign in to react': 'Войди, чтобы оставить реакцию',
    'Sign in to rate this drama': 'Войди, чтобы оценить эту дораму',
    'Sign in to react to this actor': 'Войди, чтобы оставить реакцию',
    'Create an account to save this drama': 'Создай аккаунт, чтобы сохранить',
    'Create an account to save to your watchlist': 'Создай аккаунт, чтобы добавить в избранное',
    'Join free to follow actors': 'Регистрация — чтобы подписываться на актёров',
    'Join free to unlock your C-Drama library': 'Регистрация — чтобы получить доступ к библиотеке',
    'Want to see more? Create a free account.': 'Хочешь видеть больше? Создай бесплатный аккаунт.',
    'Sign up to unlock more details.': 'Зарегистрируйся, чтобы увидеть больше.',
    'Create an account to access full actor profiles.': 'Создай аккаунт, чтобы получить доступ к полным профилям актёров.',
    'Register to see full drama details, ratings, reactions, and recommendations.': 'Зарегистрируйся, чтобы видеть полные детали, оценки и рекомендации.',
    'Join free to save dramas, follow actors, and get personalized AI recommendations.': 'Регистрация — сохраняйте дорамы, подписывайтесь на актёров и получайте персональные рекомендации.',
    'Sign in to rate, react, comment, and build your C-Drama library.': 'Войди, чтобы оценивать, реагировать и комментировать.',
    'Create your free account to unlock fandom rankings, watchlists, reviews, and AI picks.': 'Создай бесплатный аккаунт — рейтинги, списки, рецензии и AI подбор.',
    'Sign Up to See More': 'Зарегистрируйся', 'Create Free Account': 'Создать аккаунт',
    'Unlock Full Profile': 'Открыть профиль', 'Unlock More Details': 'Подробнее',
    'Sign In to Continue': 'Войти', 'Register to Save': 'Регистрация',
    'Register to Rate': 'Регистрация', 'Register to React': 'Регистрация',
    'Your Ratings': 'Ваши оценки', 'Your Reactions': 'Ваши реакции',
    'Overall Rating': 'Общая оценка', 'OST Rating': 'Саундтрек',
    'Side Characters Rating': 'Второстепенные герои', 'Chemistry Score': 'Химия',
    'Recommendation Score': 'Рекомендация', 'Rewatch Score': 'Пересмотреть',
    'Rate this drama': 'Оценить дораму', 'React to this drama': 'Реакция на дораму',
    '1–10 scale · auto-saved': 'шкала 1–10 · автосохранение',
    'ratings': 'оценок', 'No ratings yet': 'Пока нет оценок',
    'Number of viewers': 'Количество зрителей',
    'User reviews': 'Оценки пользователей', 'Review': 'Оценить',
    'Ratings': 'Рейтинг', 'Rate it': 'Оценить', 'votes': 'голосов',
    'Details': 'Подробности',
    'Watching': 'Смотрю', 'Plan to Watch': 'В планах', 'Completed': 'Просмотрено',
    'Favorites': 'Любимое', 'Rewatch': 'Пересматриваю', 'Dropped': 'Брошено',
    'recommend': 'рекомендуют',
    'Add your own tags (max 5, comma-separated)…': 'Добавь свои теги (до 5, через запятую)…',
    'tags': 'тегов',
    'Rate below to see your scores': 'Оцени ниже, чтобы увидеть свой балл по критериям',
    'Sign in to rate by criteria': 'Войди, чтобы оценить по критериям',
    'React to this actor': 'Реакция на актёра', 'Vote on traits': 'Голосовать за черты',
    'Agree': 'Согласен', 'Disagree': 'Не согласен',
    'Welcome back': 'С возвращением', 'Create your account': 'Создай аккаунт',
    'Email': 'Email', 'Password': 'Пароль', 'Display name': 'Имя',
    'Already have an account?': 'Уже есть аккаунт?', 'Need an account?': 'Нет аккаунта?',
    'Continue': 'Продолжить', 'Cancel': 'Отмена',
    'Follow': 'Подписаться', 'Following': 'Вы подписаны'
  },
  de: {
    'Home': 'Startseite', 'Chinese Dramas': 'C-Dramen', 'Chinese Actors': 'Chinesische Schauspieler',
    'Mao Picks': 'Mao Picks', 'Rankings': 'Rankings', 'News': 'News',
    'Community': 'Community', 'Watchlist': 'Merkliste', 'Profile': 'Profil',
    'Sign In': 'Anmelden', 'Join Free': 'Kostenlos beitreten', 'Sign Up': 'Registrieren',
    'Log Out': 'Abmelden', 'My Profile': 'Mein Profil', 'My Library': 'Meine Bibliothek',
    'My Ratings': 'Meine Bewertungen', 'My Reactions': 'Meine Reaktionen',
    'My Lists': 'Meine Listen', 'Account Settings': 'Einstellungen', 'Language': 'Sprache',
    'Upgrade to Premium': 'Premium',
    'Discover Chinese Dramas & Actors': 'Entdecke chinesische Dramen & Schauspieler',
    'Explore C-Dramas': 'C-Dramen erkunden', 'Browse Chinese Actors': 'Schauspieler entdecken',
    'Get AI Recommendations': 'KI-Empfehlungen',
    'Sign in to rate': 'Anmelden zum Bewerten',
    'Sign in to react': 'Anmelden zum Reagieren',
    'Sign in to rate this drama': 'Anmelden, um dieses Drama zu bewerten',
    'Sign in to react to this actor': 'Anmelden, um zu reagieren',
    'Create an account to save this drama': 'Konto erstellen zum Speichern',
    'Create an account to save to your watchlist': 'Konto erstellen für Merkliste',
    'Join free to follow actors': 'Kostenlos beitreten, um Schauspielern zu folgen',
    'Join free to unlock your C-Drama library': 'Kostenlos beitreten und Bibliothek freischalten',
    'Want to see more? Create a free account.': 'Mehr sehen? Erstelle ein kostenloses Konto.',
    'Sign up to unlock more details.': 'Registriere dich für mehr Details.',
    'Create an account to access full actor profiles.': 'Konto erstellen für vollständige Profile.',
    'Register to see full drama details, ratings, reactions, and recommendations.': 'Registriere dich für volle Drama-Details, Bewertungen und Empfehlungen.',
    'Join free to save dramas, follow actors, and get personalized AI recommendations.': 'Kostenlos beitreten — Dramen speichern, Schauspielern folgen und personalisierte KI-Empfehlungen.',
    'Sign in to rate, react, comment, and build your C-Drama library.': 'Anmelden zum Bewerten, Reagieren und Kommentieren.',
    'Create your free account to unlock fandom rankings, watchlists, reviews, and AI picks.': 'Erstelle dein kostenloses Konto für Rankings, Listen und KI-Empfehlungen.',
    'Sign Up to See More': 'Für mehr registrieren', 'Create Free Account': 'Konto erstellen',
    'Unlock Full Profile': 'Profil freischalten', 'Unlock More Details': 'Details freischalten',
    'Sign In to Continue': 'Weiter mit Login', 'Register to Save': 'Zum Speichern',
    'Register to Rate': 'Zum Bewerten', 'Register to React': 'Zum Reagieren',
    'Your Ratings': 'Deine Bewertungen', 'Your Reactions': 'Deine Reaktionen',
    'Overall Rating': 'Gesamtbewertung', 'OST Rating': 'Soundtrack',
    'Side Characters Rating': 'Nebenfiguren', 'Chemistry Score': 'Chemie',
    'Recommendation Score': 'Empfehlung', 'Rewatch Score': 'Wieder ansehen',
    'Rate this drama': 'Drama bewerten', 'React to this drama': 'Auf Drama reagieren',
    'React to this actor': 'Auf Schauspieler reagieren', 'Vote on traits': 'Über Eigenschaften abstimmen',
    'Agree': 'Zustimmen', 'Disagree': 'Ablehnen',
    'Welcome back': 'Willkommen zurück', 'Create your account': 'Konto erstellen',
    'Email': 'E-Mail', 'Password': 'Passwort', 'Display name': 'Anzeigename',
    'Already have an account?': 'Schon ein Konto?', 'Need an account?': 'Konto benötigt?',
    'Continue': 'Weiter', 'Cancel': 'Abbrechen',
    'Follow': 'Folgen', 'Following': 'Folgt'
  },
  uk: {
    'Home': 'Головна', 'Chinese Dramas': 'Дорами Китаю', 'Chinese Actors': 'Актори',
    'Mao Picks': 'Mao Picks', 'Rankings': 'Рейтинги', 'News': 'Новини',
    'Community': 'Спільнота', 'Watchlist': 'Список', 'Profile': 'Профіль',
    'Sign In': 'Увійти', 'Join Free': 'Реєстрація', 'Sign Up': 'Створити обліковий запис',
    'Log Out': 'Вийти', 'My Profile': 'Мій профіль', 'My Library': 'Моя бібліотека',
    'My Ratings': 'Мої оцінки', 'My Reactions': 'Мої реакції',
    'My Lists': 'Мої списки', 'Account Settings': 'Налаштування', 'Language': 'Мова',
    'Upgrade to Premium': 'Преміум',
    'Discover Chinese Dramas & Actors': 'Відкрий китайські дорами та акторів',
    'Explore C-Dramas': 'Дивитися дорами', 'Browse Chinese Actors': 'Актори Китаю',
    'Get AI Recommendations': 'AI рекомендації',
    'Sign in to rate': 'Увійдіть, щоб оцінити',
    'Sign in to react': 'Увійдіть, щоб залишити реакцію',
    'Sign in to rate this drama': 'Увійдіть, щоб оцінити дораму',
    'Sign in to react to this actor': 'Увійдіть, щоб залишити реакцію',
    'Create an account to save this drama': 'Створіть обліковий запис, щоб зберегти',
    'Create an account to save to your watchlist': 'Створіть обліковий запис, щоб додати',
    'Join free to follow actors': 'Реєструйтеся безкоштовно, щоб підписатися на акторів',
    'Join free to unlock your C-Drama library': 'Реєструйтеся, щоб отримати доступ до бібліотеки',
    'Want to see more? Create a free account.': 'Хочете більше? Створіть безкоштовний обліковий запис.',
    'Sign up to unlock more details.': 'Зареєструйтеся, щоб побачити більше.',
    'Create an account to access full actor profiles.': 'Створіть обліковий запис для повних профілів.',
    'Register to see full drama details, ratings, reactions, and recommendations.': 'Зареєструйтеся для повних деталей дорами, оцінок та рекомендацій.',
    'Join free to save dramas, follow actors, and get personalized AI recommendations.': 'Реєстрація — зберігайте дорами, підписуйтеся на акторів і отримуйте персональні рекомендації.',
    'Sign in to rate, react, comment, and build your C-Drama library.': 'Увійдіть, щоб оцінювати, реагувати та коментувати.',
    'Create your free account to unlock fandom rankings, watchlists, reviews, and AI picks.': 'Створіть безкоштовний обліковий запис — рейтинги, списки, рецензії та AI підбір.',
    'Sign Up to See More': 'Зареєструйтеся', 'Create Free Account': 'Створити обліковий запис',
    'Unlock Full Profile': 'Відкрити профіль', 'Unlock More Details': 'Детальніше',
    'Sign In to Continue': 'Увійти', 'Register to Save': 'Реєстрація',
    'Register to Rate': 'Реєстрація', 'Register to React': 'Реєстрація',
    'Your Ratings': 'Ваші оцінки', 'Your Reactions': 'Ваші реакції',
    'Overall Rating': 'Загальна оцінка', 'OST Rating': 'Саундтрек',
    'Side Characters Rating': 'Другорядні', 'Chemistry Score': 'Хімія',
    'Recommendation Score': 'Рекомендація', 'Rewatch Score': 'Передивитися',
    'Rate this drama': 'Оцінити дораму', 'React to this drama': 'Реакція на дораму',
    'React to this actor': 'Реакція на актора', 'Vote on traits': 'Голосувати за риси',
    'Agree': 'Згоден', 'Disagree': 'Не згоден',
    'Welcome back': 'Раді бачити знову', 'Create your account': 'Створіть обліковий запис',
    'Email': 'Email', 'Password': 'Пароль', 'Display name': 'Імʼя',
    'Already have an account?': 'Вже маєте обліковий запис?', 'Need an account?': 'Немає облікового запису?',
    'Continue': 'Продовжити', 'Cancel': 'Скасувати',
    'Follow': 'Підписатися', 'Following': 'Підписані'
  }
};

const I18nContext = createContext({ lang: 'en', setLang: () => {}, t: (s) => s });

function useI18n() {return useContext(I18nContext);}

function I18nProvider({ children }) {
  const [lang, setLang] = useState(() => localStorage.getItem('ds_lang') || 'en');
  // Оверрайды из админки: { 'en': {key: value, ...}, 'ru': {...}, ... }
  // Грузим на mount + при смене языка. Tick — для перерендера когда оверрайды
  // приехали (объект мутируется, обычный setState на mutated obj не триггерит).
  const [overrideTick, setOverrideTick] = useState(0);
  const overridesRef = useRef({});

  useEffect(() => {
    localStorage.setItem('ds_lang', lang);
    document.documentElement.lang = lang;
    window.__dsLang = lang;
    window.dispatchEvent(new CustomEvent('ds-lang-change', { detail: lang }));
  }, [lang]);

  // Тянем оверрайды для текущего языка. /api/site-texts кэшируется на edge.
  useEffect(() => {
    if (overridesRef.current[lang]) return;  // уже загружено
    fetch(`${API_BASE}/api/site-texts?lang=${encodeURIComponent(lang)}`)
      .then(r => r.ok ? r.json() : null)
      .then(map => {
        if (map && typeof map === 'object') {
          overridesRef.current[lang] = map;
          setOverrideTick(t => t + 1);
        }
      })
      .catch(() => {});
  }, [lang]);

  const t = (key) => {
    if (!key) return key;
    // 1. Сначала оверрайды из админки (если есть)
    const ovr = overridesRef.current[lang];
    if (ovr && ovr[key]) return ovr[key];
    // 2. Фиксированный словарь из I18N
    const dict = I18N[lang];
    if (dict && dict[key]) return dict[key];
    // 3. Fallback: taxonomy переводы из data/taxonomy.json + /api/genres
    if (typeof window !== 'undefined') {
      if (lang === 'ru' && window.__dsTaxonomyRu && window.__dsTaxonomyRu[key]) {
        return window.__dsTaxonomyRu[key];
      }
      // EN-режим: если у дорамы genre/keyword пришёл на RU (например "Парни"),
      // резолвим обратно в EN через симметричную карту RU → EN.
      if (lang === 'en' && window.__dsTaxonomyEn && window.__dsTaxonomyEn[key]) {
        return window.__dsTaxonomyEn[key];
      }
    }
    // 4. Иначе сам ключ
    return key;
  };
  // overrideTick включён в зависимости через объект value — React увидит новую
  // ссылку при изменении и заново передаст consumers.
  const ctxValue = { lang, setLang, t, _overrideTick: overrideTick };
  return <I18nContext.Provider value={ctxValue}>{children}</I18nContext.Provider>;
}

// ── AUTH ──────────────────────────────────────────────────────────────────────
// API_BASE — Worker URL.
//   Локалка (localhost / 127.0.0.1 / 192.168.x.x / 10.x.x.x / 172.16-31.x.x) → dev-Worker :8787
//   Custom-домен (maodrama.com + поддомены) → api.maodrama.com (основной prod)
//   Legacy (dramascope.asia если когда-нибудь активируется) → api.dramascope.asia
//   Прочий прод (pages.dev / preview-deploys) → dramascope-api.uberfoto-eu.workers.dev
// Важно: 1st-party cookies требуют чтобы фронт и api сидели на одном eTLD+1
// (maodrama.com + api.maodrama.com — same-site), иначе POST/PUT падают с 401
// из-за блокировки 3rd-party cookies в современных браузерах.
function _isLocalHost(h) {
  if (!h) return false;
  if (h === 'localhost' || h === '127.0.0.1') return true;
  if (/^192\.168\./.test(h)) return true;
  if (/^10\./.test(h)) return true;
  if (/^172\.(1[6-9]|2\d|3[01])\./.test(h)) return true;
  return false;
}
function _resolveApiBase() {
  if (typeof window === 'undefined') return 'https://api.maodrama.com';
  const host = window.location.hostname;
  if (_isLocalHost(host)) return `http://${host}:8787`;
  // MaoDrama — основной production-домен
  if (host === 'maodrama.com' || host.endsWith('.maodrama.com')) {
    return 'https://api.maodrama.com';
  }
  // Legacy dramascope.asia (на переходный период если кто-то заходит по старому домену)
  if (host === 'dramascope.asia' || host.endsWith('.dramascope.asia')) {
    return 'https://api.dramascope.asia';
  }
  // Pages preview / fallback — пользуется workers.dev (3rd-party cookies,
  // mutating requests могут падать; preview-окружение не для production-use)
  return 'https://dramascope-api.uberfoto-eu.workers.dev';
}
const API_BASE = _resolveApiBase();

// OAuth-провайдеры (Google/Facebook/Telegram) — пока ещё не настроены креды в Wrangler.
// Установить в true, когда добавим GOOGLE_CLIENT_ID/SECRET через `wrangler secret put`
const OAUTH_ENABLED = false;

// Читает значение cookie по имени. CSRF cookie не HttpOnly — JS может прочитать.
function readCookie(name) {
  if (typeof document === 'undefined') return null;
  const m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/[.$?*|{}()[\]\\\/\+^]/g, '\\$&') + '=([^;]*)'));
  return m ? decodeURIComponent(m[1]) : null;
}

async function apiFetch(path, opts = {}) {
  const method = (opts.method || 'GET').toUpperCase();
  const headers = { ...(opts.headers || {}) };
  // Content-Type только для JSON-payload. Если body — FormData/Blob, браузер
  // САМ выставит правильный multipart/form-data; boundary=... — не трогаем.
  const isFormBody = (typeof FormData !== 'undefined' && opts.body instanceof FormData)
                 || (typeof Blob !== 'undefined' && opts.body instanceof Blob);
  if (!isFormBody && !headers['Content-Type'] && !headers['content-type']) {
    headers['Content-Type'] = 'application/json';
  }
  // CSRF: на mutating-запросах добавляем X-CSRF-Token из ds_csrf cookie.
  // Worker сверит token против HMAC(session, secret). Без header'а будет 403.
  if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
    const csrf = readCookie('ds_csrf');
    if (csrf) headers['X-CSRF-Token'] = csrf;
  }
  const r = await fetch(API_BASE + path, {
    ...opts,
    credentials: 'include',
    headers,
  });
  // FormData/Blob endpoints (upload) могут вернуть и не-JSON — но наши /api/me/avatar
  // возвращает JSON. Если парс провалится — оставим data=null, error будет HTTP <code>.
  let data = null;
  try { data = await r.json(); } catch {}
  if (!r.ok) throw new Error((data && data.error) || `HTTP ${r.status}`);
  return data;
}

const AuthContext = createContext({ user: null, signIn: () => {}, signUp: () => {}, signOut: () => {}, refreshUser: () => {} });

function useAuth() { return useContext(AuthContext); }

// ── Миграция гостевых данных в аккаунт пользователя ─────────────────────────
// Все данные кабинета хранятся в localStorage с ключом вида
// `ds_<feature>_<email|guest>` (или `ds_<feature>_<id>_<email|guest>` для
// per-drama/per-actor рейтингов). Когда юзер логинится — переносим всё, что
// он успел сделать в гостевом режиме, в его аккаунт. Без потерь данных:
//   - массивы (library, fav_dramas, fav_people, user_lists, quiz_results) →
//     additive merge с dedup по id;
//   - объекты (рейтинги дорам/актёров) → shallow merge (user-данные имеют
//     приоритет — он явно их перезаписал после логина);
//   - примитивы (bio, settings) → user wins, guest fallback.
// После успешного слияния guest-копия удаляется.
function migrateGuestData(email) {
  if (!email) return;
  const guestSuffix = '_guest';
  const userSuffix = '_' + email;
  // Собираем все guest-ключи (нельзя итерироваться и удалять одновременно).
  const guestKeys = [];
  for (let i = 0; i < localStorage.length; i++) {
    const k = localStorage.key(i);
    if (k && k.endsWith(guestSuffix) && k.startsWith('ds_')) guestKeys.push(k);
  }
  let mergedCount = 0;
  for (const gKey of guestKeys) {
    const uKey = gKey.slice(0, gKey.length - guestSuffix.length) + userSuffix;
    let gVal, uVal;
    try { gVal = JSON.parse(localStorage.getItem(gKey)); } catch { gVal = null; }
    try { uVal = JSON.parse(localStorage.getItem(uKey)); } catch { uVal = null; }
    if (gVal == null) { localStorage.removeItem(gKey); continue; }
    let merged;
    if (Array.isArray(gVal)) {
      // additive merge с dedup по id (если есть) или по sig (quiz_results)
      const arr = Array.isArray(uVal) ? [...uVal] : [];
      const seen = new Set(arr.map(x => (x && (x.id != null ? String(x.id) : (x.sig || JSON.stringify(x))))));
      for (const item of gVal) {
        const key = item && (item.id != null ? String(item.id) : (item.sig || JSON.stringify(item)));
        if (!seen.has(key)) { arr.push(item); seen.add(key); }
      }
      merged = arr;
    } else if (gVal && typeof gVal === 'object') {
      // shallow merge: user поверх guest (user явно поставил после логина)
      merged = { ...gVal, ...(uVal && typeof uVal === 'object' ? uVal : {}) };
    } else {
      // примитив (string/number/bool) — user wins
      merged = (uVal != null) ? uVal : gVal;
    }
    try {
      localStorage.setItem(uKey, JSON.stringify(merged));
      localStorage.removeItem(gKey);
      mergedCount++;
    } catch {}
  }
  if (mergedCount > 0) {
    // Подёргать все слушатели, чтобы кабинет/виджеты перечитали данные.
    const events = [
      'ds-library-changed', 'ds-fav-dramas-changed', 'ds-fav-people-changed',
      'ds-userlists-changed', 'ds-ratings-changed', 'ds-actor-ratings-changed',
      'ds-quiz-results-changed', 'storage',
    ];
    for (const e of events) {
      try { window.dispatchEvent(new CustomEvent(e)); } catch {}
    }
  }
  return mergedCount;
}

function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [modalMode, setModalMode] = useState(null); // null | 'signin' | 'signup'

  // Map API user → UI shape (display_name → name alias for legacy UI)
  const normalizeUser = (u) => u ? { ...u, name: u.display_name || u.name, joinedAt: u.created_at } : null;

  // Restore session from cookie on mount
  const refreshUser = useCallback(async () => {
    try {
      const data = await apiFetch('/api/me');
      setUser(normalizeUser(data.user));
    } catch {
      setUser(null);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => { refreshUser(); }, [refreshUser]);

  // Слушаем глобальный event ds-user-changed — позволяет любому компоненту
  // (например ProfilePage после загрузки нового аватара) пере-pull'нуть user
  // и обновить навбар/AvatarMenu без перезагрузки страницы.
  useEffect(() => {
    const h = () => { refreshUser(); };
    window.addEventListener('ds-user-changed', h);
    return () => window.removeEventListener('ds-user-changed', h);
  }, [refreshUser]);

  const signIn = async ({ email, password }) => {
    const data = await apiFetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password }),
    });
    const u = normalizeUser(data.user);
    try { migrateGuestData(u?.email); } catch {}
    setUser(u);
    setModalMode(null);
    return data.user;
  };

  const signUp = async ({ email, name, password, turnstile_token }) => {
    const data = await apiFetch('/api/auth/register', {
      method: 'POST',
      body: JSON.stringify({ email, password, display_name: name, turnstile_token }),
    });
    const u = normalizeUser(data.user);
    try { migrateGuestData(u?.email); } catch {}
    setUser(u);
    setModalMode(null);
    return data.user;
  };

  const signOut = async () => {
    try { await apiFetch('/api/auth/logout', { method: 'POST' }); } catch {}
    setUser(null);
  };

  const openSignIn = () => setModalMode('signin');
  const openSignUp = () => setModalMode('signup');
  const closeModal = () => setModalMode(null);

  return (
    <AuthContext.Provider value={{ user, loading, signIn, signUp, signOut, openSignIn, openSignUp, refreshUser, apiFetch }}>
      {children}
      {modalMode && <AuthModal mode={modalMode} setMode={setModalMode} onClose={closeModal} />}
    </AuthContext.Provider>);

}

// Expose for non-React code if нужно
if (typeof window !== 'undefined') {
  window.__dsApi = { base: API_BASE, fetch: apiFetch };
  // Выставляем I18N в window — админ UI текстов перечисляет известные ключи.
  window.__dsI18N = I18N;
}

// ── Перевод серверных error-сообщений в дружелюбный текст для юзера ─────────
// Worker возвращает короткие английские строки (см. worker/src/auth.js).
// Тут мы выбираем перевод по lang + добавляем подсказки.
function friendlyAuthError(rawMsg, mode, isRu) {
  const m = String(rawMsg || '').toLowerCase();
  const ru = isRu;
  // Auth errors
  if (m.includes('invalid credentials')) return ru ? 'Неверный email или пароль' : 'Wrong email or password';
  if (m.includes('email already registered')) return ru ? 'Этот email уже зарегистрирован. Попробуй войти.' : 'This email is already registered. Try signing in.';
  if (m.includes('invalid email')) return ru ? 'Неверный формат email' : 'Invalid email format';
  if (m.includes('display name required')) return ru ? 'Введи имя (минимум 2 символа)' : 'Display name required (min 2 chars)';
  if (m.includes('email and password required')) return ru ? 'Заполни email и пароль' : 'Email and password required';
  // Password policy (server-side)
  if (m.includes('password must be at least')) return ru ? 'Пароль должен быть минимум 8 символов' : 'Password must be at least 8 characters';
  if (m.includes('password is too long')) return ru ? 'Пароль слишком длинный (максимум 20 символов)' : 'Password is too long (max 20 characters)';
  if (m.includes('must contain at least one lowercase')) return ru ? 'Нужна хотя бы одна строчная буква' : 'Need at least one lowercase letter';
  if (m.includes('must contain at least one uppercase')) return ru ? 'Нужна хотя бы одна заглавная буква' : 'Need at least one uppercase letter';
  if (m.includes('must contain at least one letter')) return ru ? 'Пароль должен содержать хотя бы одну букву' : 'Password must contain at least one letter';
  if (m.includes('must contain at least one digit')) return ru ? 'Пароль должен содержать хотя бы одну цифру' : 'Password must contain at least one digit';
  if (m.includes('must contain at least one special')) return ru ? 'Нужен хотя бы один спецсимвол (например, ! ? # @ $)' : 'Need at least one special character (e.g. ! ? # @ $)';
  if (m.includes('password is too simple')) return ru ? 'Слишком простой пароль — попробуй сложнее' : 'Password is too simple — try something less guessable';
  // HIBP breach — текст содержит число утечек, парсим и вставляем в перевод
  if (m.includes('appeared in') && m.includes('data breaches')) {
    const match = /appeared in ([\d,]+) /.exec(rawMsg);
    const count = match ? match[1] : '';
    return ru
      ? `Этот пароль найден в утечках (${count} раз). Выбери другой пароль — он давно скомпрометирован.`
      : `This password has appeared in ${count} data breaches. Please choose a different one.`;
  }
  // Rate-limit — нейтральный текст, не упоминаем IP / детали механизма.
  // Сервер теперь шлёт "Too many attempts. Try again later." (один общий текст
  // и для login и для register) — у нас на фронте можем ещё уточнить про время.
  if (m.includes('too many attempts')) return ru ? 'Слишком много попыток. Попробуй позже.' : 'Too many attempts. Try again later.';
  // Legacy-match для совместимости со старыми ответами сервера (если кэшированы)
  if (m.includes('too many login attempts')) return ru ? 'Слишком много попыток. Попробуй через 15 минут.' : 'Too many attempts. Try again in 15 minutes.';
  if (m.includes('too many registrations')) return ru ? 'Слишком много попыток. Попробуй позже.' : 'Too many attempts. Try again later.';
  // CSRF
  if (m.includes('csrf')) return ru ? 'Сессия устарела. Перезагрузи страницу и войди заново.' : 'Session expired. Refresh and sign in again.';
  // Google
  if (m.includes('google token verification failed')) return ru ? 'Не удалось проверить Google-логин. Попробуй ещё раз.' : 'Google sign-in failed. Try again.';
  if (m.includes('google login not configured')) return ru ? 'Google-логин временно недоступен.' : 'Google sign-in is temporarily unavailable.';
  // Network
  if (m.includes('failed to fetch') || m.includes('networkerror')) return ru ? 'Нет связи с сервером. Проверь интернет.' : 'Can\'t reach server. Check your connection.';
  // По умолчанию — оставляем как есть, но без HTTP-кода
  return rawMsg.replace(/^HTTP \d+/, '').trim() || (ru ? 'Ошибка' : 'Error');
}

// ── Google Sign-In кнопка ───────────────────────────────────────────────────
// Рендерит официальный Google-button через GIS (Google Identity Services).
// При клике пользователь видит попап Google → подтверждает аккаунт → мы
// получаем ID-токен и шлём его на /api/auth/google. После успеха —
// migrateGuestData + setUser (как обычный signIn).
function GoogleSignInButton({ onSuccess, onError, isRu, mode }) {
  const containerRef = useRef(null);
  const [ready, setReady] = useState(typeof window !== 'undefined' && !!window.google?.accounts?.id);
  const [unconfigured, setUnconfigured] = useState(false);

  // Google Sign-In в Telegram WebView не работает (Google блокирует OAuth).
  // Решение — оставлено на будущее (Sign in with Telegram или специальный UX).
  // Пока просто показываем кнопку как обычно — юзер сам разберётся.

  // Дождаться загрузки GIS-скрипта (он async/defer, может ещё не быть готов).
  useEffect(() => {
    if (ready) return;
    const clientId = (typeof window !== 'undefined') && window.__dsGoogleClientId;
    if (!clientId) { setUnconfigured(true); return; }
    let cancelled = false;
    const check = () => {
      if (cancelled) return;
      if (window.google?.accounts?.id) setReady(true);
      else setTimeout(check, 120);
    };
    check();
    return () => { cancelled = true; };
  }, [ready]);

  // Когда GIS готов — рендерим кнопку.
  useEffect(() => {
    if (!ready) return;
    const clientId = window.__dsGoogleClientId;
    if (!clientId || !containerRef.current) return;
    try {
      window.google.accounts.id.initialize({
        client_id: clientId,
        callback: async (resp) => {
          if (!resp || !resp.credential) {
            if (onError) onError(new Error('No credential from Google'));
            return;
          }
          try {
            const data = await apiFetch('/api/auth/google', {
              method: 'POST',
              body: JSON.stringify({ credential: resp.credential }),
            });
            if (onSuccess) onSuccess(data.user);
          } catch (e) {
            if (onError) onError(e);
          }
        },
      });
      // Очистить контейнер (на случай ре-рендера)
      containerRef.current.innerHTML = '';
      window.google.accounts.id.renderButton(containerRef.current, {
        theme: 'outline',
        size: 'large',
        type: 'standard',
        shape: 'rectangular',
        text: mode === 'signup' ? 'signup_with' : 'signin_with',
        logo_alignment: 'left',
        width: 340,
      });
    } catch (e) {
      if (onError) onError(e);
    }
  }, [ready, mode]);

  if (unconfigured) {
    // Не настроен CLIENT_ID — просто прячемся, не показываем placeholder
    // (чтобы тестеры не путались).
    return null;
  }

  return (
    <div style={{ display: 'flex', justifyContent: 'center', marginBottom: 14, minHeight: 40 }}>
      <div ref={containerRef} />
      {!ready && (
        <div style={{ fontSize: 12, color: 'var(--text3)', padding: '10px 0' }}>
          {isRu ? 'Загружаем Google…' : 'Loading Google…'}
        </div>
      )}
    </div>
  );
}

// Маленькое красное сообщение под полем ввода. Для inline-валидации.
function FieldError({ text }) {
  return (
    <div role="alert" style={{ fontSize: 11.5, color: '#e05858', marginTop: 5, lineHeight: 1.35 }}>
      {text}
    </div>
  );
}

// Чек-листочек правила пароля. Зелёный когда выполнено, серый — пока нет.
function PolicyCheck({ ok, text }) {
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 3,
      color: ok ? '#3dd68c' : 'var(--text3)',
      transition: 'color 0.2s'
    }}>
      <span style={{ fontSize: 12, lineHeight: 1 }}>{ok ? '✓' : '○'}</span>
      <span>{text}</span>
    </span>
  );
}

// ── Forgot password form ──────────────────────────────────────────────────
// Простая форма: email → POST /api/auth/forgot-password → show "check inbox"
// Backend всегда возвращает 200 (чтобы не палить какие email'ы зарегистрированы),
// поэтому UX просто показывает success-сообщение.
function ForgotPasswordForm({ isRu, apiFetch, onBack }) {
  const [email, setEmail] = useState('');
  const [busy, setBusy] = useState(false);
  const [sent, setSent] = useState(false);
  const submit = async (e) => {
    e.preventDefault();
    if (!email.trim() || !email.includes('@')) return;
    setBusy(true);
    try {
      await apiFetch('/api/auth/forgot-password', {
        method: 'POST',
        body: JSON.stringify({ email: email.trim() }),
      });
      setSent(true);
    } catch {
      // Даже при сетевой ошибке показываем success (чтобы не было ситуации
      // когда юзер думает что email не зарегистрирован — мы не палим этот факт)
      setSent(true);
    } finally {
      setBusy(false);
    }
  };

  if (sent) {
    return (
      <div style={{ textAlign: 'center', padding: '20px 0' }}>
        <div style={{ fontSize: 40, marginBottom: 16 }}>📬</div>
        <h3 style={{ fontSize: 16, fontWeight: 700, marginBottom: 12 }}>
          {isRu ? 'Письмо отправлено' : 'Email sent'}
        </h3>
        <p style={{ fontSize: 13, color: 'var(--text3)', lineHeight: 1.5, marginBottom: 20 }}>
          {isRu
            ? `Если email ${email} зарегистрирован — пришлём ссылку для сброса пароля в течение минуты. Проверь папку «Спам» если не нашёл.`
            : `If ${email} is registered — we will send a reset link within a minute. Check Spam folder if you don't see it.`}
        </p>
        <button type="button" onClick={onBack} style={{
          padding: '10px 22px', borderRadius: 8, background: 'var(--accent)',
          color: '#fff', fontWeight: 700, fontSize: 13, border: 'none', cursor: 'pointer'
        }}>
          {isRu ? 'Вернуться к входу' : 'Back to sign in'}
        </button>
      </div>
    );
  }

  return (
    <form onSubmit={submit} noValidate>
      <div style={{ marginBottom: 14 }}>
        <label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text3)', display: 'block', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>Email</label>
        <input type="email" value={email} maxLength={254} onChange={(e) => setEmail(e.target.value)}
          autoComplete="email" autoFocus
          style={{ width: '100%', padding: '11px 14px', borderRadius: 8, background: 'var(--bg3)', border: '1px solid rgba(74,158,255,0.15)', color: 'var(--text)', fontSize: 14, outline: 'none' }} />
      </div>
      <button type="submit" disabled={busy} style={{
        width: '100%', padding: '12px', borderRadius: 8, background: busy ? 'var(--bg3)' : 'var(--accent)',
        color: busy ? 'var(--text3)' : '#fff', fontWeight: 700, fontSize: 14, marginTop: 14, marginBottom: 14,
        cursor: busy ? 'wait' : 'pointer', border: 'none'
      }}>
        {busy ? (isRu ? 'Отправляем…' : 'Sending…') : (isRu ? 'Отправить ссылку' : 'Send reset link')}
      </button>
      <div style={{ textAlign: 'center', fontSize: 12 }}>
        <button type="button" onClick={onBack} style={{ color: 'var(--accent)', background: 'transparent', border: 'none', cursor: 'pointer', fontWeight: 600 }}>
          ← {isRu ? 'Вернуться к входу' : 'Back to sign in'}
        </button>
      </div>
    </form>
  );
}

function AuthModal({ mode, setMode, onClose }) {
  const { signIn, signUp, refreshUser } = useAuth();
  const { t, lang } = useI18n();
  const isRu = lang === 'ru';
  const [email, setEmail] = useState('');
  const [name, setName] = useState('');
  const [password, setPassword] = useState('');
  const [password2, setPassword2] = useState(''); // confirm password (только signup)
  const [showPassword, setShowPassword] = useState(false);
  const [fieldErrors, setFieldErrors] = useState({}); // { name?, email?, password?, password2? }
  const [topErr, setTopErr] = useState(''); // общая ошибка от сервера
  const [busy, setBusy] = useState(false);
  const firstFieldRef = useRef(null);
  // Turnstile token — заполняется async callback'ом из CF api.js. Без него
  // signUp() пошлёт пустую строку, worker ответит "Captcha verification failed".
  const [turnstileToken, setTurnstileToken] = useState('');
  const turnstileRef = useRef(null);
  const turnstileWidgetIdRef = useRef(null);

  // Авто-фокус на первое поле при открытии (signup — name, signin — email).
  useEffect(() => {
    const id = setTimeout(() => { try { firstFieldRef.current?.focus(); } catch {} }, 50);
    return () => clearTimeout(id);
  }, [mode]);

  const hasGoogle = typeof window !== 'undefined' && !!window.__dsGoogleClientId;
  const hasTurnstile = typeof window !== 'undefined' && !!window.__dsTurnstileSiteKey;

  // Turnstile widget — рендерим только в режиме signup. CF api.js загружается
  // асинхронно через <script defer>, поэтому ждём пока window.turnstile появится.
  useEffect(() => {
    if (mode !== 'signup') return;
    if (!hasTurnstile) return;
    let cancelled = false;
    let widgetId = null;
    const tryRender = () => {
      if (cancelled) return;
      if (!window.turnstile) {
        setTimeout(tryRender, 150);
        return;
      }
      if (!turnstileRef.current) return;
      // Если widget уже отрендерен (re-mount при смене темы и т.п.) — снести старый
      try {
        if (turnstileWidgetIdRef.current != null) {
          window.turnstile.remove(turnstileWidgetIdRef.current);
        }
      } catch {}
      try {
        widgetId = window.turnstile.render(turnstileRef.current, {
          sitekey: window.__dsTurnstileSiteKey,
          theme: (document.documentElement.getAttribute('data-theme') === 'day') ? 'light' : 'dark',
          callback: (token) => { if (!cancelled) setTurnstileToken(token); },
          'error-callback': () => { if (!cancelled) setTurnstileToken(''); },
          'expired-callback': () => { if (!cancelled) setTurnstileToken(''); },
        });
        turnstileWidgetIdRef.current = widgetId;
      } catch (e) {
        // Если widget уже есть на этом контейнере — игнорируем (StrictMode двойной mount)
      }
    };
    tryRender();
    return () => {
      cancelled = true;
      try {
        if (widgetId != null && window.turnstile) {
          window.turnstile.remove(widgetId);
        }
      } catch {}
      turnstileWidgetIdRef.current = null;
    };
  }, [mode, hasTurnstile]);

  // ── Password policy checks (live, локально перед отправкой) ───────────────
  // Те же правила что и на сервере (worker/src/password.js) — HIBP проверка
  // только на сервере (т.к. fetch к стороннему API из браузера = утечка пароля).
  // Правила: 8..20 символов, ≥1 строчная, ≥1 заглавная, ≥1 цифра, ≥1 спецсимвол.
  const TRIVIAL = /^(0123456789|1234567890|abcdef|qwerty|password|letmein|admin123|welcome1|monkey12|(.)\1+)$/i;
  const SPECIAL = /[!@#$%^&*()_\-+=?.,:;'"`~/\\|<>\[\]{}]/;
  const pwdChecks = {
    length: password.length >= 8 && password.length <= 20,
    lower: /[a-z]/.test(password),
    upper: /[A-Z]/.test(password),
    digit: /[0-9]/.test(password),
    special: SPECIAL.test(password),
    notTrivial: password.length === 0 || !TRIVIAL.test(password.toLowerCase()),
  };
  const pwdAllPass = pwdChecks.length && pwdChecks.lower && pwdChecks.upper
    && pwdChecks.digit && pwdChecks.special && pwdChecks.notTrivial;
  // Strength meter — 0..4. Считаем сколько критериев пройдено + бонус за длину.
  const pwdStrength = (() => {
    if (!password) return 0;
    let score = 0;
    if (pwdChecks.length) score++;
    if (pwdChecks.lower) score++;
    if (pwdChecks.upper) score++;
    if (pwdChecks.digit) score++;
    if (pwdChecks.special) score++;
    if (pwdChecks.notTrivial) score++;
    if (password.length >= 12) score++; // бонус за длину
    return Math.min(4, Math.floor(score / 2));
  })();
  const strengthLabel = ['', isRu?'очень слабый':'very weak', isRu?'слабый':'weak', isRu?'средний':'okay', isRu?'сильный':'strong'][pwdStrength];
  const strengthColor = ['#3a4256','#e05858','#e0a058','#d9b35a','#3dd68c'][pwdStrength];

  const NAME_MAX = 80;
  const PASSWORD_MAX = 20;

  // ── Валидация всех полей одновременно ────────────────────────────────────
  const validate = () => {
    const errs = {};
    if (mode === 'signup') {
      const nm = name.trim();
      if (!nm) errs.name = isRu ? 'Введи имя' : 'Enter your name';
      else if (nm.length < 2) errs.name = isRu ? 'Минимум 2 символа' : 'At least 2 characters';
      else if (nm.length > NAME_MAX) errs.name = isRu ? `Максимум ${NAME_MAX} символов` : `Max ${NAME_MAX} characters`;
    }
    const em = email.trim();
    if (!em) errs.email = isRu ? 'Введи email' : 'Enter your email';
    else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(em)) errs.email = isRu ? 'Неверный формат email' : 'Invalid email format';
    if (!password) errs.password = isRu ? 'Введи пароль' : 'Enter your password';
    else if (mode === 'signup') {
      if (!pwdChecks.length) errs.password = isRu ? 'Минимум 8, максимум 20 символов' : 'Min 8, max 20 characters';
      else if (!pwdChecks.lower) errs.password = isRu ? 'Нужна хотя бы одна строчная буква' : 'Need at least one lowercase letter';
      else if (!pwdChecks.upper) errs.password = isRu ? 'Нужна хотя бы одна ЗАГЛАВНАЯ буква' : 'Need at least one uppercase letter';
      else if (!pwdChecks.digit) errs.password = isRu ? 'Нужна хотя бы одна цифра' : 'Need at least one digit';
      else if (!pwdChecks.special) errs.password = isRu ? 'Нужен хотя бы один спецсимвол (например, ! ? # @ $)' : 'Need at least one special character (e.g. ! ? # @ $)';
      else if (!pwdChecks.notTrivial) errs.password = isRu ? 'Слишком простой — попробуй сложнее' : 'Too simple — try something less guessable';
    }
    if (mode === 'signup') {
      if (!password2) errs.password2 = isRu ? 'Повтори пароль' : 'Confirm your password';
      else if (password2 !== password) errs.password2 = isRu ? 'Пароли не совпадают' : 'Passwords do not match';
    }
    return errs;
  };

  const submit = async (e) => {
    e.preventDefault();
    setTopErr('');
    const errs = validate();
    setFieldErrors(errs);
    if (Object.keys(errs).length > 0) return;
    // На signup требуем Turnstile token. Если widget ещё не отработал — не пускаем
    // submit'нуться и подсвечиваем сообщение.
    if (mode === 'signup' && window.__dsTurnstileSiteKey && !turnstileToken) {
      setTopErr(isRu
        ? 'Подождите, проверка безопасности ещё идёт…'
        : 'Please wait, security check in progress…');
      return;
    }
    setBusy(true);
    try {
      if (mode === 'signup') {
        await signUp({ email: email.trim(), name: name.trim(), password, turnstile_token: turnstileToken });
      } else {
        await signIn({ email: email.trim(), password });
      }
    } catch (e) {
      const msg = friendlyAuthError(e?.message, mode, isRu);
      // Серверные ошибки про пароль/email привязываем к полю
      const m = String(e?.message || '').toLowerCase();
      if (m.includes('password') || m.includes('breach') || m.includes('appeared in')) {
        setFieldErrors({ password: msg });
      } else if (m.includes('email')) {
        setFieldErrors({ email: msg });
      } else if (m.includes('captcha')) {
        setTopErr(msg + (isRu ? ' Обновляем проверку — попробуй ещё раз.' : ' Refreshing the check — try again.'));
      } else {
        setTopErr(msg);
      }
      // ВАЖНО: Turnstile-токен ОДНОРАЗОВЫЙ — Cloudflare его уже потратил во
      // время verify (или истечёт через 5 мин в любом случае). Поэтому ПОСЛЕ
      // ЛЮБОЙ ошибки submit'а (не только captcha) — резетим widget, иначе
      // retry с тем же токеном гарантированно получит captcha-error.
      if (mode === 'signup' && window.__dsTurnstileSiteKey) {
        setTurnstileToken('');
        try {
          if (window.turnstile && turnstileWidgetIdRef.current != null) {
            window.turnstile.reset(turnstileWidgetIdRef.current);
          }
        } catch {}
      }
    } finally {
      setBusy(false);
    }
  };

  // Ручной refresh capтчи — если юзер видит что виджет «застрял» или
  // получил captcha-ошибку. Это не обязательно (auto-reset делается выше),
  // но успокаивает юзера, что у него есть контроль.
  const refreshTurnstile = () => {
    if (!window.turnstile) return;
    setTurnstileToken('');
    setTopErr('');
    try {
      if (turnstileWidgetIdRef.current != null) {
        window.turnstile.reset(turnstileWidgetIdRef.current);
      }
    } catch {}
  };

  const onGoogleSuccess = async () => {
    // После /api/auth/google session-cookie уже выставлен. Подтягиваем
    // юзера + мигрируем guest-данные.
    try {
      await refreshUser();
      // refreshUser уже сделал setUser; миграцию вызовем по email вручную,
      // т.к. signIn/signUp не были задействованы.
      const me = await apiFetch('/api/me').catch(() => null);
      const email = me?.user?.email;
      if (email) try { migrateGuestData(email); } catch {}
    } catch {}
    onClose();
  };
  const onGoogleError = (e) => {
    setErr(friendlyAuthError(e?.message, mode, isRu));
  };

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 10000, background: 'rgba(4,6,14,0.78)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24, backdropFilter: 'blur(8px)',
      animation: 'fadeUp 0.2s ease'
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%', maxWidth: 420, background: 'var(--bg2)', border: '1px solid var(--border)',
        borderRadius: 16, padding: 32, position: 'relative'
      }}>
        <button onClick={onClose} aria-label="Close" style={{ position: 'absolute', top: 14, right: 18, fontSize: 22, color: 'var(--text3)', background: 'transparent', border: 'none', cursor: 'pointer' }}>×</button>
        <div style={{ textAlign: 'center', marginBottom: 22 }}>
          <svg width="40" height="40" viewBox="0 0 48 48" style={{ margin: '0 auto 10px', display: 'block' }}>
            <circle cx="24" cy="24" r="22" fill="none" stroke="var(--accent)" strokeWidth="2" />
            <circle cx="24" cy="24" r="13" fill="rgba(74,158,255,0.1)" />
            <ellipse cx="24" cy="24" rx="8" ry="22" fill="none" stroke="var(--accent)" strokeWidth="1.5" opacity="0.55" />
            <ellipse cx="24" cy="24" rx="22" ry="8" fill="none" stroke="var(--accent2)" strokeWidth="1.5" opacity="0.35" />
            <circle cx="24" cy="24" r="3.5" fill="var(--accent)" />
          </svg>
          <h2 style={{ fontSize: 20, fontWeight: 800 }}>
            {mode === 'signup' ? t('Create your account')
              : mode === 'forgot' ? (isRu ? 'Сбросить пароль' : 'Reset password')
              : t('Welcome back')}
          </h2>
          <p style={{ fontSize: 12, color: 'var(--text3)', marginTop: 4 }}>
            {mode === 'signup'
              ? (isRu ? 'Регистрация занимает 10 секунд' : 'Sign up in 10 seconds')
              : mode === 'forgot'
                ? (isRu ? 'Введи email — пришлём ссылку для сброса' : 'Enter email — we will send reset link')
                : (isRu ? 'Войди в свой MaoDrama' : 'Sign in to your MaoDrama')}
          </p>
        </div>

        {/* Forgot-password mode — отдельная упрощённая форма (только email + submit) */}
        {mode === 'forgot' ? (
          <ForgotPasswordForm isRu={isRu} apiFetch={apiFetch} onBack={() => setMode('signin')} />
        ) : (
        <>

        {hasGoogle && (
          <>
            <GoogleSignInButton
              mode={mode}
              isRu={isRu}
              onSuccess={onGoogleSuccess}
              onError={onGoogleError}
            />
            <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14, color: 'var(--text3)', fontSize: 11 }}>
              <div style={{ flex: 1, height: 1, background: 'rgba(74,158,255,0.15)' }} />
              <span style={{ textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700 }}>{isRu ? 'или' : 'or'}</span>
              <div style={{ flex: 1, height: 1, background: 'rgba(74,158,255,0.15)' }} />
            </div>
          </>
        )}

        <form onSubmit={submit} noValidate>
          {mode === 'signup' && (
            <div style={{ marginBottom: 12 }}>
              <label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text3)', display: 'block', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{t('Display name')}</label>
              <input
                ref={mode === 'signup' ? firstFieldRef : null}
                value={name}
                maxLength={NAME_MAX}
                onChange={(e) => { setName(e.target.value); setFieldErrors(prev => ({ ...prev, name: undefined })); }}
                autoComplete="name"
                style={{ width: '100%', padding: '11px 14px', borderRadius: 8, background: 'var(--bg3)', border: fieldErrors.name ? '1px solid #e05858' : '1px solid rgba(74,158,255,0.15)', color: 'var(--text)', fontSize: 14, outline: 'none' }}
              />
              {fieldErrors.name && <FieldError text={fieldErrors.name} />}
            </div>
          )}
          <div style={{ marginBottom: 12 }}>
            <label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text3)', display: 'block', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{t('Email')}</label>
            <input
              ref={mode === 'signin' ? firstFieldRef : null}
              type="email"
              value={email}
              maxLength={254}
              onChange={(e) => { setEmail(e.target.value); setFieldErrors(prev => ({ ...prev, email: undefined })); }}
              autoComplete="email"
              inputMode="email"
              style={{ width: '100%', padding: '11px 14px', borderRadius: 8, background: 'var(--bg3)', border: fieldErrors.email ? '1px solid #e05858' : '1px solid rgba(74,158,255,0.15)', color: 'var(--text)', fontSize: 14, outline: 'none' }}
            />
            {fieldErrors.email && <FieldError text={fieldErrors.email} />}
          </div>
          <div style={{ marginBottom: mode === 'signup' ? 12 : 6 }}>
            <label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text3)', display: 'block', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>{t('Password')}</label>
            <div style={{ position: 'relative' }}>
              <input
                type={showPassword ? 'text' : 'password'}
                value={password}
                maxLength={PASSWORD_MAX}
                onChange={(e) => { setPassword(e.target.value); setFieldErrors(prev => ({ ...prev, password: undefined })); }}
                autoComplete={mode === 'signup' ? 'new-password' : 'current-password'}
                style={{ width: '100%', padding: '11px 44px 11px 14px', borderRadius: 8, background: 'var(--bg3)', border: fieldErrors.password ? '1px solid #e05858' : '1px solid rgba(74,158,255,0.15)', color: 'var(--text)', fontSize: 14, outline: 'none' }}
              />
              <button type="button" onClick={() => setShowPassword(v => !v)} aria-label={showPassword ? (isRu ? 'Скрыть пароль' : 'Hide password') : (isRu ? 'Показать пароль' : 'Show password')} style={{
                position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)',
                background: 'transparent', border: 'none', cursor: 'pointer', padding: 6,
                color: 'var(--text3)', display: 'flex', alignItems: 'center'
              }}>
                {showPassword ? (
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
                ) : (
                  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
                )}
              </button>
            </div>
            {fieldErrors.password && <FieldError text={fieldErrors.password} />}
            {mode === 'signup' && (
              <>
                {/* Live policy checks — 5 критериев + текст-заголовок */}
                <div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 8, marginBottom: 4 }}>
                  {isRu ? 'Пароль должен содержать:' : 'Password must contain:'}
                </div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11.5, color: 'var(--text3)' }}>
                  <PolicyCheck ok={pwdChecks.length} text={isRu ? '8 символов (не более 20)' : '8 characters (max 20)'} />
                  <PolicyCheck ok={pwdChecks.lower && pwdChecks.digit} text={isRu ? '1 буква и 1 цифра' : '1 letter and 1 digit'} />
                  <PolicyCheck ok={pwdChecks.upper} text={isRu ? '1 заглавная буква' : '1 uppercase letter'} />
                  <PolicyCheck ok={pwdChecks.special} text={isRu ? '1 специальный символ (например, # ? ! $ & @)' : '1 special character (e.g. # ? ! $ & @)'} />
                </div>
                {/* Strength meter */}
                {password.length > 0 && (
                  <div style={{ marginTop: 6, display: 'flex', alignItems: 'center', gap: 8 }}>
                    <div style={{ flex: 1, height: 4, background: 'rgba(140,180,235,0.12)', borderRadius: 2, overflow: 'hidden' }}>
                      <div style={{ width: `${(pwdStrength / 4) * 100}%`, height: '100%', background: strengthColor, transition: 'width 0.2s, background 0.2s' }} />
                    </div>
                    <span style={{ fontSize: 10, color: strengthColor, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', minWidth: 60, textAlign: 'right' }}>
                      {strengthLabel}
                    </span>
                  </div>
                )}
              </>
            )}
          </div>
          {/* Confirm password — только signup */}
          {mode === 'signup' && (
            <div style={{ marginBottom: 6 }}>
              <label style={{ fontSize: 11, fontWeight: 700, color: 'var(--text3)', display: 'block', marginBottom: 6, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
                {isRu ? 'Повтори пароль' : 'Confirm password'}
              </label>
              <input
                type={showPassword ? 'text' : 'password'}
                value={password2}
                maxLength={PASSWORD_MAX}
                onChange={(e) => { setPassword2(e.target.value); setFieldErrors(prev => ({ ...prev, password2: undefined })); }}
                autoComplete="new-password"
                style={{ width: '100%', padding: '11px 14px', borderRadius: 8, background: 'var(--bg3)', border: fieldErrors.password2 ? '1px solid #e05858' : '1px solid rgba(74,158,255,0.15)', color: 'var(--text)', fontSize: 14, outline: 'none' }}
              />
              {fieldErrors.password2 && <FieldError text={fieldErrors.password2} />}
            </div>
          )}
          {/* Turnstile widget — только на signup. В signin он не нужен.
             Refresh-кнопка под виджетом — если юзер застрял с просроченной
             проверкой (мы и так резетим автоматически при любой ошибке, но
             ручная кнопка успокаивает). */}
          {mode === 'signup' && hasTurnstile && (
            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', margin: '12px 0 6px', gap: 6 }}>
              <div ref={turnstileRef} />
              <button type="button" onClick={refreshTurnstile} title={isRu ? 'Обновить проверку' : 'Refresh check'} style={{
                fontSize: 10.5, color: 'var(--text3)', background: 'transparent',
                border: 'none', cursor: 'pointer', padding: '2px 8px',
                display: 'inline-flex', alignItems: 'center', gap: 4
              }}>
                <svg viewBox="0 0 24 24" width="11" height="11" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
                  <polyline points="23 4 23 10 17 10" />
                  <polyline points="1 20 1 14 7 14" />
                  <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
                </svg>
                {isRu ? 'Обновить проверку' : 'Refresh check'}
              </button>
            </div>
          )}
          {topErr && (
            <div role="alert" style={{ fontSize: 12.5, color: '#e05858', background: 'rgba(196,69,69,0.10)', border: '1px solid rgba(196,69,69,0.25)', padding: '9px 12px', borderRadius: 7, margin: '12px 0 10px' }}>
              {topErr}
            </div>
          )}
          <button type="submit" disabled={busy} style={{
            width: '100%', padding: '12px', borderRadius: 8, background: busy ? 'var(--bg3)' : 'var(--accent)',
            color: busy ? 'var(--text3)' : '#fff', fontWeight: 700, fontSize: 14, marginTop: 14, marginBottom: 14,
            cursor: busy ? 'wait' : 'pointer', border: 'none'
          }}>
            {busy
              ? (isRu ? 'Подожди…' : 'Please wait…')
              : (mode === 'signup' ? t('Sign Up') : t('Sign In'))}
          </button>
          <div style={{ textAlign: 'center', fontSize: 12, color: 'var(--text3)' }}>
            {mode === 'signup' ? t('Already have an account?') : t('Need an account?')}{' '}
            <button type="button" onClick={() => { setTopErr(''); setMode(mode === 'signup' ? 'signin' : 'signup'); }} style={{ color: 'var(--accent)', fontWeight: 600, background: 'transparent', border: 'none', cursor: 'pointer' }}>
              {mode === 'signup' ? t('Sign In') : t('Sign Up')}
            </button>
          </div>
          {/* Forgot password — только на signin. На signup юзер только что
              регистрируется, ему не нужно ничего восстанавливать. */}
          {mode === 'signin' && (
            <div style={{ textAlign: 'center', fontSize: 12, marginTop: 8 }}>
              <button type="button" onClick={() => { setTopErr(''); setMode('forgot'); }}
                style={{ color: 'var(--text3)', background: 'transparent', border: 'none', cursor: 'pointer', textDecoration: 'underline' }}>
                {isRu ? 'Забыл пароль?' : 'Forgot password?'}
              </button>
            </div>
          )}
        </form>
        </>
        )}
      </div>
    </div>);

}

// ── HEADER WIDGETS ────────────────────────────────────────────────────────────
function LanguageMenu() {
  const { lang, setLang, t } = useI18n();
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  const closeTimerRef = useRef(null);
  useEffect(() => {
    const h = (e) => {if (ref.current && !ref.current.contains(e.target)) setOpen(false);};
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);
  // Hover-open мгновенно, hover-close с задержкой 350мс — чтобы успеть
  // перевести мышь с триггера на дропдаун. Любое возвращение в зону отменяет таймер.
  const cancelClose = () => {
    if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; }
  };
  const scheduleClose = () => {
    cancelClose();
    closeTimerRef.current = setTimeout(() => setOpen(false), 350);
  };
  useEffect(() => () => cancelClose(), []);
  const cur = LANGUAGES.find((l) => l.code === lang) || LANGUAGES[0];
  // Лейбл под глобусом — текущий язык (RU / EN / …) с правильным капсом
  const shortLabel = (cur.short || cur.code || '').toUpperCase().replace(/^([A-Z])/, m => m) ;
  // Хочется «Ru» / «En» (PascalCase) вместо «RU» — мягче читается.
  const niceShort = cur.short ? (cur.short[0] + cur.short.slice(1).toLowerCase()) : (cur.code || '').toUpperCase();
  return (
    <div ref={ref}
      onMouseEnter={() => { cancelClose(); setOpen(true); }}
      onMouseLeave={scheduleClose}
      style={{ position: 'relative', display: 'inline-flex' }}>
      {/* Триггер: глобус + лейбл «Язык» под ним. Клик тоже toggle. */}
      <button onClick={() => setOpen((v) => !v)}
        className="ds-lang-trigger"
        aria-label={t('Language')} title={t('Language')}
        style={{
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
          padding: '4px 6px', background: 'transparent', border: 'none', cursor: 'pointer',
          color: 'inherit', lineHeight: 1
        }}>
        <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor"
          strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
          <circle cx="12" cy="12" r="9"/>
          <path d="M3 12h18"/>
          <path d="M12 3a14 14 0 0 1 0 18"/>
          <path d="M12 3a14 14 0 0 0 0 18"/>
        </svg>
        <span style={{ fontSize: 10, fontWeight: 500, opacity: 0.85 }}>{niceShort}</span>
      </button>

      {/* Дропдаун со списком языков — theme-aware через .ds-auth-dropdown.
          top:100% (вплотную к триггеру, без gap) + paddingTop:12 даёт визуальный
          отступ ВНУТРИ зоны hover. Так курсор не теряется при переходе. */}
      {open && (
        <div className="ds-auth-dropdown"
          onMouseEnter={cancelClose}
          onMouseLeave={scheduleClose}
          style={{
          position: 'absolute', top: '100%', right: 0,
          minWidth: 200, padding: '12px 8px 8px', borderRadius: 12,
          boxShadow: '0 12px 32px rgba(0,0,0,0.25)',
          backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
          zIndex: 300
        }}>
          <div className="ds-auth-dropdown-arrow" style={{
            position: 'absolute', top: -5, right: 22,
            width: 10, height: 10, transform: 'rotate(45deg)'
          }} />
          {LANGUAGES.map((l) => (
            <button key={l.code} onClick={() => { setLang(l.code); setOpen(false); }}
              className="ds-lang-item"
              data-active={lang === l.code ? 'true' : 'false'}
              style={{
                width: '100%', padding: '10px 12px', borderRadius: 6, fontSize: 14, textAlign: 'left',
                display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                background: 'transparent', border: 'none', cursor: 'pointer',
                fontWeight: lang === l.code ? 700 : 500,
                marginBottom: 2
              }}>
              <span>{l.label}</span>
              <span style={{ fontSize: 10, opacity: 0.55, fontWeight: 700 }}>{l.short}</span>
            </button>
          ))}
        </div>
      )}
    </div>);

}

function AuthButtons() {
  const { openSignIn, openSignUp } = useAuth();
  const { t, lang } = useI18n();
  const isRu = lang === 'ru';
  const [open, setOpen] = useState(false);
  const wrapRef = useRef(null);
  const closeTimerRef = useRef(null);
  const cancelClose = () => { if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } };
  const scheduleClose = () => {
    cancelClose();
    closeTimerRef.current = setTimeout(() => setOpen(false), 350);
  };
  useEffect(() => () => cancelClose(), []);
  // Закрываем дропдаун при клике вне
  useEffect(() => {
    const h = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);

  return (
    <div ref={wrapRef} className="ds-authbuttons"
      onMouseEnter={() => { cancelClose(); setOpen(true); }}
      onMouseLeave={scheduleClose}
      style={{ position: 'relative', display: 'inline-flex' }}>
      {/* Триггер: иконка-человечек + лейбл под ней. Клик сразу открывает модалку. */}
      <button className="ds-auth-signin" onClick={openSignIn}
        aria-label={t('Sign In')} title={t('Sign In')}
        style={{
          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
          padding: '4px 6px', background: 'transparent', border: 'none', cursor: 'pointer',
          color: 'inherit', lineHeight: 1
        }}>
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor"
          strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="8" r="4" />
          <path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8" />
        </svg>
        <span style={{ fontSize: 10, fontWeight: 500, opacity: 0.85 }}>{t('Sign In')}</span>
      </button>

      {/* Дропдаун-подсказка с кнопкой логина. Цвета фона/кнопки/текста — через
          theme CSS (ds-auth-dropdown, ds-auth-dropdown-btn) — day: белый+dark-blue,
          night: тёмный+tiffany. */}
      {open && (
        <div className="ds-auth-dropdown"
          onMouseEnter={cancelClose}
          onMouseLeave={scheduleClose}
          style={{
          position: 'absolute', top: '100%', right: 0,
          minWidth: 240, padding: '24px 18px 18px', borderRadius: 12,
          boxShadow: '0 12px 32px rgba(0,0,0,0.25)',
          backdropFilter: 'blur(10px)', WebkitBackdropFilter: 'blur(10px)',
          zIndex: 300, textAlign: 'center'
        }}>
          {/* Маленький указатель-стрелка вверх */}
          <div className="ds-auth-dropdown-arrow" style={{
            position: 'absolute', top: -5, right: 22,
            width: 10, height: 10, transform: 'rotate(45deg)'
          }} />
          <div className="ds-auth-dropdown-text" style={{ fontSize: 13, marginBottom: 14, lineHeight: 1.4 }}>
            {isRu ? 'Войди чтобы оценивать и сохранять дорамы' : 'Login to rate and save dramas'}
          </div>
          <button className="ds-auth-dropdown-btn"
            onClick={(e) => { e.stopPropagation(); setOpen(false); openSignIn(); }}
            style={{
              width: '100%', padding: '11px 20px', borderRadius: 8,
              fontSize: 14, fontWeight: 700, border: 'none', cursor: 'pointer',
              transition: 'background 0.15s, box-shadow 0.15s'
            }}>
            {t('Sign In')}
          </button>
          {/* Текстовая кнопка «Регистрация» с подчёркиванием под основной */}
          <button type="button"
            onClick={(e) => { e.stopPropagation(); setOpen(false); openSignUp(); }}
            style={{
              marginTop: 10,
              background: 'transparent', border: 'none', padding: '4px 8px',
              fontSize: 13, fontWeight: 500, cursor: 'pointer',
              color: 'inherit', textDecoration: 'underline',
              textUnderlineOffset: 3, textDecorationThickness: 1,
              opacity: 0.85,
            }}
            onMouseEnter={(e) => { e.currentTarget.style.opacity = '1'; }}
            onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.85'; }}>
            {isRu ? 'Регистрация' : 'Register'}
          </button>
        </div>
      )}
    </div>);

}

// Тонкие линейные SVG-иконки для меню — единый стиль (stroke: currentColor)
const LineIcons = {
  user: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="8" r="4" />
      <path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8" />
    </svg>
  ),
  gear: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="12" cy="12" r="3" />
      <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
    </svg>
  ),
  paw: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="6" cy="10" r="1.8" />
      <circle cx="10" cy="5.5" r="1.8" />
      <circle cx="14" cy="5.5" r="1.8" />
      <circle cx="18" cy="10" r="1.8" />
      <path d="M8 17.5c0-3 1.8-5 4-5s4 2 4 5c0 1.7-1.3 3-3 3h-2c-1.7 0-3-1.3-3-3z" />
    </svg>
  ),
  // Корона — тот же SVG что в hero на главной
  crown: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 18l2-11 5 6 2-9 2 9 5-6 2 11z" />
      <path d="M3 21h18" />
    </svg>
  ),
  // Маленькая корона для бейджа рядом с именем
  crownSm: (
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <path d="M3 18l2-11 5 6 2-9 2 9 5-6 2 11z" />
      <path d="M3 21h18" />
    </svg>
  ),
  bell: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M18 16v-5a6 6 0 0 0-12 0v5l-2 3h16l-2-3z" />
      <path d="M10 21a2 2 0 0 0 4 0" />
    </svg>
  ),
  envelope: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <rect x="3" y="5" width="18" height="14" rx="2" />
      <path d="M3 7l9 6 9-6" />
    </svg>
  ),
  // ── Rating dimension icons (Drama Rating Panel) ──
  starOutline: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3l2.7 5.6 6.2.9-4.5 4.4 1 6.2L12 17.3 6.6 20.1l1-6.2L3.1 9.5l6.2-.9L12 3z" />
    </svg>
  ),
  music: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M9 18V5l11-2v13" />
      <circle cx="6" cy="18" r="3" />
      <circle cx="17" cy="16" r="3" />
    </svg>
  ),
  usersTwo: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <circle cx="9" cy="8" r="3.2" />
      <path d="M3 20c0-3.3 2.7-6 6-6s6 2.7 6 6" />
      <circle cx="17" cy="9" r="2.6" />
      <path d="M15.5 14.2c3-.2 5.5 1.7 5.5 4.8" />
    </svg>
  ),
  heartOutline: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 20.5s-7-4.4-9.3-9C1.2 8.4 3.2 5 6.7 5 9 5 10.7 6.5 12 8c1.3-1.5 3-3 5.3-3 3.5 0 5.5 3.4 4 6.5-2.3 4.6-9.3 9-9.3 9z" />
    </svg>
  ),
  thumbsUp: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M7 11v9H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3z" />
      <path d="M7 11l4-7c1.5 0 2.5 1 2.5 2.5V10h5a2 2 0 0 1 2 2.3l-1.3 6A2 2 0 0 1 17.2 20H7" />
    </svg>
  ),
  repeat: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <polyline points="17 1 21 5 17 9" />
      <path d="M3 11V9a4 4 0 0 1 4-4h14" />
      <polyline points="7 23 3 19 7 15" />
      <path d="M21 13v2a4 4 0 0 1-4 4H3" />
    </svg>
  ),
  // ── Reaction / Mao Picks icons ──
  heartBroken: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 20.5s-7-4.4-9.3-9C1.2 8.4 3.2 5 6.7 5 9 5 10.7 6.5 12 8c1.3-1.5 3-3 5.3-3 3.5 0 5.5 3.4 4 6.5-2.3 4.6-9.3 9-9.3 9z" />
      <path d="M12 8l-2 4 3 2-2 3" />
    </svg>
  ),
  flame: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 22c4 0 7-3 7-7 0-3-2-5-3-7-.5 2-2 3-3 3.5C12 9 11 6 12 3c-3 1-7 5-7 11 0 4 3 8 7 8z" />
    </svg>
  ),
  skull: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3c-4.4 0-8 3.4-8 7.5 0 2.4 1.2 4.5 3 5.8V19a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2v-2.7c1.8-1.3 3-3.4 3-5.8C20 6.4 16.4 3 12 3z" />
      <circle cx="9" cy="11" r="1.3" />
      <circle cx="15" cy="11" r="1.3" />
      <path d="M11 17h2" />
    </svg>
  ),
  sparkles: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3l1.5 4.5L18 9l-4.5 1.5L12 15l-1.5-4.5L6 9l4.5-1.5L12 3z" />
      <path d="M19 14l.7 2.1L22 17l-2.3.9L19 20l-.7-2.1L16 17l2.3-.9L19 14z" />
      <path d="M5 14l.7 2.1L8 17l-2.3.9L5 20l-.7-2.1L2 17l2.3-.9L5 14z" />
    </svg>
  ),
  moon: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M21 12.8A8.5 8.5 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
    </svg>
  ),
  starShine: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1" />
      <circle cx="12" cy="12" r="3" />
    </svg>
  ),
  leaf: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M5 20s2-9 8-13c3.5-2.3 8-2 8-2s.5 6-3 10-9 5-13 5z" />
      <path d="M5 20c2-4 5-7 9-9" />
    </svg>
  ),
  flag: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M5 21V4" />
      <path d="M5 4h12l-2 4 2 4H5" />
    </svg>
  ),
  handsUp: (
    <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
      <path d="M6 13c0-2 1-3 2-3v-3a1.5 1.5 0 0 1 3 0v4" />
      <path d="M11 7V4a1.5 1.5 0 0 1 3 0v6" />
      <path d="M14 9V6a1.5 1.5 0 0 1 3 0v6" />
      <path d="M17 8c1-1 3 0 3 2v3c0 4-3 7-7 7s-7-3-7-7" />
      <path d="M4 5l-1 2M20 5l1 2" />
    </svg>
  ),
};

function formatLastSeen(iso, lang = 'ru') {
  if (!iso) return '';
  const d = new Date(iso);
  const now = new Date();
  const diffMs = now - d;
  const m = Math.floor(diffMs / 60000);
  const RU = m < 1 ? 'только что' : null;
  const EN = m < 1 ? 'just now' : null;
  if (m < 1) return lang === 'ru' ? RU : EN;
  if (m < 60) return lang === 'ru' ? `${m} мин назад` : `${m}m ago`;
  const h = Math.floor(m / 60);
  if (h < 24) return lang === 'ru' ? `${h} ч назад` : `${h}h ago`;
  const days = Math.floor(h / 24);
  if (days < 7) return lang === 'ru' ? `${days} дн. назад` : `${days}d ago`;
  // Старше недели — короткая числовая дата ДД.ММ.ГГГГ (28.05.2026)
  const dd = String(d.getDate()).padStart(2, '0');
  const mm = String(d.getMonth() + 1).padStart(2, '0');
  return `${dd}.${mm}.${d.getFullYear()}`;
}

// Статус пользователя как в мессенджерах — для отображения ДРУГИМ людям
// (публичный профиль, личка). В собственном меню всегда «В сети».
//   online=true               → «В сети»
//   online=false + lastSeen   → «был(а) <дата/относительно>»
//   hidden=true               → размытый статус: «был(а) недавно» (≤30 дн)
//                               или «был(а) давно» (>30 дн) — точное время не палится
function formatStatus({ online, lastSeenIso, hidden, lang = 'ru' } = {}) {
  const ru = lang === 'ru';
  if (hidden) {
    let days = 9999;
    if (lastSeenIso) days = Math.floor((Date.now() - new Date(lastSeenIso)) / 86400000);
    if (days <= 30) return ru ? 'был(а) недавно' : 'seen recently';
    return ru ? 'был(а) давно' : 'seen a while ago';
  }
  if (online) return ru ? 'В сети' : 'Online';
  if (lastSeenIso) return (ru ? 'был(а) ' : 'last seen ') + formatLastSeen(lastSeenIso, lang);
  return ru ? 'не в сети' : 'Offline';
}
if (typeof window !== 'undefined') window.dsFormatStatus = formatStatus;

function AvatarMenu({ onNavigate }) {
  const { user, signOut } = useAuth();
  const { t, lang } = useI18n();
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  // «Последний раз в сети» — берём из localStorage прошлый визит (реальные данные),
  // фиксируем один раз за сессию. ref хранит ПРОШЛОЕ значение (до записи «сейчас»).
  const lastSeenRef = useRef(undefined);
  useEffect(() => {
    const h = (e) => {if (ref.current && !ref.current.contains(e.target)) setOpen(false);};
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);
  useEffect(() => {
    if (!user || !user.email) return;
    try {
      const sk = 'ds_seen_marked_' + user.email;
      if (!sessionStorage.getItem(sk)) {
        localStorage.setItem('ds_last_seen_' + user.email, new Date().toISOString());
        sessionStorage.setItem(sk, '1');
      }
    } catch {}
  }, [user && user.email]);
  // Тикер чтобы "минут назад" обновлялось пока открыто
  const [, setTick] = useState(0);
  useEffect(() => {
    if (!open) return;
    const id = setInterval(() => setTick(t => t + 1), 30000);
    return () => clearInterval(id);
  }, [open]);
  if (!user) return null;
  const initial = (user.name || user.email || '?').charAt(0).toUpperCase();
  // Ленивая инициализация: читаем ПРОШЛЫЙ визит ОДИН раз, до того как effect запишет «сейчас».
  if (lastSeenRef.current === undefined) {
    try { lastSeenRef.current = localStorage.getItem('ds_last_seen_' + user.email) || user.updated_at || user.created_at || null; }
    catch { lastSeenRef.current = user.updated_at || user.created_at || null; }
  }
  const lastSeenIso = lastSeenRef.current;
  // «Скрывать статус в сети» — из localStorage (мгновенно) или из глобального кеша приватности.
  let hideOnline = false;
  try { hideOnline = localStorage.getItem('ds_hide_online_' + user.email) === '1' || !!(typeof window !== 'undefined' && window.__dsUserPrivacy && window.__dsUserPrivacy.hide_online); } catch {}

  // Порядок согласован с Marina (см. brief 2026-05-18):
  // профиль → библиотека → оценки → реакции → списки → div → Мао → div → сообщения → уведомления → div → настройки → премиум → div → выйти
  // Админка — отдельный пункт только для is_admin юзеров, перед signout.
  const isRu = lang === 'ru';
  const items = [
    { id: 'profile', label: t('My Profile'), page: 'profile', icon: 'user' },
    // Library убрана из дропдауна — она есть в кабинете отдельной карточкой (Marina, 30.05.2026).
    // { id: 'library', label: t('My Library'), page: 'watchlist' },
    // По просьбе Marina скрыто (вернуть когда понадобится):
    // { id: 'ratings', label: t('My Ratings'), page: 'profile' },
    // { id: 'reactions', label: t('My Reactions'), page: 'profile' },
    // { id: 'lists', label: t('My Lists'), page: 'watchlist' },
    // { id: 'mao', label: t('My Mao'), page: 'mao', mao: true, icon: 'paw' },
    null,
    { id: 'messages', label: t('Messages'), page: 'messages', icon: 'envelope' },
    { id: 'notifications', label: t('Notifications'), page: 'notifications', icon: 'bell' },
    null,
    { id: 'settings', label: t('Settings'), page: 'profile', icon: 'gear' },
    // Premium-пункт меню скрыт до запуска монетизации. Включить:
    // window.__dsPremiumEnabled = true (в index.html).
    ...(typeof window !== 'undefined' && window.__dsPremiumEnabled
      ? [{ id: 'premium', label: t('Premium'), page: 'premium', premium: true, icon: 'crown' }]
      : []),
    ...(user.is_admin ? [null, { id: 'admin', label: isRu ? '🛠 Админка' : '🛠 Admin', page: 'admin' }] : []),
    null,
    { id: 'signout', label: t('Sign Out'), action: () => { signOut(); setOpen(false); } }
  ];

  // Счётчики непрочитанных — из ds-features SAMPLE_NOTIFICATIONS / SAMPLE_MESSAGES
  const unread = (window.__dsGetUnreadCounts ? window.__dsGetUnreadCounts() : { notifications: 0, messages: 0 });
  const totalUnread = (unread.notifications || 0) + (unread.messages || 0);

  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button onClick={() => setOpen((v) => !v)} style={{
        width: 36, height: 36, borderRadius: '50%',
        background: user.avatar_url ? `url(${user.avatar_url}) center/cover` : 'linear-gradient(135deg, var(--accent), var(--purple))',
        color: '#fff', fontWeight: 700, fontSize: 14, display: 'flex', alignItems: 'center', justifyContent: 'center',
        border: '2px solid transparent',
        // Тонкое кольцо через box-shadow (не сдвигает layout): закрыто — --avatar-ring
        // (Tiffany ночью / серое днём), открыто — акцент.
        boxShadow: open ? '0 0 0 2px var(--accent2)' : '0 0 0 1px var(--avatar-ring, rgba(120,130,150,0.45))',
        transition: 'box-shadow 0.15s',
        position: 'relative'
      }}>{user.avatar_url ? '' : initial}
        {totalUnread > 0 && (
          <span className="ds-notif-badge" style={{
            position: 'absolute', top: -3, right: -3,
            minWidth: 18, height: 18, borderRadius: 9,
            background: 'var(--red, #d44a4a)', color: '#fff',
            fontSize: 10, fontWeight: 400,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            padding: '0 4px',
            border: 'none',
            boxShadow: 'none'
          }}>{totalUnread}</span>
        )}
      </button>
      {open &&
      <div style={{
        position: 'absolute', top: 'calc(100% + 8px)', right: 0, zIndex: 300,
        background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 12,
        padding: 8, minWidth: 250, boxShadow: '0 12px 36px rgba(0,0,0,0.65)',
        fontFamily: "'Inter', -apple-system, system-ui, sans-serif"
      }}>
          {/* Header: имя + бейджи (premium/VIP/admin/rating), email, статус, last seen */}
          <div style={{ padding: '10px 12px 12px', borderBottom: '1px solid rgba(255,255,255,0.10)', marginBottom: 6 }}>
            <div style={{ fontSize: 13, fontWeight: 700, display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
              <span>{user.name}</span>
              {/* Premium/VIP-плашки СКРЫТЫ до запуска монетизации (#205).
                  Раньше у админов с is_premium=1 + is_vip=1 + activity_rating=0
                  показывалось «Имя [crown] 0 [Админ]» что выглядело как «Имя 00».
                  Когда запустим монетизацию — вернуть условный рендер обратно. */}
              {false && (user.is_premium || user.is_vip) && (
                <span title={user.is_vip ? 'VIP' : 'Premium'} style={{ display: 'inline-flex', color: '#ffd770' }}>
                  {LineIcons.crownSm}
                </span>
              )}
              {false && user.is_vip && typeof user.activity_rating === 'number' && (
                <span className="ds-vip-rating-badge" style={{
                  fontSize: 11, fontWeight: 700, padding: '2px 8px', borderRadius: 6,
                  background: '#1487cb',
                  color: '#ffffff',
                  border: '1px solid rgba(255,255,255,0.18)'
                }}>
                  {user.activity_rating}
                </span>
              )}
              {/* Role: admin / moderator / expert / viewer (newbie не показываем) */}
              {(() => {
                const r = user.role || (user.is_admin ? 'admin' : 'newbie');
                const isRu = lang === 'ru';
                const ROLE_STYLES = {
                  admin:     { label: isRu ? 'Админ'     : 'Admin',     bg: 'linear-gradient(135deg, #c44545, #ff6b6b)', color: '#fff' },
                  moderator: { label: isRu ? 'Модератор' : 'Moderator', bg: 'linear-gradient(135deg, #7C3AED, #A855F7)', color: '#fff' },
                  expert:    { label: isRu ? 'Эксперт'   : 'Expert',    bg: 'linear-gradient(135deg, #0abab5, #1ed4ce)', color: '#0a1226' },
                  viewer:    { label: isRu ? 'Зритель'   : 'Viewer',    bg: 'rgba(140,180,235,0.18)', color: '#b8d9ee' },
                };
                const s = ROLE_STYLES[r];
                if (!s) return null;
                return (
                  <span className="ds-role-badge" data-role={r} style={{
                    fontSize: 11, fontWeight: 500, padding: '2px 9px', borderRadius: 6,
                    background: s.bg, color: s.color, letterSpacing: 0
                  }}>{s.label}</span>
                );
              })()}
            </div>
            <div style={{ fontSize: 11, color: 'var(--text3)', marginTop: 2 }}>{user.email}</div>
            {/* Собственное меню — пользователь всегда онлайн, поэтому только «В сети».
                «был(а) …» показывается ДРУГИМ, когда тебя нет (см. formatStatus). */}
            <div style={{ display: 'flex', alignItems: 'center', gap: 5, marginTop: 5, fontSize: 10, color: 'var(--text3)', whiteSpace: 'nowrap', flexWrap: 'nowrap' }}>
              <span style={{ width: 6, height: 6, borderRadius: '50%', background: '#3dd68c', boxShadow: '0 0 5px rgba(61,214,140,0.7)', flexShrink: 0 }} />
              <span style={{ whiteSpace: 'nowrap' }}>{t('Online')}</span>
              {hideOnline && <span style={{ opacity: 0.55, whiteSpace: 'nowrap' }}>· {lang === 'ru' ? 'скрыто для других' : 'hidden from others'}</span>}
            </div>
          </div>
          {items.map((it, i) => {
            if (it === null) return <div key={'sep' + i} style={{ height: 0, margin: '5px 8px' }} />;

            // Mao — фиолетовая CTA-кнопка (в day-theme периwinkle gradient через .ds-ai-purple)
            if (it.mao) {
              return (
                <button key={it.id} className="ds-ai-purple ds-avatar-mao" onClick={() => { setOpen(false); onNavigate(it.page); }} style={{
                  width: 'calc(100% - 8px)', margin: '4px', padding: '10px 14px', borderRadius: 8,
                  fontSize: 13, fontWeight: 700, textAlign: 'left',
                  color: '#fff', border: 'none', cursor: 'pointer',
                  background: 'linear-gradient(135deg, #7C3AED 0%, #A855F7 100%)',
                  boxShadow: '0 4px 14px rgba(124,58,237,0.32)',
                  display: 'flex', alignItems: 'center', gap: 10
                }}
                  onMouseEnter={(e) => e.currentTarget.style.filter = 'brightness(1.1)'}
                  onMouseLeave={(e) => e.currentTarget.style.filter = 'none'}>
                  <span style={{ display: 'inline-flex', color: '#fff' }}>{LineIcons.paw}</span>
                  <span>{it.label}</span>
                </button>
              );
            }

            // Premium — sky gradient в day-теме через .ds-avatar-premium override
            if (it.premium) {
              return (
                <button key={it.id} className="ds-avatar-premium" onClick={() => { setOpen(false); onNavigate(it.page); }} style={{
                  width: 'calc(100% - 8px)', margin: '4px', padding: '10px 14px', borderRadius: 8,
                  fontSize: 13, fontWeight: 700, textAlign: 'left',
                  color: '#ffd770', cursor: 'pointer',
                  background: 'linear-gradient(135deg, rgba(34,22,62,0.85) 0%, rgba(58,40,104,0.85) 100%)',
                  border: '1px solid rgba(255,215,112,0.45)',
                  boxShadow: '0 4px 14px rgba(0,0,0,0.35), inset 0 0 0 1px rgba(255,215,112,0.08)',
                  display: 'flex', alignItems: 'center', gap: 10
                }}
                  onMouseEnter={(e) => e.currentTarget.style.filter = 'brightness(1.12)'}
                  onMouseLeave={(e) => e.currentTarget.style.filter = 'none'}>
                  <span className="ds-premium-icon" style={{ display: 'inline-flex', color: '#ffd770' }}>{LineIcons.crown}</span>
                  <span className="ds-premium-label" style={{ color: '#ffd770' }}>{it.label}</span>
                </button>
              );
            }

            const color = it.id === 'signout' ? 'var(--red)' : 'var(--text2)';
            const itemCount = it.id === 'notifications' ? unread.notifications
                            : it.id === 'messages' ? unread.messages : 0;
            return (
              <button key={it.id} onClick={() => { setOpen(false); it.action ? it.action() : onNavigate(it.page); }} style={{
                width: '100%', padding: '8px 12px', borderRadius: 6, fontSize: 13, textAlign: 'left',
                color, fontWeight: 500,
                display: 'flex', alignItems: 'center', gap: 10
              }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'rgba(74,158,255,0.07)'}
                onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
                {it.icon && <span style={{ display: 'inline-flex', width: 18 }}>{LineIcons[it.icon]}</span>}
                {!it.icon && <span style={{ display: 'inline-block', width: 18 }} />}
                <span style={{ flex: 1 }}>{it.label}</span>
                {itemCount > 0 && (
                  <span className="ds-notif-badge" style={{
                    minWidth: 20, height: 20, borderRadius: 10,
                    background: 'var(--red, #d44a4a)', color: '#fff',
                    fontSize: 10, fontWeight: 800,
                    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                    padding: '0 5px',
                    flexShrink: 0
                  }}>{itemCount}</span>
                )}
              </button>
            );
          })}
        </div>
      }
    </div>);

}

// ── GUEST GATE ────────────────────────────────────────────────────────────────
// Wrap any guest-locked action with this; shows a sign-in CTA when no user.
function GuestGate({ message, ctaLabel, variant = 'inline', children }) {
  const { user, openSignUp } = useAuth();
  const { t } = useI18n();
  if (user) return children || null;
  if (variant === 'overlay' && children) {
    return (
      <div style={{ position: 'relative' }}>
        <div style={{ pointerEvents: 'none', filter: 'blur(2px) brightness(0.7)', opacity: 0.5 }}>{children}</div>
        <div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 10, padding: 20, background: 'rgba(8,10,22,0.6)', borderRadius: 12 }}>
          <div style={{ fontSize: 13, color: 'var(--text2)', textAlign: 'center', maxWidth: 340 }}>{t(message)}</div>
          <button onClick={openSignUp} style={{ padding: '9px 20px', borderRadius: 8, background: 'var(--accent)', color: '#fff', fontWeight: 700, fontSize: 13 }}>{t(ctaLabel || 'Create Free Account')}</button>
        </div>
      </div>);

  }
  // banner
  return (
    <div style={{
      background: 'linear-gradient(135deg, rgba(74,158,255,0.08), rgba(192,132,252,0.05))',
      border: '1px dashed rgba(74,158,255,0.3)', borderRadius: 10, padding: '16px 20px',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, flexWrap: 'wrap'
    }}>
      <div style={{ flex: 1, minWidth: 200 }}>
        <div style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6 }}>{t(message)}</div>
      </div>
      <button onClick={openSignUp} style={{
        padding: '9px 18px', borderRadius: 8, background: 'var(--accent)', color: '#fff',
        fontWeight: 700, fontSize: 12.5, flexShrink: 0, whiteSpace: 'nowrap'
      }}>{t(ctaLabel || 'Sign Up to See More')}</button>
    </div>);

}

// ── DRAMA RATING PANEL ────────────────────────────────────────────────────────
const DRAMA_RATING_KEYS = [
  { id: 'overall',        label: 'Overall Rating',         icon: 'starOutline' },
  { id: 'ost',            label: 'OST Rating',             icon: 'music' },
  { id: 'side',           label: 'Side Characters Rating', icon: 'usersTwo' },
  { id: 'chemistry',      label: 'Chemistry Score',        icon: 'heartOutline' },
  { id: 'recommendation', label: 'Recommendation Score',   icon: 'thumbsUp' },
  { id: 'rewatch',        label: 'Rewatch Score',          icon: 'repeat' }
];

// Цветовой градиент 1–10 (красный→жёлтый→зелёный) — приглушённые пастельные оттенки
const RATING_COLOR = ['#ee8585','#ee9870','#eea870','#eebb72','#eecc78','#c8d878','#a8d078','#80c878','#5fbf78','#40b070'];
const RATING_LABEL_RU = ['Ужасно','Очень плохо','Плохо','Так себе','Средне','Нормально','Хорошо','Очень хорошо','Отлично','Шедевр'];
const RATING_LABEL_EN = ['Awful','Very bad','Bad','Meh','Average','OK','Good','Very good','Great','Masterpiece'];

// ── DRAMA RATINGS SUMMARY (компактный обзор рейтинга для hero блока) ─────────
// Показывает общий рейтинг (TMDB) + количество оценок + per-dimension оценки текущего юзера
function DramaRatingsSummary({ dramaId, voteAverage, voteCount }) {
  const { user } = useAuth();
  const { t } = useI18n();
  const [hoveredKey, setHoveredKey] = useState(null);
  const storageKey = user ? `ds_ratings_${dramaId}_${user.email}` : null;
  // Реактивно: храним userRatings в state и пере-читаем при ds-ratings-changed.
  // Раньше читали один раз при рендере → когда юзер ставил оценку в DramaRatingPanel ниже,
  // плашка сверху не обновлялась до перезагрузки страницы.
  const readUserRatings = () => {
    if (!storageKey) return {};
    try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; }
  };
  const [userRatings, setUserRatings] = useState(readUserRatings);
  useEffect(() => {
    const refresh = () => setUserRatings(readUserRatings());
    refresh(); // подхватить при смене dramaId / user
    window.addEventListener('ds-ratings-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-ratings-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [dramaId, user?.email]);

  const ratedKeys = DRAMA_RATING_KEYS.filter(k => userRatings[k.id] > 0);
  const userAvg = ratedKeys.length > 0
    ? (ratedKeys.reduce((s, k) => s + userRatings[k.id], 0) / ratedKeys.length)
    : null;

  // Используем TMDB как «общий» рейтинг + voteCount (или fallback на user-avg если TMDB пуст)
  const overall = voteAverage > 0 ? voteAverage : userAvg;
  const overallColor = overall ? RATING_COLOR[Math.min(9, Math.max(0, Math.round(Number(overall)) - 1))] : null;

  // Сколько человек рекомендуют дораму — реактивно на ds-recommend-changed.
  const readRecommend = () => {
    try {
      const agg = JSON.parse(localStorage.getItem(`ds_recommend_agg_${dramaId}`) || '{"yes":0}');
      return agg.yes || 0;
    } catch { return 0; }
  };
  const [recommendCount, setRecommendCount] = useState(readRecommend);
  useEffect(() => {
    const refresh = () => setRecommendCount(readRecommend());
    refresh();
    window.addEventListener('ds-recommend-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-recommend-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [dramaId]);

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 14, alignItems: 'flex-start' }}>
      {/* Верхняя строка: ★ общий рейтинг + кол-во оценок + сколько рекомендуют */}
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        {overall ? (
          <>
            <span className="ds-rating-star" style={{ fontSize: 22 }}>★</span>
            <span style={{ fontSize: 22, fontWeight: 800, color: overallColor || 'var(--text)' }}>{Number(overall).toFixed(1)}</span>
            <span style={{ fontSize: 12, color: 'var(--text3)' }}>/ 10</span>
            {voteCount > 0 && (
              <span style={{ fontSize: 12, color: 'var(--text3)' }}>· {voteCount.toLocaleString()} {t('ratings')}</span>
            )}
          </>
        ) : (
          <span style={{ fontSize: 13, color: 'var(--text3)' }}>{t('No ratings yet')}</span>
        )}
        <span style={{
          display: 'inline-flex', alignItems: 'center', gap: 4,
          fontSize: 12, fontWeight: 700, color: '#3dd68c',
          padding: '3px 9px', borderRadius: 12,
          background: 'rgba(61,214,140,0.12)', border: '1px solid rgba(61,214,140,0.30)'
        }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#3dd68c" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block', flexShrink: 0 }}>
            <path d="M7 11v9H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1h3z" />
            <path d="M7 11l4-7c1.5 0 2.5 1 2.5 2.5V10h5a2 2 0 0 1 2 2.3l-1.3 6A2 2 0 0 1 17.2 20H7" />
          </svg>
          <span style={{ color: '#3dd68c' }}>{recommendCount.toLocaleString()} {t('recommend')}</span>
        </span>
      </div>

      {/* Per-dimension: иконки + оценка юзера. Inline-flex чтобы фрейм сжимался по контенту. */}
      <div className="ds-rating-dims-pill" style={{ display: 'inline-flex', alignItems: 'center', gap: 14, padding: '8px 16px', background: '#0a1226', border: 'none', borderRadius: 999, flexWrap: 'wrap' }}>
        {DRAMA_RATING_KEYS.map(rk => {
          const v = userRatings[rk.id] || 0;
          const isHov = hoveredKey === rk.id;
          return (
            <div
              key={rk.id}
              onMouseEnter={() => setHoveredKey(rk.id)}
              onMouseLeave={() => setHoveredKey(null)}
              style={{ position: 'relative', display: 'flex', alignItems: 'center', gap: 4, fontSize: 12, cursor: 'default' }}
            >
              <span style={{ display: 'inline-flex', color: v > 0 ? RATING_COLOR[v - 1] : '#ffffff' }}>{LineIcons[rk.icon]}</span>
              <span style={{ fontWeight: 700, color: v > 0 ? RATING_COLOR[v - 1] : 'var(--text3)', minWidth: 20 }}>{v > 0 ? v.toFixed(1) : '—'}</span>
              {isHov && (
                <div style={{
                  position: 'absolute', bottom: 'calc(100% + 6px)', left: '50%', transform: 'translateX(-50%)',
                  background: 'rgba(8,12,26,0.96)', color: '#fff',
                  fontSize: 11, fontWeight: 600, padding: '5px 9px', borderRadius: 5,
                  whiteSpace: 'nowrap', zIndex: 50,
                  border: '1px solid rgba(74,158,255,0.25)',
                  boxShadow: '0 4px 12px rgba(0,0,0,0.4)', pointerEvents: 'none'
                }}>
                  {t(rk.label)}
                  <span style={{
                    position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
                    borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
                    borderTop: '5px solid rgba(8,12,26,0.96)'
                  }} />
                </div>
              )}
            </div>
          );
        })}
        {ratedKeys.length === 0 && (
          <span style={{ fontSize: 11, color: 'var(--text3)', fontStyle: 'italic' }}>{user ? t('Rate below to see your scores') : t('Sign in to rate by criteria')}</span>
        )}
      </div>
    </div>
  );
}

// ── POSTER RATING BADGE (мини-плашка в углу постера) ────────────────────────
// Та же логика overall что и в RatingHistogram: voteAverage → user-avg → mock
function PosterRatingBadge({ dramaId, voteAverage }) {
  // ── MaoDrama rating: среднее ВСЕХ юзерских оценок с сервера (агрегация по
  //    user_ratings: per-user среднее dimensions, потом усреднение по юзерам).
  //    Локальный userAvg (моя оценка) НЕ показывается — это вводило в заблуждение.
  //    Если серверный avg=null (никто ещё не оценил) — плашки нет.
  //    TMDB/MDL voteAverage больше НЕ используется на постере. ──
  const [serverAvg, setServerAvg] = useState(null);
  useEffect(() => {
    if (!dramaId) return;
    let aborted = false;
    const load = () => {
      const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
      fetch(`${apiBase}/api/dramas/${dramaId}/ratings-aggregate`, { credentials: 'include' })
        .then(r => r.ok ? r.json() : null)
        .then(j => { if (!aborted && j) setServerAvg(j.avg); })
        .catch(() => {});
    };
    load();
    const onChanged = () => load();
    window.addEventListener('ds-ratings-changed', onChanged);
    return () => { aborted = true; window.removeEventListener('ds-ratings-changed', onChanged); };
  }, [dramaId]);
  // Никто не оценил — плашки нет вообще (постер чистый).
  if (serverAvg === null || serverAvg === undefined) return null;
  return (
    <div className="ds-poster-rating-badge" style={{
      position: 'absolute', top: 6, right: 6, zIndex: 3,
      display: 'inline-flex', alignItems: 'center', gap: 4,
      padding: '4px 9px 4px 7px', borderRadius: 999,
      background: 'rgba(8,12,26,0.85)',
      border: 'none',
      boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
      backdropFilter: 'blur(4px)'
    }}>
      <svg className="ds-poster-rating-star-svg" width="11" height="11" viewBox="0 0 24 24" fill="#ffd770" stroke="#ffd770" strokeWidth="0.5" strokeLinejoin="round" style={{ display: 'block' }}>
        <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
      </svg>
      <span className="ds-poster-rating-num" style={{ fontSize: 10, fontWeight: 400, color: '#fff', fontVariantNumeric: 'tabular-nums', lineHeight: 1 }}>
        {Number(serverAvg).toFixed(1)}
      </span>
    </div>
  );
}

// ── RATING HISTOGRAM (sparkline + per-dimension) ─────────────────────────────
// overall = тот же расчёт, что в DramaRatingsSummary: voteAverage если есть,
// иначе среднее по выставленным юзером per-dimension оценкам.
// Когда будет backend агрегации — заменим на /api/reviews/stats?dramaId=
function RatingHistogram({ dramaId, voteAverage, voteCount }) {
  const { user } = useAuth();
  const { t } = useI18n();
  const [hoveredDim, setHoveredDim] = useState(null);
  // ── СЕРВЕРНАЯ АГРЕГАЦИЯ (Marina, 30 мая 2026) ──
  // Раньше виджет показывал ТОЛЬКО локальные оценки юзера + mock-распределение
  // (Marina видела свою «100 голосов 9.7» вместо общего MaoDrama-рейтинга).
  // Теперь дёргаем /ratings-aggregate который возвращает:
  //   avg          — среднее всех проголосовавших юзеров
  //   count        — реальное число проголосовавших (DISTINCT user_id)
  //   byDim        — { story: {avg, count}, acting: {...}, ... } — per-dimension средние
  //   distribution — массив[10] — сколько юзеров попало в bucket 1..10
  const [agg, setAgg] = useState({ avg: null, count: 0, byDim: {}, distribution: null });
  useEffect(() => {
    if (!dramaId) return;
    let aborted = false;
    const load = () => {
      const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
      fetch(`${apiBase}/api/dramas/${dramaId}/ratings-aggregate`, { credentials: 'include' })
        .then(r => r.ok ? r.json() : null)
        .then(j => {
          if (aborted || !j) return;
          setAgg({
            avg: j.avg ?? null,
            count: j.count || 0,
            byDim: j.byDim || {},
            distribution: Array.isArray(j.distribution) ? j.distribution : null,
          });
        })
        .catch(() => {});
    };
    load();
    const onChanged = () => load();
    window.addEventListener('ds-ratings-changed', onChanged);
    return () => { aborted = true; window.removeEventListener('ds-ratings-changed', onChanged); };
  }, [dramaId]);

  // ── Fallback на локальные оценки (Marina, 30 мая 2026) ──
  // Если сервер вернул 0 голосов, НО у залогиненного юзера есть локальная оценка
  // (sync ещё не прошёл / в пути / failed) — показываем её с пометкой "только ты".
  // Так Marina видит свою свежепоставленную оценку сразу, не дожидаясь sync.
  const localRatings = (() => {
    if (!user) return {};
    try { return JSON.parse(localStorage.getItem(`ds_ratings_${dramaId}_${user.email}`)) || {}; } catch { return {}; }
  })();
  const localRated = DRAMA_RATING_KEYS.filter(k => localRatings[k.id] > 0);
  const localAvg = localRated.length > 0
    ? localRated.reduce((s, k) => s + localRatings[k.id], 0) / localRated.length
    : null;

  const serverHas = agg.avg !== null && agg.avg !== undefined && agg.count > 0;
  const usingLocal = !serverHas && localAvg !== null;
  const hasRealOverall = serverHas || usingLocal;
  const avg = serverHas ? agg.avg : (usingLocal ? localAvg : 0);
  const totalReviews = agg.count || (usingLocal ? 1 : 0);

  // Distribution — приоритет: серверный (если есть оценки) → fallback на локальную (1 в bucket).
  let counts;
  if (agg.distribution && agg.distribution.some(x => x > 0)) {
    counts = agg.distribution;
  } else if (usingLocal) {
    counts = Array(10).fill(0);
    const b = Math.max(1, Math.min(10, Math.round(localAvg)));
    counts[b - 1] = 1;
  } else {
    counts = Array(10).fill(0);
  }
  const maxBar = Math.max(...counts, 1);

  // Per-dimension — серверный если есть голоса, иначе локальный fallback.
  const dims = DRAMA_RATING_KEYS.map(rk => ({
    ...rk,
    v: agg.byDim[rk.id]?.avg ?? (usingLocal ? (localRatings[rk.id] || 0) : 0),
  }));

  const fmtK = (n) => n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K' : n.toLocaleString();

  // Дискретные столбики 1..10 в стиле IMDB, но в нашем тиффани.
  const chartW = 200, chartH = 52;
  const slot = chartW / 10;            // ширина «ячейки» под каждый столбик
  const barW = slot * 0.78;            // сам столбик
  const barPad = (slot - barW) / 2;    // отступ слева/справа внутри ячейки
  // Индекс пика (где находится avg) — самый высокий столбик
  const peakIdx = Math.max(0, Math.min(9, Math.round(avg) - 1));

  return (
    <div style={{
      padding: '10px 14px 12px',
      background: 'linear-gradient(135deg, rgba(10,186,181,0.06), rgba(123,195,255,0.04))',
      border: '1px solid rgba(10,186,181,0.18)',
      borderRadius: 10, height: '100%',
      display: 'flex', flexDirection: 'column', boxSizing: 'border-box'
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          textTransform: 'uppercase', letterSpacing: '1.5px'
        }}>{t('Ratings')}</span>
        <button
          onClick={() => {
            const el = document.getElementById('ds-rate-panel-anchor');
            if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
          }}
          style={{
            fontSize: 11, color: '#0abab5', fontWeight: 600,
            padding: '3px 10px', borderRadius: 999,
            border: '1px solid rgba(10,186,181,0.35)', background: 'transparent',
            display: 'inline-flex', alignItems: 'center', gap: 3,
            cursor: 'pointer'
          }}>+ {t('Rate it')}</button>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14, flex: 1 }}>
        {/* Левая колонка: outlined tiffany звезда + цифра в tiffany */}
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flexShrink: 0, minWidth: 78 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#0abab5" strokeWidth="1.8" strokeLinejoin="round" style={{ display: 'block', transform: 'translateY(2px)' }}>
              <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
            </svg>
            <span style={{ fontSize: 26, fontWeight: 800, color: '#0abab5', lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.5px' }}>
              {hasRealOverall ? avg.toFixed(1) : '—'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>/10</span>
          </div>
          <span style={{ fontSize: 10, color: 'var(--text3)', marginTop: 3, letterSpacing: '0.3px' }}>
            {fmtK(totalReviews)} {t('votes')}
          </span>
        </div>
        {/* Правая колонка: дискретные столбики 1..10 (как у IMDB) + цифры под ними */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <svg width="100%" height={chartH} viewBox={`0 0 ${chartW} ${chartH}`} preserveAspectRatio="none" style={{ display: 'block', overflow: 'visible' }}>
            {counts.map((c, i) => {
              const h = (c / maxBar) * (chartH - 2);
              const y = chartH - h;
              const x = i * slot + barPad;
              const isPeak = i === peakIdx;
              return (
                <rect
                  key={i}
                  x={x.toFixed(2)} y={y.toFixed(2)}
                  width={barW.toFixed(2)} height={h.toFixed(2)}
                  rx="2" ry="2"
                  fill="#0abab5"
                  opacity={isPeak ? 1 : 0.78}
                />
              );
            })}
          </svg>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(10, 1fr)', marginTop: 3 }}>
            {Array.from({ length: 10 }, (_, i) => (
              <span key={i} style={{
                fontSize: 9.5, color: i === peakIdx ? '#0abab5' : 'var(--text3)',
                fontWeight: i === peakIdx ? 700 : 500,
                textAlign: 'center',
                fontVariantNumeric: 'tabular-nums'
              }}>{i + 1}</span>
            ))}
          </div>
        </div>
      </div>
      {/* Per-dimension strip — overall формируется из этих оценок.
          Раньше был чёрный pill (borderRadius:999, inline-flex) — плохо смотрелся
          на мобильных. Теперь — полоса edge-to-edge: выходит за padding карточки
          через отрицательные margin'ы и матчится с нижними скруглениями. */}
      <div className="ds-rating-dims-pill" style={{
        marginTop: 12, marginLeft: -14, marginRight: -14, marginBottom: -12,
        display: 'flex', flexWrap: 'wrap', alignItems: 'center',
        justifyContent: 'space-around', gap: 8,
        padding: '10px 14px',
        background: '#0a1226',
        border: 'none',
        borderRadius: '0 0 10px 10px'
      }}>
        {dims.map((d, i) => {
          const rated = d.v > 0;
          const isHov = hoveredDim === d.id;
          return (
            <React.Fragment key={d.id}>
              {i > 0 && <span style={{ width: 1, height: 12, background: 'rgba(255,255,255,0.18)', flexShrink: 0 }} />}
              <span
                onMouseEnter={() => setHoveredDim(d.id)}
                onMouseLeave={() => setHoveredDim(null)}
                style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'default' }}
              >
                <span style={{ display: 'inline-flex', color: '#ffffff', transform: 'scale(0.85)' }}>{LineIcons[d.icon]}</span>
                <span style={{
                  fontSize: 12, fontWeight: 800,
                  color: rated ? '#fff' : '#ffffff',
                  fontVariantNumeric: 'tabular-nums', minWidth: 18
                }}>
                  {rated ? d.v.toFixed(1) : '—'}
                </span>
                {isHov && (
                  <div style={{
                    position: 'absolute', bottom: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)',
                    background: 'rgba(8,12,26,0.97)', color: '#fff',
                    fontSize: 11, fontWeight: 600, padding: '6px 10px', borderRadius: 6,
                    whiteSpace: 'nowrap', zIndex: 50,
                    border: '1px solid rgba(74,158,255,0.30)',
                    boxShadow: '0 6px 16px rgba(0,0,0,0.5)', pointerEvents: 'none'
                  }}>
                    {t(d.label)}
                    <span style={{
                      position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
                      borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
                      borderTop: '5px solid rgba(8,12,26,0.97)'
                    }} />
                  </div>
                )}
              </span>
            </React.Fragment>
          );
        })}
      </div>
    </div>
  );
}

// ── ACTOR RATING HISTOGRAM ─────────────────────────────────────────────────
// Та же визуализация что и у дорамы (RatingHistogram) — звезда + цифра + столбики 1..10,
// но строится по оценкам АКТЁРА. overall = средняя по 7 измерениям актёра (ACTOR_RATING_KEYS).
//
// ── СЕРВЕРНАЯ АГРЕГАЦИЯ (Marina, 31 мая 2026) ──
// Раньше показывали ТОЛЬКО локальные оценки юзера + bell-curve mock по seedHash
// (Marina видела свою «одну оценку» вместо общего MaoDrama-рейтинга по актёру).
// Теперь дёргаем /api/actors/:id/ratings-aggregate — реальные avg/count/distribution
// из user_ratings (зеркало логики /api/dramas/:id/ratings-aggregate).
// Fallback на локальную оценку — если сервер вернул 0 голосов, но юзер только что
// поставил оценку (sync в пути) — видим её сразу.
function ActorRatingHistogram({ actorId }) {
  const { user } = useAuth();
  const { t } = useI18n();
  const [hoveredDim, setHoveredDim] = useState(null);
  const storageKey = user ? `ds_actor_ratings_${actorId}_${user.email}` : null;

  const readRatings = () => {
    if (!storageKey) return {};
    try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; }
  };
  const [ratings, setRatings] = useState(readRatings);
  useEffect(() => {
    const refresh = () => setRatings(readRatings());
    refresh();
    window.addEventListener('ds-actor-ratings-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-actor-ratings-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [actorId, user?.email]);

  // Серверная агрегация — реальные цифры из БД (user_ratings WHERE target_type='actor')
  const [agg, setAgg] = useState({ avg: null, count: 0, byDim: {}, distribution: null });
  useEffect(() => {
    if (!actorId) return;
    let aborted = false;
    const load = () => {
      const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
      fetch(`${apiBase}/api/actors/${actorId}/ratings-aggregate`, { credentials: 'include' })
        .then(r => r.ok ? r.json() : null)
        .then(j => {
          if (aborted || !j) return;
          setAgg({
            avg: j.avg ?? null,
            count: j.count || 0,
            byDim: j.byDim || {},
            distribution: Array.isArray(j.distribution) ? j.distribution : null,
          });
        })
        .catch(() => {});
    };
    load();
    const onChanged = () => load();
    window.addEventListener('ds-actor-ratings-changed', onChanged);
    return () => { aborted = true; window.removeEventListener('ds-actor-ratings-changed', onChanged); };
  }, [actorId]);

  const ratedKeys = ACTOR_RATING_KEYS.filter(k => ratings[k.id] > 0);
  const userAvg = ratedKeys.length > 0
    ? ratedKeys.reduce((s, k) => s + ratings[k.id], 0) / ratedKeys.length
    : null;

  const serverHas = agg.avg !== null && agg.avg !== undefined && agg.count > 0;
  const usingLocal = !serverHas && userAvg !== null;
  const hasRealOverall = serverHas || usingLocal;
  const avg = serverHas ? agg.avg : (usingLocal ? userAvg : 0);
  const totalReviews = agg.count || (usingLocal ? 1 : 0);

  // Distribution: серверный → fallback на локальную оценку (1 в bucket) → пустой.
  let counts;
  if (agg.distribution && agg.distribution.some(x => x > 0)) {
    counts = agg.distribution;
  } else if (usingLocal) {
    counts = Array(10).fill(0);
    const b = Math.max(1, Math.min(10, Math.round(userAvg)));
    counts[b - 1] = 1;
  } else {
    counts = Array(10).fill(0);
  }
  const maxBar = Math.max(...counts, 1);

  // Per-dimension — серверный если есть голоса, иначе локальный fallback.
  const dims = ACTOR_RATING_KEYS.map(rk => ({
    ...rk,
    v: agg.byDim[rk.id]?.avg ?? (usingLocal ? (ratings[rk.id] || 0) : 0),
  }));
  const fmtK = (n) => n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K' : n.toLocaleString();

  const chartW = 200, chartH = 52;
  const slot = chartW / 10;
  const barW = slot * 0.78;
  const barPad = (slot - barW) / 2;
  const peakIdx = Math.max(0, Math.min(9, Math.round(avg) - 1));

  return (
    <div className="ds-actor-reaction-panel" style={{
      padding: '14px 18px 18px',
      borderRadius: 18,
      display: 'flex', flexDirection: 'column', boxSizing: 'border-box'
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          textTransform: 'uppercase', letterSpacing: '1.5px'
        }}>{t('Ratings')}</span>
        <button
          onClick={() => {
            const el = document.getElementById('ds-actor-rate-panel-anchor');
            if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
          }}
          style={{
            fontSize: 11, color: '#0abab5', fontWeight: 600,
            padding: '3px 10px', borderRadius: 999,
            border: '1px solid rgba(10,186,181,0.35)', background: 'transparent',
            display: 'inline-flex', alignItems: 'center', gap: 3,
            cursor: 'pointer'
          }}>+ {t('Rate it')}</button>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 14, flex: 1 }}>
        {/* Левая колонка: outlined tiffany звезда + цифра */}
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', flexShrink: 0, minWidth: 78 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#0abab5" strokeWidth="1.8" strokeLinejoin="round" style={{ display: 'block', transform: 'translateY(2px)' }}>
              <polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
            </svg>
            <span style={{ fontSize: 26, fontWeight: 800, color: '#0abab5', lineHeight: 1, fontVariantNumeric: 'tabular-nums', letterSpacing: '-0.5px' }}>
              {hasRealOverall ? avg.toFixed(1) : '—'}
            </span>
            <span style={{ fontSize: 11, color: 'var(--text3)', fontWeight: 500 }}>/10</span>
          </div>
          <span style={{ fontSize: 10, color: 'var(--text3)', marginTop: 3, letterSpacing: '0.3px' }}>
            {fmtK(totalReviews)} {t('votes')}
          </span>
        </div>
        {/* Правая колонка: дискретные столбики */}
        <div style={{ flex: 1, minWidth: 0 }}>
          <svg width="100%" height={chartH} viewBox={`0 0 ${chartW} ${chartH}`} preserveAspectRatio="none" style={{ display: 'block', overflow: 'visible' }}>
            {counts.map((c, i) => {
              const h = (c / maxBar) * (chartH - 2);
              const y = chartH - h;
              const x = i * slot + barPad;
              const isPeak = i === peakIdx;
              return (
                <rect
                  key={i}
                  x={x.toFixed(2)} y={y.toFixed(2)}
                  width={barW.toFixed(2)} height={h.toFixed(2)}
                  rx="2" ry="2"
                  fill="#0abab5"
                  opacity={isPeak ? 1 : 0.78}
                />
              );
            })}
          </svg>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(10, 1fr)', marginTop: 3 }}>
            {Array.from({ length: 10 }, (_, i) => (
              <span key={i} style={{
                fontSize: 9.5, color: i === peakIdx ? '#0abab5' : 'var(--text3)',
                fontWeight: i === peakIdx ? 700 : 500,
                textAlign: 'center',
                fontVariantNumeric: 'tabular-nums'
              }}>{i + 1}</span>
            ))}
          </div>
        </div>
      </div>
      {/* Per-dimension strip — те же 7 категорий актёра.
          Edge-to-edge через отрицательные margin'ы и закругление под низ карточки. */}
      <div className="ds-rating-dims-pill" style={{
        marginTop: 12, marginLeft: -14, marginRight: -14, marginBottom: -12,
        display: 'flex', flexWrap: 'wrap', alignItems: 'center',
        justifyContent: 'space-around', gap: 8,
        padding: '10px 14px',
        background: '#0a1226',
        border: 'none',
        borderRadius: '0 0 10px 10px'
      }}>
        {dims.map((d, i) => {
          const rated = d.v > 0;
          const isHov = hoveredDim === d.id;
          return (
            <React.Fragment key={d.id}>
              {i > 0 && <span style={{ width: 1, height: 12, background: 'rgba(255,255,255,0.18)', flexShrink: 0 }} />}
              <span
                onMouseEnter={() => setHoveredDim(d.id)}
                onMouseLeave={() => setHoveredDim(null)}
                style={{ position: 'relative', display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'default' }}
              >
                <span style={{ display: 'inline-flex', color: '#ffffff', transform: 'scale(0.85)' }}>{LineIcons[d.icon]}</span>
                <span style={{
                  fontSize: 12, fontWeight: 800,
                  color: rated ? '#fff' : '#ffffff',
                  fontVariantNumeric: 'tabular-nums', minWidth: 18
                }}>
                  {rated ? d.v.toFixed(1) : '—'}
                </span>
                {isHov && (
                  <div style={{
                    position: 'absolute', bottom: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)',
                    background: 'rgba(8,12,26,0.97)', color: '#fff',
                    fontSize: 11, fontWeight: 600, padding: '6px 10px', borderRadius: 6,
                    whiteSpace: 'nowrap', zIndex: 50,
                    border: '1px solid rgba(74,158,255,0.30)',
                    boxShadow: '0 6px 16px rgba(0,0,0,0.5)', pointerEvents: 'none'
                  }}>
                    {t(d.label)}
                    <span style={{
                      position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
                      borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
                      borderTop: '5px solid rgba(8,12,26,0.97)'
                    }} />
                  </div>
                )}
              </span>
            </React.Fragment>
          );
        })}
      </div>
    </div>
  );
}

// ── VIEWERS SUMMARY (сводка по статусам библиотеки) ──────────────────────────
// Показывает «Количество зрителей: TOTAL» + список «Смотрю / В планах / Просмотрено / ...»
// с прогресс-барами. Сейчас числа — детерминированный mock по dramaId, позже подменим
// на агрегацию из D1.
function ViewersSummary({ dramaId }) {
  const { t } = useI18n();
  // Контрастные цвета для каждого статуса — перекрывают цвета из таксономии,
  // чтобы все сегменты на шкале были чётко различимы.
  // Цвета синхронизированы с LIBRARY_STATUSES_FALLBACK в ds-features.jsx
  const STATUS_COLORS = {
    watching:  '#1e9eff',   // ярко-голубой (Marina, май 2026)
    plan:      '#7bc3ff',   // светло-голубой
    completed: '#22c55e',   // зелёный
    rewatch:   '#8a98a8',   // серый
    dropped:   '#e05858',   // красный
    favorite:  '#f06292',   // розовый (для отдельного бейджа)
  };
  const _rawStatuses = (typeof window !== 'undefined' && window.__dsLibraryStatuses) || [
    { id:'watching',  label:'Watching' },
    { id:'plan',      label:'Plan to Watch' },
    { id:'completed', label:'Completed' },
    { id:'rewatch',   label:'Rewatch' },
    { id:'dropped',   label:'Dropped' },
  ];
  // favorite убран — для него есть отдельная кнопка-сердце на странице дорамы
  const statuses = _rawStatuses
    .filter(s => s.id !== 'favorite')
    .map(s => ({ ...s, color: STATUS_COLORS[s.id] || s.color || '#88a8b8' }));
  // ── СЕРВЕРНЫЙ агрегат (Marina, 30 мая 2026) ──
  // Раньше — seedHash-mock «1.1K зрителей» где у нас 0 юзеров.
  // Теперь — реальный COUNT(DISTINCT user_id) GROUP BY status из user_drama_status.
  const [agg, setAgg] = useState({ total: 0, byStatus: {} });
  useEffect(() => {
    if (!dramaId) return;
    let aborted = false;
    const load = () => {
      const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
      fetch(`${apiBase}/api/dramas/${dramaId}/library-aggregate`, { cache: 'no-store' })
        .then(r => r.ok ? r.json() : null)
        .then(j => {
          if (aborted || !j) return;
          setAgg({ total: j.total || 0, byStatus: j.byStatus || {} });
        })
        .catch(() => {});
    };
    load();
    const onChanged = () => setTimeout(load, 600);
    window.addEventListener('ds-library-changed', onChanged);
    return () => { aborted = true; window.removeEventListener('ds-library-changed', onChanged); };
  }, [dramaId]);
  const counts = {};
  let total = agg.total;
  statuses.forEach(s => {
    counts[s.id] = agg.byStatus[s.id] || 0;
  });
  const maxCount = Math.max(...Object.values(counts), 1);

  // SVG donut chart: каждый сегмент — circle со strokeDasharray
  const cx = 50, cy = 50, r = 38, sw = 11;
  const C = 2 * Math.PI * r;
  let runningOffset = 0;
  const segments = statuses.map(s => {
    const portion = total > 0 ? counts[s.id] / total : 0;
    const dashLen = portion * C;
    const seg = {
      ...s,
      count: counts[s.id],
      portion,
      dashLen,
      gapLen: C - dashLen,
      dashOffset: -runningOffset
    };
    runningOffset += dashLen;
    return seg;
  });
  const fmt = (n) => n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K' : n.toLocaleString();

  return (
    <div style={{
      padding: '12px 16px 14px',
      background: 'linear-gradient(135deg, rgba(123,195,255,0.04), rgba(10,186,181,0.05))',
      border: '1px solid rgba(140,180,235,0.18)',
      borderRadius: 10, height: '100%',
      display: 'flex', flexDirection: 'column', boxSizing: 'border-box'
    }}>
      {/* Header: заголовок слева, total справа — счётчик в стиле заголовка */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 10 }}>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          textTransform: 'uppercase', letterSpacing: '1.5px'
        }}>{t('Number of viewers')}</span>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          letterSpacing: '1.5px', lineHeight: 1, fontVariantNumeric: 'tabular-nums'
        }}>{fmt(total)}</span>
      </div>
      {/* Stacked horizontal bar — все сегменты в одной полосе */}
      <div style={{
        display: 'flex', height: 11, borderRadius: 5, overflow: 'hidden',
        background: 'rgba(140,180,235,0.08)', marginBottom: 12,
        boxShadow: 'inset 0 0 0 1px rgba(140,180,235,0.08)'
      }}>
        {segments.map(seg => seg.portion > 0 && (
          <div key={seg.id} style={{
            flex: `${seg.portion} 0 0`,
            background: seg.color,
            transition: 'flex 0.4s ease'
          }} title={`${t(seg.label)}: ${seg.count.toLocaleString()}`} />
        ))}
      </div>
      {/* Legend — 2 колонки */}
      <div style={{
        flex: 1,
        display: 'grid', gridTemplateColumns: '1fr 1fr',
        columnGap: 14, rowGap: 6, alignContent: 'start'
      }}>
        {segments.map(seg => (
          <div key={seg.id} style={{ display: 'flex', alignItems: 'center', gap: 7, minWidth: 0 }}>
            <span style={{
              flexShrink: 0, width: 8, height: 8, borderRadius: 2,
              background: seg.color, boxShadow: `0 0 6px ${seg.color}55`
            }} />
            <span style={{
              fontSize: 11, color: 'var(--text2)',
              whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              flex: 1, minWidth: 0
            }}>{t(seg.label)}</span>
            <span style={{ fontSize: 11, color: '#fff', fontWeight: 600, fontVariantNumeric: 'tabular-nums', flexShrink: 0 }}>
              {fmt(seg.count)}
            </span>
          </div>
        ))}
      </div>
    </div>
  );
}

function DramaRatingPanel({ dramaId }) {
  const { user } = useAuth();
  const { t, lang } = useI18n();
  const storageKey = `ds_ratings_${dramaId}_${user?.email || 'guest'}`;
  const [ratings, setRatings] = useState(() => { try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; } });
  const [hover, setHover] = useState({});

  const setRating = (id, val) => {
    if (!user) return;
    const next = { ...ratings, [id]: ratings[id] === val ? 0 : val };
    setRatings(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
    // Сигналим всем PosterRatingBadge / DramaCard на сайте — мгновенно перерендерить плашку.
    window.dispatchEvent(new CustomEvent('ds-ratings-changed', { detail: { dramaId } }));
    // ПРИНУДИТЕЛЬНЫЙ PUT в /api/ratings прямо здесь, не полагаясь только на ds-sync
    // event-listener (раньше event иногда терялся из-за race с pushAllowed/debounce —
    // Marina ставила оценки, но в user_ratings они не доезжали → виджет показывал 0).
    try {
      const api = window.__dsApi;
      if (api?.fetch) {
        api.fetch(`/api/ratings/drama/${dramaId}`, {
          method: 'PUT',
          body: JSON.stringify(next || {}),
        }).catch(() => {});
      }
    } catch (e) {}
  };

  const ratedKeys = DRAMA_RATING_KEYS.filter(k => ratings[k.id]);
  const avg = ratedKeys.length > 0
    ? (ratedKeys.reduce((s, k) => s + ratings[k.id], 0) / ratedKeys.length).toFixed(1)
    : null;

  return (
    <div className="ds-drama-panel" style={{ background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 22, marginTop: 24 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 18, flexWrap: 'wrap', gap: 10 }}>
        <h3 style={{ fontSize: 16, fontWeight: 700 }}>{t('Rate this drama')}</h3>
        {avg && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--text3)' }}>
            <span>{t('Your average')}:</span>
            <span style={{
              padding: '3px 10px', borderRadius: 14, fontWeight: 800, fontSize: 13,
              color: '#0a1226', background: RATING_COLOR[Math.round(Number(avg)) - 1] || 'var(--bg3)'
            }}>{avg}</span>
          </div>
        )}
        {!avg && user && <span style={{ fontSize: 11, color: 'var(--text3)' }}>{t('1–10 scale · auto-saved')}</span>}
      </div>

      {!user &&
        <div style={{ marginBottom: 18 }}>
          <GuestGate message="Sign in to rate this drama" ctaLabel="Register to Rate" />
        </div>
      }

      <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
        {DRAMA_RATING_KEYS.map((rk) => {
          const sel = ratings[rk.id] || 0;
          const hov = hover[rk.id] || 0;
          const display = hov || sel;
          const labelArr = lang === 'ru' ? RATING_LABEL_RU : RATING_LABEL_EN;
          const labelText = display > 0 ? labelArr[display - 1] : (sel > 0 ? labelArr[sel - 1] : '');
          return (
            <div key={rk.id} className="ds-rating-row">
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, gap: 12, flexWrap: 'wrap' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, fontWeight: 600, color: user ? 'var(--text)' : 'var(--text3)' }}>
                  <span style={{ display: 'inline-flex', color: 'var(--accent2, #5ed7c6)' }}>{LineIcons[rk.icon]}</span>
                  <span>{t(rk.label)}</span>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  {display > 0 && (
                    <span style={{ fontSize: 11, color: 'var(--text3)', fontStyle: 'italic' }}>{labelText}</span>
                  )}
                  <span style={{
                    fontSize: 13, fontWeight: 800,
                    color: sel > 0 ? RATING_COLOR[sel - 1] : 'var(--text3)',
                    minWidth: 36, textAlign: 'right'
                  }}>{sel > 0 ? `${sel}/10` : '—'}</span>
                </div>
              </div>

              {/* Шкала из 10 сегментов */}
              <div
                onMouseLeave={() => setHover((h) => ({ ...h, [rk.id]: 0 }))}
                style={{
                  display: 'grid', gridTemplateColumns: 'repeat(10, 1fr)', gap: 4,
                  opacity: user ? 1 : 0.5, pointerEvents: user ? 'auto' : 'none'
                }}
              >
                {Array.from({ length: 10 }, (_, i) => {
                  const v = i + 1;
                  const active = v <= display;
                  return (
                    <button key={v}
                      onMouseEnter={() => setHover((h) => ({ ...h, [rk.id]: v }))}
                      onClick={() => setRating(rk.id, v)}
                      title={`${v}/10 — ${labelArr[i]}`}
                      style={{
                        height: 28, borderRadius: 6,
                        border: active ? 'none' : '1px solid var(--rating-btn-border, transparent)',
                        cursor: user ? 'pointer' : 'not-allowed',
                        padding: 0, position: 'relative',
                        background: active ? RATING_COLOR[v - 1] : 'rgba(255,255,255,0.05)',
                        transition: 'background .12s, transform .12s',
                        transform: hov === v ? 'scaleY(1.15)' : 'scaleY(1)',
                        boxShadow: sel === v ? '0 0 0 2px rgba(255,255,255,0.5)' : 'none',
                        fontSize: 10, fontWeight: 700,
                        color: active ? '#ffffff' : 'var(--rating-num-inactive, var(--text3))'
                      }}>{v}</button>
                  );
                })}
              </div>
            </div>
          );
        })}
        {Object.keys(ratings).length > 0 && (
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 4 }}>
            <button
              onClick={() => {
                if (!confirm(t('Reset all ratings for this drama?'))) return;
                setRatings({});
                localStorage.removeItem(storageKey);
              }}
              style={{
                padding: '6px 12px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                background: 'rgba(196,69,69,0.15)', color: '#e05858', border: 'none', cursor: 'pointer'
              }}>{t('Reset ratings')}</button>
          </div>
        )}
      </div>
    </div>);

}

// ── DRAMA REACTION PANEL ──────────────────────────────────────────────────────
// Fallback используется только если taxonomy.json не загрузился
const DRAMA_REACTIONS_FALLBACK = [
{ id: 'emotional_damage', label: 'Emotional Damage', icon: 'heartBroken' },
{ id: 'comfort_drama', label: 'Comfort Drama', icon: 'heartOutline' },
{ id: 'insane_chemistry', label: 'Insane Chemistry', icon: 'flame' },
{ id: 'toxic_romance', label: 'Toxic Romance', icon: 'skull' },
{ id: 'masterpiece', label: 'Masterpiece', icon: 'crown' }];


// Хелпер: читает localStorage с реакциями текущего пользователя по дораме
function useDramaReactionsState(dramaId) {
  const { user } = useAuth();
  const storageKey = `ds_dreact_${dramaId}_${user?.email || 'guest'}`;
  const [reactions, setReactions] = useState(() => { try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; } });
  useEffect(() => {
    const refresh = () => { try { setReactions(JSON.parse(localStorage.getItem(storageKey)) || {}); } catch { setReactions({}); } };
    window.addEventListener('ds-dreact-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-dreact-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);
  // Подтягиваем мои реакции с сервера + one-shot push если в localStorage есть данные,
  // а на сервере нет (migration для существующих юзеров, у которых реакции жили
  // только локально до 30 мая 2026).
  useEffect(() => {
    if (!user || !dramaId) return;
    const api = window.__dsApi;
    if (!api?.fetch) return;
    let aborted = false;
    api.fetch(`/api/reactions/drama/${dramaId}`).then(j => {
      if (aborted || !j) return;
      const serverList = Array.isArray(j.reactions) ? j.reactions : [];
      const local = (() => { try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; } })();
      const localActive = Object.keys(local).filter(k => local[k]);
      // One-shot migration: localStorage has reactions, server doesn't → bulk-PUT.
      if (localActive.length > 0 && serverList.length === 0) {
        api.fetch(`/api/reactions/drama/${dramaId}`, {
          method: 'PUT',
          body: JSON.stringify({ reactions: localActive }),
        }).then(() => {
          window.dispatchEvent(new CustomEvent('ds-dreact-changed'));
        }).catch(() => {});
        return;
      }
      // Server is source of truth → синкаем localStorage из server.
      const next = {};
      for (const r of serverList) next[r] = true;
      try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch {}
      setReactions(next);
    }).catch(() => {});
    return () => { aborted = true; };
  }, [dramaId, user?.email]);
  return { user, storageKey, reactions, setReactions };
}

// ── DRAMA REACTION CHART — диаграмма-сводка (community), без кнопок ──
function DramaReactionPanel({ dramaId }) {
  const { user, reactions } = useDramaReactionsState(dramaId);
  const { t, lang } = useI18n();
  const reactionsList = (window.__dsDramaReactions || DRAMA_REACTIONS_FALLBACK);

  // ── СЕРВЕРНЫЕ счётчики реакций (Marina, 30 мая 2026) ──
  // Раньше показывали seedHash-mock «5.3K голосов» там, где у нас 0 юзеров.
  // Теперь дёргаем /api/dramas/:id/reactions-aggregate → { counts:{reaction_id:n}, total }.
  // Пока никто не реагировал — все нули, бары пустые.
  const [serverCounts, setServerCounts] = useState({});
  useEffect(() => {
    if (!dramaId) return;
    let aborted = false;
    const load = () => {
      const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
      // cache:'no-store' — нужен свежий agg сразу после своего же POST, иначе
      // edge-cache (30с) отдаёт устаревший 0.
      fetch(`${apiBase}/api/dramas/${dramaId}/reactions-aggregate`, { cache: 'no-store' })
        .then(r => r.ok ? r.json() : null)
        .then(j => { if (!aborted && j && j.counts) setServerCounts(j.counts); })
        .catch(() => {});
    };
    load();
    // После каждого ds-dreact-changed ждём ~600мс (дать POST/DELETE долететь
    // + кеш-инвалидации завершиться) и перезагружаем.
    const onChanged = () => setTimeout(load, 600);
    window.addEventListener('ds-dreact-changed', onChanged);
    return () => { aborted = true; window.removeEventListener('ds-dreact-changed', onChanged); };
  }, [dramaId]);
  // FALLBACK: если сервер вернул 0 для конкретной реакции, но юзер сам её активировал,
  // показываем +1 (его собственный голос). Так Marina не видит «0» там где она
  // только что кликнула — пока POST/DELETE долетают до сервера и кеш истекает.
  const counts = reactionsList.map(r => {
    const srv = serverCounts[r.id] || 0;
    const mine = user && reactions[r.id] ? 1 : 0;
    return Math.max(srv, mine);
  });
  const maxCount = Math.max(...counts, 1);
  const total = counts.reduce((s, c) => s + c, 0);
  const fmtK = (n) => n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K' : n.toLocaleString();
  const isRu = lang === 'ru';

  return (
    <div style={{
      padding: '14px 18px 16px',
      background: 'linear-gradient(135deg, rgba(10,186,181,0.06), rgba(123,195,255,0.04))',
      border: '1px solid rgba(10,186,181,0.18)',
      borderRadius: 12,
      height: '100%', boxSizing: 'border-box',
      display: 'flex', flexDirection: 'column'
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 10 }}>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          textTransform: 'uppercase', letterSpacing: '1.5px'
        }}>{isRu ? 'Реакция на дораму' : 'Reactions'}</span>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          letterSpacing: '1.5px', lineHeight: 1, fontVariantNumeric: 'tabular-nums'
        }}>{fmtK(total)}</span>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1 }}>
        {reactionsList.map((r, i) => {
          const c = counts[i];
          const pct = (c / maxCount) * 100;
          const active = user && reactions[r.id];
          return (
            <div key={r.id} style={{ display: 'grid', gridTemplateColumns: 'minmax(0,1fr) 38px', alignItems: 'center', gap: 8, fontSize: 11.5 }}>
              <div style={{ minWidth: 0 }}>
                <div style={{
                  display: 'flex', alignItems: 'center', gap: 5,
                  color: active ? '#1487cb' : 'var(--text2)',
                  fontWeight: active ? 700 : 500, marginBottom: 2
                }}>
                  {r.icon && LineIcons[r.icon] && (
                    <span style={{ display: 'inline-flex', color: active ? '#1487cb' : 'var(--text3)', transform: 'scale(0.72)', flexShrink: 0 }}>{LineIcons[r.icon]}</span>
                  )}
                  <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t(r.label)}</span>
                </div>
                <div style={{ height: 6, background: 'rgba(140,180,235,0.08)', borderRadius: 3, overflow: 'hidden' }}>
                  <div style={{
                    width: pct + '%', height: '100%',
                    background: 'linear-gradient(to right, #1487cb, #4fa1dc)',
                    borderRadius: 3, transition: 'width 0.3s'
                  }} />
                </div>
              </div>
              <span style={{ fontSize: 10.5, color: 'var(--text3)', fontVariantNumeric: 'tabular-nums', textAlign: 'right' }}>
                {fmtK(c)}
              </span>
            </div>
          );
        })}
      </div>
    </div>);
}

// ── DRAMA MY REACTION TOGGLE — отдельный блок кнопок «Моя реакция» ──
function DramaMyReactionPanel({ dramaId }) {
  const { user, openSignIn } = useAuth();
  const { t, lang } = useI18n();
  const { reactions, setReactions, storageKey } = useDramaReactionsState(dramaId);
  const reactionsList = (window.__dsDramaReactions || DRAMA_REACTIONS_FALLBACK);

  const toggle = (id) => {
    if (!user) { openSignIn(); return; }
    const wasActive = !!reactions[id];
    const next = { ...reactions, [id]: !wasActive };
    setReactions(next);
    try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch {}
    // СЕРВЕРНЫЙ POST/DELETE — без него агрегат не растёт (Marina, 30 мая 2026).
    try {
      const api = window.__dsApi;
      if (api?.fetch && dramaId) {
        const url = `/api/reactions/drama/${dramaId}/${id}`;
        api.fetch(url, { method: wasActive ? 'DELETE' : 'POST' })
          .then(() => window.dispatchEvent(new CustomEvent('ds-dreact-changed')))
          .catch(() => {});
      } else {
        window.dispatchEvent(new CustomEvent('ds-dreact-changed'));
      }
    } catch (e) {
      window.dispatchEvent(new CustomEvent('ds-dreact-changed'));
    }
  };

  const isRu = lang === 'ru';
  return (
    <div className="ds-drama-panel" style={{ background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 22, marginTop: 16 }}>
      <h3 style={{ fontSize: 16, fontWeight: 700, marginBottom: 14 }}>
        {isRu ? 'Моя реакция' : 'My reaction'}
      </h3>
      {!user &&
        <div style={{ marginBottom: 14 }}>
          <GuestGate message="Sign in to react to this drama" ctaLabel="Register to React" />
        </div>
      }
      <div style={{
        display: 'flex',
        flexWrap: 'wrap',
        gap: 8
      }}>
        {reactionsList.map((r) => {
          const active = user && reactions[r.id];
          return (
            <button key={r.id} onClick={() => toggle(r.id)}
              style={{
                padding: '10px 16px', borderRadius: 24, fontSize: 13, fontWeight: 400,
                background: active ? '#1487cb' : 'var(--reaction-tag-bg, rgba(123,195,255,0.10))',
                color: active ? '#ffffff' : '#1487cb',
                border: `1.5px solid ${active ? '#1487cb' : 'rgba(74,158,255,0.22)'}`,
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 6,
                cursor: 'pointer', transition: 'all 0.15s',
                opacity: user ? 1 : 0.6,
                whiteSpace: 'nowrap',
                flex: '0 0 auto'
              }}>
              {r.icon && LineIcons[r.icon] && (
                <span style={{ display: 'inline-flex', flexShrink: 0, color: active ? '#ffffff' : '#1487cb' }}>{LineIcons[r.icon]}</span>
              )}
              <span>{t(r.label)}</span>
            </button>);
        })}
      </div>
    </div>);
}

// ── ACTOR REACTION PANEL ──────────────────────────────────────────────────────
const ACTOR_REACTIONS = [
{ id: 'love', label: 'Love', icon: 'heartOutline' },
{ id: 'hot', label: 'Hot', icon: 'flame' },
{ id: 'soft_boy', label: 'Soft Boy', icon: 'leaf' },
{ id: 'worship', label: 'Worship', icon: 'handsUp' },
{ id: 'emotional_damage', label: 'Emotional Damage', icon: 'heartBroken' },
{ id: 'red_flag', label: 'Red Flag', icon: 'flag' }];


// mode: 'full' (default) — диаграмма + теги; 'distribution' — только бары community;
//       'tags' — только теги «Моя реакция» (для вынесения под ActorRatingPanel).
function ActorReactionPanel({ actorId, mode = 'full' }) {
  const { user, openSignIn } = useAuth();
  const { t, lang } = useI18n();
  const storageKey = `ds_areact_${actorId}_${user?.email || 'guest'}`;
  const [reactions, setReactions] = useState(() => {try {return JSON.parse(localStorage.getItem(storageKey)) || {};} catch {return {};}});
  const toggle = (id) => {
    if (!user) { openSignIn(); return; }
    const wasActive = !!reactions[id];
    const next = { ...reactions, [id]: !wasActive };
    setReactions(next);
    try { localStorage.setItem(storageKey, JSON.stringify(next)); } catch {}
    // Server-side POST/DELETE для агрегата (Marina, 30 мая 2026).
    try {
      const api = window.__dsApi;
      if (api?.fetch && actorId) {
        const url = `/api/reactions/actor/${actorId}/${id}`;
        api.fetch(url, { method: wasActive ? 'DELETE' : 'POST' })
          .then(() => window.dispatchEvent(new CustomEvent('ds-areact-changed')))
          .catch(() => {});
      } else {
        window.dispatchEvent(new CustomEvent('ds-areact-changed'));
      }
    } catch (e) {
      window.dispatchEvent(new CustomEvent('ds-areact-changed'));
    }
  };

  // ── Распределение реакций (community) ──
  // Серверный агрегат из таблицы reactions (Marina, 30 мая 2026): убрали
  // придуманные seedHash-числа. Пока никто не реагировал — все нули.
  const [serverCounts, setServerCounts] = useState({});
  useEffect(() => {
    if (!actorId) return;
    let aborted = false;
    const load = () => {
      const apiBase = (window.__dsApi?.base || window.DS_API_BASE || '');
      fetch(`${apiBase}/api/actors/${actorId}/reactions-aggregate`, { cache: 'no-store' })
        .then(r => r.ok ? r.json() : null)
        .then(j => { if (!aborted && j && j.counts) setServerCounts(j.counts); })
        .catch(() => {});
    };
    load();
    const onChanged = () => setTimeout(load, 600);
    window.addEventListener('ds-areact-changed', onChanged);
    return () => { aborted = true; window.removeEventListener('ds-areact-changed', onChanged); };
  }, [actorId]);
  // FALLBACK: max(server, моя). Если сервер ещё не сагрегировал, но юзер активировал — +1.
  const counts = ACTOR_REACTIONS.map(r => {
    const srv = serverCounts[r.id] || 0;
    const mine = user && reactions[r.id] ? 1 : 0;
    return Math.max(srv, mine);
  });
  const maxCount = Math.max(...counts, 1);
  const total = counts.reduce((s, c) => s + c, 0);
  const fmtK = (n) => n >= 1000 ? (n / 1000).toFixed(1).replace(/\.0$/, '') + 'K' : n.toLocaleString();
  const isRu = lang === 'ru';

  // Подблоки: header+bars (distribution) и теги «Моя реакция» (tags).
  const distributionBlock = (
    <>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
        <span style={{
          fontSize: 10, fontWeight: 800, color: '#7bc3ff',
          textTransform: 'uppercase', letterSpacing: '1.5px'
        }}>{isRu ? 'Реакция на актёра' : 'Reactions'}</span>
        <span style={{ fontSize: 10, color: '#7bc3ff', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.8px' }}>
          {fmtK(total)} {isRu ? 'голосов' : 'votes'}
        </span>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {ACTOR_REACTIONS.map((r, i) => {
          const c = counts[i];
          const pct = (c / maxCount) * 100;
          const active = user && reactions[r.id];
          return (
            <div key={r.id} style={{ display: 'grid', gridTemplateColumns: '120px 1fr 48px', alignItems: 'center', gap: 10, fontSize: 12 }}>
              <div style={{
                display: 'flex', alignItems: 'center', gap: 6,
                color: active ? '#0abab5' : 'var(--text2)',
                fontWeight: active ? 700 : 500, minWidth: 0
              }}>
                {r.icon && LineIcons[r.icon] && (
                  <span style={{ display: 'inline-flex', color: active ? '#0abab5' : 'var(--text3)', transform: 'scale(0.78)', flexShrink: 0 }}>{LineIcons[r.icon]}</span>
                )}
                <span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{t(r.label)}</span>
              </div>
              <div style={{ height: 8, background: 'rgba(140,180,235,0.08)', borderRadius: 4, overflow: 'hidden', position: 'relative' }}>
                <div style={{
                  width: pct + '%', height: '100%',
                  background: 'linear-gradient(to right, #0abab5, #5ed7c6)',
                  borderRadius: 4,
                  transition: 'width 0.3s'
                }} />
              </div>
              <span style={{ fontSize: 11, color: 'var(--text3)', fontVariantNumeric: 'tabular-nums', textAlign: 'right' }}>
                {fmtK(c)}
              </span>
            </div>
          );
        })}
      </div>
    </>
  );

  const tagsBlock = (
    <>
      <div style={{ fontSize: 10, color: '#7bc3ff', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.8px', marginBottom: 10 }}>
        {isRu ? 'Моя реакция' : 'My reaction'}
      </div>
      {!user &&
        <div style={{ marginBottom: 12 }}>
          <GuestGate message="Sign in to react to this actor" ctaLabel="Register to React" />
        </div>
      }
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
        {ACTOR_REACTIONS.map((r) => {
          const active = user && reactions[r.id];
          return (
            <button key={r.id} onClick={() => toggle(r.id)}
              style={{
                padding: '5px 11px', borderRadius: 16, fontSize: 11, fontWeight: 500,
                background: active ? 'rgba(10,186,181,0.22)' : 'var(--bg3)',
                color: active ? '#0abab5' : user ? 'var(--text2)' : 'var(--text3)',
                border: `1.5px solid ${active ? '#0abab5' : 'rgba(74,158,255,0.10)'}`,
                display: 'flex', alignItems: 'center', gap: 4,
                cursor: 'pointer', transition: 'all 0.15s', lineHeight: 1.2,
                opacity: user ? 1 : 0.6,
                boxShadow: active ? '0 0 0 2px rgba(10,186,181,0.18)' : 'none'
              }}>
              {r.icon && LineIcons[r.icon] && (
                <span style={{ display: 'inline-flex', color: active ? '#0abab5' : 'var(--text3)', transform: 'scale(0.75)' }}>{LineIcons[r.icon]}</span>
              )}
              <span>{t(r.label)}</span>
            </button>);
        })}
      </div>
    </>
  );

  // Внешний контейнер — общий стиль плашки (Apple-style padding/radius).
  // В полном режиме теги отделены тонкой линией сверху от диаграммы.
  // marginTop: 20 нужен только в полном режиме (когда плашка под другим контентом).
  // В режимах 'distribution'/'tags' внешние отступы задаёт родитель.
  return (
    <div className="ds-actor-reaction-panel" style={{
      padding: '14px 18px 18px',
      borderRadius: 18,
      marginTop: mode === 'full' ? 20 : 0
    }}>
      {mode === 'distribution' && distributionBlock}
      {mode === 'tags' && tagsBlock}
      {mode === 'full' && (
        <>
          <div style={{ marginBottom: 18 }}>{distributionBlock}</div>
          <div style={{ paddingTop: 14, borderTop: '1px solid rgba(140,180,235,0.12)' }}>{tagsBlock}</div>
        </>
      )}
    </div>);

}

// ── ACTOR TRAITS PANEL ────────────────────────────────────────────────────────
const ACTOR_TRAITS = [
{ id: 'chemistry', label: 'Chemistry' },
{ id: 'acting_skills', label: 'Acting Skills' },
{ id: 'smile', label: 'Smile' },
{ id: 'emotional_acting', label: 'Emotional Acting' },
{ id: 'charisma', label: 'Charisma' },
{ id: 'overhyped', label: 'Overhyped' },
{ id: 'repetitive_roles', label: 'Repetitive Roles' }];

// ── ACTOR RATING (1–10 по разным критериям, как DramaRatingPanel) ────────────
const ACTOR_RATING_KEYS = [
  { id: 'acting',          label: 'Acting Performance',  icon: 'handsUp' },
  { id: 'versatility',     label: 'Role Versatility',    icon: 'repeat' },
  { id: 'chemistry',       label: 'On-screen Chemistry', icon: 'heartOutline' },
  { id: 'charisma',        label: 'Screen Charisma',     icon: 'flame' },
  { id: 'difficult_scenes',label: 'Difficult Scenes',    icon: 'starShine' },
  { id: 'emotional',       label: 'Emotional Acting',    icon: 'heartBroken' },
  { id: 'attractiveness',  label: 'Attractiveness',      icon: 'sparkles' }
];

function ActorRatingPanel({ actorId }) {
  const { user } = useAuth();
  const { t, lang } = useI18n();
  const storageKey = `ds_actor_ratings_${actorId}_${user?.email || 'guest'}`;
  const [ratings, setRatings] = useState(() => { try { return JSON.parse(localStorage.getItem(storageKey)) || {}; } catch { return {}; } });
  const [hover, setHover] = useState({});

  const setRating = (id, val) => {
    if (!user) return;
    const next = { ...ratings, [id]: ratings[id] === val ? 0 : val };
    setRatings(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
    // Сигналим всем плашкам «Рейтинг DramaScope» / «Твой рейтинг» в ActorDetailPage — мгновенно перечитать.
    window.dispatchEvent(new CustomEvent('ds-actor-ratings-changed', { detail: { actorId } }));
  };

  const ratedKeys = ACTOR_RATING_KEYS.filter(k => ratings[k.id]);
  const avg = ratedKeys.length > 0
    ? (ratedKeys.reduce((s, k) => s + ratings[k.id], 0) / ratedKeys.length).toFixed(1)
    : null;

  return (
    <div className="ds-actor-rate-panel" style={{ background: 'var(--bg2)', border: 'none', borderRadius: 12, padding: 22, marginTop: 16, boxShadow: 'none' }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 18, flexWrap: 'wrap', gap: 10 }}>
        <h3 style={{ fontSize: 16, fontWeight: 700 }}>{t('Rate this actor')}</h3>
        {avg && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--text3)' }}>
            <span>{t('Your average')}:</span>
            <span style={{
              padding: '3px 10px', borderRadius: 14, fontWeight: 800, fontSize: 13,
              color: '#0a1226', background: RATING_COLOR[Math.round(Number(avg)) - 1] || 'var(--bg3)'
            }}>{avg}</span>
          </div>
        )}
        {!avg && user && <span style={{ fontSize: 11, color: 'var(--text3)' }}>{t('1–10 scale · auto-saved')}</span>}
      </div>

      {!user &&
        <div style={{ marginBottom: 18 }}>
          <GuestGate message="Sign in to rate this actor" ctaLabel="Register to Rate" />
        </div>
      }

      <div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
        {ACTOR_RATING_KEYS.map((rk) => {
          const sel = ratings[rk.id] || 0;
          const hov = hover[rk.id] || 0;
          const display = hov || sel;
          const labelArr = lang === 'ru' ? RATING_LABEL_RU : RATING_LABEL_EN;
          const labelText = display > 0 ? labelArr[display - 1] : (sel > 0 ? labelArr[sel - 1] : '');
          return (
            <div key={rk.id} className="ds-rating-row">
              <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8, gap: 12, flexWrap: 'wrap' }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, fontWeight: 400, color: user ? 'var(--text)' : 'var(--text3)' }}>
                  <span style={{ display: 'inline-flex', color: 'var(--accent2, #5ed7c6)' }}>{LineIcons[rk.icon]}</span>
                  <span>{t(rk.label)}</span>
                </div>
                <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                  {display > 0 && (
                    <span style={{ fontSize: 11, color: 'var(--text3)', fontStyle: 'italic' }}>{labelText}</span>
                  )}
                  <span style={{
                    fontSize: 13, fontWeight: 800,
                    color: sel > 0 ? RATING_COLOR[sel - 1] : 'var(--text3)',
                    minWidth: 36, textAlign: 'right'
                  }}>{sel > 0 ? `${sel}/10` : '—'}</span>
                </div>
              </div>
              <div
                onMouseLeave={() => setHover((h) => ({ ...h, [rk.id]: 0 }))}
                style={{
                  display: 'grid', gridTemplateColumns: 'repeat(10, 1fr)', gap: 4,
                  opacity: user ? 1 : 0.5, pointerEvents: user ? 'auto' : 'none'
                }}
              >
                {Array.from({ length: 10 }, (_, i) => {
                  const v = i + 1;
                  const active = v <= display;
                  return (
                    <button key={v}
                      onMouseEnter={() => setHover((h) => ({ ...h, [rk.id]: v }))}
                      onClick={() => setRating(rk.id, v)}
                      title={`${v}/10 — ${labelArr[i]}`}
                      style={{
                        height: 28, borderRadius: 6,
                        border: active ? 'none' : '1px solid var(--rating-btn-border, transparent)',
                        cursor: user ? 'pointer' : 'not-allowed',
                        padding: 0, position: 'relative',
                        background: active ? RATING_COLOR[v - 1] : 'rgba(255,255,255,0.05)',
                        transition: 'background .12s, transform .12s',
                        transform: hov === v ? 'scaleY(1.15)' : 'scaleY(1)',
                        boxShadow: sel === v ? '0 0 0 2px rgba(255,255,255,0.5)' : 'none',
                        fontSize: 10, fontWeight: 700,
                        color: active ? '#ffffff' : 'var(--rating-num-inactive, var(--text3))'
                      }}>{v}</button>
                  );
                })}
              </div>
            </div>
          );
        })}
        {Object.keys(ratings).length > 0 && (
          <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 4 }}>
            <button
              onClick={() => {
                if (!confirm(t('Reset all ratings for this actor?'))) return;
                setRatings({});
                localStorage.removeItem(storageKey);
              }}
              style={{
                padding: '6px 12px', borderRadius: 6, fontSize: 11, fontWeight: 600,
                background: 'rgba(196,69,69,0.15)', color: '#e05858', border: 'none', cursor: 'pointer'
              }}>{t('Reset ratings')}</button>
          </div>
        )}
      </div>
    </div>);
}


function ActorTraitsPanel({ actorId }) {
  const { user, openSignIn } = useAuth();
  const { t } = useI18n();
  const storageKey = `ds_traits_${actorId}_${user?.email || 'guest'}`;
  const [votes, setVotes] = useState(() => {try {return JSON.parse(localStorage.getItem(storageKey)) || {};} catch {return {};}});
  const vote = (traitId, val) => {
    if (!user) return;
    const next = { ...votes, [traitId]: votes[traitId] === val ? null : val };
    setVotes(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
  };
  return (
    <div style={{ background: 'var(--bg2)', border: '1px solid var(--border)', borderRadius: 12, padding: 22, marginTop: 16 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14, flexWrap: 'wrap', gap: 8 }}>
        <h3 style={{ fontSize: 16, fontWeight: 700 }}>{t('Vote on traits')}</h3>
        {user && <span style={{ fontSize: 11, color: 'var(--text3)' }}>👍 {t('Agree')} / 👎 {t('Disagree')}</span>}
      </div>
      {!user &&
      <div style={{ marginBottom: 14 }}>
          <GuestGate message="Sign in to react to this actor" ctaLabel="Register to React" />
        </div>
      }
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {ACTOR_TRAITS.map((tr) =>
        <div key={tr.id} style={{
          display: 'flex', justifyContent: 'space-between', alignItems: 'center',
          padding: '10px 14px', background: 'var(--bg3)', borderRadius: 8,
          border: '1px solid rgba(74,158,255,0.06)'
        }}>
            <span style={{ fontSize: 13, fontWeight: 500, color: user ? 'var(--text)' : 'var(--text3)' }}>{t(tr.label)}</span>
            <div style={{ display: 'flex', gap: 6 }}>
              <button onClick={() => user ? vote(tr.id, 'agree') : openSignIn()} style={{
              padding: '5px 12px', borderRadius: 6, fontSize: 12, fontWeight: 600,
              background: votes[tr.id] === 'agree' ? 'rgba(61,214,140,0.2)' : 'var(--bg2)',
              color: votes[tr.id] === 'agree' ? 'var(--green)' : 'var(--text3)',
              border: `1px solid ${votes[tr.id] === 'agree' ? 'rgba(61,214,140,0.4)' : 'rgba(255,255,255,0.05)'}`,
              opacity: user ? 1 : 0.5, display: 'flex', alignItems: 'center', gap: 4
            }}>👍 {t('Agree')}</button>
              <button onClick={() => user ? vote(tr.id, 'disagree') : openSignIn()} style={{
              padding: '5px 12px', borderRadius: 6, fontSize: 12, fontWeight: 600,
              background: votes[tr.id] === 'disagree' ? 'rgba(255,107,107,0.18)' : 'var(--bg2)',
              color: votes[tr.id] === 'disagree' ? 'var(--red)' : 'var(--text3)',
              border: `1px solid ${votes[tr.id] === 'disagree' ? 'rgba(255,107,107,0.4)' : 'rgba(255,255,255,0.05)'}`,
              opacity: user ? 1 : 0.5, display: 'flex', alignItems: 'center', gap: 4
            }}>👎 {t('Disagree')}</button>
            </div>
          </div>
        )}
      </div>
    </div>);

}

// ── FOLLOW BUTTON ─────────────────────────────────────────────────────────────
// Маленький кастомный hover-tooltip — мгновенный, в стиле проекта.
// Используется вокруг любых кнопок с подсказкой.
function DSHoverTip({ tip, children }) {
  const [hov, setHov] = useState(false);
  // На touch-устройствах: показываем при touchstart, скрываем при touchend/cancel/scroll.
  // На десктопе работают mouse-события. Click НЕ оставляет тултип висеть.
  useEffect(() => {
    if (!hov) return;
    const hide = () => setHov(false);
    window.addEventListener('scroll', hide, { passive: true });
    window.addEventListener('touchend', hide);
    window.addEventListener('touchcancel', hide);
    window.addEventListener('blur', hide);
    return () => {
      window.removeEventListener('scroll', hide);
      window.removeEventListener('touchend', hide);
      window.removeEventListener('touchcancel', hide);
      window.removeEventListener('blur', hide);
    };
  }, [hov]);
  if (!tip) return children;
  return (
    <span style={{ position: 'relative', display: 'inline-flex' }}
      onMouseEnter={() => setHov(true)}
      onMouseLeave={() => setHov(false)}
      onTouchStart={() => setHov(true)}
      onTouchEnd={() => setHov(false)}
      onTouchCancel={() => setHov(false)}
      onPointerLeave={() => setHov(false)}>
      {children}
      {hov && (
        <span style={{
          position: 'absolute', bottom: 'calc(100% + 8px)', left: '50%',
          transform: 'translateX(-50%)',
          background: 'rgba(8,12,26,0.97)', color: '#fff',
          fontSize: 11, fontWeight: 500, padding: '7px 11px', borderRadius: 6,
          whiteSpace: 'normal', minWidth: 180, maxWidth: 280,
          width: 'max-content',
          textAlign: 'center', lineHeight: 1.45,
          border: '1px solid rgba(74,158,255,0.30)',
          boxShadow: '0 6px 18px rgba(0,0,0,0.55)',
          zIndex: 200, pointerEvents: 'none'
        }}>
          {tip}
          <span style={{
            position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)',
            borderLeft: '5px solid transparent', borderRight: '5px solid transparent',
            borderTop: '5px solid rgba(8,12,26,0.97)'
          }} />
        </span>
      )}
    </span>
  );
}

function FollowButton({ actorId, actorName, actorNameRu, photoUrl, compact = false, square = false, round = false }) {
  const { user, openSignUp } = useAuth();
  const { t, lang } = useI18n();
  const storageKey = `ds_follows_${user?.email || 'guest'}`;
  const [follows, setFollows] = useState(() => {try {return JSON.parse(localStorage.getItem(storageKey)) || [];} catch {return [];}});
  const isFollowing = follows.some((f) => f.id === actorId);

  const toggle = () => {
    if (!user) {openSignUp();return;}
    if (isFollowing) {
      const next = follows.filter((f) => f.id !== actorId);
      setFollows(next);
      localStorage.setItem(storageKey, JSON.stringify(next));
    } else {
      // При подписке сохраняем расширенную запись: имя, name_ru, photoUrl и timestamp.
      // SubscriptionsBlock использует subscribedAt чтобы определить "новые" новости/дорамы
      // (всё, что появилось ПОСЛЕ подписки).
      const entry = {
        id: actorId,
        name: actorName,
        name_ru: actorNameRu || null,
        photoUrl: photoUrl || null,
        subscribedAt: new Date().toISOString()
      };
      const next = [...follows, entry];
      setFollows(next);
      localStorage.setItem(storageKey, JSON.stringify(next));
    }
    window.dispatchEvent(new CustomEvent('ds-follows-changed'));
  };

  const isRu = lang === 'ru';
  const tooltip = isRu
    ? (isFollowing
        ? 'Вы подписаны. Мао будет сообщать о новых проектах и новостях этого актёра.'
        : 'Подписаться на обновления и получать сообщения от Мао о новых проектах и новостях этого актёра')
    : (isFollowing
        ? 'You are subscribed. Mao will message you about new projects and news.'
        : 'Subscribe to updates and get messages from Mao about new projects and news of this actor');

  // ROUND — маленькая круглая 24×24, в стиле drama-poster library-button.
  // Используется в левом нижнем углу карточек актёра (HomePage celebs, ActorCard, cast в дораме).
  // Стиль один для day и night (solid sky-blue + белая обводка) — оба смотрятся одинаково
  // ярко на любом фоне (день: контраст с кремовым фоном, ночь: с тёмным).
  if (round) {
    return (
      <DSHoverTip tip={tooltip}>
        <button onClick={(e) => { e.preventDefault(); e.stopPropagation(); toggle(); }}
          onMouseDown={(e) => e.stopPropagation()}
          aria-label={isRu ? 'Подписаться' : 'Subscribe'} title={tooltip}
          className="ds-follow-round"
          style={{
            width: 24, height: 24, borderRadius: '50%', flexShrink: 0, padding: 0,
            background: 'rgba(255,255,255,0.45)',
            color: isFollowing ? '#1487cb' : '#1a1a1a',
            border: '1px solid rgba(0,0,0,0.15)',
            backdropFilter: 'blur(4px)', WebkitBackdropFilter: 'blur(4px)',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer',
            boxShadow: '0 1px 4px rgba(0,0,0,0.25)'
          }}>
          {isFollowing ? (
            <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
              <polyline points="20 6 9 17 4 12" />
            </svg>
          ) : (
            // Тонкий «+» — SVG чтобы контролировать толщину линии
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
              <line x1="12" y1="5" x2="12" y2="19" />
              <line x1="5" y1="12" x2="19" y2="12" />
            </svg>
          )}
        </button>
      </DSHoverTip>
    );
  }

  // SQUARE — компактная иконка-кнопка 40×40, без подписи
  if (square) {
    return (
      <DSHoverTip tip={tooltip}>
        <button onClick={toggle} aria-label={isRu ? 'Подписаться' : 'Subscribe'} style={{
          width: 40, height: 40, borderRadius: 8, padding: 0,
          background: isFollowing ? 'rgba(20,135,203,0.18)' : '#1487cb',
          color: isFollowing ? '#1487cb' : '#ffffff',
          border: isFollowing ? '1px solid rgba(20,135,203,0.45)' : '1px solid #1487cb',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          cursor: 'pointer',
          boxShadow: isFollowing ? 'none' : '0 2px 8px rgba(20,135,203,0.25)'
        }}>
          {/* Колокольчик для подписки */}
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" />
            <path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" />
            {isFollowing && <circle cx="18" cy="6" r="3" fill="currentColor" />}
          </svg>
        </button>
      </DSHoverTip>
    );
  }

  // Русские/английские подписи (compact-режим)
  const labelText = compact
    ? (isFollowing ? (isRu ? 'Подписан' : 'Following') : (isRu ? 'Подписаться' : 'Subscribe'))
    : (isFollowing ? (isRu ? 'Подписан — уведомления включены' : 'Following — updates on') : (isRu ? 'Подписаться на обновления' : 'Follow actor for updates'));
  const prefix = !user ? '🔒 ' : (isFollowing ? '✓ ' : '+ ');

  return (
    <DSHoverTip tip={tooltip}>
      <button onClick={toggle} style={{
        width: compact ? 'auto' : '100%',
        height: compact ? 36 : 'auto',
        padding: compact ? '0 14px' : '12px 18px',
        borderRadius: compact ? 8 : 10,
        fontSize: compact ? 12 : 12.5, fontWeight: 700,
        background: isFollowing ? 'rgba(20,135,203,0.18)' : '#1487cb',
        color: isFollowing ? '#1487cb' : '#ffffff',
        border: isFollowing ? '1px solid rgba(20,135,203,0.45)' : '1px solid #1487cb',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 5,
        letterSpacing: '0.1px', cursor: 'pointer', whiteSpace: 'nowrap',
        boxShadow: isFollowing ? 'none' : (compact ? '0 2px 8px rgba(20,135,203,0.25)' : '0 4px 14px rgba(20,135,203,0.30)'),
        lineHeight: 1
      }}>
        {prefix}{labelText}
      </button>
    </DSHoverTip>);

}

// ── FAVORITE PERSON MARK ─────────────────────────────────────────────────────
// Маленькое красное сердце, которое появляется перед именем актёра ВЕЗДЕ,
// где он показывается, если он в списке любимых текущего пользователя.
// Читает localStorage.ds_fav_people_<email|guest>, слушает 'ds-fav-people-changed'.
function FavoritePersonMark({ actorId, size = 14, style }) {
  const { user } = useAuth();
  const key = `ds_fav_people_${user?.email || 'guest'}`;
  const check = () => {
    if (!actorId) return false;
    try { return JSON.parse(localStorage.getItem(key) || '[]').some(x => x.id === actorId); } catch { return false; }
  };
  const [isFav, setIsFav] = useState(check);
  useEffect(() => {
    const refresh = () => setIsFav(check());
    refresh();
    window.addEventListener('ds-fav-people-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-fav-people-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [actorId, user?.email]);
  if (!isFav) return null;
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center',
      verticalAlign: 'middle', marginRight: 5,
      flexShrink: 0,
      ...(style || {})
    }} title="В вашем списке любимых">
      <svg width={size} height={size} viewBox="0 0 24 24"
        fill="#ff3d5e" stroke="#ff3d5e" strokeWidth="1.4" strokeLinejoin="round"
        style={{ filter: (style && style.filter !== undefined) ? style.filter : 'drop-shadow(0 0 3px rgba(255,61,94,0.45))' }}>
        <path d="M12 20.5s-7-4.4-9.3-9C1.2 8.4 3.2 5 6.7 5 9 5 10.7 6.5 12 8c1.3-1.5 3-3 5.3-3 3.5 0 5.5 3.4 4 6.5-2.3 4.6-9.3 9-9.3 9z"/>
      </svg>
    </span>
  );
}

// ── FAVORITE PERSON ──────────────────────────────────────────────────────────
// Кнопка-сердце: сохраняет актёра/режиссёра в список «любимые люди» юзера.
// Хранится в localStorage.ds_fav_people_<email>, видна на профиле юзера.
function FavoritePersonButton({ actorId, actorName, photoUrl, compact = false }) {
  const { user, openSignUp } = useAuth();
  const { lang } = useI18n();
  const storageKey = `ds_fav_people_${user?.email || 'guest'}`;
  const [list, setList] = useState(() => { try { return JSON.parse(localStorage.getItem(storageKey)) || []; } catch { return []; } });
  // Реактивно слушаем глобальное событие — чтобы клик на одной карточке мгновенно
  // отразился на heart-кнопке на других местах (список ↔ детальная).
  useEffect(() => {
    const refresh = () => {
      try { setList(JSON.parse(localStorage.getItem(storageKey)) || []); } catch { setList([]); }
    };
    window.addEventListener('ds-fav-people-changed', refresh);
    window.addEventListener('storage', refresh);
    return () => {
      window.removeEventListener('ds-fav-people-changed', refresh);
      window.removeEventListener('storage', refresh);
    };
  }, [storageKey]);
  const isFav = list.some(x => x.id === actorId);
  const toggle = (e) => {
    e.preventDefault(); e.stopPropagation();
    if (!user) { openSignUp(); return; }
    const next = isFav
      ? list.filter(x => x.id !== actorId)
      : [...list, { id: actorId, name: actorName, photoUrl, addedAt: new Date().toISOString() }];
    setList(next);
    localStorage.setItem(storageKey, JSON.stringify(next));
    window.dispatchEvent(new CustomEvent('ds-fav-people-changed'));
  };
  const isRu = lang === 'ru';
  const tooltip = !user
    ? (isRu ? 'Войдите, чтобы добавить в любимых' : 'Sign in to favorite')
    : isFav
      ? (isRu ? 'Убрать из любимых' : 'Remove from favorites')
      : (isRu ? 'Добавить в любимые' : 'Add to favorites');
  // ── Compact-вариант: маленький кружок с сердечком на постере актёра.
  //    Размер 22×22, фон и обводка по теме через .ds-fav-heart-btn
  //    (day=белый круг+чёрная обводка, night=тёмный круг+белая обводка, active=красное без обводки). ──
  if (compact) {
    return (
      <DSHoverTip tip={tooltip}>
        <button onClick={toggle}
          className="ds-fav-heart-btn"
          data-active={isFav ? '1' : '0'}
          title={tooltip}
          aria-label="Favorite this person"
          style={{
            width: 24, height: 24, borderRadius: 0,
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer', padding: 0,
            boxShadow: 'none',
            transition: 'transform 0.15s, color 0.15s',
            flexShrink: 0
          }}
          onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.18)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.transform = 'none'; }}>
          <svg width="24" height="24" viewBox="0 0 24 24" strokeLinejoin="round" strokeLinecap="round">
            {/* halo — внешний stroke (инвертируется при активном — для контраста с красной заливкой) */}
            <path className="ds-fav-heart-halo" data-active={isFav ? '1' : '0'} d="M12 20.5s-7-4.4-9.3-9C1.2 8.4 3.2 5 6.7 5 9 5 10.7 6.5 12 8c1.3-1.5 3-3 5.3-3 3.5 0 5.5 3.4 4 6.5-2.3 4.6-9.3 9-9.3 9z" />
            {/* внутренний fill — противоположный цвет; если активно — красный */}
            <path className="ds-fav-heart-inner" data-active={isFav ? '1' : '0'} d="M12 20.5s-7-4.4-9.3-9C1.2 8.4 3.2 5 6.7 5 9 5 10.7 6.5 12 8c1.3-1.5 3-3 5.3-3 3.5 0 5.5 3.4 4 6.5-2.3 4.6-9.3 9-9.3 9z" />
          </svg>
        </button>
      </DSHoverTip>
    );
  }
  // ── Обычный (большой) вариант — для action-bar на детальной странице актёра.
  //    Использует общий класс .ds-action-icon-btn .ds-action-fav как у share-кнопки —
  //    тонкая 1px-обводка, тема-aware bg. Активное состояние: красное сердце. ──
  return (
    <DSHoverTip tip={tooltip}>
      <button onClick={toggle}
        className="ds-action-icon-btn ds-action-fav"
        data-active={isFav ? '1' : '0'}
        aria-label="Favorite this person"
        style={{
          width: 36, height: 36, borderRadius: '50%',
          color: isFav ? '#ff3b5e' : '#ff5e7e',
          display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
          cursor: 'pointer', padding: 0,
          transition: 'all 0.15s, transform 0.15s',
          flexShrink: 0
        }}
        onMouseEnter={(e) => { e.currentTarget.style.transform = 'scale(1.08)'; }}
        onMouseLeave={(e) => { e.currentTarget.style.transform = 'none'; }}>
        <svg width="18" height="18" viewBox="0 0 24 24"
          fill={isFav ? 'currentColor' : 'none'}
          stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round">
          <path d="M12 20.5s-7-4.4-9.3-9C1.2 8.4 3.2 5 6.7 5 9 5 10.7 6.5 12 8c1.3-1.5 3-3 5.3-3 3.5 0 5.5 3.4 4 6.5-2.3 4.6-9.3 9-9.3 9z"/>
        </svg>
      </button>
    </DSHoverTip>
  );
}

// Export everything to window
Object.assign(window, {
  // i18n
  LANGUAGES, I18nContext, I18nProvider, useI18n,
  // auth
  AuthContext, AuthProvider, useAuth, AuthModal,
  // header
  LanguageMenu, AuthButtons, AvatarMenu,
  // gates and panels
  GuestGate, DramaRatingPanel, DramaRatingsSummary, RatingHistogram, ActorRatingHistogram, ViewersSummary, PosterRatingBadge, DramaReactionPanel, DramaMyReactionPanel,
  ActorReactionPanel, ActorTraitsPanel, ActorRatingPanel, FollowButton, FavoritePersonButton, FavoritePersonMark, DSHoverTip,
  // rating keys (for personal cabinet aggregation)
  DRAMA_RATING_KEYS, ACTOR_RATING_KEYS
});